Harden UI layout for font scale and long strings
Full build / Import + headless tests (push) Successful in 36s
Full build / Full desktop build + optional release (push) Skipped

- Measure and cap the HUD target chip after each font-scale change; clip HUD top labels.
- Ellipsize long action-button labels so panels cannot grow past the viewport.
- Add accessibility/button layout assertions and refresh the gallery captures.
- Record the font/layout pass in TODO, docs, changelog and memory.
This commit is contained in:
Gig Line Dev committed 2026-11-02 16:23:46 +01:00
1 parent 26e6dd6413
commit f6aa26e6ca
20 files changed
+69 -7

No files matched your search

+3
View File
@@ -17,6 +17,9 @@ Semantic Versioning.
- Esc and right-click now close the top panel consistently, restore the venue - Esc and right-click now close the top panel consistently, restore the venue
menu after menu Settings, and recapture the mouse without leaving the guard menu after menu Settings, and recapture the mouse without leaving the guard
frozen. Help no longer opens Pause behind itself. frozen. Help no longer opens Pause behind itself.
- The HUD target chip grows and caps with the selected font scale, top labels
clip safely, and long action-button labels ellipsize instead of pushing panels
wider than the screen.
- NPC reason answers now use role-specific dialogue contexts, so staff, guests, - NPC reason answers now use role-specific dialogue contexts, so staff, guests,
delivery workers, performers and other archetypes answer instead of silently delivery workers, performers and other archetypes answer instead of silently
deflecting the Reason verb. deflecting the Reason verb.
+5
View File
@@ -61,6 +61,11 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
gallery is 13 good / 0 bad, the screen-design judge is 13/13 and a clean gallery is 13 good / 0 bad, the screen-design judge is 13/13 and a clean
portrait measured a 0.732 vision gap against the photoreal concept. No portrait measured a 0.732 vision gap against the photoreal concept. No
release build, version bump or tag was created at this gate. release build, version bump or tag was created at this gate.
- **Font/layout limits (2026-11-02):** HUD scale is clamped to 0.75–1.75 and only
applies to HUD labels. `Hud.set_target_label()` measures the scaled font, widens
the chip to 180–420 px and re-measures after a scale change; top labels clip.
`UiStyles.apply_button()` sets `clip_text` + ellipsis so long localized labels
cannot force a panel past the viewport. Do not replace these with Control.scale.
- **Modal close (2026-11-02):** `GameManager._close_top_modal()` is the single - **Modal close (2026-11-02):** `GameManager._close_top_modal()` is the single
Esc/right-click path for radial, inspection, wand, radio, timeline, Help, Esc/right-click path for radial, inspection, wand, radio, timeline, Help,
Settings, Pause and review. Settings remembers whether it came from the menu and Settings, Pause and review. Settings remembers whether it came from the menu and
+4 -2
View File
@@ -90,8 +90,10 @@ screen-design judge before any model/texture changes. No gameplay rules change.
top-modal close path, menu/settings return handling and pointer recapture are top-modal close path, menu/settings return handling and pointer recapture are
recorded in `docs/ui-copy.md`; session smoke covers menu Settings, Help recorded in `docs/ui-copy.md`; session smoke covers menu Settings, Help
right-click and pause Settings.) right-click and pause Settings.)
- [ ] Test buttons at minimum/maximum HUD font scale and with 1.5–2× longer - [x] Test buttons at minimum/maximum HUD font scale and with 1.5–2× longer
strings; fix clipping, overflow and overlapping layouts. strings; fix clipping, overflow and overlapping layouts. (Target chip now
measures/caps scaled text, HUD top labels clip, and action buttons ellipsize;
regression checks are in `test_accessibility.gd` and `test_ui_buttons.gd`.)
- [ ] Test destructive/irreversible actions (campaign reset, tutorial replay, - [ ] Test destructive/irreversible actions (campaign reset, tutorial replay,
emergency send) require the intended confirmation/hold and cannot double-fire. emergency send) require the intended confirmation/hold and cannot double-fire.
- [ ] Verify radio normal-send vs emergency hold, inspection/wand allow/deny, - [ ] Verify radio normal-send vs emergency hold, inspection/wand allow/deny,
+1
View File
@@ -51,6 +51,7 @@ Implemented so far:
- Joke Shop / Sunset Strip map (map optimization): the second venue is now a stylized reconstruction of The Comedy Store block at 8433 Sunset Blvd, with a 27 m black hall, signature-covered sloping canopy, circular **THE JOKE SHOP** blade sign, red velvet curtain bays, a west-end entrance, cream and white low-rise neighbours, a dry hillside with stucco houses and palms behind the roofline, and a low retail/billboard frontage across Sunset. The same geometry serves day and late lighting through the phase system. Real-site references and limits are in [`joke-shop-map.md`](joke-shop-map.md); the map measures **22,680 triangles across 454 meshes** in the budget test. - Joke Shop / Sunset Strip map (map optimization): the second venue is now a stylized reconstruction of The Comedy Store block at 8433 Sunset Blvd, with a 27 m black hall, signature-covered sloping canopy, circular **THE JOKE SHOP** blade sign, red velvet curtain bays, a west-end entrance, cream and white low-rise neighbours, a dry hillside with stucco houses and palms behind the roofline, and a low retail/billboard frontage across Sunset. The same geometry serves day and late lighting through the phase system. Real-site references and limits are in [`joke-shop-map.md`](joke-shop-map.md); the map measures **22,680 triangles across 454 meshes** in the budget test.
- Venue geometry (v0.7.0, Joke Shop frontage superseded by the map-optimization pass): the TV Studio builds a stone facade, bulb canopy, glossy black car and barrier line. `WorldBuilder.VENUE_SPECS` drives facade/street/nav dimensions, and `GameManager` rebuilds only the world container when the venue changes. - Venue geometry (v0.7.0, Joke Shop frontage superseded by the map-optimization pass): the TV Studio builds a stone facade, bulb canopy, glossy black car and barrier line. `WorldBuilder.VENUE_SPECS` drives facade/street/nav dimensions, and `GameManager` rebuilds only the world container when the venue changes.
- Gear fit (post-v0.6.0): Blender gear is measured against the avatar head/skirt silhouette and the checks run in both the Python tooling and the headless suite. Sunglasses, the shoulder bag and the firefighter helmet were fitted to the new faces; the Influencer portrait and gallery were recaptured with a before/after report. - Gear fit (post-v0.6.0): Blender gear is measured against the avatar head/skirt silhouette and the checks run in both the Python tooling and the headless suite. Sunglasses, the shoulder bag and the firefighter helmet were fitted to the new faces; the Influencer portrait and gallery were recaptured with a before/after report.
- Font/layout pass (0.9.3 work): HUD font scale was exercised at 0.75× and 1.75×; the target chip measures and caps scaled names, top labels clip, and shared action buttons ellipsize long 1.5–2× labels instead of overflowing. Covered by accessibility/button tests and recorded in `docs/ui-copy.md`.
- Modal-close pass (0.9.3 work): one top-modal close path handles Esc/right-click for every overlay, Settings opened from the menu restores the menu, Help no longer opens Pause behind it, and pointer/guard freeze state is recaptured after each close. The session smoke covers these paths. - Modal-close pass (0.9.3 work): one top-modal close path handles Esc/right-click for every overlay, Settings opened from the menu restores the menu, Help no longer opens Pause behind it, and pointer/guard freeze state is recaptured after each close. The session smoke covers these paths.
- Keyboard/gamepad focus pass (0.9.3 work): `UiFocus` rings wrap through visible/enabled controls, hidden Continue and disabled timeline actions are skipped, each modal has a default focus, and A/B are bound to `ui_accept`/`ui_cancel` for pad-only navigation. Recorded in `docs/ui-copy.md`. - Keyboard/gamepad focus pass (0.9.3 work): `UiFocus` rings wrap through visible/enabled controls, hidden Continue and disabled timeline actions are skipped, each modal has a default focus, and A/B are bound to `ui_accept`/`ui_cancel` for pad-only navigation. Recorded in `docs/ui-copy.md`.
- Button-state pass (0.9.3 work): a shared `UiStyles` helper now gives every action button explicit normal, hover, pressed, focus and disabled styles; the timeline disables Previous/Next at its boundaries, while hidden continue states and the keyboard-only tutorial skip remain intentional. Recorded in `docs/ui-copy.md`. - Button-state pass (0.9.3 work): a shared `UiStyles` helper now gives every action button explicit normal, hover, pressed, focus and disabled styles; the timeline disables Previous/Next at its boundaries, while hidden continue states and the keyboard-only tutorial skip remain intentional. Recorded in `docs/ui-copy.md`.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 KiB

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 KiB

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 669 KiB

