Files
stickman/scripts/shared/ui/asset_selector.gd
ryan ddb298f631 Add visual harmonization tests for editor UI components and ground finish chooser
- Implemented `test_editor_visual_harmonization.gd` to validate the Stickman editor and prop editor UI layouts, ensuring proper functionality and visual consistency.
- Created `test_editor_visual_theme.gd` to verify the loading and application of visual themes in the editor, including semantic overrides and default values.
- Introduced `test_phase19_ground_finish_chooser.gd` to test the ground finish chooser's functionality, including layout responsiveness, selection behavior, and interaction with the stage.
- Added new scripts for ground finish preview and chooser UI components to enhance the editor's visual representation of ground finishes.
2026-10-23 15:20:12 -04:00

1664 lines
77 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name AssetSelector
extends PopupPanel
signal item_selected(entry: Dictionary)
signal cancelled()
signal browse_requested()
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
const PAGE_SIZE := COLUMNS * ROWS
const GRID_H_SEPARATION: float = 8.0
const GRID_V_SEPARATION: float = 8.0
const POPUP_SIDE_PADDING: float = 40.0
const POPUP_VERTICAL_PADDING: float = 32.0
const POPUP_SAFE_SCREEN_MARGIN: float = 24.0
const ARTWORK_FILTER_IDS: Array[String] = ["all", "nature", "buildings", "walls", "roads", "objects"]
const ARTWORK_FILTER_LABELS: Array[String] = ["All artwork", "Nature", "Buildings", "Walls", "Roads & paths", "Objects"]
const CELL_MIN_SIZE := Vector2(190.0, 220.0)
const THUMB_SIZE := Vector2(156.0, 156.0)
const ASSET_CARD := preload("res://scripts/shared/ui/asset_card.gd")
const BACKGROUND_CARD_PREVIEW := preload("res://scripts/stage/scenery/stage_background_card_preview.gd")
const STARTER_CARD_PREVIEW := preload("res://scripts/stage/scenery/stage_starter_card_preview.gd")
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
const SURFACE_PREVIEW := preload("res://scripts/stage/scenery/stage_surface_preview.gd")
const SURFACE_COLOR_CONTROLS := preload("res://scripts/shared/stage_ui/stage_surface_color_controls.gd")
const SURFACE_COLOR_PICKER_VIEW := preload("res://scripts/shared/stage_ui/stage_surface_color_picker_view.gd")
const STAGE_PANEL_CHOICE_GROUP := preload("res://scripts/shared/stage_ui/stage_panel_choice_group.gd")
var kind: String = ""
var _entries: Array[Dictionary] = []
var _page: int = 0
var _thumbnails: Dictionary = {}
var _cell_texrects: Dictionary = {}
var _placeholder_tex: ImageTexture = null
var _ui_font: Font = null
var _emoji_font: Font = null
var _strong_ui_font: Font = null
var _strong_action_font: Font = null
var _sandbox_theme: Variant = null
var _gallery_tab: String = "backgrounds"
var _gallery_tab_group: ButtonGroup
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
var _setup_kind: StringName = &""
var _setup_finish: StringName = &""
var _setup_color: Color = Color.WHITE
var _setup_original_color: Color = Color.WHITE
var _setup_color_chosen: bool = false
var _color_picker_open: bool = false
var _setup_preview: StageSurfacePreview
var _setup_color_controls: StageSurfaceColorControls
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
@onready var _content_stack: VBoxContainer = %ContentStack
@onready var _grid_scroll: ScrollContainer = %GridScroll
@onready var _surface_cards_row: HBoxContainer = %SurfaceCardsRow
@onready var _empty_label: Label = %EmptyLabel
@onready var _page_label: Label = %PageLabel
@onready var _prev_button: Button = %PrevButton
@onready var _next_button: Button = %NextButton
@onready var _browse_button: Button = %BrowseButton
@onready var _refresh_button: Button = %RefreshButton
@onready var _close_button: Button = %CloseButton
@onready var _footer_spacer: Control = %FooterSpacer
@onready var _tabs: HFlowContainer = %GalleryTabs
@onready var _backgrounds_tab: Button = %BackgroundsTab
@onready var _artwork_tab: Button = %ArtworkTab
@onready var _filter_chips: HFlowContainer = %ArtworkFilters
@onready var _make_artwork_button: Button = %MakeArtworkButton
@onready var _edit_scenery_button: Button = %EditSceneryButton
@onready var _title_bar: Control = $MarginContainer/VBoxContainer/TitleBar
@onready var _footer: Control = $MarginContainer/VBoxContainer/Footer
func _ready() -> void:
exclusive = true
_gallery_tab_group = ButtonGroup.new()
_gallery_tab_group.allow_unpress = false
_filter_chip_group = ButtonGroup.new()
_filter_chip_group.allow_unpress = false
for tab: Button in [_backgrounds_tab, _artwork_tab]:
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)
_browse_button.pressed.connect(func() -> void: browse_requested.emit())
_refresh_button.pressed.connect(func() -> void: refresh_requested.emit())
_close_button.pressed.connect(func() -> void: cancelled.emit())
_backgrounds_tab.pressed.connect(func() -> void: _set_gallery_tab("backgrounds"))
_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()
func _on_root_size_changed() -> void:
if visible:
_update_popup_layout()
_rebuild()
func open(
p_kind: String,
entries: Array[Dictionary],
current_sky_id: String = "",
current_background_kind: String = "sky",
plain_color: Color = Color("#F5EFE0"),
browse_state: Dictionary = {}
) -> void:
var restore_scenery_browse_state: bool = p_kind == "scenery" and not browse_state.is_empty()
var restored_gallery_tab: String = String(browse_state.get("gallery_tab", "backgrounds")) if restore_scenery_browse_state else "backgrounds"
var restored_filter_id: String = String(browse_state.get("tag_id", "all")) if restore_scenery_browse_state else "all"
var restored_scroll: int = int(browse_state.get("scroll_vertical", 0)) if restore_scenery_browse_state else 0
_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 a 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
_make_artwork_button.visible = false
_edit_scenery_button.visible = false
_browse_button.visible = false
_refresh_button.visible = false
_empty_label.text = "No scenery found. Choose Make artwork to draw a new piece." if kind == "scenery" else ("No props in the library yet." if kind == "prop" else ("No starter scenes are available." if kind == "starters" else "No stickmen in the library yet."))
_gallery_tab = restored_gallery_tab
_tag_id = restored_filter_id
_backgrounds_tab.set_pressed_no_signal(_gallery_tab == "backgrounds")
_artwork_tab.set_pressed_no_signal(_gallery_tab == "artwork")
set_entries(entries)
if kind == "scenery":
_set_gallery_tab(_gallery_tab)
popup_centered(size)
call_deferred("_settle_popup_after_open")
if restore_scenery_browse_state:
call_deferred("_restore_browse_scroll_after_open", restored_scroll, restored_gallery_tab)
func capture_browse_state() -> Dictionary:
if kind != "scenery":
return {}
return {
"gallery_tab": _gallery_tab,
"tag_id": _tag_id,
"scroll_vertical": _grid_scroll.scroll_vertical if _grid_scroll != null else 0,
}
func _restore_browse_scroll_after_open(scroll_vertical: int, gallery_tab: String) -> void:
await get_tree().process_frame
if not is_inside_tree() or not visible or kind != "scenery" or _gallery_tab != gallery_tab:
return
_grid_scroll.scroll_vertical = maxi(0, scroll_vertical)
func _settle_popup_after_open() -> void:
await get_tree().process_frame
await get_tree().process_frame
if is_inside_tree() and visible:
# PopupPanel can resize after the first native layout pass. Reapply the
# content-sized width once child minimum sizes and DPI scaling have settled.
var size_before_settle: Vector2i = size
_update_popup_layout(true)
if size != size_before_settle:
popup_centered(size)
func set_entries(entries: Array[Dictionary]) -> void:
_entries = entries
if _grid_scroll != null:
_grid_scroll.scroll_vertical = 0
if kind == "scenery":
_rebuild_artwork_filter_chips()
_thumbnails.clear()
_cell_texrects.clear()
_page = 0
_update_popup_layout()
_rebuild()
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:
var key := _entry_key(entry)
_thumbnails[key] = tex
if _cell_texrects.has(key):
(_cell_texrects[key] as TextureRect).texture = tex
func set_current_sky(sky_id: StringName) -> void:
var next_id: String = String(sky_id)
if kind != "scenery" or _current_sky_id == next_id:
return
_current_sky_id = next_id
if _gallery_tab == "backgrounds":
_rebuild()
func close() -> void:
hide()
func apply_font(ui_font: Font, emoji_font: Font) -> void:
_ui_font = ui_font
_emoji_font = emoji_font
var base_font: Font = ui_font if ui_font != null else emoji_font
_strong_ui_font = _make_strong_font(base_font, 0.3)
_strong_action_font = _make_strong_font(base_font, 0.65)
var controls: Array = [
_title_label, _empty_label, _page_label,
_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,
_color_back_button, _color_use_button,
]
for c: Control in controls:
_apply_font_to(c)
_apply_chooser_visual_style()
func apply_theme(theme: Variant) -> void:
_sandbox_theme = theme
if theme != null:
self.theme = theme.control_theme()
add_theme_stylebox_override("panel", theme.stylebox("popups.asset_selector"))
var cell_size: Variant = theme.value("popups.asset_selector.cell_size", CELL_MIN_SIZE)
if cell_size is Array and (cell_size as Array).size() == 2:
_cell_min_size = Vector2(float(cell_size[0]), float(cell_size[1]))
var thumbnail_size: Variant = theme.value("popups.asset_selector.thumbnail_size", THUMB_SIZE)
if thumbnail_size is Array and (thumbnail_size as Array).size() == 2:
_thumbnail_size = Vector2(float(thumbnail_size[0]), float(thumbnail_size[1]))
var wall_choices: StagePanelChoiceGroup = get_meta("wall_look_group", null) as StagePanelChoiceGroup
var ground_choices: StagePanelChoiceGroup = get_meta("ground_look_group", null) as StagePanelChoiceGroup
if wall_choices != null:
wall_choices.apply_theme(theme as SandboxTheme)
if ground_choices != null:
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:
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()
func _build_surface_setup_view() -> void:
_setup_view = VBoxContainer.new()
_setup_view.name = "SurfaceSetupView"
_setup_view.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_setup_view.add_theme_constant_override("separation", _theme_integer("popups.asset_selector.setup_gap", 8, 4, 24))
_setup_view.visible = false
_content_stack.add_child(_setup_view)
var heading := Label.new()
heading.name = "SetupHeading"
heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL
heading.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
heading.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.setup_heading_size", 26, 18, 48))
_setup_view.add_child(heading)
var wall_choices: StagePanelChoiceGroup = STAGE_PANEL_CHOICE_GROUP.new() as StagePanelChoiceGroup
wall_choices.name = "WallLookChoices"
wall_choices.configure_layout(
false,
float(_theme_integer("popups.asset_selector.setup_choice_height", 68, 48, 104)),
_theme_integer("popups.asset_selector.setup_choice_font_size", 22, 16, 40),
_theme_integer("popups.asset_selector.setup_choice_corner_radius", 22, 8, 32),
_theme_integer("popups.asset_selector.setup_choice_border_width", 2, 1, 5)
)
wall_choices.add_choice(&"finish_wall_paint", "Paint", _make_look_cue(&"wall", &"finish_wall_paint"))
wall_choices.add_choice(&"finish_wall_brick", "Brick", _make_look_cue(&"wall", &"finish_wall_brick"))
wall_choices.add_choice(&"finish_wall_wood", "Wood", _make_look_cue(&"wall", &"finish_wall_wood"))
wall_choices.choice_selected.connect(_on_setup_look_choice)
wall_choices.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_setup_view.add_child(wall_choices)
var ground_choices: StagePanelChoiceGroup = STAGE_PANEL_CHOICE_GROUP.new() as StagePanelChoiceGroup
ground_choices.name = "BackgroundGroundLookChoices"
ground_choices.configure_layout(
false,
float(_theme_integer("popups.asset_selector.setup_choice_height", 68, 48, 104)),
_theme_integer("popups.asset_selector.setup_choice_font_size", 22, 16, 40),
_theme_integer("popups.asset_selector.setup_choice_corner_radius", 22, 8, 32),
_theme_integer("popups.asset_selector.setup_choice_border_width", 2, 1, 5)
)
ground_choices.add_choice(&"finish_background_grass", "Grass", _make_look_cue(&"background_ground", &"finish_background_grass"))
ground_choices.add_choice(&"finish_background_concrete", "Concrete", _make_look_cue(&"background_ground", &"finish_background_concrete"))
ground_choices.add_choice(&"finish_background_carpet", "Carpet", _make_look_cue(&"background_ground", &"finish_background_carpet"))
ground_choices.choice_selected.connect(_on_setup_look_choice)
ground_choices.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_setup_view.add_child(ground_choices)
set_meta("wall_look_group", wall_choices)
set_meta("ground_look_group", ground_choices)
var preview_frame := PanelContainer.new()
preview_frame.name = "SurfacePreviewFrame"
var preview_size: Vector2 = _theme_vector2("popups.asset_selector.surface_preview_size", Vector2(420.0, 132.0))
preview_size.y = minf(preview_size.y, 96.0)
preview_frame.custom_minimum_size.y = preview_size.y + 10.0
preview_frame.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_setup_preview = SURFACE_PREVIEW.new() as StageSurfacePreview
_setup_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"))
)
_setup_preview.custom_minimum_size = preview_size
_setup_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
preview_frame.add_child(_setup_preview)
_setup_view.add_child(preview_frame)
_setup_color_controls = SURFACE_COLOR_CONTROLS.new() as StageSurfaceColorControls
_setup_color_controls.name = "SurfaceColorControls"
_setup_color_controls.set_compact_mode(true)
_setup_color_controls.color_selected.connect(_on_setup_color_selected)
_setup_color_controls.custom_color_requested.connect(_open_setup_color_view)
_setup_view.add_child(_setup_color_controls)
var helper := Label.new()
helper.name = "SurfaceRoleHelper"
helper.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
helper.add_theme_color_override("font_color", _theme_color("global.muted_text_color", Color("#AAB2C0")))
_setup_view.add_child(helper)
_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_color_picker_preview)
_color_picker_view.visible = false
_content_stack.add_child(_color_picker_view)
_setup_cancel_button = Button.new()
_setup_cancel_button.name = "CancelSurfaceSetupButton"
_setup_cancel_button.text = "Cancel"
_setup_cancel_button.custom_minimum_size = Vector2(116.0, float(_theme_integer("stage_panels.action.min_target_size", 48, 44, 96)))
_setup_cancel_button.focus_mode = Control.FOCUS_ALL
_setup_cancel_button.pressed.connect(_cancel_surface_setup)
_footer.add_child(_setup_cancel_button)
_setup_draw_button = Button.new()
_setup_draw_button.name = "DrawSurfaceButton"
_setup_draw_button.custom_minimum_size = Vector2(244.0, float(_theme_integer("popups.asset_selector.tab_height", 60, 48, 96)))
_setup_draw_button.focus_mode = Control.FOCUS_ALL
_setup_draw_button.pressed.connect(_draw_selected_surface)
_footer.add_child(_setup_draw_button)
_color_back_button = Button.new()
_color_back_button.name = "BackToSurfaceButton"
_color_back_button.text = "Back to wall panel"
_color_back_button.custom_minimum_size = Vector2(200.0, float(_theme_integer("stage_panels.action.min_target_size", 48, 44, 96)))
_color_back_button.focus_mode = Control.FOCUS_ALL
_color_back_button.pressed.connect(_back_from_setup_color_view)
_footer.add_child(_color_back_button)
_color_use_button = Button.new()
_color_use_button.name = "UseColorButton"
_color_use_button.text = "Use color"
_color_use_button.custom_minimum_size = Vector2(160.0, float(_theme_integer("stage_panels.choice.minimum_height", 56, 44, 112)))
_color_use_button.focus_mode = Control.FOCUS_ALL
_color_use_button.pressed.connect(_use_setup_color)
_footer.add_child(_color_use_button)
set_meta("setup_heading", heading)
set_meta("setup_helper", helper)
_apply_font_to(heading)
_apply_font_to(helper)
_apply_font_to(_setup_cancel_button)
_apply_font_to(_setup_draw_button)
_apply_font_to(_color_back_button)
_apply_font_to(_color_use_button)
_apply_chooser_visual_style()
for choice: StagePanelChoiceGroup in [wall_choices, ground_choices]:
choice.apply_theme(_sandbox_theme as SandboxTheme)
_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))
cue_size.x = minf(cue_size.x, 56.0)
cue_size.y = minf(cue_size.y, 36.0)
preview.custom_minimum_size = cue_size
preview.set_rounded_frame(
float(_theme_integer("popups.asset_selector.look_corner_radius", 8, 2, 20)),
_theme_color("popups.style_defaults.bg_color", Color("#202633FA"))
)
preview.configure(kind, finish, SURFACE_RENDERER.default_tint(finish))
return preview
func _open_surface_setup(kind: StringName) -> void:
_setup_kind = kind
_setup_color_chosen = false
_setup_finish = &"finish_wall_paint" if kind == &"wall" else &"finish_background_grass"
_setup_color = SURFACE_RENDERER.default_tint(_setup_finish)
_setup_original_color = _setup_color
var wall_choices: StagePanelChoiceGroup = get_meta("wall_look_group") as StagePanelChoiceGroup
var ground_choices: StagePanelChoiceGroup = get_meta("ground_look_group") as StagePanelChoiceGroup
wall_choices.visible = kind == &"wall"
ground_choices.visible = kind == &"background_ground"
wall_choices.select_no_signal(_setup_finish)
ground_choices.select_no_signal(_setup_finish)
(get_meta("setup_heading") as Label).text = "Wall panel" if kind == &"wall" else "Background ground"
var setup_helper: Label = get_meta("setup_helper") as Label
setup_helper.text = "Looks like a wall · Stickmen can pass through." if kind == &"wall" else ""
setup_helper.visible = kind == &"wall"
_setup_draw_button.text = "Draw wall panel" if kind == &"wall" else "Draw background ground"
_color_back_button.text = "Back to wall panel" if kind == &"wall" else "Back to background ground"
_setup_color_controls.visible = _supports_setup_color()
_setup_color_controls.configure(_setup_color, _surface_color_history, _preset_surface_colors(), _sandbox_theme as SandboxTheme)
_update_setup_preview()
_page = 0
_rebuild()
# The setup controls are first measured before the containers have recalculated
# their minimum sizes. Re-measure after the next layout pass so the popup can
# shrink to the setup content instead of keeping the gallery height.
_update_popup_layout()
call_deferred("_update_popup_layout_after_layout")
_setup_draw_button.grab_focus()
func _on_setup_look_choice(id: StringName) -> void:
var supported: bool = SURFACE_RENDERER.is_wall_finish(id) if _setup_kind == &"wall" else SURFACE_RENDERER.is_background_finish(id)
if not supported:
return
_setup_finish = id
if not _setup_color_chosen:
_setup_color = SURFACE_RENDERER.default_tint(_setup_finish)
_setup_color_controls.visible = _supports_setup_color()
_setup_color_controls.set_selected_no_signal(_setup_color)
_update_setup_preview()
_update_popup_layout()
func _supports_setup_color() -> bool:
return _setup_kind == &"wall" or _setup_finish == &"finish_background_carpet"
func _on_setup_color_selected(color: Color) -> void:
_setup_color = _opaque_color(color)
_setup_color_chosen = true
_update_setup_preview()
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
_setup_preview.configure(_setup_kind, _setup_finish, _setup_color)
_setup_color_controls.set_selected_no_signal(_setup_color)
func _preset_surface_colors() -> Array[Color]:
if _sandbox_theme == null:
return [Color.WHITE, Color.BLACK, Color("#4C9AFF"), Color("#29A36A"), Color("#FFD84D"), Color("#D94F70")]
return (_sandbox_theme as SandboxTheme).swatches()
func _open_setup_color_view() -> void:
if _setup_kind.is_empty() or not _supports_setup_color():
return
_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()
call_deferred("_update_popup_layout_after_layout")
_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()
surface_color_confirmed.emit(_setup_color)
_close_setup_color_view()
_setup_color_controls.focus_custom_button()
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()
_close_setup_color_view()
_setup_color_controls.focus_custom_button()
func _close_setup_color_view() -> void:
_color_picker_open = false
_title_label.text = "Choose Scenery"
_tabs.visible = kind == "scenery"
_rebuild()
_update_popup_layout()
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)
func _draw_selected_surface() -> void:
if _setup_kind.is_empty():
return
var background_kind: String = "wall" if _setup_kind == &"wall" else "background_ground"
item_selected.emit({"background_kind": background_kind, "finish_id": String(_setup_finish), "color": _setup_color})
func _cancel_surface_setup() -> void:
_color_picker_open = false
_color_picker_view.visible = false
_setup_kind = &""
_setup_view.visible = false
cancelled.emit()
var _cell_min_size: Vector2 = CELL_MIN_SIZE
var _thumbnail_size: Vector2 = THUMB_SIZE
func _unhandled_input(event: InputEvent) -> void:
if visible and event is InputEventKey:
var key := event as InputEventKey
if key.pressed and not key.echo and key.keycode == KEY_ESCAPE:
get_viewport().set_input_as_handled()
if _color_picker_open:
_back_from_setup_color_view()
else:
cancelled.emit()
static func page_bounds(total: int, page: int, page_size: int = PAGE_SIZE) -> Dictionary:
var page_count := maxi(1, int(ceil(float(total) / float(page_size))))
var start := page * page_size
if start >= total:
start = 0
var end := mini(start + page_size, total)
return { "start": start, "end": end, "total": total, "page_count": page_count }
func _entry_key(entry: Dictionary) -> String:
if kind == "stickman":
return String(entry.get("path", ""))
return String(entry.get("id", ""))
func _rebuild_artwork_filter_chips() -> void:
if _filter_chips == null:
return
for child: Node in _filter_chips.get_children():
_filter_chips.remove_child(child)
child.queue_free()
_artwork_filter_buttons.clear()
_filter_chip_group = ButtonGroup.new()
_filter_chip_group.allow_unpress = false
var filter_ids: Array[String] = ARTWORK_FILTER_IDS.duplicate()
var filter_labels: Array[String] = ARTWORK_FILTER_LABELS.duplicate()
if _has_unsorted_artwork():
filter_ids.append("unsorted")
filter_labels.append("Unsorted")
if not filter_ids.has(_tag_id):
_tag_id = "all"
for index: int in range(filter_ids.size()):
var filter_id: String = filter_ids[index]
var chip: Button = Button.new()
chip.name = "ArtworkFilter_%s" % filter_id.capitalize()
chip.toggle_mode = true
chip.button_group = _filter_chip_group
chip.focus_mode = Control.FOCUS_ALL
chip.custom_minimum_size.y = float(_theme_integer("popups.asset_selector.filter_chip_height", 48, 44, 80))
chip.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
chip.pressed.connect(func() -> void: _on_artwork_filter_selected(filter_id))
_filter_chips.add_child(chip)
_artwork_filter_buttons.append(chip)
_sync_artwork_filter_chips(filter_ids, filter_labels)
_apply_chooser_visual_style()
func _sync_artwork_filter_chips(filter_ids: Array[String] = [], filter_labels: Array[String] = []) -> void:
var ids: Array[String] = filter_ids if not filter_ids.is_empty() else ARTWORK_FILTER_IDS.duplicate()
var labels: Array[String] = filter_labels if not filter_labels.is_empty() else ARTWORK_FILTER_LABELS.duplicate()
if ids.size() != _artwork_filter_buttons.size() or labels.size() != _artwork_filter_buttons.size():
return
for index: int in range(_artwork_filter_buttons.size()):
var chip: Button = _artwork_filter_buttons[index]
var selected: bool = ids[index] == _tag_id
chip.text = ("✓ " if selected else "") + labels[index]
chip.set_pressed_no_signal(selected)
func _on_artwork_filter_selected(filter_id: String) -> void:
_tag_id = filter_id
_sync_artwork_filter_chips(_current_filter_ids(), _current_filter_labels())
_page = 0
_update_popup_layout(true)
_rebuild()
func _current_filter_ids() -> Array[String]:
var ids: Array[String] = ARTWORK_FILTER_IDS.duplicate()
if _has_unsorted_artwork():
ids.append("unsorted")
return ids
func _current_filter_labels() -> Array[String]:
var labels: Array[String] = ARTWORK_FILTER_LABELS.duplicate()
if _has_unsorted_artwork():
labels.append("Unsorted")
return labels
func _has_unsorted_artwork() -> bool:
for entry: Dictionary in _entries:
if String(entry.get("gallery_tab", "artwork")) != "artwork":
continue
var tags: Variant = entry.get("tags", [])
if not tags is Array or (tags as Array).is_empty():
return true
return false
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"
_make_artwork_button.visible = kind == "scenery" and tab == "artwork"
_edit_scenery_button.visible = kind == "scenery"
_browse_button.visible = false
_refresh_button.visible = false
_empty_label.text = "No artwork here yet." if tab == "artwork" else "Choose a Sky look or wall finish."
_page = 0
_update_popup_layout(true)
_rebuild()
func _filtered_entries() -> Array[Dictionary]:
if kind != "scenery":
return _entries
var filtered: Array[Dictionary] = []
for entry: Dictionary in _entries:
if String(entry.get("gallery_tab", "artwork")) != _gallery_tab:
continue
if _gallery_tab == "artwork" and _tag_id != "all":
var tags: Variant = entry.get("tags", [])
var tag_list: Array = tags as Array if tags is Array else []
if _tag_id == "unsorted":
if not tag_list.is_empty():
continue
elif _tag_id == "roads":
if not tag_list.has("roads") and not tag_list.has("paths"):
continue
elif not tag_list.has(_tag_id):
continue
filtered.append(entry)
return filtered
func _on_prev() -> void:
if _page > 0:
_page -= 1
_update_popup_layout()
_rebuild()
func _on_next() -> void:
if (_page + 1) * _page_capacity < _filtered_entries().size():
_page += 1
_update_popup_layout()
_rebuild()
func _page_count() -> int:
if _is_scenery_background_gallery() or _is_scenery_artwork_gallery():
return 1
var entries: Array[Dictionary] = _filtered_entries()
if entries.is_empty():
return 1
return int(ceil(float(entries.size()) / float(_page_capacity)))
func _usable_screen_size() -> Vector2:
var root_window: Window = get_tree().root
var available: Vector2 = Vector2(root_window.size)
var screen_index: int = DisplayServer.window_get_current_screen()
var usable_rect: Rect2i = DisplayServer.screen_get_usable_rect(screen_index)
if usable_rect.size.x > 0 and usable_rect.size.y > 0:
available.x = minf(available.x, float(usable_rect.size.x))
available.y = minf(available.y, float(usable_rect.size.y))
return available
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 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 asset_scrolling_gallery: bool = _uses_scrolling_asset_gallery() and not in_subview
var filtered: Array[Dictionary] = _filtered_entries()
_tabs.visible = kind == "scenery" and not in_subview
_filter_chips.visible = scenery_artwork_gallery
_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_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 asset_scrolling_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_subview or kind == "scenery" else 8
)
var available: Vector2 = _usable_screen_size()
var width_ratio: float = 0.98 if in_subview or asset_scrolling_gallery 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 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 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)
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 asset_scrolling_gallery or kind == "starters" else _active_columns * ROWS
_grid.columns = _active_columns
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))
_surface_cell_size = Vector2(surface_width, surface_height)
var filtered_count: int = filtered.size()
var page_count: int = maxi(1, int(ceil(float(filtered_count) / float(_page_capacity))))
_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_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 + _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
_filter_chips.custom_minimum_size.x = grid_width if _filter_chips.visible else 0.0
var fixed_height: float = _title_bar.get_combined_minimum_size().y + _footer.get_combined_minimum_size().y + POPUP_VERTICAL_PADDING
var visible_sections: int = 3
if _tabs.visible:
# HFlowContainer can report zero height before its first layout pass. Use
# the child button target as a floor so the first popup measurement reserves
# the tab row instead of letting the gallery scroll area consume that space.
fixed_height += maxf(
_tabs.get_combined_minimum_size().y,
float(_theme_integer("popups.asset_selector.tab_height", 60, 48, 96))
)
visible_sections += 1
if _filter_chips.visible:
fixed_height += _filter_chips.get_combined_minimum_size().y
visible_sections += 1
fixed_height += float(visible_sections - 1) * 8.0
if scenery_background_gallery:
var has_surface_row: bool = _has_surface_entries(filtered)
var content_rows: int = grid_rows + (1 if has_surface_row else 0)
var row_gaps: float = float(maxi(0, grid_rows - 1)) * GRID_V_SEPARATION
if has_surface_row and grid_rows > 0:
row_gaps += GRID_V_SEPARATION
if content_rows > 0:
var available_body_height: float = maxf(1.0, max_height - fixed_height)
var max_row_height: float = (available_body_height - row_gaps) / float(content_rows)
if max_row_height < _effective_cell_size.y or (has_surface_row and max_row_height < _surface_cell_size.y):
var compact_row_height: float = maxf(180.0, max_row_height)
if grid_rows > 0:
_effective_cell_size.y = minf(_effective_cell_size.y, compact_row_height)
if has_surface_row:
_surface_cell_size.y = minf(_surface_cell_size.y, compact_row_height)
body_height = 0.0
if grid_rows > 0:
body_height += float(grid_rows) * _effective_cell_size.y + float(maxi(0, grid_rows - 1)) * GRID_V_SEPARATION
if has_surface_row:
if grid_rows > 0:
body_height += GRID_V_SEPARATION
body_height += _surface_cell_size.y
if body_height <= 0.0:
body_height = maxf(120.0, _empty_label.get_combined_minimum_size().y)
var popup_height: float = minf(fixed_height + body_height, minf(preferred_height, max_height))
var popup_position: Vector2i = previous_position
if visible and keep_visible_position:
var minimum_anchor_height: float = minf(fixed_height + 120.0, minf(preferred_height, max_height))
var minimum_y: int = mini(roundi(POPUP_SAFE_SCREEN_MARGIN), maxi(0, floori(available.y - minimum_anchor_height)))
var maximum_y: int = maxi(minimum_y, floori(available.y - minimum_anchor_height - POPUP_SAFE_SCREEN_MARGIN))
popup_position.y = clampi(previous_position.y, minimum_y, maximum_y)
var height_below_anchor: float = maxf(1.0, available.y - float(popup_position.y) - POPUP_SAFE_SCREEN_MARGIN)
popup_height = minf(popup_height, height_below_anchor)
if in_color_picker:
var action_width: float = maxf(100.0, (popup_width - POPUP_SIDE_PADDING - 8.0) * 0.5)
_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))
var max_y: int = maxi(0, floori(available.y - float(size.y) - POPUP_SAFE_SCREEN_MARGIN))
var min_x: int = mini(roundi(POPUP_SAFE_SCREEN_MARGIN), max_x)
var min_y: int = mini(roundi(POPUP_SAFE_SCREEN_MARGIN), max_y)
position = Vector2i(
clampi(previous_position.x, min_x, max_x),
clampi(popup_position.y, min_y, max_y)
)
else:
popup_centered(size)
func _update_popup_layout_after_layout() -> void:
await get_tree().process_frame
if is_inside_tree():
_update_popup_layout(visible)
func _rebuild() -> void:
_sky_card_group = ButtonGroup.new()
for container: Control in [_grid, _surface_cards_row]:
for child: Node in container.get_children():
container.remove_child(child)
child.queue_free()
_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 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 asset_scrolling_gallery: bool = _uses_scrolling_asset_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_subview
_make_artwork_button.visible = scenery_artwork_gallery
_browse_button.visible = false
_refresh_button.visible = false
_footer_spacer.visible = scenery_artwork_gallery
_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
_next_button.visible = not single
_page_label.visible = not single
_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_subview:
_prev_button.visible = false
_next_button.visible = false
_page_label.visible = false
_browse_button.visible = false
_refresh_button.visible = false
_make_artwork_button.visible = false
return
if filtered.is_empty():
return
if scenery_background_gallery:
for entry: Dictionary in filtered:
var background_kind: String = String(entry.get("background_kind", ""))
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))
return
var bounds := page_bounds(filtered.size(), _page, _page_capacity)
for i: int in range(int(bounds["start"]), int(bounds["end"])):
_grid.add_child(_build_cell(filtered[i]))
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)
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:
return kind == "scenery" and _gallery_tab == "backgrounds"
func _is_scenery_artwork_gallery() -> bool:
return kind == "scenery" and _gallery_tab == "artwork"
func _uses_scrolling_asset_gallery() -> bool:
return kind == "prop" or kind == "stickman"
func _count_scene_background_entries(entries: Array[Dictionary]) -> int:
var count: int = 0
for entry: Dictionary in entries:
if String(entry.get("background_kind", "")) in ["sky", "plain"]:
count += 1
return count
func _has_surface_entries(entries: Array[Dictionary]) -> bool:
for entry: Dictionary in entries:
var background_kind: String = String(entry.get("background_kind", ""))
if background_kind == "wall_setup" or background_kind == "background_ground_setup":
return true
return false
func _visible_content_minimum_height(container: VBoxContainer) -> float:
var height: float = 0.0
var visible_children: int = 0
for child: Node in container.get_children():
if not child is Control or not (child as Control).visible:
continue
height += (child as Control).get_combined_minimum_size().y
visible_children += 1
if visible_children > 1:
height += float(visible_children - 1) * float(container.get_theme_constant("separation"))
return height
func _build_cell(entry: Dictionary, requested_size: Vector2 = Vector2.ZERO) -> Control:
var key := _entry_key(entry)
var cell: Variant = ASSET_CARD.new()
# 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
var is_background_ground_setup: bool = String(entry.get("background_kind", "")) == "background_ground_setup"
var thumb_height_budget: float = card_size.y - (74.0 if is_background_ground_setup else 64.0)
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":
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 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" or kind == "starters" or _uses_scrolling_asset_gallery():
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(
card_neutral_surface,
card_neutral_border,
card_border_width,
card_corner_radius
))
card_button.add_theme_stylebox_override("hover", _make_rounded_style(
card_hover_surface,
_theme_color("mode_colors.edit", Color("#22C6FF")),
card_hover_border_width,
card_corner_radius
))
card_button.add_theme_stylebox_override("pressed", _make_rounded_style(
card_selected_surface,
_theme_color("mode_colors.edit", Color("#22C6FF")),
card_hover_border_width,
card_corner_radius
))
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),
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_scene_background_choice:
cell.button_group = _sky_card_group
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, thumb_size)
cell.set_preview_control(preview)
elif entry.has("background_kind"):
var preview: StageBackgroundCardPreview = BACKGROUND_CARD_PREVIEW.new() as StageBackgroundCardPreview
preview.configure(entry)
cell.set_preview_control(preview)
else:
cell.set_thumbnail(tex if tex != null else _get_placeholder())
_cell_texrects[key] = cell.thumbnail_rect()
return cell
func _theme_color(path: String, fallback: Color) -> Color:
if _sandbox_theme != null:
return _sandbox_theme.color(path, fallback)
return fallback
func _theme_integer(path: String, fallback: int, minimum: int, maximum: int) -> 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
var raw: Variant = _sandbox_theme.value(path, [fallback.x, fallback.y])
if not raw is Array or (raw as Array).size() != 2:
return fallback
return Vector2(float((raw as Array)[0]), float((raw as Array)[1]))
func _apply_chooser_visual_style() -> void:
var radius: int = _theme_integer("popups.asset_selector.corner_radius", 16, 8, 32)
var button_border_width: int = _theme_integer("popups.asset_selector.button_border_width", 2, 1, 5)
var accent: Color = _theme_color("mode_colors.edit", Color("#22C6FF"))
var text_color: Color = _theme_color("stage_panels.type.text_color", Color("#F5F7FA"))
var neutral: Color = _theme_color("stage_panels.choice.neutral_surface", Color("#171B24"))
var hover: Color = _theme_color("stage_panels.choice.hover_surface", Color("#303A4B"))
var selected: Color = _theme_color("stage_panels.choice.selected_surface", Color("#12394A"))
var border: Color = _theme_color("stage_panels.choice.border_color", Color("#FFFFFF33"))
if _sandbox_theme != null:
self.theme = _sandbox_theme.control_theme()
var shell: StyleBoxFlat = _sandbox_theme.stylebox("popups.asset_selector")
shell.set_corner_radius_all(radius)
shell.content_margin_left = 16.0
shell.content_margin_right = 16.0
shell.content_margin_top = 14.0
shell.content_margin_bottom = 14.0
add_theme_stylebox_override("panel", shell)
var preview_background: Color = _theme_color("popups.style_defaults.bg_color", Color("#202633FA"))
if _setup_preview != null:
_setup_preview.set_rounded_frame(
float(_theme_integer("popups.asset_selector.surface_corner_radius", 14, 4, 32)),
preview_background
)
var preview_frame: PanelContainer = null
if _setup_view != null:
preview_frame = _setup_view.get_node_or_null("SurfacePreviewFrame") as PanelContainer
if preview_frame != null:
var frame_style := StyleBoxFlat.new()
frame_style.bg_color = preview_background
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:
var title_size: int = _theme_integer("popups.asset_selector.starter_title_size", 30, 22, 48) if kind == "starters" else _theme_integer("popups.asset_selector.setup_title_size", 30, 22, 48)
_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]:
if tab == null:
continue
tab.custom_minimum_size.y = float(_theme_integer("popups.asset_selector.tab_height", 60, 48, 96))
tab.size_flags_horizontal = Control.SIZE_EXPAND_FILL
tab.focus_mode = Control.FOCUS_ALL
tab.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.tab_font_size", 22, 16, 40))
tab.add_theme_color_override("font_color", text_color)
tab.add_theme_color_override("font_hover_color", text_color)
tab.add_theme_color_override("font_pressed_color", text_color)
tab.add_theme_stylebox_override("normal", _make_rounded_style(neutral, border, button_border_width, radius))
tab.add_theme_stylebox_override("hover", _make_rounded_style(hover, accent, button_border_width + 1, radius))
tab.add_theme_stylebox_override("pressed", _make_rounded_style(selected, accent, button_border_width + 1, radius))
tab.add_theme_stylebox_override("hover_pressed", _make_rounded_style(selected.lightened(0.06), accent, button_border_width + 1, radius))
tab.add_theme_stylebox_override("focus", _make_rounded_style(Color(0, 0, 0, 0), accent, button_border_width + 1, radius))
_apply_emphasis_font(tab)
if _tabs != null:
_tabs.add_theme_constant_override("h_separation", 8)
_tabs.add_theme_constant_override("v_separation", 8)
if _filter_chips != null:
_filter_chips.add_theme_constant_override("h_separation", _theme_integer("popups.asset_selector.filter_chip_gap", 8, 4, 24))
_filter_chips.add_theme_constant_override("v_separation", _theme_integer("popups.asset_selector.filter_chip_gap", 8, 4, 24))
for chip: Button in _artwork_filter_buttons:
chip.custom_minimum_size.y = float(_theme_integer("popups.asset_selector.filter_chip_height", 48, 44, 80))
chip.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.filter_chip_font_size", 20, 16, 32))
chip.add_theme_color_override("font_color", text_color)
chip.add_theme_color_override("font_hover_color", text_color)
chip.add_theme_color_override("font_pressed_color", text_color)
chip.add_theme_stylebox_override("normal", _make_rounded_style(neutral, border, button_border_width, radius))
chip.add_theme_stylebox_override("hover", _make_rounded_style(hover, accent, button_border_width + 1, radius))
chip.add_theme_stylebox_override("pressed", _make_rounded_style(selected, accent, button_border_width + 1, radius))
chip.add_theme_stylebox_override("hover_pressed", _make_rounded_style(selected.lightened(0.06), accent, button_border_width + 1, radius))
chip.add_theme_stylebox_override("focus", _make_rounded_style(Color(0, 0, 0, 0), accent, button_border_width + 1, radius))
_apply_emphasis_font(chip)
var heading: Label = get_meta("setup_heading", null) as Label
if heading != null:
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)
_apply_rounded_button(_color_back_button, false, accent, text_color, neutral, hover, border, radius)
_apply_rounded_button(_color_use_button, true, accent, text_color, neutral, hover, border, radius)
if _close_button != null:
_close_button.custom_minimum_size = Vector2(136.0, float(_theme_integer("stage_panels.action.min_target_size", 48, 44, 96)))
_apply_rounded_button(_close_button, false, accent, text_color, neutral, hover, border, radius)
if _make_artwork_button != null:
_make_artwork_button.custom_minimum_size = Vector2(224.0, float(_theme_integer("stage_panels.action.min_target_size", 48, 44, 96)))
_apply_rounded_button(_make_artwork_button, false, accent, text_color, neutral, hover, border, radius)
if _edit_scenery_button != null:
_edit_scenery_button.text = "✋ Edit placed scenery"
_edit_scenery_button.custom_minimum_size = Vector2(292.0, float(_theme_integer("popups.asset_selector.tab_height", 60, 48, 96)))
_edit_scenery_button.focus_mode = Control.FOCUS_ALL
_edit_scenery_button.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.tab_font_size", 22, 16, 40))
_edit_scenery_button.add_theme_color_override("font_color", text_color)
_edit_scenery_button.add_theme_color_override("font_hover_color", text_color)
_edit_scenery_button.add_theme_color_override("font_pressed_color", text_color)
_edit_scenery_button.add_theme_stylebox_override("normal", _make_rounded_style(neutral, border, button_border_width, radius))
_edit_scenery_button.add_theme_stylebox_override("hover", _make_rounded_style(hover, accent, button_border_width + 1, radius))
_edit_scenery_button.add_theme_stylebox_override("pressed", _make_rounded_style(selected, accent, button_border_width + 1, radius))
_edit_scenery_button.add_theme_stylebox_override("hover_pressed", _make_rounded_style(selected.lightened(0.06), accent, button_border_width + 1, radius))
_edit_scenery_button.add_theme_stylebox_override("focus", _make_rounded_style(Color(0, 0, 0, 0), accent, button_border_width + 1, radius))
_apply_emphasis_font(_edit_scenery_button)
func _apply_rounded_button(
button: Button,
primary: bool,
accent: Color,
text_color: Color,
neutral: Color,
hover: Color,
border: Color,
radius: int
) -> void:
if button == null:
return
var primary_blue: Color = _theme_color("popups.asset_selector.action_color", Color("#087DBA"))
var base: Color = primary_blue if primary else neutral
var over: Color = primary_blue.lightened(0.12) if primary else hover
var down: Color = primary_blue.darkened(0.12) if primary else _selected_button_color()
var edge: Color = accent.lightened(0.15) if primary else border
var border_width: int = _theme_integer("popups.asset_selector.button_border_width", 2, 1, 5)
button.add_theme_stylebox_override("normal", _make_rounded_style(base, edge, border_width, radius))
button.add_theme_stylebox_override("hover", _make_rounded_style(over, edge, border_width + 1, radius))
button.add_theme_stylebox_override("pressed", _make_rounded_style(down, accent, border_width + 1, radius))
button.add_theme_stylebox_override("hover_pressed", _make_rounded_style(down.lightened(0.05), accent, border_width + 1, radius))
button.add_theme_stylebox_override("focus", _make_rounded_style(Color(0, 0, 0, 0), accent, border_width + 1, radius))
var label_color: Color = Color.WHITE if primary else text_color
button.add_theme_color_override("font_color", label_color)
button.add_theme_color_override("font_hover_color", label_color)
button.add_theme_color_override("font_pressed_color", label_color)
button.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.setup_button_font_size", 20, 14, 40))
_apply_emphasis_font(button)
func _selected_button_color() -> Color:
return _theme_color("stage_panels.choice.selected_surface", Color("#12394A"))
func _make_rounded_style(background: Color, edge: Color, edge_width: int, radius: int) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = background
style.border_color = edge
style.set_border_width_all(edge_width)
style.set_corner_radius_all(radius)
style.content_margin_left = 12.0
style.content_margin_right = 12.0
style.content_margin_top = 8.0
style.content_margin_bottom = 8.0
return style
func _make_strong_font(font: Font, embolden: float) -> Font:
if font == null:
return null
var variation := FontVariation.new()
variation.base_font = font
variation.variation_embolden = embolden
return variation
func _apply_emphasis_font(control: Control, extra_bold: bool = false) -> void:
if control == null:
return
var font: Font = _strong_action_font if extra_bold else _strong_ui_font
if font != null:
control.add_theme_font_override("font", font)
func _on_cell_pressed(entry: Dictionary) -> void:
var background_kind: String = String(entry.get("background_kind", ""))
if background_kind == "wall_setup":
_open_surface_setup(&"wall")
return
if background_kind == "background_ground_setup":
_open_surface_setup(&"background_ground")
return
item_selected.emit(entry)
func _apply_font_to(c: Control) -> void:
if c == null:
return
if _ui_font != null:
c.add_theme_font_override("font", _ui_font)
elif _emoji_font != null:
c.add_theme_font_override("font", _emoji_font)
func _get_placeholder() -> ImageTexture:
if _placeholder_tex == null:
var img := Image.create(int(_thumbnail_size.x), int(_thumbnail_size.y), false, Image.FORMAT_RGBA8)
img.fill(Color(0.16, 0.16, 0.16, 1.0))
_placeholder_tex = ImageTexture.create_from_image(img)
return _placeholder_tex