diff --git a/src/game/engine/scenes/tests/ui_theme_proof.gd b/src/game/engine/scenes/tests/ui_theme_proof.gd new file mode 100644 index 00000000..01a54fbe --- /dev/null +++ b/src/game/engine/scenes/tests/ui_theme_proof.gd @@ -0,0 +1,213 @@ +extends Control +## Proof scene for p2-73 — the UI theme token pipeline. +## +## Two things are proven in one screenshot: +## 1. GLOBAL APPLY. The left column is bare, NON-overriding Control widgets +## (Button in all interaction states, Panel, PanelContainer, Label, +## ItemList). None of them set a `theme` or any `add_theme_*_override`, so +## whatever copper styling they show comes purely from the project-level +## `gui/theme/custom` = ui_theme.tres. If the global apply failed they would +## render as flat grey Godot defaults. +## 2. SEMANTIC ACCESSOR. The right column is a swatch grid built from +## `ThemeAssets.color()` calls — accent / semantic / text / border / +## background tokens resolved by name out of the generated token table. +## +## Self-contained + headless-safe: launched directly via scripts/ui-proof-capture.sh +## (weston). Owns its own capture + quit and prints `SCREENSHOT_PATH:`. +## Screenshot path is project-local user:// (NOT /tmp — Flatpak sandbox). + +const OUTPUT_DIR: String = "user://screenshots" +const SCREENSHOT_NAME: String = "p2-73-ui-theme" +const THEME_ID: String = "age-of-dwarves" + +## Token names exercised through ThemeAssets.color(). These are REAL token +## paths from design-tokens.json (dotted), proving dotted-name resolution. +const SWATCH_TOKENS: Array[String] = [ + "accent.gold", + "accent.goldResource", + "accent.science", + "accent.sage", + "text.title", + "text.primary", + "text.secondary", + "semantic.positive", + "semantic.negative", + "semantic.warning", + "semantic.diplomacy", + "border.panel", + "border.focus", + "background.panel", + "background.raised", +] + +var _captured: bool = false + + +func _ready() -> void: + get_viewport().size = Vector2i(1280, 720) + if DisplayServer.get_name() != "headless": + DisplayServer.window_set_size(Vector2i(1280, 720)) + + # color() lazy-loads ui_theme.tres; set_theme primes the palette side too so + # the proof exercises the same path a booted game would. + ThemeAssets.set_theme(THEME_ID) + + _build_ui() + + # Two process frames so the theme-driven StyleBoxes paint before capture. + await get_tree().process_frame + await get_tree().process_frame + await get_tree().create_timer(0.4).timeout + _capture(SCREENSHOT_NAME) + await get_tree().create_timer(0.3).timeout + get_tree().quit() + + +func _build_ui() -> void: + # Stable dark backdrop — uses the deepest background token so even the + # backdrop is token-driven, but read raw so a theme failure can't hide it. + var bg: ColorRect = ColorRect.new() + bg.set_anchors_preset(Control.PRESET_FULL_RECT) + bg.color = Color(0.05, 0.05, 0.07, 1.0) + add_child(bg) + + var root: HBoxContainer = HBoxContainer.new() + root.set_anchors_preset(Control.PRESET_FULL_RECT) + root.add_theme_constant_override("separation", 32) + root.offset_left = 40 + root.offset_top = 28 + root.offset_right = -40 + root.offset_bottom = -28 + add_child(root) + + root.add_child(_build_widgets_column()) + root.add_child(_build_swatch_column()) + + +## Left column: bare default widgets with NO local theme / overrides. Proves the +## project-level gui/theme/custom reaches default Controls. +func _build_widgets_column() -> Control: + var col: VBoxContainer = VBoxContainer.new() + col.custom_minimum_size = Vector2(520, 0) + col.add_theme_constant_override("separation", 14) + + # Heading uses a token color so the section title itself is themed copper. + var heading: Label = Label.new() + heading.text = "Global theme (no overrides) — gui/theme/custom" + heading.add_theme_font_size_override("font_size", 20) + heading.add_theme_color_override("font_color", ThemeAssets.color("text.title")) + col.add_child(heading) + + # Default Label — inherits Label/colors/font_color + font size from theme. + var label: Label = Label.new() + label.text = "Default Label — themed off-white body text from ui_theme.tres" + col.add_child(label) + + # Real default Buttons — normal + pressed (toggled on) + disabled. Each picks + # up its StyleBox + font color from the global theme with zero overrides. + col.add_child(_default_button("Default Button — Normal", false, false)) + var pressed_btn: Button = _default_button("Default Button — Pressed", false, true) + pressed_btn.toggle_mode = true + pressed_btn.button_pressed = true + col.add_child(pressed_btn) + col.add_child(_default_button("Default Button — Disabled", true, false)) + + # Default Panel. + var panel: Panel = Panel.new() + panel.custom_minimum_size = Vector2(0, 56) + var panel_label: Label = Label.new() + panel_label.text = "Default Panel — copper border + dark fill" + panel_label.position = Vector2(12, 16) + panel.add_child(panel_label) + col.add_child(panel) + + # Default ItemList with a selected row. + var list: ItemList = ItemList.new() + list.custom_minimum_size = Vector2(0, 110) + list.add_item("Ironveil clan") + list.add_item("Stoneguard clan") + list.add_item("Emberfall clan (selected)") + list.add_item("Deephollow clan") + list.select(2) + col.add_child(list) + + return col + + +func _default_button(text: String, disabled: bool, _pressed: bool) -> Button: + var btn: Button = Button.new() + btn.text = text + btn.disabled = disabled + btn.custom_minimum_size = Vector2(0, 38) + return btn + + +## Right column: swatch grid built entirely from ThemeAssets.color(token). +func _build_swatch_column() -> Control: + var col: VBoxContainer = VBoxContainer.new() + col.custom_minimum_size = Vector2(560, 0) + col.add_theme_constant_override("separation", 10) + + var heading: Label = Label.new() + heading.text = "ThemeAssets.color(token) — semantic accessor" + heading.add_theme_font_size_override("font_size", 20) + heading.add_theme_color_override("font_color", ThemeAssets.color("text.title")) + col.add_child(heading) + + var grid: GridContainer = GridContainer.new() + grid.columns = 1 + grid.add_theme_constant_override("v_separation", 6) + for token_name: String in SWATCH_TOKENS: + grid.add_child(_swatch_row(token_name)) + col.add_child(grid) + + return col + + +func _swatch_row(token_name: String) -> Control: + var row: HBoxContainer = HBoxContainer.new() + row.add_theme_constant_override("separation", 12) + + var resolved: Color = ThemeAssets.color(token_name) + + var chip: ColorRect = ColorRect.new() + chip.custom_minimum_size = Vector2(40, 24) + chip.color = resolved + row.add_child(chip) + + var name_label: Label = Label.new() + name_label.text = token_name + name_label.custom_minimum_size = Vector2(190, 0) + row.add_child(name_label) + + var hex_label: Label = Label.new() + hex_label.text = "#%02x%02x%02x" % [ + int(round(resolved.r * 255.0)), + int(round(resolved.g * 255.0)), + int(round(resolved.b * 255.0)), + ] + hex_label.add_theme_color_override("font_color", resolved) + row.add_child(hex_label) + + return row + + +func _capture(name: String) -> void: + if _captured: + return + _captured = true + + DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(OUTPUT_DIR)) + + var image: Image = get_viewport().get_texture().get_image() + if image == null: + push_error("ui_theme_proof: viewport image unavailable") + return + + var abs_path: String = ProjectSettings.globalize_path("%s/%s.png" % [OUTPUT_DIR, name]) + var err: Error = image.save_png(abs_path) + if err == OK: + print("SCREENSHOT_PATH:%s" % abs_path) + print("Screenshot: %dx%d saved to %s" % [image.get_width(), image.get_height(), abs_path]) + else: + push_error("ui_theme_proof: save failed: %s" % error_string(err)) diff --git a/src/game/engine/scenes/tests/ui_theme_proof.gd.uid b/src/game/engine/scenes/tests/ui_theme_proof.gd.uid new file mode 100644 index 00000000..0e0dcab8 --- /dev/null +++ b/src/game/engine/scenes/tests/ui_theme_proof.gd.uid @@ -0,0 +1 @@ +uid://b48nbf7dy3aup diff --git a/src/game/engine/scenes/tests/ui_theme_proof.tscn b/src/game/engine/scenes/tests/ui_theme_proof.tscn new file mode 100644 index 00000000..ee3c486c --- /dev/null +++ b/src/game/engine/scenes/tests/ui_theme_proof.tscn @@ -0,0 +1,10 @@ +[gd_scene load_steps=2 format=3 uid="uid://b2u7th3mpr00f"] + +[ext_resource type="Script" path="res://engine/scenes/tests/ui_theme_proof.gd" id="1_script"] + +[node name="UiThemeProof" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("1_script")