diff --git a/tests/fur_pattern_validation.gd b/tests/fur_pattern_validation.gd index 07efe54..a795d53 100644 --- a/tests/fur_pattern_validation.gd +++ b/tests/fur_pattern_validation.gd @@ -287,7 +287,10 @@ func _validate_fur_color_ui(snapshot: Dictionary) -> void: ) as ColorPickerButton var picker_popup := picker.get_popup() var picker_control := picker.get_picker() - assert(color_panel != null and color_panel.size.x > 360.0) + var option_list := profile_page.get("_option_list") as Control + assert(color_panel != null and option_list != null) + assert(color_panel.size.x > 0.0) + assert(color_panel.size.x <= option_list.size.x + 0.5) assert(channel_grid != null and channel_grid.columns == 2) assert(channel_grid.get_child_count() == 4) assert(palette_grid != null and palette_grid.columns == 6) diff --git a/tests/inventory_notepad_art_validation.gd b/tests/inventory_notepad_art_validation.gd index 3faec66..c42883c 100644 --- a/tests/inventory_notepad_art_validation.gd +++ b/tests/inventory_notepad_art_validation.gd @@ -5,6 +5,11 @@ const HotbarScene = preload("res://ui/hotbar.tscn") const UIReferencePresentationType = preload( "res://ui/ui_reference_presentation.gd" ) +const FishSaleResultType = preload("res://economy/fish_sale_result.gd") +const CurrencyPresentationType = preload( + "res://ui/currency_presentation.gd" +) +const PelicanBuyer = preload("res://economy/buyers/pelicans.tres") const EXPECTED_HOST_SIZE := Vector2(278.0, 484.0) const EXPECTED_ART_SIZE := Vector2(306.28125, 580.8) const EXPECTED_ART_POSITION := Vector2(-4.140625, -28.4) @@ -66,6 +71,7 @@ func _run() -> void: await _validate_profile_content_bounds(player_menu) await _validate_hotbar_centering(presentation_stage) _validate_inventory_layering(player_menu) + _validate_sale_confirmation_copy(player_menu) _validate_tackle_to_items_transition(player_menu) _validate_cooler_notepad_typography(player_menu) _validate_resolution_matrix() @@ -211,11 +217,129 @@ func _validate_profile_content_bounds(player_menu: PlayerMenu) -> void: var body := option_list.get_parent() as Control var preview := profile.get("_preview") as Control var shell := profile.find_child("UtilityMainBox", true, false) as Control - assert(option_list != null and body != null and preview != null and shell != null) - for category_id: String in ["fur_pattern", "voice"]: + var content_zone := profile.find_child( + "ProfileContentZone", true, false + ) as Control + var profile_layout := profile.find_child( + "ProfileLayout", true, false + ) as Control + var account_row := profile.find_child( + "ProfileAccountRow", true, false + ) as Control + var body_panel := profile.find_child( + "ProfileBodyPanel", true, false + ) as Control + assert(option_list != null and body != null and preview != null) + assert(shell != null and content_zone != null and profile_layout != null) + assert(account_row != null and body_panel != null) + var content_frame_size := Vector2( + UtilityPageStyle.LAPTOP_RECT.size.x - 40.0, + UtilityPageStyle.LAPTOP_RECT.size.y - 42.0, + ) + assert( + content_zone.size.is_equal_approx(content_frame_size), + ( + "profile content zone changed size: %s != %s; " + + "layout min=%s account min=%s body min=%s" + ) % [ + content_zone.size, + content_frame_size, + profile_layout.get_combined_minimum_size(), + account_row.get_combined_minimum_size(), + body_panel.get_combined_minimum_size(), + ], + ) + assert( + profile_layout.size.is_equal_approx( + UtilityPageStyle.LAPTOP_CONTENT_SIZE + ), + "profile layout changed size: %s != %s" % [ + profile_layout.size, + UtilityPageStyle.LAPTOP_CONTENT_SIZE, + ], + ) + assert( + profile_layout.get_combined_minimum_size().x + <= profile_layout.size.x, + "profile layout exceeded shared content width: %s > %s" % [ + profile_layout.get_combined_minimum_size().x, + profile_layout.size.x, + ], + ) + var content_rect: Rect2 = profile_layout.get_global_rect() + for section: Control in [account_row, body_panel]: + var section_rect: Rect2 = section.get_global_rect() + assert(section_rect.position.x >= content_rect.position.x - 0.5) + assert(section_rect.end.x <= content_rect.end.x + 0.5) + var category_ids: Array[String] = [] + for category_id: String in CharacterCustomizationCatalog.CATEGORY_IDS: + category_ids.append(category_id) + category_ids.append("voice") + for category_id: String in category_ids: profile.call("_select_category", category_id) await process_frame await process_frame + assert( + shell.position.is_equal_approx( + UtilityPageStyle.LAPTOP_RECT.position + ), + "%s profile shell position changed: %s" % [ + category_id, + shell.position, + ], + ) + assert( + shell.size.is_equal_approx(UtilityPageStyle.LAPTOP_RECT.size), + "%s profile shell size changed: %s != %s" % [ + category_id, + shell.size, + UtilityPageStyle.LAPTOP_RECT.size, + ], + ) + assert( + content_zone.size.is_equal_approx(content_frame_size), + "%s profile content zone changed size: %s != %s" % [ + category_id, + content_zone.size, + content_frame_size, + ], + ) + assert( + profile_layout.size.is_equal_approx( + UtilityPageStyle.LAPTOP_CONTENT_SIZE + ), + "%s profile layout changed size: %s != %s" % [ + category_id, + profile_layout.size, + UtilityPageStyle.LAPTOP_CONTENT_SIZE, + ], + ) + assert( + profile_layout.get_combined_minimum_size().x + <= profile_layout.size.x, + "%s profile layout exceeded shared content width: %s > %s" % [ + category_id, + profile_layout.get_combined_minimum_size().x, + profile_layout.size.x, + ], + ) + var category_body_rect: Rect2 = body_panel.get_global_rect() + assert( + category_body_rect.end.x <= content_rect.end.x + 0.5, + "%s body overflowed shared content: %s > %s" % [ + category_id, + category_body_rect.end.x, + content_rect.end.x, + ], + ) + assert( + category_body_rect.end.y <= content_rect.end.y + 0.5, + "%s body overflowed shared content vertically: %s > %s" % [ + category_id, + category_body_rect.end.y, + content_rect.end.y, + ], + ) assert( body.get_combined_minimum_size().x <= body.size.x, "%s customization content overflowed its body: %s > %s" % [ @@ -226,9 +350,36 @@ func _validate_profile_content_bounds(player_menu: PlayerMenu) -> void: ) assert( preview.get_global_rect().end.x - <= shell.get_global_rect().end.x, + <= content_rect.end.x + 0.5, "%s preview overflowed the utility content area" % category_id, ) + profile.call("_select_category", "fur_pattern") + profile.call("_select_fur_section", "colors") + await process_frame + await process_frame + var color_panel := profile.find_child( + "FurColorOptionsPanel", true, false + ) as Control + var color_scroll := profile.find_child( + "FurColorScroll", true, false + ) as ScrollContainer + assert(color_panel != null and color_scroll != null) + assert( + color_panel.get_global_rect().end.y + <= body_panel.get_global_rect().end.y + 0.5, + "fur color panel overflowed profile body: %s > %s" % [ + color_panel.get_global_rect().end.y, + body_panel.get_global_rect().end.y, + ], + ) + assert( + profile_layout.get_combined_minimum_size().y + <= profile_layout.size.y, + "fur colors expanded profile layout vertically: %s > %s" % [ + profile_layout.get_combined_minimum_size().y, + profile_layout.size.y, + ], + ) profile.visible = profile_was_visible player_menu.visible = menu_was_visible @@ -312,6 +463,28 @@ func _validate_inventory_layering(player_menu: PlayerMenu) -> void: ) +func _validate_sale_confirmation_copy(player_menu: PlayerMenu) -> void: + var preview := FishSaleResultType.new() + preview.fish_count = 1 + preview.payout = 3 + preview.base_value = 12 + var message: String = str(player_menu.call( + "_sale_confirmation_text", + preview, + PelicanBuyer, + preview.base_value, + )) + assert(message == ( + "[center]You can sell this fish to the pelicans now for %s, " + + "but the shop is willing to pay %s![/center]" + ) % [ + CurrencyPresentationType.bbcode_amount(preview.payout, 22), + CurrencyPresentationType.bbcode_amount(preview.base_value, 22), + ]) + assert((player_menu.get_node("%ConfirmSaleButton") as Button).text == "sell now") + assert((player_menu.get_node("%CancelSaleButton") as Button).text == "nevermind") + + func _validate_tackle_to_items_transition(player_menu: PlayerMenu) -> void: var inventory_notepad := player_menu.get_node( "%BagDetailBubble" @@ -513,7 +686,10 @@ func _capture_inventory_pages(player_menu: PlayerMenu) -> void: ) as Label assert(sale_confirmation != null) assert(confirmation_message != null) - confirmation_message.text = "sell selected fish?" + confirmation_message.text = ( + "You can sell this fish to the pelicans now, " + + "but the shop will pay more!" + ) sale_confirmation.visible = true await process_frame await process_frame diff --git a/tests/ui_scaling_runtime_validation.gd b/tests/ui_scaling_runtime_validation.gd index 53675fe..399faee 100644 --- a/tests/ui_scaling_runtime_validation.gd +++ b/tests/ui_scaling_runtime_validation.gd @@ -62,6 +62,21 @@ func _run() -> void: root.add_child(presenter) await process_frame chat_ui.set_available(true) + var speech_layer := chat_ui.get("_speech_layer") as Control + assert(speech_layer != null and speech_layer.visible) + game_ui.call("_on_player_menu_visibility_changed", true) + assert(not chat_ui.is_world_speech_visible()) + assert(not speech_layer.visible) + game_ui.call("_on_player_menu_visibility_changed", false) + assert(chat_ui.is_world_speech_visible()) + assert(speech_layer.visible) + chat_ui.set_available(true) + chat_ui.call( + "_set_presentation_state", + ChatUI.PresentationState.EXPANDED, + false, + false, + ) print( "UI viewport embedding: root=%s subviewport=%s" % [root.gui_embed_subwindows, ui_viewport.gui_embed_subwindows] @@ -186,6 +201,13 @@ func _run() -> void: + ChatUI.CALENDAR_TOP_GAP, ))) assert(calendar_panel.size.is_equal_approx(ChatUI.CALENDAR_SIZE)) + assert( + chat_panel.position.y + >= calendar_panel.position.y + + calendar_panel.size.y + + ChatUI.EXPANDED_CHAT_TOP_GAP + - 0.01 + ) assert(is_equal_approx( status_effect_column.position.x, calendar_panel.position.x diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index 5f84601..c79d80c 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -10,7 +10,6 @@ const CHAT_SURFACE_COLOR := Color(0.025, 0.13, 0.19, 0.94) const PANEL_WIDTH: float = 290.0 const COMPACT_HEIGHT: float = 250.0 const BOTTOM_MARGIN: float = 12.0 -const EXPANDED_TOP_MARGIN: float = 94.0 const MIN_TOP_MARGIN: float = 24.0 const MIN_HISTORY_HEIGHT: float = 92.0 const HISTORY_FONT_SIZE: int = 20 @@ -45,6 +44,14 @@ const CALENDAR_SIZE := Vector2( ) const CALENDAR_FONT_SIZE: int = 16 const CALENDAR_TOP_GAP: float = 4.0 +const EXPANDED_CHAT_TOP_GAP: float = 12.0 +const EXPANDED_TOP_MARGIN: float = ( + CLOCK_EDGE_MARGIN + + CLOCK_SIZE.y + + CALENDAR_TOP_GAP + + CALENDAR_SIZE.y + + EXPANDED_CHAT_TOP_GAP +) const STATUS_EFFECT_ICON_SIZE := Vector2(40.0, 40.0) const STATUS_EFFECT_TOP_GAP: float = 6.0 const STATUS_EFFECT_ICON_GAP: int = 4 @@ -116,6 +123,7 @@ var _height_tween: Tween var _opened: bool = false var _available: bool = false var _hud_hidden: bool = false +var _world_speech_visible: bool = true var _presentation_state := PresentationState.COMPACT var _visible_state_before_collapse := PresentationState.COMPACT var _panel_hovered: bool = false @@ -1200,6 +1208,16 @@ func is_hud_hidden() -> bool: return _hud_hidden +func set_world_speech_visible(should_be_visible: bool) -> void: + _world_speech_visible = should_be_visible + if _speech_layer != null: + _speech_layer.visible = should_be_visible + + +func is_world_speech_visible() -> bool: + return _world_speech_visible + + func _refresh_handle_labels(state: PresentationState) -> void: var collapsed := state == PresentationState.COLLAPSED var height_state := _visible_state_before_collapse if collapsed else state diff --git a/ui/game_ui.gd b/ui/game_ui.gd index e202ec6..65cf93b 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -404,6 +404,7 @@ func setup( wallet, sale_service, default_buyer, + main_shop_buyer, catalog, fishing_spot, bag, @@ -2247,6 +2248,7 @@ func _update_experience_bubble_position() -> void: func _on_player_menu_visibility_changed(is_open: bool) -> void: _player_menu_open = is_open + _chat_ui.set_world_speech_visible(not is_open) if is_open: _end_virtual_mouse() _refresh_surface_drawing_activation() diff --git a/ui/player_menu.gd b/ui/player_menu.gd index 2df48b1..cb69b6a 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -88,6 +88,7 @@ const NAVIGATION_CANONICAL_POSITION := Vector2( ) const NAVIGATION_SELECTED_SCALE: float = 1.02 const MAIN_SHOP_BUYER_ID: StringName = &"main_fishing_shop" +const PELICAN_BUYER_ID: StringName = &"pelicans" signal menu_visibility_changed(is_open: bool) signal inventory_hotbar_context_changed(show_hotbar: bool) @@ -274,6 +275,7 @@ var _network_profile_service: NetworkProfileService var _network_player_list: NetworkPlayerListService var _controller_mapping_manager: ControllerMappingManagerType var _default_buyer: FishBuyerProfileType +var _shop_buyer: FishBuyerProfileType var _sale_buyer_override: FishBuyerProfileType var _shop_cooler_context_active: bool = false var _cooler_original_parent: Node @@ -531,6 +533,7 @@ func setup( wallet: PlayerWalletType, sale_service: FishSaleServiceType, default_buyer: FishBuyerProfileType, + shop_buyer: FishBuyerProfileType, catalog: FishPoolType, fishing_spot: FishingSpotType, bag: PlayerBagType, @@ -556,6 +559,7 @@ func setup( _wallet = wallet _sale_service = sale_service _default_buyer = default_buyer + _shop_buyer = shop_buyer _catalog = catalog _fishing_spot = fishing_spot _bag = bag @@ -5086,17 +5090,19 @@ func _begin_sale_confirmation(catch_ids: Array[StringName]) -> void: _confirmation_buyer = active_buyer _confirmation_buyer_id = active_buyer.id _confirmation_generation = _menu_generation - var fish_label: String = "fish" - _confirmation_message.text = ( - "[center]sell %d %s to the %s for %s?\n" - + "combined base value: %s[/center]" - ) % [ - preview.fish_count, - fish_label, - _get_buyer_display_group(active_buyer), - CurrencyPresentationType.bbcode_amount(preview.payout, 22), - CurrencyPresentationType.bbcode_amount(preview.base_value, 22), - ] + var shop_payout: int = preview.base_value + if _shop_buyer != null and _shop_buyer.is_valid(): + var shop_preview: FishSaleResultType = _sale_service.preview_batch( + catch_ids, + _shop_buyer, + ) + if shop_preview != null and shop_preview.is_success(): + shop_payout = shop_preview.payout + _confirmation_message.text = _sale_confirmation_text( + preview, + active_buyer, + shop_payout, + ) _sale_confirmation.visible = true if _shop_cooler_context_active: shop_cooler_modal_changed.emit(true) @@ -5108,6 +5114,36 @@ func _begin_sale_confirmation(catch_ids: Array[StringName]) -> void: _cancel_sale_button.grab_focus() +func _sale_confirmation_text( + preview: FishSaleResultType, + buyer: FishBuyerProfileType, + shop_payout: int, +) -> String: + var catch_label: String = ( + "this fish" + if preview.fish_count == 1 + else "these %d fish" % preview.fish_count + ) + var payout_text: String = CurrencyPresentationType.bbcode_amount( + preview.payout, + 22, + ) + if buyer != null and buyer.id == PELICAN_BUYER_ID: + return ( + "[center]You can sell %s to the pelicans now for %s, " + + "but the shop is willing to pay %s![/center]" + ) % [ + catch_label, + payout_text, + CurrencyPresentationType.bbcode_amount(shop_payout, 22), + ] + return "[center]Sell %s to the %s now for %s?[/center]" % [ + catch_label, + _get_buyer_display_group(buyer), + payout_text, + ] + + func _on_confirm_sale_pressed() -> void: if ( _sale_in_progress diff --git a/ui/player_menu.tscn b/ui/player_menu.tscn index 55c3b02..18f9918 100644 --- a/ui/player_menu.tscn +++ b/ui/player_menu.tscn @@ -914,13 +914,13 @@ alignment = 1 [node name="ConfirmSaleButton" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/SaleConfirmation/ConfirmationMargin/ConfirmationLayout/ConfirmationButtons"] unique_name_in_owner = true layout_mode = 2 -text = "confirm" +text = "sell now" custom_minimum_size = Vector2(140, 44) [node name="CancelSaleButton" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/SaleConfirmation/ConfirmationMargin/ConfirmationLayout/ConfirmationButtons"] unique_name_in_owner = true layout_mode = 2 -text = "cancel" +text = "nevermind" custom_minimum_size = Vector2(140, 44) [node name="NavigationCluster" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"] diff --git a/ui/profile_page.gd b/ui/profile_page.gd index 08dcb9e..3824a72 100644 --- a/ui/profile_page.gd +++ b/ui/profile_page.gd @@ -13,6 +13,10 @@ const FUR_PART_TAB_WIDTH: float = 58.0 const FUR_COLOR_PICKER_POPUP_SIZE: Vector2i = Vector2i(720, 560) const FUR_COLOR_PICKER_SV_SIZE: Vector2i = Vector2i(520, 300) const VOICE_OPTION_BUTTON_SIZE: Vector2 = Vector2(68.0, 32.0) +const HEADER_ACTION_FONT_SIZE: int = 18 +const BODY_COLUMN_SEPARATION: int = 12 +const SIZE_ENDPOINT_FONT_SIZE: int = 18 +const SIZE_SLIDER_MIN_WIDTH: float = 180.0 const FUR_SECTION_PATTERNS: String = "patterns" const FUR_SECTION_COLORS: String = "colors" const FEATURE_DRAWER_ANIMATION_SECONDS: float = 0.16 @@ -572,11 +576,14 @@ func has_modal_confirmation() -> bool: func _build_ui() -> void: UtilityPageStyle.apply_page(self) var margin: MarginContainer = UtilityPageStyle.build_laptop_screen(self) + margin.name = "ProfileContentZone" var layout := VBoxContainer.new() + layout.name = "ProfileLayout" layout.add_theme_constant_override("separation", 8) margin.add_child(layout) var account_row := HBoxContainer.new() + account_row.name = "ProfileAccountRow" account_row.add_theme_constant_override("separation", 14) layout.add_child(account_row) var account_stack := VBoxContainer.new() @@ -668,6 +675,7 @@ func _build_ui() -> void: experience_row.add_child(_experience_value) var actions := HBoxContainer.new() + actions.name = "ProfileActionRow" actions.alignment = BoxContainer.ALIGNMENT_END actions.size_flags_vertical = Control.SIZE_SHRINK_BEGIN actions.add_theme_constant_override("separation", 7) @@ -676,28 +684,41 @@ func _build_ui() -> void: _customize_button.text = "customize" _customize_button.custom_minimum_size.x = 96.0 UtilityPageStyle.apply_compact_ocean_button(_customize_button) + _customize_button.add_theme_font_size_override( + "font_size", HEADER_ACTION_FONT_SIZE + ) _customize_button.pressed.connect(_enter_controller_customization) actions.add_child(_customize_button) _apply_button = Button.new() _apply_button.text = "apply" _apply_button.custom_minimum_size.x = 72.0 UtilityPageStyle.apply_compact_ocean_button(_apply_button) + _apply_button.add_theme_font_size_override( + "font_size", HEADER_ACTION_FONT_SIZE + ) _apply_button.pressed.connect(_apply) actions.add_child(_apply_button) _revert_button = Button.new() _revert_button.text = "revert" _revert_button.custom_minimum_size.x = 76.0 UtilityPageStyle.apply_compact_ocean_button(_revert_button) + _revert_button.add_theme_font_size_override( + "font_size", HEADER_ACTION_FONT_SIZE + ) _revert_button.pressed.connect(_revert) actions.add_child(_revert_button) _defaults_button = Button.new() _defaults_button.text = "defaults" _defaults_button.custom_minimum_size.x = 82.0 UtilityPageStyle.apply_compact_ocean_button(_defaults_button) + _defaults_button.add_theme_font_size_override( + "font_size", HEADER_ACTION_FONT_SIZE + ) _defaults_button.pressed.connect(_show_confirmation.bind("defaults")) actions.add_child(_defaults_button) var body_panel := PanelContainer.new() + body_panel.name = "ProfileBodyPanel" body_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL body_panel.add_theme_stylebox_override( "panel", UtilityPageStyle.row_style(false) @@ -711,7 +732,9 @@ func _build_ui() -> void: body_panel.add_child(body_margin) var body := HBoxContainer.new() body.alignment = BoxContainer.ALIGNMENT_BEGIN - body.add_theme_constant_override("separation", 16) + body.add_theme_constant_override( + "separation", BODY_COLUMN_SEPARATION + ) body_margin.add_child(body) var category_scroll := ScrollContainer.new() category_scroll.name = "CategoryScroll" @@ -1134,6 +1157,9 @@ func _build_scale_option() -> void: var small_label := Label.new() small_label.text = "small" small_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + small_label.add_theme_font_size_override( + "font_size", SIZE_ENDPOINT_FONT_SIZE + ) small_label.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) @@ -1144,7 +1170,7 @@ func _build_scale_option() -> void: slider.min_value = CharacterCustomizationCatalog.MIN_CHARACTER_SCALE slider.max_value = CharacterCustomizationCatalog.MAX_CHARACTER_SCALE slider.step = CharacterCustomizationCatalog.CHARACTER_SCALE_STEP - slider.custom_minimum_size = Vector2(220.0, 40.0) + slider.custom_minimum_size = Vector2(SIZE_SLIDER_MIN_WIDTH, 40.0) slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL slider.tooltip_text = "character size" slider.value = CharacterCustomizationCatalog.character_scale( @@ -1159,6 +1185,9 @@ func _build_scale_option() -> void: var large_label := Label.new() large_label.text = "large" large_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + large_label.add_theme_font_size_override( + "font_size", SIZE_ENDPOINT_FONT_SIZE + ) large_label.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) @@ -1581,7 +1610,7 @@ func _build_fur_pattern_options() -> void: func _build_fur_color_channels(options: Array) -> void: var color_panel := PanelContainer.new() color_panel.name = "FurColorOptionsPanel" - color_panel.custom_minimum_size.y = 250.0 + color_panel.custom_minimum_size.y = 180.0 color_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL color_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL color_panel.add_theme_stylebox_override( @@ -1595,12 +1624,18 @@ func _build_fur_color_channels(options: Array) -> void: color_margin.add_theme_constant_override("margin_right", 14) color_margin.add_theme_constant_override("margin_bottom", 12) color_panel.add_child(color_margin) + var color_scroll := ScrollContainer.new() + color_scroll.name = "FurColorScroll" + color_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL + color_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + color_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + color_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO + color_margin.add_child(color_scroll) var color_stack := VBoxContainer.new() color_stack.name = "FurColorStack" color_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL - color_stack.size_flags_vertical = Control.SIZE_EXPAND_FILL color_stack.add_theme_constant_override("separation", 10) - color_margin.add_child(color_stack) + color_scroll.add_child(color_stack) var channel_grid := GridContainer.new() channel_grid.name = "FurColorChannelGrid"