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