After

Width:  |  Height:  |  Size: 661 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 KiB

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 760 KiB

After

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 KiB

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 838 KiB

After

Width:  |  Height:  |  Size: 830 KiB

+1
View File
@@ -45,3 +45,4 @@
43. The venue menu offers Hollywood Boulevard — Day and — Night; either starts the same map, and Night starts in the late phase with a full shift clock. 43. The venue menu offers Hollywood Boulevard — Day and — Night; either starts the same map, and Night starts in the late phase with a full shift clock.
44. The Joke Shop opens on the Sunset Strip club: the black hall, signature canopy, circular **THE JOKE SHOP** sign, red curtains, west entrance, palms, dry hillside with houses, neighbouring frontage and the opposite side of Sunset all read from the door camera. Day lighting and late lighting both look correct; the guard can walk the sidewalk without leaving the nav bounds. 44. The Joke Shop opens on the Sunset Strip club: the black hall, signature canopy, circular **THE JOKE SHOP** sign, red curtains, west entrance, palms, dry hillside with houses, neighbouring frontage and the opposite side of Sunset all read from the door camera. Day lighting and late lighting both look correct; the guard can walk the sidewalk without leaving the nav bounds.
45. Esc and right-click close only the top panel (Help, Settings, Pause, Review, Radio, timeline, inspection/wand, action wheel); Settings opened from the venue menu returns to the menu, Help no longer opens Pause behind it, and closing a modal recaptures the mouse without leaving the guard frozen. 45. Esc and right-click close only the top panel (Help, Settings, Pause, Review, Radio, timeline, inspection/wand, action wheel); Settings opened from the venue menu returns to the menu, Help no longer opens Pause behind it, and closing a modal recaptures the mouse without leaving the guard frozen.
46. At 0.75× and 1.75× HUD font scale, long target names stay inside the target chip, the chip grows/caps correctly, and long button labels ellipsize instead of pushing panels off-screen.
+18 -3
View File
@@ -1,6 +1,6 @@
# UI copy and button inventory # UI copy and button inventory
Status: dialogue, button-state, focus and modal-close passes complete; layout/end-to-end audit in progress for 0.9.3. Status: dialogue, button-state, focus, modal-close and font/layout passes complete; destructive-action and end-to-end audit in progress for 0.9.3.
## Screen inventory ## Screen inventory
@@ -88,7 +88,7 @@ silent profiles can still return an empty context line.
| Tutorial / Help | The wheel verb is **Ask**, but help and the HUD key strip called it “polite direction”. | Both now say **Ask**; the tutorial tells the player to choose Ask after closing the panel. | | Tutorial / Help | The wheel verb is **Ask**, but help and the HUD key strip called it “polite direction”. | Both now say **Ask**; the tutorial tells the player to choose Ask after closing the panel. |
| Review / campaign | Continue/return/quit labels and campaign-next copy checked. | No ambiguity found; no change. | | Review / campaign | Continue/return/quit labels and campaign-next copy checked. | No ambiguity found; no change. |
| Repetition | Shared tourist/confused/delivery lines recur across venues by design; role-specific reason lines prevent every archetype sounding alike. | Accepted for the shared-archetype model. | | Repetition | Shared tourist/confused/delivery lines recur across venues by design; role-specific reason lines prevent every archetype sounding alike. | Accepted for the shared-archetype model. |
| Localization length | Longest authored NPC/door line is 56 characters; longest UI string is `hud.controls` at 172 characters. | Dialogue fits the HUD log; the 1.5–2× string/font-scale layout test remains a separate 0.9.3 task. | | Localization length | Longest authored NPC/door line is 56 characters; longest UI string is `hud.controls` at 172 characters. | Dialogue fits the HUD log; long-string/font-scale layout is covered below. |
## Button state audit ## Button state audit
@@ -152,10 +152,25 @@ resumes; `_show_start_menu()` keeps the pointer visible at the menu.
The session smoke now covers menu → Settings → Cancel → menu, Help closing on The session smoke now covers menu → Settings → Cancel → menu, Help closing on
right-click, and Settings cancel from pause leaving the guard unfrozen. right-click, and Settings cancel from pause leaving the guard unfrozen.
## Font-scale and long-string layout
The HUD scale is tested at its **0.75×** and **1.75×** bounds. HUD top/time labels
clip instead of crossing into each other, and the target chip now measures the
scaled target text: normal long names widen the chip, while an extreme synthetic
name is capped at 420 px and ellipsized. Action-button labels use `clip_text` and
ellipsis in `UiStyles`, so a 1.5–2× pseudo-string cannot push a panel beyond the
viewport. The menu shell intentionally keeps its own fixed typography; the HUD
scale setting only changes HUD text.
Regression coverage lives in `test_accessibility.gd` (target-chip width/cap) and
`test_ui_buttons.gd` (long action-label minimum width). The pseudo-locale
translator workflow remains a 0.10.0 task; this pass uses deterministic long
strings and the maximum supported HUD scale.
## Remaining audit work ## Remaining audit work
- Player-facing labels are now routed through `content/strings.en.json`; numeric - Player-facing labels are now routed through `content/strings.en.json`; numeric
values, empty-state clears and debug-only text remain in code by design. values, empty-state clears and debug-only text remain in code by design.
- Focus/hover/pressed/disabled states and keyboard/gamepad rings are recorded above. - Focus/hover/pressed/disabled states and keyboard/gamepad rings are recorded above.
- Test long strings and 1.75x HUD font scale. - Long strings and 0.75×/1.75× HUD scale are covered above.
- Capture every dialog/button state and run `screen_designs.py compare` plus the pinned judge. - Capture every dialog/button state and run `screen_designs.py compare` plus the pinned judge.
+17
View File
@@ -82,9 +82,11 @@ func _init() -> void:
top_label = Label.new() top_label = Label.new()
top_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL top_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
top_label.clip_text = true
top.add_child(top_label) top.add_child(top_label)
time_label = Label.new() time_label = Label.new()
time_label.clip_text = true
top.add_child(time_label) top.add_child(time_label)
var spacer := Control.new() var spacer := Control.new()
@@ -122,6 +124,7 @@ func _init() -> void:
target_label = Label.new() target_label = Label.new()
target_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER target_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
target_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER target_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
target_label.clip_text = true
target_label.add_theme_font_size_override("font_size", 17) target_label.add_theme_font_size_override("font_size", 17)
_target_chip.add_child(target_label) _target_chip.add_child(target_label)
@@ -394,6 +397,17 @@ func set_target_label(text: String) -> void:
target_label.text = text target_label.text = text
if _target_chip: if _target_chip:
_target_chip.visible = text.strip_edges() != "" _target_chip.visible = text.strip_edges() != ""
# The chip is fixed around the crosshair; widen it for long names and
# larger HUD font scales instead of letting the text spill over the world.
var font := target_label.get_theme_font("font")
var font_size := target_label.get_theme_font_size("font_size")
var text_width := font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size).x
var chip_width := clampf(text_width + 32.0, 180.0, 420.0)
_target_chip.offset_left = -chip_width * 0.5
_target_chip.offset_right = chip_width * 0.5
var chip_height := maxf(40.0, font.get_height(font_size) + 16.0)
_target_chip.offset_top = 38.0
_target_chip.offset_bottom = 38.0 + chip_height
## How many people the aim-free ring (Tab / mouse wheel) can cycle through. ## How many people the aim-free ring (Tab / mouse wheel) can cycle through.
func set_nearby_hint(count: int) -> void: func set_nearby_hint(count: int) -> void:
@@ -447,6 +461,9 @@ func set_font_scale(scale: float) -> void:
_base_font_sizes[label] = label.get_theme_font_size("font_size") _base_font_sizes[label] = label.get_theme_font_size("font_size")
var base: int = int(_base_font_sizes[label]) var base: int = int(_base_font_sizes[label])
label.add_theme_font_size_override("font_size", maxi(8, int(round(float(base) * clamped)))) label.add_theme_font_size_override("font_size", maxi(8, int(round(float(base) * clamped))))
# Recompute the target chip after its font size changed.
if target_label:
set_target_label(target_label.text)
func _make_icon(texture: Texture2D) -> TextureRect: func _make_icon(texture: Texture2D) -> TextureRect:
var tr := TextureRect.new() var tr := TextureRect.new()
+4
View File
@@ -21,6 +21,10 @@ static func apply_button(button: Button, font_size: int = 0, compact: bool = fal
return return
if font_size > 0: if font_size > 0:
button.add_theme_font_size_override("font_size", font_size) button.add_theme_font_size_override("font_size", font_size)
# Long localized labels must not push a panel past the viewport. The
# buttons keep their existing minimum width and ellipsize if needed.
button.clip_text = true
button.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
var margin_h := 12 if compact else 18 var margin_h := 12 if compact else 18
var margin_v := 7 if compact else 10 var margin_v := 7 if compact else 10
var normal := _box(SURFACE, margin_h, margin_v) var normal := _box(SURFACE, margin_h, margin_v)
+11 -1
View File
@@ -49,8 +49,18 @@ func test_font_scale_applies_to_core_labels() -> bool:
hud.set_font_scale(9.0) hud.set_font_scale(9.0)
var capped: int = hud.top_label.get_theme_font_size("font_size") var capped: int = hud.top_label.get_theme_font_size("font_size")
var f := TestUtils.check(capped <= int(round(float(base_top) * 1.75)) + 1, "scale clamps at 1.75") var f := TestUtils.check(capped <= int(round(float(base_top) * 1.75)) + 1, "scale clamps at 1.75")
# The target chip must widen with the scaled font instead of spilling over
# the world; an extreme name is capped rather than pushing off-screen.
hud.set_font_scale(1.75)
hud.set_target_label("Intoxicated Pedestrian")
var chip = hud.get("_target_chip")
var chip_width: float = float(chip.offset_right) - float(chip.offset_left)
var g := TestUtils.check(chip_width > 300.0, "target chip widens at max font scale")
hud.set_target_label("An Extraordinarily Long Display Name For A Visitor In A Pseudo Locale")
var long_width: float = float(chip.offset_right) - float(chip.offset_left)
var h := TestUtils.check(long_width <= 420.5, "target chip caps an extreme name")
hud.free() hud.free()
return a and b and c and d and e and f return a and b and c and d and e and f and g and h
func test_settings_font_scale_is_applied_after_ui_exists() -> bool: func test_settings_font_scale_is_applied_after_ui_exists() -> bool:
## The HUD is created after settings load, so the manager must re-apply. ## The HUD is created after settings load, so the manager must re-apply.
+5 -1
View File
@@ -23,8 +23,12 @@ func test_shared_button_states_are_complete() -> bool:
var c := TestUtils.check(disabled != null and normal != null var c := TestUtils.check(disabled != null and normal != null
and disabled.bg_color != normal.bg_color, and disabled.bg_color != normal.bg_color,
"the disabled state is visually distinct from normal") "the disabled state is visually distinct from normal")
button.text = "A very long localized action label that must not push the panel beyond the viewport"
var d := TestUtils.check(button.clip_text
and button.get_combined_minimum_size().x < 100.0,
"long action labels ellipsize instead of forcing a wide panel")
button.free() button.free()
return a and b and c return a and b and c and d
func test_focus_helper_skips_hidden_and_disabled_controls() -> bool: func test_focus_helper_skips_hidden_and_disabled_controls() -> bool:
var hidden := Button.new() var hidden := Button.new()