Unify action-button states across the UI
- Add UiStyles with explicit normal, hover, pressed, focus and disabled styles and apply it to every action button. - Disable timeline Previous/Next at the first/last event and keep Close always available. - Style settings reset-dialog buttons and cover the button-state audit in docs/ui-copy.md. - Add shared-state and timeline-boundary regression tests.
This commit is contained in:
1 parent
98c31fe44c
commit
9692f262f1
15 files changed
+172
-75
No files matched your search
@@ -8,6 +8,9 @@ Semantic Versioning.
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- All session, settings, radio, Help, review and timeline action buttons now
|
||||||
|
share explicit normal, hover, pressed, focus and disabled styles. Timeline
|
||||||
|
Previous/Next disable at their boundaries.
|
||||||
- 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.
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ 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.
|
||||||
|
- **Button states (2026-11-02):** `src/game/ui/ui_styles.gd` owns the five action-button
|
||||||
|
states and font colours for the dark session/UI shell. Use `UiStyles.apply_button()`
|
||||||
|
for every new `Button`; CheckButton/OptionButton keep the engine theme. Timeline
|
||||||
|
Previous/Next disable at the ends; F1 tutorial skip is keyboard-only by design.
|
||||||
- **Version and roadmap renumber (2026-11-02):** source is now **0.9.2**; the
|
- **Version and roadmap renumber (2026-11-02):** source is now **0.9.2**; the
|
||||||
completed map/NPC work is the patch content. Postponed work is renumbered to
|
completed map/NPC work is the patch content. Postponed work is renumbered to
|
||||||
**0.9.3 UI**, **0.9.4 NPC**, **0.9.5 textures**, **0.9.6 integration**, then the
|
**0.9.3 UI**, **0.9.4 NPC**, **0.9.5 textures**, **0.9.6 integration**, then the
|
||||||
@@ -702,7 +706,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
|||||||
- Venue `door_staff_lines` are logged through `GameManager` (inspect / wand / refuse). They are not a separate dialogue UI.
|
- Venue `door_staff_lines` are logged through `GameManager` (inspect / wand / refuse). They are not a separate dialogue UI.
|
||||||
- Access loadout is implemented; do not re-fix `override_access` as if it were still unread.
|
- Access loadout is implemented; do not re-fix `override_access` as if it were still unread.
|
||||||
- `Save.SETTINGS_FILE` is declared and unused; settings persistence goes through `Save.load_settings()` / `save_settings()`.
|
- `Save.SETTINGS_FILE` is declared and unused; settings persistence goes through `Save.load_settings()` / `save_settings()`.
|
||||||
- Headless suite is **258/258**; the windowed smokes, screenshot compare and judge pass. Remaining visual follow-ups are in `TODO.md`.
|
- Headless suite is **259/259**; the windowed smokes, screenshot compare and judge pass. Remaining visual follow-ups are in `TODO.md`.
|
||||||
- Typed spawn signal extra args: handlers must accept the 4th `event: Dictionary` or Godot will not call them (`test_shift_coordinator_runs_a_shift` failed until `_on_spawn` took four args).
|
- Typed spawn signal extra args: handlers must accept the 4th `event: Dictionary` or Godot will not call them (`test_shift_coordinator_runs_a_shift` failed until `_on_spawn` took four args).
|
||||||
- GDScript inference trap that broke the main scene: a `var` typed as a base class
|
- GDScript inference trap that broke the main scene: a `var` typed as a base class
|
||||||
(`radial: CanvasLayer`) exposes script members as `Variant`, so `var x := radial.is_open` is a
|
(`radial: CanvasLayer`) exposes script members as `Variant`, so `var x := radial.is_open` is a
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ see `AGENTS.md` for the workflow and `MEMORY.md` for the decisions behind these
|
|||||||
|
|
||||||
Baseline: **v0.9.2** (2026-11-02). Three playable venues, tutorial/campaign/checkpoint resume,
|
Baseline: **v0.9.2** (2026-11-02). Three playable venues, tutorial/campaign/checkpoint resume,
|
||||||
difficulty/help, audio/accessibility settings, a 24-NPC budget baseline, a complete
|
difficulty/help, audio/accessibility settings, a 24-NPC budget baseline, a complete
|
||||||
escalation/access/radio loop, shift review, the Blender asset library, 258 headless + 97 Python
|
escalation/access/radio loop, shift review, the Blender asset library, 259 headless + 97 Python
|
||||||
tests, a clean screen-design judge and an optional Forward+ Desktop HQ build. This is a strong
|
tests, a clean screen-design judge and an optional Forward+ Desktop HQ build. This is a strong
|
||||||
release-candidate foundation, not yet a 0.10.0 release candidate.
|
release-candidate foundation, not yet a 0.10.0 release candidate.
|
||||||
|
|
||||||
@@ -76,9 +76,11 @@ screen-design judge before any model/texture changes. No gameplay rules change.
|
|||||||
|
|
||||||
### Button and panel behavior
|
### Button and panel behavior
|
||||||
|
|
||||||
- [ ] Audit normal/hover/pressed/focus/disabled states on every button, including
|
- [x] Audit normal/hover/pressed/focus/disabled states on every button, including
|
||||||
tutorial skip, venue continue, settings apply/cancel/reset, judge/review,
|
tutorial skip, venue continue, settings apply/cancel/reset, judge/review,
|
||||||
radio send/emergency hold and Help close.
|
radio send/emergency hold and Help close. (Shared `UiStyles` states applied;
|
||||||
|
timeline boundary disabling and the keyboard-only F1 skip are documented in
|
||||||
|
`docs/ui-copy.md`.)
|
||||||
- [ ] Verify keyboard-only and gamepad-only navigation: focus order, wrapping,
|
- [ ] Verify keyboard-only and gamepad-only navigation: focus order, wrapping,
|
||||||
default focus, confirm/cancel behavior and no focus traps.
|
default focus, confirm/cancel behavior and no focus traps.
|
||||||
- [ ] Verify every modal closes with Esc/right-click where documented, recaptures
|
- [ ] Verify every modal closes with Esc/right-click where documented, recaptures
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Implemented so far:
|
|||||||
- Godot 4.7.2 project scaffold, input map, procedural world builder (CI/local/release unified)
|
- Godot 4.7.2 project scaffold, input map, procedural world builder (CI/local/release unified)
|
||||||
- Venue-driven simulation core: `ShiftCoordinator`, `VenueScenario`, `EventScheduler`, `NPCController` + FSM, `AccessVerifier`, `EscalationManager`, `RadioSystem`, `IncidentRecorder`, `ReputationEvaluator`, `CrowdSystem`
|
- Venue-driven simulation core: `ShiftCoordinator`, `VenueScenario`, `EventScheduler`, `NPCController` + FSM, `AccessVerifier`, `EscalationManager`, `RadioSystem`, `IncidentRecorder`, `ReputationEvaluator`, `CrowdSystem`
|
||||||
- Three authored scenarios (El Capitan Entertainment Centre / Hollywood Boulevard, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools
|
- Three authored scenarios (El Capitan Entertainment Centre / Hollywood Boulevard, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools
|
||||||
- Headless unit tests: **258 passing** (access rules and venue-correct access loadout, escalation, radio cooldown/battery, NPC reaction determinism, door lock, flashlight perception, persistence round-trip, settings round-trip, input remaps, navmesh, step colliders, skyline alpha, scenario data, full-shift simulation, wand screening, responder dispatch, localization strings, focus cone, action wheel, session shell, PLAN verbs and incident recording, crowd presentation, emergency aftermath, body-camera timeline, NPC theater poses, audio, accessibility, authored scenario content, string formatting, `.tres` venue round-trip, surface materials and environment, focus ranking and outfit/gait helpers, generated prop models, web export preset, prop atlas material, player movement, NPC character layers/LOD/queue, input bindings)
|
- Headless unit tests: **259 passing** (access rules and venue-correct access loadout, escalation, radio cooldown/battery, NPC reaction determinism, door lock, flashlight perception, persistence round-trip, settings round-trip, input remaps, navmesh, step colliders, skyline alpha, scenario data, full-shift simulation, wand screening, responder dispatch, localization strings, focus cone, action wheel, session shell, PLAN verbs and incident recording, crowd presentation, emergency aftermath, body-camera timeline, NPC theater poses, audio, accessibility, authored scenario content, string formatting, `.tres` venue round-trip, surface materials and environment, focus ranking and outfit/gait helpers, generated prop models, web export preset, prop atlas material, player movement, NPC character layers/LOD/queue, input bindings)
|
||||||
- Simulation extras: protester archetype, CALL_SOMEONE reaction, door lock/secure mechanic (visual indicator), flashlight with night perception bonus, LATE shift phase reaching the violent-threat event
|
- Simulation extras: protester archetype, CALL_SOMEONE reaction, door lock/secure mechanic (visual indicator), flashlight with night perception bonus, LATE shift phase reaching the violent-threat event
|
||||||
- First-person prototype with interaction HUD, access inspection, wand screening, radio report form (incl. emergency hold-to-confirm), dispatched responder avatars, aim-free targeting, venue cycling between all three scenarios, color-coded escalation feedback, procedural audio cues, navigation-mesh NPC paths, JSON-backed localized strings, persisted shift history, debug overlay, and shift review
|
- First-person prototype with interaction HUD, access inspection, wand screening, radio report form (incl. emergency hold-to-confirm), dispatched responder avatars, aim-free targeting, venue cycling between all three scenarios, color-coded escalation feedback, procedural audio cues, navigation-mesh NPC paths, JSON-backed localized strings, persisted shift history, debug overlay, and shift review
|
||||||
- Control pass: focus cone on NPC heads with a billboarded blue circle in front of the face and name, action wheel (LMB/Space) with de-escalation-first slices, mouse-look freeze while a panel is open, acceleration-based walking, and named input actions (`action_menu`, `confirm`, `cancel`, `inspect`, `wand`)
|
- Control pass: focus cone on NPC heads with a billboarded blue circle in front of the face and name, action wheel (LMB/Space) with de-escalation-first slices, mouse-look freeze while a panel is open, acceleration-based walking, and named input actions (`action_menu`, `confirm`, `cancel`, `inspect`, `wand`)
|
||||||
@@ -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.
|
||||||
|
- 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`.
|
||||||
- Dialogue pass (0.9.3 work): NPC reason answers now use the authored `reason` context, with an in-character answer for every shared archetype; Hollywood and Studio gained door-staff lines; radio cancellation, tutorial Ask terminology, help, start hints and campaign reset copy were clarified. Audit recorded in `docs/ui-copy.md`.
|
- Dialogue pass (0.9.3 work): NPC reason answers now use the authored `reason` context, with an in-character answer for every shared archetype; Hollywood and Studio gained door-staff lines; radio cancellation, tutorial Ask terminology, help, start hints and campaign reset copy were clarified. Audit recorded in `docs/ui-copy.md`.
|
||||||
- Pre-build NPC review (2026-11-02): all eight library characters were staged at portrait, conversation and crowd distance on Hollywood and the Joke Shop, in day and night lighting, with the normal crowd hidden (`src/test/capture_npc_review.gd`). No P0 issues were found. Two P1 gear-fit issues were fixed with runtime attach offsets: the authorized-guest lanyard badge now clears the jacket and the firefighter helmet sits above the eyes. Backpack straps, caps, hands and the intentionally low-poly faces were accepted for the stylized target; close-range flashlight wash remains an optional-lighting follow-up. The 24-NPC budget stayed at 6.93 ms, the gallery passed 13 good / 0 bad and the screen-design judge stayed 13/13, and a clean portrait scored a 0.732 vision gap against the photoreal concept.
|
- Pre-build NPC review (2026-11-02): all eight library characters were staged at portrait, conversation and crowd distance on Hollywood and the Joke Shop, in day and night lighting, with the normal crowd hidden (`src/test/capture_npc_review.gd`). No P0 issues were found. Two P1 gear-fit issues were fixed with runtime attach offsets: the authorized-guest lanyard badge now clears the jacket and the firefighter helmet sits above the eyes. Backpack straps, caps, hands and the intentionally low-poly faces were accepted for the stylized target; close-range flashlight wash remains an optional-lighting follow-up. The 24-NPC budget stayed at 6.93 ms, the gallery passed 13 good / 0 bad and the screen-design judge stayed 13/13, and a clean portrait scored a 0.732 vision gap against the photoreal concept.
|
||||||
|
|
||||||
|
|||||||
+20
-1
@@ -1,6 +1,6 @@
|
|||||||
# UI copy and button inventory
|
# UI copy and button inventory
|
||||||
|
|
||||||
Status: dialogue pass complete; button/focus/font-scale audit in progress for 0.9.3.
|
Status: dialogue and button-state passes complete; focus/layout audit in progress for 0.9.3.
|
||||||
|
|
||||||
## Screen inventory
|
## Screen inventory
|
||||||
|
|
||||||
@@ -90,6 +90,25 @@ silent profiles can still return an empty context line.
|
|||||||
| 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; the 1.5–2× string/font-scale layout test remains a separate 0.9.3 task. |
|
||||||
|
|
||||||
|
## Button state audit
|
||||||
|
|
||||||
|
All `Button` controls now use `src/game/ui/ui_styles.gd`, which defines explicit
|
||||||
|
**normal**, **hover**, **pressed**, **focus** and **disabled** styles plus the
|
||||||
|
matching font colours. This removes the engine-theme fallback that previously
|
||||||
|
appeared when a custom dark button was disabled or focused by keyboard.
|
||||||
|
|
||||||
|
| Surface | States / behavior checked | Result |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Start menu | Venue rows, Continue, Help, Settings, Quit; focus clamped to the list; Continue hidden when the campaign is complete. | All buttons styled; hidden state is intentional, not a disabled state. |
|
||||||
|
| Tutorial | F1 skip is a keyboard shortcut, not a button; its hint is non-interactive. | Keyboard-only by design; no hover/focus state to style. |
|
||||||
|
| Pause | Resume, Restart, Settings, Quit to menu; Resume receives focus on open. | All four states styled; focus is distinct from hover. |
|
||||||
|
| Settings | Replay tutorial, Reset campaign, key-binding rows, Apply settings, Cancel; rebinding shows “Press…” while listening. | All action buttons styled; reset-confirmation dialog OK/Cancel styled; fields keep the engine control theme. |
|
||||||
|
| Help | Close; Esc still closes. | Styled; no disabled state is used. |
|
||||||
|
| Shift review | Continue campaign, Body camera timeline, Return to menu, Quit game; Continue hidden when the campaign is complete. | All styled; hidden state is intentional. |
|
||||||
|
| Timeline | Previous/Next/Close; Previous is disabled at the first event, Next at the last, and both when empty. | Disabled state verified in `test_event_timeline.gd`; Close stays enabled. |
|
||||||
|
| Radio | Send request, Send emergency, Cancel. Emergency hold uses press/release, so the action button stays enabled while holding. | All styled; disabling it during the hold would break confirmation. |
|
||||||
|
| Toggles | CheckButton/OptionButton controls keep the engine theme. | Intentional: the shared style targets action buttons, not checkbox/radio affordances. |
|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ signal closed()
|
|||||||
|
|
||||||
const Strings := preload("res://src/core/strings.gd")
|
const Strings := preload("res://src/core/strings.gd")
|
||||||
const UiArt := preload("res://src/game/ui/art.gd")
|
const UiArt := preload("res://src/game/ui/art.gd")
|
||||||
|
const UiStyles := preload("res://src/game/ui/ui_styles.gd")
|
||||||
|
|
||||||
var events: Array[Dictionary] = []
|
var events: Array[Dictionary] = []
|
||||||
var index := 0
|
var index := 0
|
||||||
var _list_label: Label = null
|
var _list_label: Label = null
|
||||||
var _detail_label: Label = null
|
var _detail_label: Label = null
|
||||||
var _counter_label: Label = null
|
var _counter_label: Label = null
|
||||||
|
var _prev_button: Button = null
|
||||||
|
var _next_button: Button = null
|
||||||
var _root: Control = null
|
var _root: Control = null
|
||||||
|
|
||||||
func _init() -> void:
|
func _init() -> void:
|
||||||
@@ -90,22 +93,25 @@ func _build() -> void:
|
|||||||
row.add_theme_constant_override("separation", 10)
|
row.add_theme_constant_override("separation", 10)
|
||||||
column.add_child(row)
|
column.add_child(row)
|
||||||
|
|
||||||
var prev := Button.new()
|
_prev_button = Button.new()
|
||||||
prev.name = "PrevButton"
|
_prev_button.name = "PrevButton"
|
||||||
prev.text = Strings.lookup("timeline.prev")
|
_prev_button.text = Strings.lookup("timeline.prev")
|
||||||
prev.pressed.connect(func(): step(-1))
|
_prev_button.pressed.connect(func(): step(-1))
|
||||||
row.add_child(prev)
|
UiStyles.apply_button(_prev_button, 15, true)
|
||||||
|
row.add_child(_prev_button)
|
||||||
|
|
||||||
var next := Button.new()
|
_next_button = Button.new()
|
||||||
next.name = "NextButton"
|
_next_button.name = "NextButton"
|
||||||
next.text = Strings.lookup("timeline.next")
|
_next_button.text = Strings.lookup("timeline.next")
|
||||||
next.pressed.connect(func(): step(1))
|
_next_button.pressed.connect(func(): step(1))
|
||||||
row.add_child(next)
|
UiStyles.apply_button(_next_button, 15, true)
|
||||||
|
row.add_child(_next_button)
|
||||||
|
|
||||||
var close := Button.new()
|
var close := Button.new()
|
||||||
close.name = "CloseButton"
|
close.name = "CloseButton"
|
||||||
close.text = Strings.lookup("timeline.close")
|
close.text = Strings.lookup("timeline.close")
|
||||||
close.pressed.connect(_on_close)
|
close.pressed.connect(_on_close)
|
||||||
|
UiStyles.apply_button(close, 15, true)
|
||||||
row.add_child(close)
|
row.add_child(close)
|
||||||
|
|
||||||
var hint := Label.new()
|
var hint := Label.new()
|
||||||
@@ -154,7 +160,12 @@ func _on_close() -> void:
|
|||||||
close()
|
close()
|
||||||
|
|
||||||
func _refresh() -> void:
|
func _refresh() -> void:
|
||||||
if events.is_empty():
|
var has_events := not events.is_empty()
|
||||||
|
if _prev_button:
|
||||||
|
_prev_button.disabled = not has_events or index <= 0
|
||||||
|
if _next_button:
|
||||||
|
_next_button.disabled = not has_events or index >= events.size() - 1
|
||||||
|
if not has_events:
|
||||||
_counter_label.text = Strings.lookup("timeline.empty")
|
_counter_label.text = Strings.lookup("timeline.empty")
|
||||||
_detail_label.text = ""
|
_detail_label.text = ""
|
||||||
_list_label.text = ""
|
_list_label.text = ""
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ extends CanvasLayer
|
|||||||
|
|
||||||
const Strings := preload("res://src/core/strings.gd")
|
const Strings := preload("res://src/core/strings.gd")
|
||||||
const InputBindings := preload("res://src/core/input_bindings.gd")
|
const InputBindings := preload("res://src/core/input_bindings.gd")
|
||||||
|
const UiStyles := preload("res://src/game/ui/ui_styles.gd")
|
||||||
|
|
||||||
signal closed()
|
signal closed()
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@ func _build() -> void:
|
|||||||
close.text = Strings.lookup("ui.help_close")
|
close.text = Strings.lookup("ui.help_close")
|
||||||
close.pressed.connect(func(): close_panel())
|
close.pressed.connect(func(): close_panel())
|
||||||
close.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
close.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
|
UiStyles.apply_button(close, 16)
|
||||||
column.add_child(close)
|
column.add_child(close)
|
||||||
|
|
||||||
func open(policy: String = "") -> void:
|
func open(policy: String = "") -> void:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ signal settings_requested()
|
|||||||
signal quit_to_menu_requested()
|
signal quit_to_menu_requested()
|
||||||
|
|
||||||
const UiArt := preload("res://src/game/ui/art.gd")
|
const UiArt := preload("res://src/game/ui/art.gd")
|
||||||
|
const UiStyles := preload("res://src/game/ui/ui_styles.gd")
|
||||||
const Strings := preload("res://src/core/strings.gd")
|
const Strings := preload("res://src/core/strings.gd")
|
||||||
|
|
||||||
var _root: Control = null
|
var _root: Control = null
|
||||||
@@ -67,19 +68,7 @@ func _add_button(parent: Control, text: String, node_name: String, cb: Callable)
|
|||||||
button.name = node_name
|
button.name = node_name
|
||||||
button.text = text
|
button.text = text
|
||||||
button.custom_minimum_size = Vector2(380, 52)
|
button.custom_minimum_size = Vector2(380, 52)
|
||||||
button.add_theme_font_size_override("font_size", 22)
|
UiStyles.apply_button(button, 22)
|
||||||
var normal := StyleBoxFlat.new()
|
|
||||||
normal.bg_color = Color(0.115, 0.125, 0.15, 0.96)
|
|
||||||
normal.corner_radius_top_left = 12
|
|
||||||
normal.corner_radius_top_right = 12
|
|
||||||
normal.corner_radius_bottom_left = 12
|
|
||||||
normal.corner_radius_bottom_right = 12
|
|
||||||
var hover := normal.duplicate() as StyleBoxFlat
|
|
||||||
hover.bg_color = Color(0.16, 0.175, 0.21, 0.98)
|
|
||||||
button.add_theme_stylebox_override("normal", normal)
|
|
||||||
button.add_theme_stylebox_override("hover", hover)
|
|
||||||
button.add_theme_stylebox_override("focus", hover)
|
|
||||||
button.add_theme_stylebox_override("pressed", hover)
|
|
||||||
button.pressed.connect(cb)
|
button.pressed.connect(cb)
|
||||||
parent.add_child(button)
|
parent.add_child(button)
|
||||||
return button
|
return button
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ signal cancelled()
|
|||||||
const Rules := preload("res://src/core/rules.gd")
|
const Rules := preload("res://src/core/rules.gd")
|
||||||
const Strings := preload("res://src/core/strings.gd")
|
const Strings := preload("res://src/core/strings.gd")
|
||||||
const UiArt := preload("res://src/game/ui/art.gd")
|
const UiArt := preload("res://src/game/ui/art.gd")
|
||||||
|
const UiStyles := preload("res://src/game/ui/ui_styles.gd")
|
||||||
|
|
||||||
var panel: PanelContainer
|
var panel: PanelContainer
|
||||||
var npc_name_label: Label
|
var npc_name_label: Label
|
||||||
@@ -107,10 +108,12 @@ func _init() -> void:
|
|||||||
action_button.button_down.connect(func(): _start_hold())
|
action_button.button_down.connect(func(): _start_hold())
|
||||||
action_button.button_up.connect(func(): _stop_hold(false))
|
action_button.button_up.connect(func(): _stop_hold(false))
|
||||||
action_button.pressed.connect(_on_action_pressed)
|
action_button.pressed.connect(_on_action_pressed)
|
||||||
|
UiStyles.apply_button(action_button, 16)
|
||||||
buttons.add_child(action_button)
|
buttons.add_child(action_button)
|
||||||
var cancel_btn := Button.new()
|
var cancel_btn := Button.new()
|
||||||
cancel_btn.text = Strings.lookup("ui.radio_cancel")
|
cancel_btn.text = Strings.lookup("ui.radio_cancel")
|
||||||
cancel_btn.pressed.connect(func(): cancelled.emit())
|
cancel_btn.pressed.connect(func(): cancelled.emit())
|
||||||
|
UiStyles.apply_button(cancel_btn, 16)
|
||||||
buttons.add_child(cancel_btn)
|
buttons.add_child(cancel_btn)
|
||||||
|
|
||||||
var _title_label: Label
|
var _title_label: Label
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ extends CanvasLayer
|
|||||||
|
|
||||||
const InputBindings := preload("res://src/core/input_bindings.gd")
|
const InputBindings := preload("res://src/core/input_bindings.gd")
|
||||||
const UiArt := preload("res://src/game/ui/art.gd")
|
const UiArt := preload("res://src/game/ui/art.gd")
|
||||||
|
const UiStyles := preload("res://src/game/ui/ui_styles.gd")
|
||||||
const Strings := preload("res://src/core/strings.gd")
|
const Strings := preload("res://src/core/strings.gd")
|
||||||
|
|
||||||
signal applied(settings: Dictionary)
|
signal applied(settings: Dictionary)
|
||||||
@@ -135,11 +136,13 @@ func _build() -> void:
|
|||||||
replay.name = "ReplayTutorialButton"
|
replay.name = "ReplayTutorialButton"
|
||||||
replay.text = Strings.lookup("ui.settings_replay")
|
replay.text = Strings.lookup("ui.settings_replay")
|
||||||
replay.pressed.connect(func(): replay_tutorial_requested.emit())
|
replay.pressed.connect(func(): replay_tutorial_requested.emit())
|
||||||
|
UiStyles.apply_button(replay, 16)
|
||||||
replay_row.add_child(replay)
|
replay_row.add_child(replay)
|
||||||
var reset_campaign := Button.new()
|
var reset_campaign := Button.new()
|
||||||
reset_campaign.name = "ResetCampaignButton"
|
reset_campaign.name = "ResetCampaignButton"
|
||||||
reset_campaign.text = Strings.lookup("ui.settings_reset_campaign")
|
reset_campaign.text = Strings.lookup("ui.settings_reset_campaign")
|
||||||
reset_campaign.pressed.connect(_confirm_reset_campaign)
|
reset_campaign.pressed.connect(_confirm_reset_campaign)
|
||||||
|
UiStyles.apply_button(reset_campaign, 16)
|
||||||
replay_row.add_child(reset_campaign)
|
replay_row.add_child(reset_campaign)
|
||||||
|
|
||||||
var controls_title := Label.new()
|
var controls_title := Label.new()
|
||||||
@@ -160,6 +163,7 @@ func _build() -> void:
|
|||||||
var button := Button.new()
|
var button := Button.new()
|
||||||
button.custom_minimum_size = Vector2(130, 0)
|
button.custom_minimum_size = Vector2(130, 0)
|
||||||
button.pressed.connect(_on_rebind.bind(action))
|
button.pressed.connect(_on_rebind.bind(action))
|
||||||
|
UiStyles.apply_button(button, 14, true)
|
||||||
grid.add_child(button)
|
grid.add_child(button)
|
||||||
_bind_buttons[action] = button
|
_bind_buttons[action] = button
|
||||||
|
|
||||||
@@ -172,12 +176,14 @@ func _build() -> void:
|
|||||||
save_button.name = "ApplyButton"
|
save_button.name = "ApplyButton"
|
||||||
save_button.text = Strings.lookup("ui.apply_settings")
|
save_button.text = Strings.lookup("ui.apply_settings")
|
||||||
save_button.pressed.connect(_on_apply)
|
save_button.pressed.connect(_on_apply)
|
||||||
|
UiStyles.apply_button(save_button, 16)
|
||||||
row.add_child(save_button)
|
row.add_child(save_button)
|
||||||
|
|
||||||
var cancel_button := Button.new()
|
var cancel_button := Button.new()
|
||||||
cancel_button.name = "CancelButton"
|
cancel_button.name = "CancelButton"
|
||||||
cancel_button.text = Strings.lookup("ui.settings_cancel")
|
cancel_button.text = Strings.lookup("ui.settings_cancel")
|
||||||
cancel_button.pressed.connect(_on_cancel)
|
cancel_button.pressed.connect(_on_cancel)
|
||||||
|
UiStyles.apply_button(cancel_button, 16)
|
||||||
row.add_child(cancel_button)
|
row.add_child(cancel_button)
|
||||||
|
|
||||||
func _add_label_row(parent: Control, text: String) -> Label:
|
func _add_label_row(parent: Control, text: String) -> Label:
|
||||||
@@ -273,6 +279,8 @@ func _confirm_reset_campaign() -> void:
|
|||||||
dialog.dialog_text = Strings.lookup("ui.settings_reset_confirm")
|
dialog.dialog_text = Strings.lookup("ui.settings_reset_confirm")
|
||||||
dialog.confirmed.connect(func(): reset_campaign_requested.emit())
|
dialog.confirmed.connect(func(): reset_campaign_requested.emit())
|
||||||
add_child(dialog)
|
add_child(dialog)
|
||||||
|
UiStyles.apply_button(dialog.get_ok_button(), 16)
|
||||||
|
UiStyles.apply_button(dialog.get_cancel_button(), 16)
|
||||||
dialog.popup_centered()
|
dialog.popup_centered()
|
||||||
dialog.confirmed.connect(dialog.queue_free)
|
dialog.confirmed.connect(dialog.queue_free)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ extends CanvasLayer
|
|||||||
|
|
||||||
const Strings := preload("res://src/core/strings.gd")
|
const Strings := preload("res://src/core/strings.gd")
|
||||||
const UiArt := preload("res://src/game/ui/art.gd")
|
const UiArt := preload("res://src/game/ui/art.gd")
|
||||||
|
const UiStyles := preload("res://src/game/ui/ui_styles.gd")
|
||||||
|
|
||||||
signal restart_requested()
|
signal restart_requested()
|
||||||
signal continue_requested()
|
signal continue_requested()
|
||||||
@@ -85,19 +86,7 @@ func _init() -> void:
|
|||||||
|
|
||||||
func _style_review_button(button: Button) -> void:
|
func _style_review_button(button: Button) -> void:
|
||||||
button.custom_minimum_size = Vector2(300, 52)
|
button.custom_minimum_size = Vector2(300, 52)
|
||||||
button.add_theme_font_size_override("font_size", 22)
|
UiStyles.apply_button(button, 22)
|
||||||
var normal := StyleBoxFlat.new()
|
|
||||||
normal.bg_color = Color(0.115, 0.125, 0.15, 0.96)
|
|
||||||
normal.corner_radius_top_left = 10
|
|
||||||
normal.corner_radius_top_right = 10
|
|
||||||
normal.corner_radius_bottom_left = 10
|
|
||||||
normal.corner_radius_bottom_right = 10
|
|
||||||
var hover := normal.duplicate() as StyleBoxFlat
|
|
||||||
hover.bg_color = Color(0.16, 0.175, 0.21, 0.98)
|
|
||||||
button.add_theme_stylebox_override("normal", normal)
|
|
||||||
button.add_theme_stylebox_override("hover", hover)
|
|
||||||
button.add_theme_stylebox_override("focus", hover)
|
|
||||||
button.add_theme_stylebox_override("pressed", hover)
|
|
||||||
|
|
||||||
func show_review(review: Dictionary, scenario: Resource, history: Array = [],
|
func show_review(review: Dictionary, scenario: Resource, history: Array = [],
|
||||||
campaign_summary: Dictionary = {}) -> void:
|
campaign_summary: Dictionary = {}) -> void:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ signal settings_requested()
|
|||||||
signal quit_requested()
|
signal quit_requested()
|
||||||
|
|
||||||
const UiArt := preload("res://src/game/ui/art.gd")
|
const UiArt := preload("res://src/game/ui/art.gd")
|
||||||
|
const UiStyles := preload("res://src/game/ui/ui_styles.gd")
|
||||||
const Strings := preload("res://src/core/strings.gd")
|
const Strings := preload("res://src/core/strings.gd")
|
||||||
|
|
||||||
const VENUES := [
|
const VENUES := [
|
||||||
@@ -108,7 +109,7 @@ func _build() -> void:
|
|||||||
_continue_button.name = "ContinueButton"
|
_continue_button.name = "ContinueButton"
|
||||||
_continue_button.custom_minimum_size = Vector2(460, 52)
|
_continue_button.custom_minimum_size = Vector2(460, 52)
|
||||||
_continue_button.pressed.connect(_on_continue_pressed)
|
_continue_button.pressed.connect(_on_continue_pressed)
|
||||||
_style_venue_button(_continue_button)
|
UiStyles.apply_button(_continue_button, 18)
|
||||||
column.add_child(_continue_button)
|
column.add_child(_continue_button)
|
||||||
|
|
||||||
for i in VENUES.size():
|
for i in VENUES.size():
|
||||||
@@ -119,7 +120,7 @@ func _build() -> void:
|
|||||||
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
button.custom_minimum_size = Vector2(460, 62)
|
button.custom_minimum_size = Vector2(460, 62)
|
||||||
button.pressed.connect(_on_venue_pressed.bind(i))
|
button.pressed.connect(_on_venue_pressed.bind(i))
|
||||||
_style_venue_button(button)
|
UiStyles.apply_button(button, 18)
|
||||||
column.add_child(button)
|
column.add_child(button)
|
||||||
_buttons.append(button)
|
_buttons.append(button)
|
||||||
|
|
||||||
@@ -136,18 +137,21 @@ func _build() -> void:
|
|||||||
help_button.name = "HelpButton"
|
help_button.name = "HelpButton"
|
||||||
help_button.text = Strings.lookup("ui.start_help")
|
help_button.text = Strings.lookup("ui.start_help")
|
||||||
help_button.pressed.connect(func(): help_requested.emit())
|
help_button.pressed.connect(func(): help_requested.emit())
|
||||||
|
UiStyles.apply_button(help_button, 16)
|
||||||
row.add_child(help_button)
|
row.add_child(help_button)
|
||||||
|
|
||||||
var settings_button := Button.new()
|
var settings_button := Button.new()
|
||||||
settings_button.name = "SettingsButton"
|
settings_button.name = "SettingsButton"
|
||||||
settings_button.text = Strings.lookup("ui.start_settings")
|
settings_button.text = Strings.lookup("ui.start_settings")
|
||||||
settings_button.pressed.connect(func(): settings_requested.emit())
|
settings_button.pressed.connect(func(): settings_requested.emit())
|
||||||
|
UiStyles.apply_button(settings_button, 16)
|
||||||
row.add_child(settings_button)
|
row.add_child(settings_button)
|
||||||
|
|
||||||
var quit_button := Button.new()
|
var quit_button := Button.new()
|
||||||
quit_button.name = "QuitButton"
|
quit_button.name = "QuitButton"
|
||||||
quit_button.text = Strings.lookup("ui.quit_game")
|
quit_button.text = Strings.lookup("ui.quit_game")
|
||||||
quit_button.pressed.connect(func(): quit_requested.emit())
|
quit_button.pressed.connect(func(): quit_requested.emit())
|
||||||
|
UiStyles.apply_button(quit_button, 16)
|
||||||
row.add_child(quit_button)
|
row.add_child(quit_button)
|
||||||
|
|
||||||
var hint := Label.new()
|
var hint := Label.new()
|
||||||
@@ -158,35 +162,6 @@ func _build() -> void:
|
|||||||
|
|
||||||
_focus_index(0)
|
_focus_index(0)
|
||||||
|
|
||||||
## Match the dark, rounded rows in the `start_menu` mockup.
|
|
||||||
func _style_venue_button(button: Button) -> void:
|
|
||||||
button.add_theme_font_size_override("font_size", 18)
|
|
||||||
button.add_theme_color_override("font_color", Color(0.94, 0.94, 0.92))
|
|
||||||
button.add_theme_color_override("font_hover_color", Color.WHITE)
|
|
||||||
var normal := StyleBoxFlat.new()
|
|
||||||
normal.bg_color = Color(0.115, 0.125, 0.15, 0.96)
|
|
||||||
normal.corner_radius_top_left = 12
|
|
||||||
normal.corner_radius_top_right = 12
|
|
||||||
normal.corner_radius_bottom_left = 12
|
|
||||||
normal.corner_radius_bottom_right = 12
|
|
||||||
normal.content_margin_left = 22
|
|
||||||
normal.content_margin_right = 22
|
|
||||||
normal.content_margin_top = 10
|
|
||||||
normal.content_margin_bottom = 10
|
|
||||||
var hover := normal.duplicate() as StyleBoxFlat
|
|
||||||
hover.bg_color = Color(0.16, 0.175, 0.21, 0.98)
|
|
||||||
var focus := normal.duplicate() as StyleBoxFlat
|
|
||||||
focus.bg_color = Color(0.14, 0.155, 0.19, 0.98)
|
|
||||||
focus.border_color = Color(0.85, 0.86, 0.88)
|
|
||||||
focus.border_width_left = 2
|
|
||||||
focus.border_width_top = 2
|
|
||||||
focus.border_width_right = 2
|
|
||||||
focus.border_width_bottom = 2
|
|
||||||
button.add_theme_stylebox_override("normal", normal)
|
|
||||||
button.add_theme_stylebox_override("hover", hover)
|
|
||||||
button.add_theme_stylebox_override("focus", focus)
|
|
||||||
button.add_theme_stylebox_override("pressed", hover)
|
|
||||||
|
|
||||||
func _focus_index(i: int) -> void:
|
func _focus_index(i: int) -> void:
|
||||||
if _buttons.is_empty():
|
if _buttons.is_empty():
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
class_name UiStyles
|
||||||
|
extends RefCounted
|
||||||
|
## Shared button states for the dark session/UI shell.
|
||||||
|
##
|
||||||
|
## Keeping one style table means every normal/hover/pressed/focused/disabled
|
||||||
|
## state is explicit. Buttons that use the Godot default theme otherwise look
|
||||||
|
## different from the custom start-menu, pause and review rows as soon as they
|
||||||
|
## are disabled or receive keyboard focus.
|
||||||
|
|
||||||
|
const TEXT := Color(0.94, 0.94, 0.92)
|
||||||
|
const TEXT_HOVER := Color.WHITE
|
||||||
|
const TEXT_DISABLED := Color(0.48, 0.50, 0.56)
|
||||||
|
const SURFACE := Color(0.115, 0.125, 0.15, 0.96)
|
||||||
|
const SURFACE_HOVER := Color(0.16, 0.175, 0.21, 0.98)
|
||||||
|
const SURFACE_PRESSED := Color(0.075, 0.085, 0.105, 0.98)
|
||||||
|
const SURFACE_DISABLED := Color(0.075, 0.08, 0.095, 0.72)
|
||||||
|
const ACCENT := Color(0.78, 0.82, 0.88, 0.9)
|
||||||
|
|
||||||
|
static func apply_button(button: Button, font_size: int = 0, compact: bool = false) -> void:
|
||||||
|
if button == null:
|
||||||
|
return
|
||||||
|
if font_size > 0:
|
||||||
|
button.add_theme_font_size_override("font_size", font_size)
|
||||||
|
var margin_h := 12 if compact else 18
|
||||||
|
var margin_v := 7 if compact else 10
|
||||||
|
var normal := _box(SURFACE, margin_h, margin_v)
|
||||||
|
var hover := _box(SURFACE_HOVER, margin_h, margin_v)
|
||||||
|
var pressed := _box(SURFACE_PRESSED, margin_h, margin_v)
|
||||||
|
var disabled := _box(SURFACE_DISABLED, margin_h, margin_v)
|
||||||
|
var focus := _box(Color(0, 0, 0, 0), margin_h, margin_v)
|
||||||
|
focus.border_color = ACCENT
|
||||||
|
focus.border_width_left = 2
|
||||||
|
focus.border_width_top = 2
|
||||||
|
focus.border_width_right = 2
|
||||||
|
focus.border_width_bottom = 2
|
||||||
|
button.add_theme_stylebox_override("normal", normal)
|
||||||
|
button.add_theme_stylebox_override("hover", hover)
|
||||||
|
button.add_theme_stylebox_override("pressed", pressed)
|
||||||
|
button.add_theme_stylebox_override("focus", focus)
|
||||||
|
button.add_theme_stylebox_override("disabled", disabled)
|
||||||
|
button.add_theme_color_override("font_color", TEXT)
|
||||||
|
button.add_theme_color_override("font_hover_color", TEXT_HOVER)
|
||||||
|
button.add_theme_color_override("font_pressed_color", TEXT_HOVER)
|
||||||
|
button.add_theme_color_override("font_focus_color", TEXT_HOVER)
|
||||||
|
button.add_theme_color_override("font_disabled_color", TEXT_DISABLED)
|
||||||
|
|
||||||
|
static func _box(color: Color, margin_h: int, margin_v: int) -> StyleBoxFlat:
|
||||||
|
var box := StyleBoxFlat.new()
|
||||||
|
box.bg_color = color
|
||||||
|
box.corner_radius_top_left = 10
|
||||||
|
box.corner_radius_top_right = 10
|
||||||
|
box.corner_radius_bottom_left = 10
|
||||||
|
box.corner_radius_bottom_right = 10
|
||||||
|
box.content_margin_left = margin_h
|
||||||
|
box.content_margin_right = margin_h
|
||||||
|
box.content_margin_top = margin_v
|
||||||
|
box.content_margin_bottom = margin_v
|
||||||
|
return box
|
||||||
@@ -79,14 +79,21 @@ func test_empty_timeline_is_safe() -> bool:
|
|||||||
func test_stepping_clamps() -> bool:
|
func test_stepping_clamps() -> bool:
|
||||||
var timeline: Node = EventTimeline.new()
|
var timeline: Node = EventTimeline.new()
|
||||||
timeline.show_timeline(_review())
|
timeline.show_timeline(_review())
|
||||||
|
var prev: Button = timeline.find_child("PrevButton", true, false) as Button
|
||||||
|
var next: Button = timeline.find_child("NextButton", true, false) as Button
|
||||||
|
var start_prev_disabled := prev != null and prev.disabled
|
||||||
|
var start_next_enabled := next != null and not next.disabled
|
||||||
timeline.step(-1)
|
timeline.step(-1)
|
||||||
var a := TestUtils.check_eq(timeline.index, 0, "cannot step before the first event")
|
var a := TestUtils.check_eq(timeline.index, 0, "cannot step before the first event")
|
||||||
timeline.step(1)
|
timeline.step(1)
|
||||||
var b := TestUtils.check_eq(timeline.index, 1, "next advances one event")
|
var b := TestUtils.check_eq(timeline.index, 1, "next advances one event")
|
||||||
timeline.step(99)
|
timeline.step(99)
|
||||||
var c := TestUtils.check_eq(timeline.index, 3, "cannot step past the last event")
|
var c := TestUtils.check_eq(timeline.index, 3, "cannot step past the last event")
|
||||||
|
var end_next_disabled := next != null and next.disabled
|
||||||
|
var d := TestUtils.check(start_prev_disabled and start_next_enabled and end_next_disabled,
|
||||||
|
"timeline boundary buttons disable at the ends")
|
||||||
timeline.free()
|
timeline.free()
|
||||||
return a and b and c
|
return a and b and c and d
|
||||||
|
|
||||||
func test_review_records_a_shift_start_stamp() -> bool:
|
func test_review_records_a_shift_start_stamp() -> bool:
|
||||||
var coordinator: Object = Coordinator.new()
|
var coordinator: Object = Coordinator.new()
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Shared UI button states: every normal/hover/pressed/focus/disabled state is
|
||||||
|
## explicitly styled so the custom dark panels do not fall back to the engine
|
||||||
|
## theme when a control is focused or disabled.
|
||||||
|
|
||||||
|
const UiStyles := preload("res://src/game/ui/ui_styles.gd")
|
||||||
|
|
||||||
|
func test_shared_button_states_are_complete() -> bool:
|
||||||
|
var button := Button.new()
|
||||||
|
UiStyles.apply_button(button, 16)
|
||||||
|
var states := ["normal", "hover", "pressed", "focus", "disabled"]
|
||||||
|
var missing: Array[String] = []
|
||||||
|
for state in states:
|
||||||
|
if not button.has_theme_stylebox_override(state):
|
||||||
|
missing.append(state)
|
||||||
|
var a := TestUtils.check(missing.is_empty(),
|
||||||
|
"all five button states have explicit styles (missing: %s)" % str(missing))
|
||||||
|
var b := TestUtils.check_eq(button.get_theme_font_size("font_size"), 16,
|
||||||
|
"the shared style applies the requested font size")
|
||||||
|
var disabled := button.get_theme_stylebox("disabled")
|
||||||
|
var normal := button.get_theme_stylebox("normal")
|
||||||
|
var c := TestUtils.check(disabled != null and normal != null
|
||||||
|
and disabled.bg_color != normal.bg_color,
|
||||||
|
"the disabled state is visually distinct from normal")
|
||||||
|
button.free()
|
||||||
|
return a and b and c
|
||||||
Reference in new issue
Block a user