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(140.0, 168.0) const THUMB_SIZE := Vector2(120.0, 120.0) 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 @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" _refresh_button.visible = kind == "stickman" 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 _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 := Button.new() cell.custom_minimum_size = CELL_MIN_SIZE cell.toggle_mode = false cell.focus_mode = Control.FOCUS_NONE cell.pressed.connect(_on_cell_pressed.bind(entry)) var vbox := VBoxContainer.new() vbox.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE vbox.add_theme_constant_override("separation", 2) cell.add_child(vbox) var tex_rect := TextureRect.new() tex_rect.custom_minimum_size = THUMB_SIZE tex_rect.size_flags_horizontal = Control.SIZE_SHRINK_CENTER tex_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE tex_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED tex_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE var tex: Texture2D = _thumbnails.get(key, null) tex_rect.texture = tex if tex != null else _get_placeholder() vbox.add_child(tex_rect) _cell_texrects[key] = tex_rect var name_label := Label.new() name_label.text = String(entry.get("name", "")) name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE name_label.add_theme_font_size_override("font_size", 14) _apply_font_to(name_label) vbox.add_child(name_label) if kind == "prop": var badge := Label.new() badge.text = String(entry.get("material_label", "")) badge.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER badge.mouse_filter = Control.MOUSE_FILTER_IGNORE badge.add_theme_font_size_override("font_size", 12) badge.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7, 1.0)) _apply_font_to(badge) vbox.add_child(badge) 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(THUMB_SIZE.x, THUMB_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