Unify action-button states across the UI
Full build / Import + headless tests (push) Successful in 39s
Full build / Full desktop build + optional release (push) Skipped

- 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:
Gig Line Dev committed 2026-11-02 15:04:38 +01:00
1 parent 98c31fe44c
commit 9692f262f1
15 files changed
+172 -75

No files matched your search

+22 -11
View File
@@ -9,12 +9,15 @@ signal closed()
const Strings := preload("res://src/core/strings.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 index := 0
var _list_label: Label = null
var _detail_label: Label = null
var _counter_label: Label = null
var _prev_button: Button = null
var _next_button: Button = null
var _root: Control = null
func _init() -> void:
@@ -90,22 +93,25 @@ func _build() -> void:
row.add_theme_constant_override("separation", 10)
column.add_child(row)
var prev := Button.new()
prev.name = "PrevButton"
prev.text = Strings.lookup("timeline.prev")
prev.pressed.connect(func(): step(-1))
row.add_child(prev)
_prev_button = Button.new()
_prev_button.name = "PrevButton"
_prev_button.text = Strings.lookup("timeline.prev")
_prev_button.pressed.connect(func(): step(-1))
UiStyles.apply_button(_prev_button, 15, true)
row.add_child(_prev_button)
var next := Button.new()
next.name = "NextButton"
next.text = Strings.lookup("timeline.next")
next.pressed.connect(func(): step(1))
row.add_child(next)
_next_button = Button.new()
_next_button.name = "NextButton"
_next_button.text = Strings.lookup("timeline.next")
_next_button.pressed.connect(func(): step(1))
UiStyles.apply_button(_next_button, 15, true)
row.add_child(_next_button)
var close := Button.new()
close.name = "CloseButton"
close.text = Strings.lookup("timeline.close")
close.pressed.connect(_on_close)
UiStyles.apply_button(close, 15, true)
row.add_child(close)
var hint := Label.new()
@@ -154,7 +160,12 @@ func _on_close() -> void:
close()
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")
_detail_label.text = ""
_list_label.text = ""
+2
View File
@@ -4,6 +4,7 @@ extends CanvasLayer
const Strings := preload("res://src/core/strings.gd")
const InputBindings := preload("res://src/core/input_bindings.gd")
const UiStyles := preload("res://src/game/ui/ui_styles.gd")
signal closed()
@@ -70,6 +71,7 @@ func _build() -> void:
close.text = Strings.lookup("ui.help_close")
close.pressed.connect(func(): close_panel())
close.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
UiStyles.apply_button(close, 16)
column.add_child(close)
func open(policy: String = "") -> void:
+2 -13
View File
@@ -10,6 +10,7 @@ signal settings_requested()
signal quit_to_menu_requested()
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")
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.text = text
button.custom_minimum_size = Vector2(380, 52)
button.add_theme_font_size_override("font_size", 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)
UiStyles.apply_button(button, 22)
button.pressed.connect(cb)
parent.add_child(button)
return button
+3
View File
@@ -10,6 +10,7 @@ signal cancelled()
const Rules := preload("res://src/core/rules.gd")
const Strings := preload("res://src/core/strings.gd")
const UiArt := preload("res://src/game/ui/art.gd")
const UiStyles := preload("res://src/game/ui/ui_styles.gd")
var panel: PanelContainer
var npc_name_label: Label
@@ -107,10 +108,12 @@ func _init() -> void:
action_button.button_down.connect(func(): _start_hold())
action_button.button_up.connect(func(): _stop_hold(false))
action_button.pressed.connect(_on_action_pressed)
UiStyles.apply_button(action_button, 16)
buttons.add_child(action_button)
var cancel_btn := Button.new()
cancel_btn.text = Strings.lookup("ui.radio_cancel")
cancel_btn.pressed.connect(func(): cancelled.emit())
UiStyles.apply_button(cancel_btn, 16)
buttons.add_child(cancel_btn)
var _title_label: Label
+8
View File
@@ -8,6 +8,7 @@ extends CanvasLayer
const InputBindings := preload("res://src/core/input_bindings.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")
signal applied(settings: Dictionary)
@@ -135,11 +136,13 @@ func _build() -> void:
replay.name = "ReplayTutorialButton"
replay.text = Strings.lookup("ui.settings_replay")
replay.pressed.connect(func(): replay_tutorial_requested.emit())
UiStyles.apply_button(replay, 16)
replay_row.add_child(replay)
var reset_campaign := Button.new()
reset_campaign.name = "ResetCampaignButton"
reset_campaign.text = Strings.lookup("ui.settings_reset_campaign")
reset_campaign.pressed.connect(_confirm_reset_campaign)
UiStyles.apply_button(reset_campaign, 16)
replay_row.add_child(reset_campaign)
var controls_title := Label.new()
@@ -160,6 +163,7 @@ func _build() -> void:
var button := Button.new()
button.custom_minimum_size = Vector2(130, 0)
button.pressed.connect(_on_rebind.bind(action))
UiStyles.apply_button(button, 14, true)
grid.add_child(button)
_bind_buttons[action] = button
@@ -172,12 +176,14 @@ func _build() -> void:
save_button.name = "ApplyButton"
save_button.text = Strings.lookup("ui.apply_settings")
save_button.pressed.connect(_on_apply)
UiStyles.apply_button(save_button, 16)
row.add_child(save_button)
var cancel_button := Button.new()
cancel_button.name = "CancelButton"
cancel_button.text = Strings.lookup("ui.settings_cancel")
cancel_button.pressed.connect(_on_cancel)
UiStyles.apply_button(cancel_button, 16)
row.add_child(cancel_button)
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.confirmed.connect(func(): reset_campaign_requested.emit())
add_child(dialog)
UiStyles.apply_button(dialog.get_ok_button(), 16)
UiStyles.apply_button(dialog.get_cancel_button(), 16)
dialog.popup_centered()
dialog.confirmed.connect(dialog.queue_free)
+2 -13
View File
@@ -4,6 +4,7 @@ extends CanvasLayer
const Strings := preload("res://src/core/strings.gd")
const UiArt := preload("res://src/game/ui/art.gd")
const UiStyles := preload("res://src/game/ui/ui_styles.gd")
signal restart_requested()
signal continue_requested()
@@ -85,19 +86,7 @@ func _init() -> void:
func _style_review_button(button: Button) -> void:
button.custom_minimum_size = Vector2(300, 52)
button.add_theme_font_size_override("font_size", 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)
UiStyles.apply_button(button, 22)
func show_review(review: Dictionary, scenario: Resource, history: Array = [],
campaign_summary: Dictionary = {}) -> void:
+6 -31
View File
@@ -11,6 +11,7 @@ signal settings_requested()
signal quit_requested()
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 VENUES := [
@@ -108,7 +109,7 @@ func _build() -> void:
_continue_button.name = "ContinueButton"
_continue_button.custom_minimum_size = Vector2(460, 52)
_continue_button.pressed.connect(_on_continue_pressed)
_style_venue_button(_continue_button)
UiStyles.apply_button(_continue_button, 18)
column.add_child(_continue_button)
for i in VENUES.size():
@@ -119,7 +120,7 @@ func _build() -> void:
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.custom_minimum_size = Vector2(460, 62)
button.pressed.connect(_on_venue_pressed.bind(i))
_style_venue_button(button)
UiStyles.apply_button(button, 18)
column.add_child(button)
_buttons.append(button)
@@ -136,18 +137,21 @@ func _build() -> void:
help_button.name = "HelpButton"
help_button.text = Strings.lookup("ui.start_help")
help_button.pressed.connect(func(): help_requested.emit())
UiStyles.apply_button(help_button, 16)
row.add_child(help_button)
var settings_button := Button.new()
settings_button.name = "SettingsButton"
settings_button.text = Strings.lookup("ui.start_settings")
settings_button.pressed.connect(func(): settings_requested.emit())
UiStyles.apply_button(settings_button, 16)
row.add_child(settings_button)
var quit_button := Button.new()
quit_button.name = "QuitButton"
quit_button.text = Strings.lookup("ui.quit_game")
quit_button.pressed.connect(func(): quit_requested.emit())
UiStyles.apply_button(quit_button, 16)
row.add_child(quit_button)
var hint := Label.new()
@@ -158,35 +162,6 @@ func _build() -> void:
_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:
if _buttons.is_empty():
return
+58
View File
@@ -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