Add StageBackgroundView and related tests for plain background functionality

- Implemented StageBackgroundView class for managing sky and plain backgrounds in the stage.
- Added functionality to set background type and color, bind camera, and apply themes.
- Created test_phase19_plain_background.gd to validate plain background behavior and migration.
- Developed test_phase19_starter_chooser_layout.gd to ensure proper layout and functionality of the starter chooser.
- Included visual capture tests for UI elements and interactions.
This commit is contained in:
2026-10-22 16:18:25 -04:00
parent db677dc194
commit 59f1817063
43 changed files with 2403 additions and 160 deletions
+467 -64
View File
@@ -8,6 +8,10 @@ signal refresh_requested()
signal make_artwork_requested()
signal edit_scenery_requested()
signal surface_color_confirmed(color: Color)
signal plain_color_preview_requested(color: Color)
signal plain_color_commit_requested(color: Color)
signal plain_color_preview_cancel_requested()
signal stage_preview_dimming_changed(should_dim: bool)
const COLUMNS := 4
const ROWS := 3
@@ -49,11 +53,14 @@ var _filter_chip_group: ButtonGroup
var _artwork_filter_buttons: Array[Button] = []
var _tag_id: String = "all"
var _current_sky_id: String = ""
var _current_background_kind: StringName = &"sky"
var _current_plain_color: Color = Color("#F5EFE0")
var _sky_card_group: ButtonGroup
var _active_columns: int = COLUMNS
var _page_capacity: int = PAGE_SIZE
var _effective_cell_size: Vector2 = CELL_MIN_SIZE
var _surface_cell_size: Vector2 = CELL_MIN_SIZE
var _surface_row_heading: Label
var _surface_color_history: Array[Color] = []
var _setup_view: VBoxContainer
var _color_picker_view: StageSurfaceColorPickerView
@@ -69,6 +76,18 @@ var _setup_cancel_button: Button
var _setup_draw_button: Button
var _color_back_button: Button
var _color_use_button: Button
var _plain_color_view: VBoxContainer
var _plain_color_preview: StageSurfacePreview
var _plain_color_controls: StageSurfaceColorControls
var _plain_color_helper: Label
var _plain_color_choices_open: bool = false
var _plain_color_original: Color = Color("#F5EFE0")
var _plain_color_draft: Color = Color("#F5EFE0")
var _color_picker_owner: StringName = &"surface"
var _backgrounds_back_button: Button
var _plain_footer_row: HBoxContainer
var _plain_footer_column: VBoxContainer
var _stage_preview_is_dimmed: bool = true
@onready var _title_label: Label = %TitleLabel
@onready var _grid: GridContainer = %GridContainer
@@ -103,6 +122,8 @@ func _ready() -> void:
tab.toggle_mode = true
tab.button_group = _gallery_tab_group
_build_surface_setup_view()
_build_plain_background_view()
_build_surface_row_heading()
_rebuild_artwork_filter_chips()
_prev_button.pressed.connect(_on_prev)
_next_button.pressed.connect(_on_next)
@@ -113,6 +134,7 @@ func _ready() -> void:
_artwork_tab.pressed.connect(func() -> void: _set_gallery_tab("artwork"))
_make_artwork_button.pressed.connect(func() -> void: make_artwork_requested.emit())
_edit_scenery_button.pressed.connect(func() -> void: edit_scenery_requested.emit())
popup_hide.connect(_on_popup_hide)
get_tree().root.size_changed.connect(_on_root_size_changed)
_apply_chooser_visual_style()
@@ -123,15 +145,27 @@ func _on_root_size_changed() -> void:
_rebuild()
func open(p_kind: String, entries: Array[Dictionary], current_sky_id: String = "") -> void:
func open(
p_kind: String,
entries: Array[Dictionary],
current_sky_id: String = "",
current_background_kind: String = "sky",
plain_color: Color = Color("#F5EFE0")
) -> void:
_cancel_plain_picker_preview(false)
kind = p_kind
_setup_kind = &""
_color_picker_open = false
_plain_color_choices_open = false
_setup_view.visible = false
_plain_color_view.visible = false
if _color_picker_view != null:
_color_picker_view.visible = false
_current_sky_id = current_sky_id if kind == "scenery" else ""
_current_background_kind = &"plain" if current_background_kind == "plain" else &"sky"
_current_plain_color = _opaque_color(plain_color)
_title_label.text = "Choose Your Stickman" if kind == "stickman" else ("Choose Scenery" if kind == "scenery" else ("Choose a starter scene" if kind == "starters" else "Choose a Prop"))
_backgrounds_back_button.visible = false
_apply_chooser_visual_style()
_tabs.visible = kind == "scenery"
_filter_chips.visible = false
@@ -165,6 +199,47 @@ func set_surface_color_history(colors: Array[Color]) -> void:
_surface_color_history = colors.duplicate()
if _setup_color_controls != null and is_instance_valid(_setup_color_controls):
_setup_color_controls.set_recent_colors(_surface_color_history)
if _plain_color_controls != null and is_instance_valid(_plain_color_controls):
_plain_color_controls.set_recent_colors(_surface_color_history)
func set_background_state(background_kind: StringName, sky_id: StringName, plain_color: Color) -> void:
_current_background_kind = &"plain" if background_kind == &"plain" else &"sky"
_current_sky_id = String(sky_id)
_current_plain_color = _opaque_color(plain_color)
for entry: Dictionary in _entries:
if String(entry.get("background_kind", "")) == "plain":
entry["plain_color"] = _current_plain_color
if not _plain_color_choices_open and not (_color_picker_open and _color_picker_owner == &"plain"):
_plain_color_draft = _current_plain_color
if _plain_color_preview != null:
_plain_color_preview.configure(&"plain_background", &"", _current_plain_color)
if _plain_color_controls != null:
_plain_color_controls.set_selected_no_signal(_current_plain_color)
if _color_picker_open and _color_picker_owner == &"plain" and _color_picker_view != null:
_color_picker_view.configure_plain_background(_current_plain_color, _sandbox_theme as SandboxTheme)
if kind == "scenery" and _gallery_tab == "backgrounds":
_rebuild()
func open_plain_background_choices(color: Color) -> void:
_current_plain_color = _opaque_color(color)
_plain_color_draft = _current_plain_color
_plain_color_choices_open = true
_color_picker_open = false
_color_picker_owner = &"plain"
_title_label.text = "Choose background color"
_backgrounds_back_button.text = "‹ Backgrounds"
_backgrounds_back_button.visible = true
_plain_color_view.visible = true
_setup_view.visible = false
_color_picker_view.visible = false
_plain_color_preview.configure(&"plain_background", &"", _current_plain_color)
_plain_color_controls.configure(_current_plain_color, _surface_color_history, _preset_surface_colors(), _sandbox_theme as SandboxTheme)
_update_popup_layout(true)
_rebuild()
call_deferred("_update_popup_layout_after_layout")
call_deferred("_focus_plain_color_controls")
func set_thumbnail(entry: Dictionary, tex: Texture2D) -> void:
@@ -198,6 +273,7 @@ func apply_font(ui_font: Font, emoji_font: Font) -> void:
_prev_button, _next_button, _browse_button, _refresh_button, _close_button,
_edit_scenery_button, _make_artwork_button,
_backgrounds_tab, _artwork_tab,
_backgrounds_back_button, _plain_color_helper,
_setup_view.get_node_or_null("SetupHeading") if _setup_view != null else null,
get_meta("setup_helper", null) as Control,
_setup_cancel_button, _setup_draw_button,
@@ -226,8 +302,13 @@ func apply_theme(theme: Variant) -> void:
ground_choices.apply_theme(theme as SandboxTheme)
if _setup_color_controls != null:
_setup_color_controls.configure(_setup_color, _surface_color_history, _preset_surface_colors(), theme as SandboxTheme)
if _plain_color_controls != null:
_plain_color_controls.configure(_current_plain_color, _surface_color_history, _preset_surface_colors(), theme as SandboxTheme)
if _color_picker_view != null:
_color_picker_view.configure(_setup_kind, _setup_finish, _setup_color, theme as SandboxTheme)
if _color_picker_owner == &"plain":
_color_picker_view.configure_plain_background(_plain_color_draft, theme as SandboxTheme)
else:
_color_picker_view.configure(_setup_kind, _setup_finish, _setup_color, theme as SandboxTheme)
_apply_chooser_visual_style()
@@ -305,7 +386,7 @@ func _build_surface_setup_view() -> void:
_color_picker_view = SURFACE_COLOR_PICKER_VIEW.new() as StageSurfaceColorPickerView
_color_picker_view.name = "SurfaceColorPickerView"
_color_picker_view.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_color_picker_view.preview_color_changed.connect(_on_setup_color_preview)
_color_picker_view.preview_color_changed.connect(_on_color_picker_preview)
_color_picker_view.visible = false
_content_stack.add_child(_color_picker_view)
_setup_cancel_button = Button.new()
@@ -349,6 +430,77 @@ func _build_surface_setup_view() -> void:
_apply_font_to(choice)
func _build_plain_background_view() -> void:
_plain_color_view = VBoxContainer.new()
_plain_color_view.name = "PlainBackgroundColorChoices"
_plain_color_view.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_plain_color_view.add_theme_constant_override("separation", _theme_integer("popups.asset_selector.setup_gap", 8, 4, 24))
_plain_color_view.visible = false
_content_stack.add_child(_plain_color_view)
_plain_color_helper = Label.new()
_plain_color_helper.name = "PlainBackgroundHelper"
_plain_color_helper.text = "Tap a color to use it on the Stage."
_plain_color_helper.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_plain_color_helper.add_theme_color_override("font_color", _theme_color("global.muted_text_color", Color("#AAB2C0")))
_plain_color_view.add_child(_plain_color_helper)
var preview_frame := PanelContainer.new()
preview_frame.name = "PlainBackgroundPreviewFrame"
var preview_size: Vector2 = _theme_vector2("popups.asset_selector.surface_preview_size", Vector2(420.0, 132.0))
preview_size.y = minf(preview_size.y, 132.0)
preview_frame.custom_minimum_size.y = preview_size.y + 6.0
preview_frame.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_plain_color_preview = SURFACE_PREVIEW.new() as StageSurfacePreview
_plain_color_preview.name = "PlainBackgroundPreview"
_plain_color_preview.set_rounded_frame(
float(_theme_integer("popups.asset_selector.surface_corner_radius", 14, 4, 32)),
_theme_color("popups.style_defaults.bg_color", Color("#202633FA"))
)
_plain_color_preview.custom_minimum_size = preview_size
_plain_color_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
preview_frame.add_child(_plain_color_preview)
_plain_color_view.add_child(preview_frame)
_plain_color_controls = SURFACE_COLOR_CONTROLS.new() as StageSurfaceColorControls
_plain_color_controls.name = "PlainBackgroundColorControls"
_plain_color_controls.color_selected.connect(_on_plain_color_selected)
_plain_color_controls.custom_color_requested.connect(_open_plain_color_picker)
_plain_color_view.add_child(_plain_color_controls)
_backgrounds_back_button = Button.new()
_backgrounds_back_button.name = "BackgroundsBackButton"
_backgrounds_back_button.text = "‹ Backgrounds"
_backgrounds_back_button.custom_minimum_size = Vector2(148.0, float(_theme_integer("stage_panels.action.min_target_size", 48, 44, 96)))
_backgrounds_back_button.focus_mode = Control.FOCUS_ALL
_backgrounds_back_button.visible = false
_backgrounds_back_button.pressed.connect(_back_to_backgrounds)
_footer.add_child(_backgrounds_back_button)
_plain_footer_row = HBoxContainer.new()
_plain_footer_row.name = "PlainBackgroundFooterRow"
_plain_footer_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_plain_footer_row.alignment = BoxContainer.ALIGNMENT_BEGIN
_plain_footer_row.add_theme_constant_override("separation", _theme_integer("popups.asset_selector.footer_gap", 16, 8, 32))
_plain_footer_row.visible = false
_footer.add_child(_plain_footer_row)
_plain_footer_column = VBoxContainer.new()
_plain_footer_column.name = "PlainBackgroundFooterColumn"
_plain_footer_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_plain_footer_column.add_theme_constant_override("separation", _theme_integer("popups.asset_selector.footer_gap", 16, 8, 32))
_plain_footer_column.visible = false
_footer.add_child(_plain_footer_column)
_apply_font_to(_plain_color_helper)
_apply_font_to(_backgrounds_back_button)
_apply_chooser_visual_style()
func _build_surface_row_heading() -> void:
_surface_row_heading = Label.new()
_surface_row_heading.name = "MoreBackgroundsHeading"
_surface_row_heading.text = "More backgrounds"
_surface_row_heading.visible = false
_surface_row_heading.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.setup_heading_size", 26, 18, 48))
_content_stack.add_child(_surface_row_heading)
_content_stack.move_child(_surface_row_heading, _surface_cards_row.get_index())
_apply_font_to(_surface_row_heading)
func _make_look_cue(kind: StringName, finish: StringName) -> Control:
var preview: StageSurfacePreview = SURFACE_PREVIEW.new() as StageSurfacePreview
var cue_size: Vector2 = _theme_vector2("popups.asset_selector.look_cue_size", Vector2(80.0, 42.0))
@@ -418,6 +570,14 @@ func _on_setup_color_selected(color: Color) -> void:
surface_color_confirmed.emit(_setup_color)
func _on_plain_color_selected(color: Color) -> void:
_current_plain_color = _opaque_color(color)
_plain_color_draft = _current_plain_color
_plain_color_preview.configure(&"plain_background", &"", _current_plain_color)
_plain_color_controls.set_selected_no_signal(_current_plain_color)
plain_color_commit_requested.emit(_current_plain_color)
func _update_setup_preview() -> void:
if _setup_preview == null:
return
@@ -437,6 +597,7 @@ func _open_setup_color_view() -> void:
_setup_original_color = _setup_color
_color_picker_view.configure(_setup_kind, _setup_finish, _setup_original_color, _sandbox_theme as SandboxTheme)
_color_picker_open = true
_color_picker_owner = &"surface"
_title_label.text = "Choose color"
_rebuild()
_update_popup_layout()
@@ -444,12 +605,45 @@ func _open_setup_color_view() -> void:
_color_picker_view.focus_picker()
func _open_plain_color_picker() -> void:
if not _plain_color_choices_open:
return
_plain_color_original = _current_plain_color
_plain_color_draft = _current_plain_color
_color_picker_owner = &"plain"
_color_picker_view.configure_plain_background(_plain_color_draft, _sandbox_theme as SandboxTheme)
_color_back_button.text = "‹ Plain Background"
_plain_color_choices_open = false
_color_picker_open = true
_title_label.text = "Choose background color"
_backgrounds_back_button.visible = false
_update_popup_layout(true)
_rebuild()
call_deferred("_update_popup_layout_after_layout")
_color_picker_view.focus_picker()
func _on_setup_color_preview(color: Color) -> void:
_setup_color = _opaque_color(color)
_update_setup_preview()
func _on_color_picker_preview(color: Color) -> void:
if _color_picker_owner == &"plain":
_plain_color_draft = _opaque_color(color)
plain_color_preview_requested.emit(_plain_color_draft)
return
_on_setup_color_preview(color)
func _use_setup_color() -> void:
if _color_picker_owner == &"plain":
_current_plain_color = _opaque_color(_color_picker_view.get_color())
_plain_color_draft = _current_plain_color
_plain_color_preview.configure(&"plain_background", &"", _current_plain_color)
plain_color_commit_requested.emit(_current_plain_color)
_close_plain_color_picker()
return
_setup_color = _opaque_color(_color_picker_view.get_color())
_setup_color_chosen = true
_update_setup_preview()
@@ -459,6 +653,9 @@ func _use_setup_color() -> void:
func _back_from_setup_color_view() -> void:
if _color_picker_owner == &"plain":
_cancel_plain_picker_preview(true)
return
_setup_color = _setup_original_color
_setup_color_controls.set_selected_no_signal(_setup_color)
_update_setup_preview()
@@ -475,6 +672,74 @@ func _close_setup_color_view() -> void:
call_deferred("_update_popup_layout_after_layout")
func _close_plain_color_picker() -> void:
_color_picker_open = false
_plain_color_choices_open = true
_title_label.text = "Choose background color"
_backgrounds_back_button.text = "‹ Backgrounds"
_backgrounds_back_button.visible = true
_plain_color_view.visible = true
_plain_color_preview.configure(&"plain_background", &"", _current_plain_color)
_plain_color_controls.set_selected_no_signal(_current_plain_color)
_update_popup_layout(true)
_rebuild()
call_deferred("_update_popup_layout_after_layout")
call_deferred("_focus_plain_color_controls")
func _cancel_plain_picker_preview(return_to_choices: bool = true) -> void:
if not _color_picker_open or _color_picker_owner != &"plain":
return
_plain_color_draft = _current_plain_color
_color_picker_open = false
plain_color_preview_cancel_requested.emit()
if _color_picker_view != null:
_color_picker_view.configure_plain_background(_current_plain_color, _sandbox_theme as SandboxTheme)
if return_to_choices and kind == "scenery":
_plain_color_choices_open = true
_title_label.text = "Choose background color"
_backgrounds_back_button.text = "‹ Backgrounds"
_backgrounds_back_button.visible = true
_plain_color_view.visible = true
_plain_color_preview.configure(&"plain_background", &"", _current_plain_color)
_plain_color_controls.set_selected_no_signal(_current_plain_color)
_update_popup_layout(true)
_rebuild()
call_deferred("_update_popup_layout_after_layout")
call_deferred("_focus_plain_color_controls")
func _back_to_backgrounds() -> void:
if _color_picker_open and _color_picker_owner == &"plain":
_cancel_plain_picker_preview(true)
return
_plain_color_choices_open = false
_plain_color_view.visible = false
_backgrounds_back_button.visible = false
_title_label.text = "Choose the scene background"
_update_popup_layout(true)
_rebuild()
call_deferred("_focus_plain_background_card")
func _focus_plain_color_controls() -> void:
if _plain_color_controls != null and _plain_color_controls.visible:
_plain_color_controls.focus_custom_button()
func _focus_plain_background_card() -> void:
for node: Node in _grid.get_children():
var card: AssetCard = node as AssetCard
if card != null and String(card.get_meta("entry_id", "")) == "plain_background":
card.grab_focus()
return
func _on_popup_hide() -> void:
if _color_picker_open and _color_picker_owner == &"plain":
_cancel_plain_picker_preview(true)
func _opaque_color(color: Color) -> Color:
return Color(color.r, color.g, color.b, 1.0)
@@ -600,11 +865,17 @@ func _has_unsorted_artwork() -> bool:
func _set_gallery_tab(tab: String) -> void:
if _color_picker_open and _color_picker_owner == &"plain":
_cancel_plain_picker_preview(false)
_color_picker_open = false
_plain_color_choices_open = false
_setup_kind = &""
_setup_view.visible = false
_plain_color_view.visible = false
_color_picker_view.visible = false
_backgrounds_back_button.visible = false
_gallery_tab = tab
_title_label.text = "Choose the scene background" if tab == "backgrounds" else "Choose Scenery"
_backgrounds_tab.set_pressed_no_signal(tab == "backgrounds")
_artwork_tab.set_pressed_no_signal(tab == "artwork")
_filter_chips.visible = kind == "scenery" and tab == "artwork"
@@ -677,49 +948,70 @@ func _usable_screen_size() -> Vector2:
func _update_popup_layout(keep_visible_position: bool = false) -> void:
var previous_position: Vector2i = position
var in_color_picker: bool = _color_picker_open
var in_plain_choices: bool = _plain_color_choices_open and not in_color_picker
var in_setup: bool = not _setup_kind.is_empty() and not in_color_picker
var scenery_background_gallery: bool = _is_scenery_background_gallery() and not in_setup and not in_color_picker
var scenery_artwork_gallery: bool = _is_scenery_artwork_gallery() and not in_setup and not in_color_picker
var in_subview: bool = in_setup or in_color_picker or in_plain_choices
var scenery_background_gallery: bool = _is_scenery_background_gallery() and not in_subview
var scenery_artwork_gallery: bool = _is_scenery_artwork_gallery() and not in_subview
var filtered: Array[Dictionary] = _filtered_entries()
_tabs.visible = kind == "scenery" and not in_color_picker
_tabs.visible = kind == "scenery" and not in_subview
_filter_chips.visible = scenery_artwork_gallery
_position_footer()
_close_button.visible = not in_setup and not in_color_picker
_setup_cancel_button.visible = in_setup
_setup_draw_button.visible = in_setup
_setup_view.visible = in_setup
_plain_color_view.visible = in_plain_choices
_backgrounds_back_button.visible = in_plain_choices
_color_picker_view.visible = in_color_picker
_surface_cards_row.visible = scenery_background_gallery and _has_surface_entries(filtered)
_surface_row_heading.visible = _surface_cards_row.visible
_color_back_button.visible = in_color_picker
_color_use_button.visible = in_color_picker
_edit_scenery_button.visible = kind == "scenery" and not in_setup and not in_color_picker
_footer_spacer.visible = scenery_artwork_gallery
_empty_label.visible = filtered.is_empty() and not in_setup and not in_color_picker
_grid.visible = not filtered.is_empty() and not in_setup and not in_color_picker
_grid_scroll.visible = not filtered.is_empty() or in_setup or in_color_picker
_footer.alignment = BoxContainer.ALIGNMENT_BEGIN if scenery_artwork_gallery else (BoxContainer.ALIGNMENT_END if in_setup or in_color_picker else BoxContainer.ALIGNMENT_CENTER)
_empty_label.visible = filtered.is_empty() and not in_subview
_grid.visible = not filtered.is_empty() and not in_subview
_grid_scroll.visible = not filtered.is_empty() or in_subview
_footer.alignment = BoxContainer.ALIGNMENT_BEGIN if scenery_artwork_gallery or in_subview else BoxContainer.ALIGNMENT_CENTER
_footer.add_theme_constant_override(
"separation",
_theme_integer("popups.asset_selector.footer_gap", 16, 8, 32) if in_setup or in_color_picker or kind == "scenery" else 8
_theme_integer("popups.asset_selector.footer_gap", 16, 8, 32) if in_subview or kind == "scenery" else 8
)
var available: Vector2 = _usable_screen_size()
var width_ratio: float = 0.98 if in_setup else 0.94
var width_ratio: float = 0.98 if in_subview else (0.96 if kind == "starters" else 0.94)
var max_width: float = maxf(1.0, minf(available.x * width_ratio, available.x - POPUP_SAFE_SCREEN_MARGIN))
var max_height: float = maxf(1.0, minf(available.y * 0.86, available.y - POPUP_SAFE_SCREEN_MARGIN * 2.0))
var preferred_width: float = CELL_MIN_SIZE.x * COLUMNS + GRID_H_SEPARATION * (COLUMNS - 1) + POPUP_SIDE_PADDING
var height_ratio: float = _theme_number("popups.asset_selector.starter_viewport_height_ratio", 0.96, 0.7, 0.98) if kind == "starters" else 0.86
var max_height: float = maxf(1.0, minf(available.y * height_ratio, available.y - POPUP_SAFE_SCREEN_MARGIN * 2.0))
var preferred_width: float = CELL_MIN_SIZE.x * (2 if kind == "starters" else COLUMNS) + GRID_H_SEPARATION * ((2 if kind == "starters" else COLUMNS) - 1) + POPUP_SIDE_PADDING
var preferred_height: float = max_height
if in_color_picker:
if kind == "starters":
preferred_width = _theme_number("popups.asset_selector.starter_dialog_width", 1160.0, 440.0, 2400.0)
if in_color_picker or in_plain_choices:
var color_dialog_size: Vector2 = _theme_vector2("popups.dialog.surface_color_size", Vector2(560.0, 640.0))
preferred_width = color_dialog_size.x
preferred_height = color_dialog_size.y
var popup_width: float = minf(preferred_width, max_width)
_position_footer(popup_width)
var grid_width: float = maxf(1.0, popup_width - POPUP_SIDE_PADDING)
_active_columns = clampi(floori((grid_width + GRID_H_SEPARATION) / (CELL_MIN_SIZE.x + GRID_H_SEPARATION)), 1, COLUMNS)
_page_capacity = maxi(1, filtered.size()) if scenery_background_gallery or scenery_artwork_gallery else _active_columns * ROWS
var minimum_card_width: float = CELL_MIN_SIZE.x
if kind == "starters":
minimum_card_width = _theme_number("popups.asset_selector.starter_card_min_width", 340.0, 220.0, 800.0)
_active_columns = clampi(floori((grid_width + GRID_H_SEPARATION) / (minimum_card_width + GRID_H_SEPARATION)), 1, 2 if kind == "starters" else COLUMNS)
_page_capacity = maxi(1, filtered.size()) if scenery_background_gallery or scenery_artwork_gallery or kind == "starters" else _active_columns * ROWS
_grid.columns = _active_columns
var card_width: float = minf(CELL_MIN_SIZE.x, (grid_width - GRID_H_SEPARATION * (_active_columns - 1)) / float(_active_columns))
var card_height_limit: float = 248.0 if kind == "starters" else CELL_MIN_SIZE.y
var card_height: float = minf(card_height_limit, card_width + (58.0 if kind == "starters" else 30.0))
var available_card_width: float = (grid_width - GRID_H_SEPARATION * (_active_columns - 1)) / float(_active_columns)
var card_width: float = available_card_width if kind == "starters" else minf(CELL_MIN_SIZE.x, available_card_width)
var card_height_limit: float = 248.0 if kind != "starters" else INF
var card_height: float
if kind == "starters":
var preview_horizontal_inset: float = _theme_number("popups.asset_selector.starter_preview_horizontal_inset", 10.0, 0.0, 32.0)
var preview_aspect: float = _theme_number("popups.asset_selector.starter_preview_aspect", 2.0, 1.2, 2.8)
var preview_top_inset: float = _theme_number("popups.asset_selector.starter_preview_top_inset", 6.0, 0.0, 24.0)
var preview_height: float = maxf(1.0, card_width - preview_horizontal_inset * 2.0) / preview_aspect
var text_and_spacing_height: float = _theme_number("popups.asset_selector.starter_card_text_height", 92.0, 72.0, 144.0)
card_height = preview_height + preview_top_inset + text_and_spacing_height
else:
card_height = minf(card_height_limit, card_width + 30.0)
_effective_cell_size = Vector2(maxf(96.0, card_width), maxf(180.0, card_height))
var surface_width: float = minf(_effective_cell_size.x, maxf(96.0, (grid_width - GRID_H_SEPARATION) * 0.5))
var surface_height: float = maxf(180.0, minf(card_height_limit, surface_width + 30.0))
@@ -729,18 +1021,20 @@ func _update_popup_layout(keep_visible_position: bool = false) -> void:
_page = clampi(_page, 0, page_count - 1)
var page_start: int = _page * _page_capacity
var page_items: int = clampi(filtered_count - page_start, 0, _page_capacity)
var grid_item_count: int = _count_sky_entries(filtered) if scenery_background_gallery else page_items
var grid_item_count: int = _count_scene_background_entries(filtered) if scenery_background_gallery else page_items
var grid_rows: int = int(ceil(float(grid_item_count) / float(_active_columns))) if grid_item_count > 0 else 0
var body_height: float
if in_color_picker:
_color_picker_view.set_available_size(Vector2(popup_width, minf(preferred_height, max_height)))
body_height = maxf(220.0, _color_picker_view.get_combined_minimum_size().y)
elif in_plain_choices:
body_height = maxf(220.0, _visible_content_minimum_height(_plain_color_view))
elif in_setup:
body_height = maxf(180.0, _visible_content_minimum_height(_setup_view))
else:
body_height = maxf(120.0, float(grid_rows) * _effective_cell_size.y + float(maxi(0, grid_rows - 1)) * GRID_V_SEPARATION)
if scenery_background_gallery and _has_surface_entries(filtered):
body_height += _surface_cell_size.y + GRID_V_SEPARATION
body_height += _surface_cell_size.y + _surface_row_heading.get_combined_minimum_size().y + GRID_V_SEPARATION * 2.0
if page_items == 0:
body_height = maxf(body_height, _empty_label.get_combined_minimum_size().y)
_tabs.custom_minimum_size.x = grid_width
@@ -798,6 +1092,8 @@ func _update_popup_layout(keep_visible_position: bool = false) -> void:
_color_back_button.custom_minimum_size.x = action_width
_color_use_button.custom_minimum_size.x = action_width
size = Vector2i(maxi(1, roundi(popup_width)), maxi(1, roundi(popup_height)))
_position_footer(float(size.x))
_sync_stage_preview_dimming()
if visible:
if keep_visible_position:
var max_x: int = maxi(0, floori(available.x - float(size.x) - POPUP_SAFE_SCREEN_MARGIN))
@@ -815,7 +1111,7 @@ func _update_popup_layout(keep_visible_position: bool = false) -> void:
func _update_popup_layout_after_layout() -> void:
await get_tree().process_frame
if is_inside_tree():
_update_popup_layout()
_update_popup_layout(visible)
func _rebuild() -> void:
@@ -827,27 +1123,33 @@ func _rebuild() -> void:
_cell_texrects.clear()
var filtered: Array[Dictionary] = _filtered_entries()
var in_color_picker: bool = _color_picker_open
var in_plain_choices: bool = _plain_color_choices_open and not in_color_picker
var in_setup: bool = not _setup_kind.is_empty() and not in_color_picker
var scenery_background_gallery: bool = _is_scenery_background_gallery() and not in_setup and not in_color_picker
var scenery_artwork_gallery: bool = _is_scenery_artwork_gallery() and not in_setup and not in_color_picker
_position_footer()
var in_subview: bool = in_setup or in_color_picker or in_plain_choices
var scenery_background_gallery: bool = _is_scenery_background_gallery() and not in_subview
var scenery_artwork_gallery: bool = _is_scenery_artwork_gallery() and not in_subview
_position_footer(float(size.x))
_sync_stage_preview_dimming()
_close_button.visible = not in_setup and not in_color_picker
_setup_view.visible = in_setup
_plain_color_view.visible = in_plain_choices
_backgrounds_back_button.visible = in_plain_choices
_color_picker_view.visible = in_color_picker
_filter_chips.visible = scenery_artwork_gallery
_surface_cards_row.visible = scenery_background_gallery and _has_surface_entries(filtered)
_surface_row_heading.visible = _surface_cards_row.visible
_setup_cancel_button.visible = in_setup
_setup_draw_button.visible = in_setup
_color_back_button.visible = in_color_picker
_color_use_button.visible = in_color_picker
_edit_scenery_button.visible = kind == "scenery" and not in_setup and not in_color_picker
_edit_scenery_button.visible = kind == "scenery" and not in_subview
_make_artwork_button.visible = scenery_artwork_gallery
_browse_button.visible = (kind == "stickman" or kind == "prop") and not in_setup and not in_color_picker
_refresh_button.visible = (kind == "stickman" or kind == "prop") and not in_setup and not in_color_picker
_footer_spacer.visible = scenery_artwork_gallery
_empty_label.visible = filtered.is_empty() and not in_setup and not in_color_picker
_grid.visible = not filtered.is_empty() and not in_setup and not in_color_picker
_grid_scroll.visible = not filtered.is_empty() or in_setup or in_color_picker
_empty_label.visible = filtered.is_empty() and not in_subview
_grid.visible = not filtered.is_empty() and not in_subview
_grid_scroll.visible = not filtered.is_empty() or in_subview
var total := _page_count()
var single := total <= 1
_prev_button.visible = not single
@@ -856,7 +1158,7 @@ func _rebuild() -> void:
_prev_button.disabled = _page <= 0
_next_button.disabled = (_page + 1) * _page_capacity >= filtered.size()
_page_label.text = "Page %d/%d" % [_page + 1, total]
if in_setup or in_color_picker:
if in_subview:
_prev_button.visible = false
_next_button.visible = false
_page_label.visible = false
@@ -869,7 +1171,7 @@ func _rebuild() -> void:
if scenery_background_gallery:
for entry: Dictionary in filtered:
var background_kind: String = String(entry.get("background_kind", ""))
if background_kind == "sky":
if background_kind == "sky" or background_kind == "plain":
_grid.add_child(_build_cell(entry))
elif background_kind == "wall_setup" or background_kind == "background_ground_setup":
_surface_cards_row.add_child(_build_cell(entry, _surface_cell_size))
@@ -879,15 +1181,70 @@ func _rebuild() -> void:
_grid.add_child(_build_cell(filtered[i]))
func _position_footer() -> void:
func _position_footer(target_width: float = -1.0) -> void:
var parent := _footer.get_parent() as VBoxContainer
if parent == null:
return
var target_index: int = parent.get_child_count() - 1
if _footer.get_index() != target_index:
parent.move_child(_footer, target_index)
if _close_button.get_parent() == _footer and _close_button.get_index() != 0:
_footer.move_child(_close_button, 0)
var in_plain_choices: bool = _plain_color_choices_open and not _color_picker_open
if in_plain_choices:
var available_width: float = target_width if target_width >= 0.0 else float(size.x)
var stack_actions: bool = available_width < 380.0
var action_container: Control = _plain_footer_column if stack_actions else _plain_footer_row
_plain_footer_row.visible = not stack_actions
_plain_footer_column.visible = stack_actions
_move_footer_control(_backgrounds_back_button, action_container, 0)
if stack_actions:
_move_footer_control(_close_button, action_container, 1)
_move_footer_control(_footer_spacer, _footer, 1)
_footer_spacer.visible = false
_backgrounds_back_button.custom_minimum_size.x = 0.0
_close_button.custom_minimum_size.x = 0.0
_backgrounds_back_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_close_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
else:
_move_footer_control(_footer_spacer, action_container, 1)
_move_footer_control(_close_button, action_container, 2)
_footer_spacer.visible = true
_backgrounds_back_button.custom_minimum_size.x = 148.0
_close_button.custom_minimum_size.x = 136.0
_backgrounds_back_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
_close_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
return
_plain_footer_row.visible = false
_plain_footer_column.visible = false
_move_footer_control(_backgrounds_back_button, _footer, 1)
_move_footer_control(_footer_spacer, _footer, 1)
_move_footer_control(_close_button, _footer, 0)
_backgrounds_back_button.custom_minimum_size.x = 148.0
_close_button.custom_minimum_size.x = 136.0
_backgrounds_back_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
_close_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
func _move_footer_control(control: Control, target: Control, index: int) -> void:
if control == null or target == null:
return
if control.get_parent() != target:
var old_parent: Node = control.get_parent()
if old_parent != null:
old_parent.remove_child(control)
target.add_child(control)
var safe_index: int = clampi(index, 0, maxi(0, target.get_child_count() - 1))
if control.get_index() != safe_index:
target.move_child(control, safe_index)
func _sync_stage_preview_dimming() -> void:
var should_dim: bool = not (_plain_color_choices_open and kind == "scenery")
if _color_picker_open and _color_picker_owner == &"plain":
should_dim = false
if should_dim == _stage_preview_is_dimmed:
return
_stage_preview_is_dimmed = should_dim
stage_preview_dimming_changed.emit(should_dim)
func _is_scenery_background_gallery() -> bool:
@@ -898,10 +1255,10 @@ func _is_scenery_artwork_gallery() -> bool:
return kind == "scenery" and _gallery_tab == "artwork"
func _count_sky_entries(entries: Array[Dictionary]) -> int:
func _count_scene_background_entries(entries: Array[Dictionary]) -> int:
var count: int = 0
for entry: Dictionary in entries:
if String(entry.get("background_kind", "")) == "sky":
if String(entry.get("background_kind", "")) in ["sky", "plain"]:
count += 1
return count
@@ -930,8 +1287,8 @@ func _visible_content_minimum_height(container: VBoxContainer) -> float:
func _build_cell(entry: Dictionary, requested_size: Vector2 = Vector2.ZERO) -> Control:
var key := _entry_key(entry)
var cell: Variant = ASSET_CARD.new()
# Give scenery cards rounded resting and interaction states. The selected sky
# state is applied separately below, so opening the gallery stays neutral.
# Scene cards have clear rounded resting and interaction states. Selected
# background state is applied separately below.
var categories: Array = entry.get("tags", []) as Array
var category: String = ", ".join(PackedStringArray(categories)) if not categories.is_empty() else String(entry.get("category", entry.get("material_label", "")))
var card_size: Vector2 = _effective_cell_size if requested_size == Vector2.ZERO else requested_size
@@ -940,56 +1297,88 @@ func _build_cell(entry: Dictionary, requested_size: Vector2 = Vector2.ZERO) -> C
var thumb_edge: float = minf(_thumbnail_size.x, minf(card_size.x - 20.0, thumb_height_budget))
var thumb_size := Vector2.ONE * maxf(72.0, thumb_edge)
if kind == "starters":
thumb_size = Vector2.ONE * minf(128.0, maxf(72.0, thumb_edge))
var preview_horizontal_inset: float = _theme_number("popups.asset_selector.starter_preview_horizontal_inset", 10.0, 0.0, 32.0)
var preview_aspect: float = _theme_number("popups.asset_selector.starter_preview_aspect", 2.0, 1.2, 2.8)
var preview_width: float = maxf(72.0, card_size.x - preview_horizontal_inset * 2.0)
thumb_size = Vector2(preview_width, preview_width / preview_aspect)
var description: String = "" if is_background_ground_setup else String(entry.get("description", ""))
var card_payload: Dictionary = {"placement_id": _entry_key(entry), "label": String(entry.get("name", entry.get("id", ""))), "icon": String(entry.get("icon", "")), "category": category, "description": description, "thumbnail_size": thumb_size}
if kind == "starters":
card_payload["label_font_size"] = _theme_integer("popups.asset_selector.starter_label_size", 22, 18, 32)
card_payload["label_min_height"] = _theme_integer("popups.asset_selector.starter_label_height", 34, 28, 52)
card_payload["description_font_size"] = _theme_integer("popups.asset_selector.starter_description_size", 20, 16, 30)
card_payload["description_min_height"] = _theme_integer("popups.asset_selector.starter_description_height", 48, 36, 80)
card_payload["preview_corner_radius"] = _theme_integer("popups.asset_selector.starter_preview_corner_radius", 12, 6, 24)
card_payload["preview_horizontal_inset"] = roundi(_theme_number("popups.asset_selector.starter_preview_horizontal_inset", 10.0, 0.0, 32.0))
card_payload["preview_top_inset"] = _theme_integer("popups.asset_selector.starter_preview_top_inset", 6, 0, 24)
if entry.has("background_kind"):
card_payload["background_kind"] = entry["background_kind"]
card_payload["finish_id"] = entry.get("finish_id", "")
card_payload["id"] = entry.get("id", "")
var is_sky_choice: bool = String(entry.get("background_kind", "")) == "sky"
var is_current_sky: bool = is_sky_choice and String(entry.get("id", "")) == _current_sky_id
if is_current_sky:
card_payload["status"] = "Current Sky"
var background_kind: String = String(entry.get("background_kind", ""))
var is_scene_background_choice: bool = background_kind == "sky" or background_kind == "plain"
var is_current_background: bool = background_kind == String(_current_background_kind) and (
background_kind == "plain" or String(entry.get("id", "")) == _current_sky_id
)
if is_current_background:
card_payload["status"] = "Current background"
card_payload["status_color"] = _theme_color("mode_colors.edit", Color("#22C6FF"))
cell.configure(card_payload, _thumbnails.get(key, null), null)
if kind == "scenery":
if kind == "scenery" or kind == "starters":
var card_button: Button = cell as Button
var card_border_width: int = _theme_integer("popups.asset_selector.button_border_width", 2, 1, 5)
var card_corner_radius: int = _theme_integer("popups.asset_selector.corner_radius", 16, 8, 32)
var card_neutral_surface: Color = _theme_color("stage_panels.choice.neutral_surface", Color("#171B24"))
var card_neutral_border: Color = _theme_color("stage_panels.choice.border_color", Color("#FFFFFF33"))
var card_hover_surface: Color = _theme_color("stage_panels.choice.hover_surface", Color("#303A4B"))
var card_selected_surface: Color = _theme_color("stage_panels.choice.selected_surface", Color("#12394A"))
var card_hover_border_width: int = card_border_width + 1
card_button.add_theme_stylebox_override("normal", _make_rounded_style(
_theme_color("stage_panels.choice.neutral_surface", Color("#171B24")),
_theme_color("stage_panels.choice.border_color", Color("#FFFFFF33")),
card_neutral_surface,
card_neutral_border,
card_border_width,
_theme_integer("popups.asset_selector.corner_radius", 16, 8, 32)
card_corner_radius
))
card_button.add_theme_stylebox_override("hover", _make_rounded_style(
_theme_color("stage_panels.choice.hover_surface", Color("#303A4B")),
card_hover_surface,
_theme_color("mode_colors.edit", Color("#22C6FF")),
card_border_width + 1,
_theme_integer("popups.asset_selector.corner_radius", 16, 8, 32)
card_hover_border_width,
card_corner_radius
))
card_button.add_theme_stylebox_override("pressed", _make_rounded_style(
_theme_color("stage_panels.choice.selected_surface", Color("#12394A")),
card_selected_surface,
_theme_color("mode_colors.edit", Color("#22C6FF")),
card_border_width + 1,
_theme_integer("popups.asset_selector.corner_radius", 16, 8, 32)
card_hover_border_width,
card_corner_radius
))
card_button.add_theme_stylebox_override("focus", _make_rounded_style(
if kind == "starters":
card_button.add_theme_stylebox_override("hover_pressed", _make_rounded_style(
card_selected_surface.lightened(0.05),
_theme_color("mode_colors.edit", Color("#22C6FF")),
card_hover_border_width,
card_corner_radius
))
var focus_color: Color = _theme_color("stage_panels.type.text_color", Color("#F5F7FA")) if kind == "starters" else _theme_color("mode_colors.edit", Color("#22C6FF"))
var focus_style: StyleBoxFlat = _make_rounded_style(
Color(0, 0, 0, 0),
_theme_color("mode_colors.edit", Color("#22C6FF")),
card_border_width + 1,
_theme_integer("popups.asset_selector.corner_radius", 16, 8, 32)
))
focus_color,
card_hover_border_width,
card_corner_radius
)
if kind == "starters":
focus_style.set_expand_margin_all(float(card_border_width))
card_button.add_theme_stylebox_override("focus", focus_style)
cell.custom_minimum_size = card_size
cell.focus_mode = Control.FOCUS_ALL
cell.set_meta("entry_id", String(entry.get("id", "")))
cell.activated.connect(func(_payload: Dictionary) -> void: _on_cell_pressed(entry))
if is_sky_choice:
if is_scene_background_choice:
cell.button_group = _sky_card_group
cell.set_selected(is_current_sky, _theme_color("stage_panels.choice.selected_surface", Color("#12394A")), _theme_color("mode_colors.edit", Color("#22C6FF")))
cell.set_selected(is_current_background, _theme_color("stage_panels.choice.selected_surface", Color("#12394A")), _theme_color("mode_colors.edit", Color("#22C6FF")))
var tex: Texture2D = _thumbnails.get(key, null)
if kind == "starters":
var preview: Control = STARTER_CARD_PREVIEW.new() as Control
preview.call("configure", entry)
preview.call("configure", entry, thumb_size)
cell.set_preview_control(preview)
elif entry.has("background_kind"):
var preview: StageBackgroundCardPreview = BACKGROUND_CARD_PREVIEW.new() as StageBackgroundCardPreview
@@ -1012,6 +1401,10 @@ func _theme_integer(path: String, fallback: int, minimum: int, maximum: int) ->
return fallback if _sandbox_theme == null else _sandbox_theme.integer(path, fallback, minimum, maximum)
func _theme_number(path: String, fallback: float, minimum: float, maximum: float) -> float:
return fallback if _sandbox_theme == null else _sandbox_theme.number(path, fallback, minimum, maximum)
func _theme_vector2(path: String, fallback: Vector2) -> Vector2:
if _sandbox_theme == null:
return fallback
@@ -1054,7 +1447,8 @@ func _apply_chooser_visual_style() -> void:
frame_style.set_corner_radius_all(_theme_integer("popups.asset_selector.surface_corner_radius", 14, 4, 32))
preview_frame.add_theme_stylebox_override("panel", frame_style)
if _title_label != null:
_title_label.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.setup_title_size", 30, 22, 48) if kind == "scenery" else 24)
var title_size: int = _theme_integer("popups.asset_selector.setup_title_size", 30, 22, 48) if kind == "scenery" else (_theme_integer("popups.asset_selector.starter_title_size", 30, 22, 48) if kind == "starters" else 24)
_title_label.add_theme_font_size_override("font_size", title_size)
_title_label.add_theme_color_override("font_color", text_color)
_apply_emphasis_font(_title_label)
for tab: Button in [_backgrounds_tab, _artwork_tab]:
@@ -1096,6 +1490,15 @@ func _apply_chooser_visual_style() -> void:
heading.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.setup_heading_size", 26, 18, 48))
heading.add_theme_color_override("font_color", text_color)
_apply_emphasis_font(heading)
if _surface_row_heading != null:
_surface_row_heading.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.setup_heading_size", 26, 18, 48))
_surface_row_heading.add_theme_color_override("font_color", text_color)
_apply_emphasis_font(_surface_row_heading)
if _plain_color_helper != null:
_plain_color_helper.add_theme_font_size_override("font_size", _theme_integer("stage_panels.type.body_size", 20, 14, 32))
_plain_color_helper.add_theme_color_override("font_color", _theme_color("stage_panels.type.muted_text_color", Color("#AAB2C0")))
_apply_font_to(_plain_color_helper)
_apply_rounded_button(_backgrounds_back_button, false, accent, text_color, neutral, hover, border, radius)
_apply_rounded_button(_setup_cancel_button, false, accent, text_color, neutral, hover, border, radius)
_apply_rounded_button(_setup_draw_button, true, accent, text_color, neutral, hover, border, radius)
_apply_emphasis_font(_setup_draw_button, true)