class_name AssetSelector extends PopupPanel signal item_selected(entry: Dictionary) signal cancelled() signal browse_requested() signal refresh_requested() const COLUMNS := 4 const ROWS := 3 const PAGE_SIZE := COLUMNS * ROWS 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") 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 @onready var _title_label: Label = %TitleLabel @onready var _grid: GridContainer = %GridContainer @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 func _ready() -> void: exclusive = true _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()) get_tree().root.size_changed.connect(_on_root_size_changed) func _on_root_size_changed() -> void: if visible: popup_centered() func open(p_kind: String, entries: Array[Dictionary]) -> void: kind = p_kind _title_label.text = "Choose Your Stickman" if kind == "stickman" else "Choose a Prop" _browse_button.visible = kind == "stickman" or kind == "prop" _refresh_button.visible = kind == "stickman" or kind == "prop" _empty_label.text = "No props found. Browse for a .prop file." if kind == "prop" else "No stickmen found." set_entries(entries) popup_centered() func set_entries(entries: Array[Dictionary]) -> void: _entries = entries _thumbnails.clear() _cell_texrects.clear() _page = 0 _rebuild() 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 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 _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_prev() -> void: if _page > 0: _page -= 1 _rebuild() func _on_next() -> void: if (_page + 1) * PAGE_SIZE < _entries.size(): _page += 1 _rebuild() func _page_count() -> int: if _entries.is_empty(): return 1 return int(ceil(float(_entries.size()) / float(PAGE_SIZE))) func _rebuild() -> void: for c: Node in _grid.get_children(): c.queue_free() _cell_texrects.clear() _empty_label.visible = _entries.is_empty() _grid.visible = not _entries.is_empty() 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_SIZE >= _entries.size() _page_label.text = "Page %d/%d" % [_page + 1, total] if _entries.is_empty(): return var bounds := page_bounds(_entries.size(), _page) for i: int in range(int(bounds["start"]), int(bounds["end"])): _grid.add_child(_build_cell(_entries[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. cell.configure({"placement_id": _entry_key(entry), "label": String(entry.get("name", entry.get("id", ""))), "icon": "", "category": String(entry.get("material_label", "")), "thumbnail_size": _thumbnail_size}, _thumbnails.get(key, null), null) cell.custom_minimum_size = _cell_min_size cell.focus_mode = Control.FOCUS_NONE cell.activated.connect(func(_payload: Dictionary) -> void: _on_cell_pressed(entry)) var tex: Texture2D = _thumbnails.get(key, null) cell.set_thumbnail(tex if tex != null else _get_placeholder()) _cell_texrects[key] = cell.thumbnail_rect() return cell func _on_cell_pressed(entry: Dictionary) -> void: 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