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

+39 -20
View File
@@ -5,6 +5,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")
const UiFocus := preload("res://src/game/ui/ui_focus.gd")
signal restart_requested()
signal continue_requested()
@@ -16,6 +17,9 @@ var _review: Dictionary = {}
var report_label: Label
var _campaign_label: Label = null
var _continue_button: Button = null
var _timeline_button: Button = null
var _restart_button: Button = null
var _quit_button: Button = null
func _init() -> void:
name = "ShiftReview"
@@ -56,33 +60,36 @@ func _init() -> void:
center.add_child(_campaign_label)
_continue_button = Button.new()
_continue_button.name = "ContinueButton"
_continue_button.text = Strings.lookup("review.continue_campaign")
_continue_button.pressed.connect(func(): continue_requested.emit())
_continue_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
center.add_child(_continue_button)
var timeline := Button.new()
timeline.name = "TimelineButton"
timeline.text = Strings.lookup("review.timeline_button")
timeline.pressed.connect(func(): timeline_requested.emit(_review))
timeline.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
center.add_child(timeline)
_timeline_button = Button.new()
_timeline_button.name = "TimelineButton"
_timeline_button.text = Strings.lookup("review.timeline_button")
_timeline_button.pressed.connect(func(): timeline_requested.emit(_review))
_timeline_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
center.add_child(_timeline_button)
var restart := Button.new()
restart.text = Strings.lookup("ui.review_return_menu")
restart.pressed.connect(func(): restart_requested.emit())
restart.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
center.add_child(restart)
_restart_button = Button.new()
_restart_button.name = "ReturnMenuButton"
_restart_button.text = Strings.lookup("ui.review_return_menu")
_restart_button.pressed.connect(func(): restart_requested.emit())
_restart_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
center.add_child(_restart_button)
var quit_btn := Button.new()
quit_btn.text = Strings.lookup("ui.quit_game")
quit_btn.pressed.connect(func(): get_tree().quit())
quit_btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
_style_review_button(timeline)
_quit_button = Button.new()
_quit_button.name = "QuitButton"
_quit_button.text = Strings.lookup("ui.quit_game")
_quit_button.pressed.connect(func(): get_tree().quit())
_quit_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
_style_review_button(_timeline_button)
_style_review_button(_continue_button)
_style_review_button(restart)
_style_review_button(quit_btn)
center.add_child(quit_btn)
_style_review_button(_restart_button)
_style_review_button(_quit_button)
center.add_child(_quit_button)
func _style_review_button(button: Button) -> void:
button.custom_minimum_size = Vector2(300, 52)
@@ -154,4 +161,16 @@ func show_review(review: Dictionary, scenario: Resource, history: Array = [],
_campaign_label.text = ""
if _continue_button:
_continue_button.visible = not campaign_complete and next_venue != ""
visible = true
visible = true
var buttons: Array = [_continue_button, _timeline_button, _restart_button, _quit_button]
UiFocus.link(buttons)
var first := UiFocus.first_enabled(buttons)
if first:
first.grab_focus()
func _unhandled_input(event: InputEvent) -> void:
if not visible:
return
if event.is_action_pressed("ui_cancel"):
restart_requested.emit()
get_viewport().set_input_as_handled()