git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

commit6cd2e4fa5e3b89d06f78862bf4686bc1c92e35d4
parentaad031f1f4
authorLucas Galante <[email protected]>
date2026-09-08 22:04
fix: keyboard focus survives every rebuild, not only a Tab step's

The page re-lays-out on its own (polls, hover), and each rebuild clones its
buttons; a rebuild that was not a Tab step's dropped the focused clone
without handing focus on, so the next Tab restarted at the first stop. The
view pass now records the focused widget's rect before wiping the registry,
on every rebuild, lights the clone at that rect, and refocuses it at the
tail.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 src/main.rs     | 17 ++++++-----------
 src/renderer.rs | 11 +++++++++++
 2 files changed, 17 insertions(+), 11 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 8b55126..3036304 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -105,10 +105,10 @@ struct SystemInterface {
     // `section_widgets()` ids are not registered and every event to them is dropped with
     // a router warning. `dispatch_page_event` suppresses dispatch across that gap.
     laid_out_page: Option<Page>,
-    /// The rect of the widget a Tab step focused, carried across the rebuild
-    /// the step triggers: the page's buttons are per-rebuild clones, so the
-    /// focused id dies with the view pass and the clone at the same rect takes
-    /// the focus back (see `focus_stepped` / the view pass tail).
+    /// The rect of the focused widget at the start of a view pass: the page's
+    /// buttons are per-rebuild clones, so the focused id dies with every
+    /// rebuild and the clone at the same rect takes the focus back (lit as it
+    /// is collected, focused at the pass's tail).
     refocus_rect: Option<(f32, f32, f32, f32)>,
     page_dropdown: cce_ui::widget::Adapted<cce_ui::widget::input::Dropdown>,
     // Switcher + Page DISSOLVED (Phase 6u): the current page is app.current_page, page
@@ -626,14 +626,9 @@ impl cce_ui::engine::Application for SystemInterface {
     }
 
     /// The geometry is cached until the next rebuild — a moved focus ring needs
-    /// one — and that rebuild clones the page's buttons, so remember where the
-    /// focus went and hand it to the clone at that rect after the view pass.
+    /// one. The view pass itself carries the focus across the button clones it
+    /// makes (see `refocus_rect`), on every rebuild.
     fn focus_stepped(&mut self) {
-        self.refocus_rect = self
-            .ui_context
-            .focused_widget
-            .and_then(|id| self.ui_context.tree.get_ptr(id))
-            .map(|ptr| unsafe { (*ptr).rect() });
         self.needs_rebuild = true;
     }
 
diff --git a/src/renderer.rs b/src/renderer.rs
index b94fe7a..0a54956 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -95,6 +95,17 @@ impl SystemInterface {
             let t = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() % 100000;
             eprintln!("[hover] t={} rebuild", t);
         }
+        // Keyboard focus on a per-rebuild button clone dies with the registry
+        // wipe below — on EVERY rebuild, not just the one a Tab step triggers
+        // (the page polls and re-lays-out on its own). Remember where the
+        // focused widget sat: this pass lights the clone at that rect as it
+        // collects its plate, and the pass's tail hands it the focus. A
+        // persistent widget resolves to itself.
+        self.refocus_rect = self
+            .ui_context
+            .focused_widget
+            .and_then(|id| self.ui_context.tree.get_ptr(id))
+            .map(|ptr| unsafe { (*ptr).rect() });
         // SectionContainer dissolved (Phase 6w): no per-rebuild section clones to
         // relink — the page's widgets dispatch directly (registration happens in
         // render_widget during the view pass below).