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) 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 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_DIALOG := preload("res://scripts/shared/stage_ui/stage_surface_color_dialog.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 _sandbox_theme: Variant = null var _gallery_tab: String = "backgrounds" var _tag_id: String = "all" var _current_sky_id: String = "" 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_color_history: Array[Color] = [] var _setup_view: VBoxContainer 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 _setup_preview: StageSurfacePreview var _setup_color_controls: StageSurfaceColorControls var _setup_color_dialog: StageSurfaceColorDialog @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 _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 _tabs: HBoxContainer = %GalleryTabs @onready var _backgrounds_tab: Button = %BackgroundsTab @onready var _artwork_tab: Button = %ArtworkTab @onready var _tag_filter: OptionButton = %TagFilter @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 _build_surface_setup_view() if _tag_filter.item_count == 0: for label: String in ["All artwork", "Nature", "Buildings", "Walls", "Roads", "Objects"]: _tag_filter.add_item(label) _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")) _tag_filter.item_selected.connect(_on_tag_filter_selected) _make_artwork_button.pressed.connect(func() -> void: make_artwork_requested.emit()) _edit_scenery_button.pressed.connect(func() -> void: edit_scenery_requested.emit()) get_tree().root.size_changed.connect(_on_root_size_changed) func _on_root_size_changed() -> void: if visible: _update_popup_layout() _rebuild() popup_centered() func open(p_kind: String, entries: Array[Dictionary], current_sky_id: String = "") -> void: kind = p_kind _setup_kind = &"" _setup_view.visible = false _current_sky_id = current_sky_id if kind == "scenery" else "" _title_label.text = "Choose Your Stickman" if kind == "stickman" else ("Choose Scenery" if kind == "scenery" else ("Choose a starter scene" if kind == "starters" else "Choose a Prop")) _tabs.visible = kind == "scenery" _tag_filter.visible = false _make_artwork_button.visible = false _edit_scenery_button.visible = false _browse_button.visible = kind == "stickman" or kind == "prop" or kind == "scenery" _refresh_button.visible = kind == "stickman" or kind == "prop" or kind == "scenery" _empty_label.text = "No scenery found. Choose Make artwork to draw a new piece." if kind == "scenery" else ("No props found. Browse for a .prop file." if kind == "prop" else ("No starter scenes are available." if kind == "starters" else "No stickmen found.")) _gallery_tab = "backgrounds" _tag_id = "all" _backgrounds_tab.set_pressed_no_signal(true) _artwork_tab.set_pressed_no_signal(false) _tag_filter.select(0) set_entries(entries) if kind == "scenery": _set_gallery_tab("backgrounds") popup_centered() func set_entries(entries: Array[Dictionary]) -> void: _entries = entries _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) 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 controls: Array = [ _title_label, _empty_label, _page_label, _prev_button, _next_button, _browse_button, _refresh_button, _close_button, ] for c: Control in controls: _apply_font_to(c) 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) 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", 10, 0, 32)) _setup_view.visible = false _content_stack.add_child(_setup_view) var heading_row := HBoxContainer.new() heading_row.add_theme_constant_override("separation", 8) _setup_view.add_child(heading_row) var back_button := Button.new() back_button.text = "‹ Back" back_button.custom_minimum_size = Vector2(float(_theme_integer("stage_panels.action.close_width", 92, 72, 180)), float(_theme_integer("stage_panels.action.min_target_size", 48, 44, 96))) back_button.focus_mode = Control.FOCUS_ALL back_button.pressed.connect(_back_from_surface_setup) heading_row.add_child(back_button) 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("stage_panels.type.title_size", 24, 12, 48)) heading_row.add_child(heading) var look_heading := Label.new() look_heading.text = "Look" look_heading.add_theme_font_size_override("font_size", _theme_integer("stage_panels.type.section_size", 20, 12, 40)) _setup_view.add_child(look_heading) var wall_choices: StagePanelChoiceGroup = STAGE_PANEL_CHOICE_GROUP.new() as StagePanelChoiceGroup wall_choices.name = "WallLookChoices" 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) _setup_view.add_child(wall_choices) var ground_choices: StagePanelChoiceGroup = STAGE_PANEL_CHOICE_GROUP.new() as StagePanelChoiceGroup ground_choices.name = "BackgroundGroundLookChoices" 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) _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_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.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.color_selected.connect(_on_setup_color_selected) _setup_color_controls.custom_color_requested.connect(_open_setup_color_dialog) _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) var draw_button := Button.new() draw_button.name = "DrawSurfaceButton" draw_button.custom_minimum_size.y = float(_theme_integer("stage_panels.choice.minimum_height", 56, 44, 112)) draw_button.focus_mode = Control.FOCUS_ALL draw_button.pressed.connect(_draw_selected_surface) _setup_view.add_child(draw_button) set_meta("setup_heading", heading) set_meta("setup_helper", helper) set_meta("setup_draw_button", draw_button) _apply_font_to(back_button) _apply_font_to(heading) _apply_font_to(look_heading) _apply_font_to(helper) _apply_font_to(draw_button) if _sandbox_theme != null: draw_button.add_theme_stylebox_override("normal", _sandbox_theme.stylebox("stage_panels.action")) draw_button.add_theme_stylebox_override("hover", _sandbox_theme.stylebox("stage_panels.action")) draw_button.add_theme_color_override("font_color", _theme_color("stage_panels.type.text_color", Color("#F5F7FA"))) draw_button.add_theme_font_size_override("font_size", _theme_integer("stage_panels.type.body_size", 20, 14, 40)) for choice: StagePanelChoiceGroup in [wall_choices, ground_choices]: choice.apply_theme(_sandbox_theme as SandboxTheme) _apply_font_to(choice) func _make_look_cue(kind: StringName, finish: StringName) -> Control: var preview: StageSurfacePreview = SURFACE_PREVIEW.new() as StageSurfacePreview preview.custom_minimum_size = _theme_vector2("popups.asset_selector.look_cue_size", Vector2(80.0, 42.0)) 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" (get_meta("setup_helper") as Label).text = "Looks like a wall · Stickmen can pass through." if kind == &"wall" else "Decorative only · Stickmen don’t walk here." (get_meta("setup_draw_button") as Button).text = "Draw wall panel" if kind == &"wall" else "Draw 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 _update_popup_layout() _rebuild() back_button_focus() func back_button_focus() -> void: var back_button: Button = _setup_view.get_child(0).get_child(0) as Button if back_button != null: back_button.grab_focus() func _back_from_surface_setup() -> void: _setup_kind = &"" _setup_view.visible = false _update_popup_layout() _rebuild() _close_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 = Color(color.r, color.g, color.b, 1.0) _setup_color_chosen = true _update_setup_preview() surface_color_confirmed.emit(_setup_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_dialog() -> void: if _setup_color_dialog == null or not is_instance_valid(_setup_color_dialog): _setup_color_dialog = SURFACE_COLOR_DIALOG.new() as StageSurfaceColorDialog add_child(_setup_color_dialog) _setup_color_dialog.preview_color_changed.connect(_on_setup_color_preview) _setup_color_dialog.color_confirmed.connect(_on_setup_color_confirmed) _setup_color_dialog.cancelled.connect(_on_setup_color_cancelled) _setup_original_color = _setup_color _setup_color_dialog.configure(_setup_kind, _setup_finish, _setup_original_color, _sandbox_theme as SandboxTheme) _setup_color_dialog.open_dialog() func _on_setup_color_preview(color: Color) -> void: _setup_color = Color(color.r, color.g, color.b, 1.0) _update_setup_preview() func _on_setup_color_confirmed(color: Color) -> void: _setup_color = Color(color.r, color.g, color.b, 1.0) _setup_color_chosen = true _update_setup_preview() surface_color_confirmed.emit(_setup_color) _setup_color_controls.focus_custom_button() func _on_setup_color_cancelled() -> void: _setup_color = _setup_original_color _setup_color_controls.set_selected_no_signal(_setup_color) _update_setup_preview() _setup_color_controls.focus_custom_button() 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}) 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() 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 _on_tag_filter_selected(index: int) -> void: var ids: Array[String] = ["all", "nature", "buildings", "walls", "roads", "objects"] _tag_id = ids[clampi(index, 0, ids.size() - 1)] _page = 0 _update_popup_layout() _rebuild() func _set_gallery_tab(tab: String) -> void: _setup_kind = &"" _setup_view.visible = false _gallery_tab = tab _backgrounds_tab.set_pressed_no_signal(tab == "backgrounds") _artwork_tab.set_pressed_no_signal(tab == "artwork") _tag_filter.visible = kind == "scenery" and tab == "artwork" _make_artwork_button.visible = kind == "scenery" and tab == "artwork" _edit_scenery_button.visible = kind == "scenery" _browse_button.visible = kind == "stickman" or kind == "prop" or (kind == "scenery" and tab == "artwork") _refresh_button.visible = kind == "stickman" or kind == "prop" or (kind == "scenery" and tab == "artwork") _empty_label.text = "No artwork in this group yet." if tab == "artwork" else "Choose a Sky look or wall finish." _page = 0 _update_popup_layout() _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: Array = entry.get("tags", []) as Array if not tags.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: 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() -> void: var available: Vector2 = _usable_screen_size() var max_width: float = maxf(1.0, minf(available.x * 0.94, available.x - POPUP_SAFE_SCREEN_MARGIN)) var max_height: float = maxf(1.0, minf(available.y * 0.90, available.y - POPUP_SAFE_SCREEN_MARGIN)) var preferred_width: float = CELL_MIN_SIZE.x * COLUMNS + GRID_H_SEPARATION * (COLUMNS - 1) + POPUP_SIDE_PADDING var popup_width: float = minf(preferred_width, max_width) var grid_width: float = maxf(1.0, popup_width - POPUP_SIDE_PADDING) _active_columns = clampi(floori((grid_width + GRID_H_SEPARATION) / (CELL_MIN_SIZE.x + GRID_H_SEPARATION)), 1, COLUMNS) _page_capacity = _active_columns * ROWS _grid.columns = _active_columns var card_width: float = minf(CELL_MIN_SIZE.x, (grid_width - GRID_H_SEPARATION * (_active_columns - 1)) / float(_active_columns)) var card_height_limit: float = 248.0 if kind == "starters" else CELL_MIN_SIZE.y var card_height: float = minf(card_height_limit, card_width + (58.0 if kind == "starters" else 30.0)) _effective_cell_size = Vector2(maxf(96.0, card_width), maxf(180.0, card_height)) var filtered_count: int = _filtered_entries().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_rows: int = int(ceil(float(page_items) / float(_active_columns))) if page_items > 0 else 0 var body_height: float if not _setup_kind.is_empty(): body_height = maxf(180.0, _setup_view.get_combined_minimum_size().y) else: body_height = maxf(120.0, float(grid_rows) * _effective_cell_size.y + float(maxi(0, grid_rows - 1)) * GRID_V_SEPARATION) if page_items == 0: body_height = maxf(body_height, _empty_label.get_combined_minimum_size().y) 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: fixed_height += _tabs.get_combined_minimum_size().y visible_sections += 1 fixed_height += float(visible_sections - 1) * 8.0 var popup_height: float = minf(fixed_height + body_height, max_height) size = Vector2i(maxi(1, roundi(popup_width)), maxi(1, roundi(popup_height))) func _rebuild() -> void: _sky_card_group = ButtonGroup.new() for c: Node in _grid.get_children(): c.queue_free() _cell_texrects.clear() var filtered: Array[Dictionary] = _filtered_entries() var in_setup: bool = not _setup_kind.is_empty() _setup_view.visible = in_setup _empty_label.visible = filtered.is_empty() and not in_setup _grid.visible = not filtered.is_empty() and not in_setup _grid_scroll.visible = not filtered.is_empty() or in_setup 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_setup: _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 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 _build_cell(entry: Dictionary) -> Control: var key := _entry_key(entry) var cell: Variant = ASSET_CARD.new() # Selector cells inherit the popup theme. Avoid a per-card state override so # opening a selector never implies that one asset is pre-selected. 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 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": thumb_size = Vector2.ONE * minf(128.0, maxf(72.0, thumb_edge)) 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": String(entry.get("description", "")), "thumbnail_size": thumb_size} if is_background_ground_setup: card_payload["description_min_height"] = 42.0 if entry.has("background_kind"): card_payload["background_kind"] = entry["background_kind"] card_payload["finish_id"] = entry.get("finish_id", "") card_payload["id"] = entry.get("id", "") var is_sky_choice: bool = String(entry.get("background_kind", "")) == "sky" var is_current_sky: bool = is_sky_choice and String(entry.get("id", "")) == _current_sky_id if is_current_sky: card_payload["status"] = "Current Sky" card_payload["status_color"] = _theme_color("mode_colors.edit", Color("#22C6FF")) cell.configure(card_payload, _thumbnails.get(key, null), null) cell.custom_minimum_size = card_size cell.focus_mode = Control.FOCUS_ALL cell.activated.connect(func(_payload: Dictionary) -> void: _on_cell_pressed(entry)) if is_sky_choice: cell.button_group = _sky_card_group cell.set_selected(is_current_sky, _theme_color("stage_panels.choice.selected_surface", Color("#12394A")), _theme_color("mode_colors.edit", Color("#22C6FF"))) var tex: Texture2D = _thumbnails.get(key, null) if kind == "starters": var preview: Control = STARTER_CARD_PREVIEW.new() as Control preview.call("configure", entry) 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_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 _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