feat(engine): keyboard focus rings on modal buttons (Phase 1)

Every button spawned via spawn_modal_button is now keyboard-navigable.
Tab/Shift-Tab cycles focus within the active modal, Enter activates
the focused button via the same Interaction::Pressed signal mouse
clicks use, and the primary action auto-focuses on modal open. Mouse
clicks transfer focus so the two input modes stay in sync.

The visual indicator is a single overlay entity that's reparented
above the topmost modal scrim and tracks the focused button's
GlobalTransform + ComputedNode each frame. Sitting outside the
modal-card subtree means the ring isn't affected by the open
animation's 0.96→1.0 scale, and sitting outside any scroll container
means it can't be clipped by Settings' Overflow::scroll_y. Z-order
sits one rung above Z_MODAL_TOP via the new Z_FOCUS_RING token.

Existing 11 modals (Help, Stats, Achievements, Settings, Profile,
Leaderboard, Pause, Forfeit confirm, GameOver, Confirm new game,
Onboarding, Home) get focus support without any call-site changes —
attach_focusable_to_modal_buttons walks the ancestry of any
ModalButton lacking Focusable to find its scrim and tags it
automatically. selection_plugin's Tab handler keeps working when no
modal is open; when one is, focus consumes Tab/Enter before the
selection system sees them.

Phase 1 scope only — HUD action bar, Home mode cards, and Settings
bespoke buttons (icon, swatch, toggle) come in Phase 2/3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-30 21:17:25 +00:00
parent c1bde18a2c
commit 12789529a1
4 changed files with 782 additions and 2 deletions
+2
View File
@@ -30,6 +30,7 @@ pub mod stats_plugin;
pub mod sync_plugin;
pub mod table_plugin;
pub mod time_attack_plugin;
pub mod ui_focus;
pub mod ui_modal;
pub mod ui_theme;
pub mod weekly_goals_plugin;
@@ -97,6 +98,7 @@ pub use resources::{DragState, GameStateResource, HintCycleIndex, SettingsScroll
pub use selection_plugin::{SelectionHighlight, SelectionPlugin, SelectionState};
pub use stats_plugin::{StatsPlugin, StatsResource, StatsScreen, StatsUpdate};
pub use sync_plugin::{SyncPlugin, SyncProviderResource};
pub use ui_focus::{Disabled, FocusGroup, Focusable, FocusedButton, UiFocusPlugin};
pub use ui_modal::{
spawn_modal, spawn_modal_actions, spawn_modal_body_text, spawn_modal_button,
spawn_modal_header, ButtonVariant, ModalActions, ModalBody, ModalButton, ModalCard,