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
@@ -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 = ""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
var timeline: Node = EventTimeline.new()
|
||||
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)
|
||||
var a := TestUtils.check_eq(timeline.index, 0, "cannot step before the first event")
|
||||
timeline.step(1)
|
||||
var b := TestUtils.check_eq(timeline.index, 1, "next advances one event")
|
||||
timeline.step(99)
|
||||
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()
|
||||
return a and b and c
|
||||
return a and b and c and d
|
||||
|
||||
func test_review_records_a_shift_start_stamp() -> bool:
|
||||
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