Add reusable UI components for Phase 7 UI Polish
- Introduced `EmojiQuickGrid`, `FontPresetPicker`, and `PresetSizeToggle` classes for emoji selection, font presets, and size toggles respectively. - Implemented configuration methods for each component to handle dynamic content and theming. - Created tests for the new components to ensure functionality and integration with the existing UI. - Added a comprehensive architecture specification for Phase 7 UI Polish detailing scope, implementation, and validation strategies. - Updated existing theme handling to support new UI elements and ensure consistent styling across the application.
This commit is contained in:
@@ -67,6 +67,10 @@ func _initialize() -> void:
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
# SandboxStage opens a recovery dialog when a prior interactive run left an
|
||||
# autosave. That native child window survives briefly across this suite's
|
||||
# short-lived stage fixtures and contaminates camera/navigation assertions.
|
||||
DirAccess.remove_absolute("user://sandbox_autosave.scene")
|
||||
print("")
|
||||
print("========================================================")
|
||||
print(" PHASE 4b.1 FIX REGRESSION TEST (headless)")
|
||||
@@ -439,18 +443,15 @@ func _test_bug5_ghost_lifecycle() -> void:
|
||||
_check(ghost.get_parent() == holder, "ghost is parented to the ghost holder, not World")
|
||||
_check(world.get_children().has(ghost) == false, "ghost is not a selectable World child")
|
||||
_check(is_equal_approx(ghost.modulate.a, 0.5), "ghost is translucent (alpha 0.5)")
|
||||
# Block-unit snapped ghost positioning (strides, not the 16-px grid/cursor).
|
||||
# Single-placement preview follows the configured snap grid; footprint stride
|
||||
# remains exclusively for drag painting.
|
||||
stage._snap_enabled = true
|
||||
var raw_pos: Vector2 = stage._camera.get_global_mouse_position() \
|
||||
+ stage._spawner.get_spawn_offset("ground")
|
||||
var expected_cell: Vector2i = stage._world_to_terrain_cell(raw_pos)
|
||||
var expected_pos: Vector2 = stage._terrain_cell_center(expected_cell)
|
||||
var expected_pos: Vector2 = stage._snap_to_grid(raw_pos)
|
||||
stage._update_ghost_position()
|
||||
_check(ghost.position.distance_to(expected_pos) <= 0.01,
|
||||
"ghost positioned at the block-unit cell center (got %s, expected %s)" % [ghost.position, expected_pos])
|
||||
_check(is_equal_approx(fmod(absf(ghost.position.x), GROUND_STRIDE_X), 0.0),
|
||||
"ghost x is a multiple of the %dpx stride (got %.1f)" % [GROUND_STRIDE_X, ghost.position.x])
|
||||
_check(is_equal_approx(fmod(absf(ghost.position.y), GROUND_STRIDE_Y), 0.0),
|
||||
"ghost y is a multiple of the 32px stride (got %.1f)" % ghost.position.y)
|
||||
"ghost positioned at the configured snap-grid point (got %s, expected %s)" % [ghost.position, expected_pos])
|
||||
|
||||
# Drag begin frees the single cursor ghost and builds per-cell ghosts.
|
||||
stage._begin_terrain_drag(stage._terrain_cell_center(Vector2i(30, 0)))
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
extends SceneTree
|
||||
|
||||
const SPAWNER := preload("res://scripts/stage_spawner.gd")
|
||||
const CARD := preload("res://scripts/ui/asset_card.gd")
|
||||
var _failures: int = 0
|
||||
|
||||
func _init() -> void:
|
||||
var world := Node2D.new()
|
||||
var spawner = SPAWNER.new(world)
|
||||
var entries: Array[Dictionary] = spawner.get_palette_entries()
|
||||
_check(entries.size() == 6, "six registry cards")
|
||||
_check(String(entries[0].get("placement_id", "")) == "ground", "registry id retained")
|
||||
var card = CARD.new(); card.configure(entries[0]); _check(card.payload().get("kind", "") == "terrain", "card copies payload")
|
||||
print("PASS" if _failures == 0 else "FAIL: %d" % _failures); quit(0 if _failures == 0 else 1)
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition: _failures += 1; push_error("Phase 7 palette: " + message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dt5c3h6p0msb2
|
||||
@@ -0,0 +1,22 @@
|
||||
extends SceneTree
|
||||
|
||||
const THEME_SCRIPT := preload("res://scripts/sandbox_theme.gd")
|
||||
var _failures: int = 0
|
||||
|
||||
func _init() -> void:
|
||||
var theme = THEME_SCRIPT.new()
|
||||
_check(theme.load_from(), "committed v2 theme loads")
|
||||
_check(theme.integer("fonts.sizes.tooltip", 0) == 18, "v2 font sizes resolve")
|
||||
_check(theme.color("mode_colors.edit", Color.BLACK) == Color("#22c6ff"), "mode color resolves")
|
||||
var first: StyleBoxFlat = theme.stylebox("palette")
|
||||
first.bg_color = Color.RED
|
||||
var second: StyleBoxFlat = theme.stylebox("palette")
|
||||
_check(second.bg_color != Color.RED, "stylebox callers receive isolated copies")
|
||||
_check(not theme.swatches().is_empty() and not theme.size_presets().is_empty(), "widget defaults available")
|
||||
print("PASS" if _failures == 0 else "FAIL: %d" % _failures)
|
||||
quit(0 if _failures == 0 else 1)
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
_failures += 1
|
||||
push_error("Phase 7 theme: " + message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dho8gbkh2u0k4
|
||||
@@ -0,0 +1,60 @@
|
||||
extends SceneTree
|
||||
|
||||
const STAGE_SCENE := preload("res://scenes/sandbox_stage.tscn")
|
||||
var _failures: int = 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
func _run() -> void:
|
||||
DirAccess.remove_absolute("user://sandbox_autosave.scene")
|
||||
var stage: Node2D = STAGE_SCENE.instantiate()
|
||||
root.add_child(stage)
|
||||
stage.get_window().size = Vector2i(1280, 720)
|
||||
await process_frame
|
||||
_check(stage._mode_badge.position.x >= stage._palette_dock.size.x, "badge is outside palette dock")
|
||||
_check(stage._mode_frame.offset_left >= stage._palette_dock.size.x, "mode frame is inset beyond palette")
|
||||
_check(stage._mode_buttons.size() == 3 and (stage._mode_buttons[0] as Button).custom_minimum_size.y >= 44.0, "mode buttons are kid-sized")
|
||||
_check(stage._file_menu.get_theme_font_size("font_size") == 22, "File menu uses themed font size")
|
||||
_check((stage._mode_buttons[0] as Button).get_theme_font_size("font_size") == 24, "mode switcher uses themed font size")
|
||||
var mode_switcher: Control = (stage._mode_buttons[0] as Button).get_parent() as Control
|
||||
_check(absf(mode_switcher.get_global_rect().get_center().x - 640.0) <= 1.0, "mode switcher remains horizontally centered at a known viewport width")
|
||||
_check(mode_switcher.get_parent().mouse_filter == Control.MOUSE_FILTER_IGNORE, "full-width mode centering wrapper is mouse-transparent")
|
||||
_check(stage._file_menu.get_popup().get_theme_font_size("font_size") == 26, "File popup uses independently themed font size")
|
||||
_check(stage._unsaved_changes_dialog.get_theme_font_size("font_size") == 22, "dialog font-size theme metric is applied")
|
||||
_check(stage._unsaved_changes_dialog.get_ok_button().get_theme_font_size("font_size") == 22, "dialog button inherits themed font size")
|
||||
_check(stage._unsaved_changes_dialog.get_label().horizontal_alignment == HORIZONTAL_ALIGNMENT_CENTER and stage._unsaved_changes_dialog.get_label().get_theme_font_size("font_size") == 26, "dialog message is centered and larger")
|
||||
var card: Button = stage._palette_buttons["area"]
|
||||
_check(card.clip_contents and card.custom_minimum_size.y >= 100.0, "palette cards clip and reserve vertical space")
|
||||
stage.set_mode(stage.StageMode.DIRECT)
|
||||
_check(is_zero_approx(stage._mode_frame.offset_left), "Direct frame reclaims hidden palette width")
|
||||
stage.set_mode(stage.StageMode.EDIT)
|
||||
_check(stage._mode_frame.offset_left >= stage._palette_dock.size.x, "Edit frame restores palette inset")
|
||||
_check(not stage.is_point_in_stage_frame(Vector2(1, 80), Rect2(100, 50, 600, 400)), "frame helper rejects a palette-side position")
|
||||
_check(stage.is_point_in_stage_frame(Vector2(200, 200), Rect2(100, 50, 600, 400)), "stage frame helper accepts an inner viewport position")
|
||||
stage._grid_size = 15.0
|
||||
stage._snap_enabled = true
|
||||
stage.set_placement_mode("step")
|
||||
stage._place_at(Vector2(22.0, 23.0))
|
||||
var placed: Node2D = stage._world.get_child(stage._world.get_child_count() - 1) as Node2D
|
||||
_check(placed.position == Vector2(15.0, 30.0), "single terrain uses configured grid snap")
|
||||
stage._open_selector("prop")
|
||||
await process_frame
|
||||
var selector_card: Variant = stage._selector._grid.get_child(0)
|
||||
_check(selector_card.custom_minimum_size.x >= 190.0 and selector_card.custom_minimum_size.y >= 220.0, "selector uses enlarged themed card metrics")
|
||||
_check(String(selector_card._label.text).length() > 0 and selector_card.thumbnail_rect().custom_minimum_size.x >= 156.0, "selector card shows a name beneath an enlarged thumbnail")
|
||||
stage._on_selector_cancelled()
|
||||
stage.set_mode(stage.StageMode.DIRECT)
|
||||
stage._pending_walk_target = true
|
||||
var right := InputEventMouseButton.new(); right.button_index = MOUSE_BUTTON_RIGHT; right.pressed = true
|
||||
stage._handle_world_click(right)
|
||||
_check(not stage._pending_walk_target, "RMB cancels an active direct target")
|
||||
stage.queue_free()
|
||||
await process_frame
|
||||
print("PASS" if _failures == 0 else "FAIL: %d" % _failures)
|
||||
quit(0 if _failures == 0 else 1)
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
_failures += 1
|
||||
push_error("Phase 7 UI follow-up: " + message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cnnlc36tnlqil
|
||||
@@ -0,0 +1,17 @@
|
||||
extends SceneTree
|
||||
|
||||
const COLOR_SWATCH := preload("res://scripts/ui/color_swatch_selector.gd")
|
||||
const SIZE_TOGGLE := preload("res://scripts/ui/preset_size_toggle.gd")
|
||||
const FONT_PICKER := preload("res://scripts/ui/font_preset_picker.gd")
|
||||
const EMOJI_GRID := preload("res://scripts/ui/emoji_quick_grid.gd")
|
||||
var _failures: int = 0
|
||||
|
||||
func _init() -> void:
|
||||
var swatch = COLOR_SWATCH.new(); swatch.configure([Color.RED], Color.RED); _check(swatch.get_selected() == Color.RED, "swatch selection")
|
||||
var sizes = SIZE_TOGGLE.new(); sizes.configure([{"id":"small","label":"Small","value":20}], "small"); _check(sizes.get_selected_value() == 20, "size value")
|
||||
var fonts = FONT_PICKER.new(); fonts.configure([{"id":"ui","label":"UI"}], "ui"); _check(fonts.get_selected_id() == "ui", "font id")
|
||||
var emoji = EMOJI_GRID.new(); emoji.configure(["😀"], 1); _check(emoji.get_child_count() == 1, "emoji controls")
|
||||
print("PASS" if _failures == 0 else "FAIL: %d" % _failures); quit(0 if _failures == 0 else 1)
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition: _failures += 1; push_error("Phase 7 widgets: " + message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://btjgad0h7xge3
|
||||
Reference in New Issue
Block a user