Add wrapping keyboard and gamepad focus rings
Full build / Import + headless tests (push) Successful in 36s
Full build / Full desktop build + optional release (push) Skipped

- Link every modal's enabled controls into a wrapping UiFocus ring and give each screen a default focus target.
- Add gamepad A/B bindings for ui_accept/ui_cancel; include Start menu footer actions and Continue in its focus ring.
- Skip hidden/disabled controls, close focus traps via per-modal cancel behavior, and return review to the menu on cancel.
- Record the focus audit and add regression coverage.
This commit is contained in:
Gig Line Dev committed 2026-11-02 16:05:53 +01:00
1 parent 9692f262f1
commit 8f65751975
17 files changed
+262 -76

No files matched your search

+21 -9
View File
@@ -10,6 +10,7 @@ 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")
const UiFocus := preload("res://src/game/ui/ui_focus.gd")
var events: Array[Dictionary] = []
var index := 0
@@ -18,6 +19,7 @@ var _detail_label: Label = null
var _counter_label: Label = null
var _prev_button: Button = null
var _next_button: Button = null
var _close_button: Button = null
var _root: Control = null
func _init() -> void:
@@ -107,12 +109,12 @@ func _build() -> void:
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)
_close_button = Button.new()
_close_button.name = "CloseButton"
_close_button.text = Strings.lookup("timeline.close")
_close_button.pressed.connect(_on_close)
UiStyles.apply_button(_close_button, 15, true)
row.add_child(_close_button)
var hint := Label.new()
hint.text = Strings.lookup("timeline.hint")
@@ -144,6 +146,9 @@ func show_timeline(review: Dictionary) -> void:
_root.visible = true
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
_refresh()
var first := UiFocus.first_enabled([_prev_button, _next_button, _close_button])
if first:
first.grab_focus()
func step(direction: int) -> void:
if events.is_empty():
@@ -165,6 +170,12 @@ func _refresh() -> void:
_prev_button.disabled = not has_events or index <= 0
if _next_button:
_next_button.disabled = not has_events or index >= events.size() - 1
UiFocus.link([_prev_button, _next_button, _close_button])
# A disabled control cannot keep focus; move to the other end of the row.
if _prev_button and _prev_button.disabled and _prev_button.has_focus() and _close_button:
_close_button.grab_focus()
if _next_button and _next_button.disabled and _next_button.has_focus() and _close_button:
_close_button.grab_focus()
if not has_events:
_counter_label.text = Strings.lookup("timeline.empty")
_detail_label.text = ""
@@ -225,6 +236,10 @@ func _stamp(at_ms: int) -> String:
func _unhandled_input(event: InputEvent) -> void:
if not visible:
return
if event.is_action_pressed("ui_cancel"):
close()
get_viewport().set_input_as_handled()
return
if event is InputEventKey and event.pressed and not event.echo:
match event.keycode:
KEY_LEFT, KEY_A:
@@ -233,6 +248,3 @@ func _unhandled_input(event: InputEvent) -> void:
KEY_RIGHT, KEY_D:
step(1)
get_viewport().set_input_as_handled()
KEY_ESCAPE:
close()
get_viewport().set_input_as_handled()