Phase 14 - Organized directories.

This commit is contained in:
2026-10-03 19:13:39 -04:00
parent 6666173c4f
commit d950fd9d9a
307 changed files with 1452 additions and 1110 deletions
+58
View File
@@ -0,0 +1,58 @@
class_name AssetCard
extends Button
signal activated(payload: Dictionary)
signal drag_started(payload: Dictionary)
var _payload: Dictionary = {}
var _press_position: Vector2 = Vector2.ZERO
var _dragging: bool = false
var _drag_threshold: float = 8.0
var _icon: TextureRect = null
var _glyph: Label = null
var _label: Label = null
func configure(payload: Dictionary, icon_texture: Texture2D = null, theme: Variant = null) -> void:
_payload = payload.duplicate(true)
_drag_threshold = theme.number("palette.drag_threshold", 8.0, 1.0, 128.0) if theme != null else 8.0
custom_minimum_size = Vector2(96, 108)
clip_contents = true
if theme != null:
var size_data: Variant = theme.value("palette.card_size", [96, 94])
if size_data is Array and (size_data as Array).size() == 2: custom_minimum_size = Vector2(float(size_data[0]), maxf(108.0, float(size_data[1])))
add_theme_stylebox_override("normal", theme.stylebox("palette"))
var thumbnail_size: Vector2 = _payload.get("thumbnail_size", Vector2(64, 64))
focus_mode = Control.FOCUS_ALL
tooltip_text = String(_payload.get("label", ""))
for child: Node in get_children(): child.queue_free()
var box := VBoxContainer.new(); box.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT); box.mouse_filter = Control.MOUSE_FILTER_IGNORE; box.clip_contents = true
box.alignment = BoxContainer.ALIGNMENT_CENTER; box.add_theme_constant_override("separation", 2); add_child(box)
_icon = TextureRect.new(); _icon.custom_minimum_size = thumbnail_size; _icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE; _icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED; _icon.texture = icon_texture; _icon.visible = icon_texture != null; _icon.mouse_filter = Control.MOUSE_FILTER_IGNORE; box.add_child(_icon)
_glyph = Label.new(); _glyph.text = String(_payload.get("icon", "")); _glyph.visible = icon_texture == null; _glyph.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER; _glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE; _glyph.add_theme_font_size_override("font_size", 30); box.add_child(_glyph)
_label = Label.new(); _label.text = String(_payload.get("label", "")); _label.custom_minimum_size = Vector2(0, 28); _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER; _label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART; _label.clip_text = true; _label.mouse_filter = Control.MOUSE_FILTER_IGNORE; box.add_child(_label)
func set_thumbnail(texture: Texture2D) -> void:
if _icon != null: _icon.texture = texture; _icon.visible = texture != null
if _glyph != null and texture != null: _glyph.visible = false
func thumbnail_rect() -> TextureRect:
return _icon
func set_selected(selected: bool) -> void:
button_pressed = selected
func payload() -> Dictionary:
return _payload.duplicate(true)
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
_press_position = event.position; _dragging = false
elif _dragging:
accept_event()
elif event is InputEventMouseMotion and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and not _dragging:
if event.position.distance_to(_press_position) >= _drag_threshold:
_dragging = true; drag_started.emit(payload()); accept_event()
func _pressed() -> void:
if not _dragging: activated.emit(payload())
+1
View File
@@ -0,0 +1 @@
uid://dayameygn6kdb
+205
View File
@@ -0,0 +1,205 @@
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
+1
View File
@@ -0,0 +1 @@
uid://b1ikg6vt3tiu3
@@ -0,0 +1,17 @@
class_name ColorSwatchSelector
extends HBoxContainer
signal color_selected(color: Color)
var _selected: Color = Color.WHITE
var _group: ButtonGroup = ButtonGroup.new()
func configure(colors: Array[Color], selected: Color, theme: Variant = null) -> void:
for child: Node in get_children(): child.queue_free()
_selected = selected
for color: Color in colors:
var button := Button.new(); button.custom_minimum_size = Vector2(44,44); button.toggle_mode = true; button.button_group = _group; button.tooltip_text = color.to_html(true)
var box := StyleBoxFlat.new(); box.bg_color = color; box.set_corner_radius_all(22); button.add_theme_stylebox_override("normal", box)
button.pressed.connect(func() -> void: set_selected(color, true)); add_child(button)
button.set_pressed_no_signal(color.is_equal_approx(selected))
func set_selected(color: Color, emit_signal: bool = false) -> void:
_selected = color
if emit_signal: color_selected.emit(color)
func get_selected() -> Color: return _selected
@@ -0,0 +1 @@
uid://djy6yykmgtxk0
+10
View File
@@ -0,0 +1,10 @@
class_name EmojiQuickGrid
extends GridContainer
signal emoji_selected(text: String)
func configure(items: Array[String], columns: int = 8, theme: Variant = null) -> void:
for child: Node in get_children(): child.queue_free()
self.columns = maxi(1, columns)
for text: String in items:
var button := Button.new(); button.text = text; button.custom_minimum_size = Vector2(44,44); button.focus_mode = Control.FOCUS_ALL
if theme != null: button.add_theme_font_override("font", theme.emoji_font())
button.pressed.connect(func() -> void: emoji_selected.emit(text)); add_child(button)
@@ -0,0 +1 @@
uid://dlstr7bm0x258
+17
View File
@@ -0,0 +1,17 @@
class_name FontPresetPicker
extends HBoxContainer
signal preset_selected(id: String)
var _selected_id: String = ""
var _group: ButtonGroup = ButtonGroup.new()
func configure(presets: Array[Dictionary], selected_id: String, theme: Variant = null) -> void:
for child: Node in get_children(): child.queue_free()
_selected_id = selected_id
for item: Dictionary in presets:
var id: String = String(item.get("id", "")); if id.is_empty(): continue
var button := Button.new(); button.text = String(item.get("label", id)); button.custom_minimum_size = Vector2(44,44); button.toggle_mode = true; button.button_group = _group
if item.get("font", null) is Font: button.add_theme_font_override("font", item["font"])
button.pressed.connect(func() -> void: set_selected(id, true)); add_child(button); button.set_pressed_no_signal(id == selected_id)
func set_selected(id: String, emit_signal: bool = false) -> void:
_selected_id = id
if emit_signal: preset_selected.emit(id)
func get_selected_id() -> String: return _selected_id
@@ -0,0 +1 @@
uid://dfwkdo5sx6i4h
+25
View File
@@ -0,0 +1,25 @@
class_name PresetSizeToggle
extends HBoxContainer
signal preset_selected(id: String, value: int)
var _presets: Array[Dictionary] = []
var _selected_id: String = ""
var _group: ButtonGroup = ButtonGroup.new()
func configure(presets: Array[Dictionary], selected_id: String, theme: Variant = null) -> void:
for child: Node in get_children(): child.queue_free()
_presets = presets.duplicate(true); _selected_id = selected_id
for item: Dictionary in _presets:
if String(item.get("id","")).is_empty(): continue
var button := Button.new(); button.text = String(item.get("label", item["id"])); button.custom_minimum_size = Vector2(44,44); button.toggle_mode = true; button.button_group = _group
button.pressed.connect(func() -> void: set_selected(String(item["id"]), true)); add_child(button); button.set_pressed_no_signal(String(item["id"]) == selected_id)
if not _presets.is_empty() and _selected_id.is_empty(): _selected_id = String(_presets[0].get("id", ""))
func set_selected(id: String, emit_signal: bool = false) -> void:
for item: Dictionary in _presets:
if String(item.get("id", "")) == id:
_selected_id = id
if emit_signal: preset_selected.emit(id, int(item.get("value", 0)))
return
func get_selected_id() -> String: return _selected_id
func get_selected_value() -> int:
for item: Dictionary in _presets:
if String(item.get("id", "")) == _selected_id: return int(item.get("value", 0))
return 0
@@ -0,0 +1 @@
uid://fx38yavxtiv4