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

commitaad031f1f4334a883a194613026384f0baf4164b
parenta4c3003340
authorLucas Galante <[email protected]>
date2026-09-08 22:01
fix: a Tab step's focus survives the rebuild it triggers

The page's buttons are per-rebuild clones, so the focused id died with the
view pass the step's rebuild ran, and every Tab restarted at the first stop.
focus_stepped records the focused widget's rect; the view pass lights the
clone at that rect as it collects its plate, and its tail hands that clone
the focus.

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

 src/main.rs     | 15 ++++++++++++++-
 src/renderer.rs | 30 +++++++++++++++++++++++++++++-
 2 files changed, 43 insertions(+), 2 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index f8843a5..8b55126 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -105,6 +105,11 @@ 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).
+    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
     // scroll is scroll_y/max_scroll_y, and the page scrollbar is this app-owned widget
@@ -243,6 +248,7 @@ impl cce_ui::engine::Application for SystemInterface {
             last_scroll_y: 0.0,
             focused_section: None,
             laid_out_page: None,
+            refocus_rect: None,
             page_dropdown,
             page_scroll_bar: crate::scroll_bar::ScrollBar::new(),
             content_h: 0.0,
@@ -619,8 +625,15 @@ impl cce_ui::engine::Application for SystemInterface {
         true
     }
 
-    /// The geometry is cached until the next rebuild — a moved focus ring needs one.
+    /// 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.
     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 bbaf759..b94fe7a 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -504,7 +504,18 @@ impl SystemInterface {
             // coords, pre-scroll, like everything else in this list.
             if cce_ui::layout::control_relief() {
                 let rect = cce_ui::scene::layout::Rect { x: base.x, y: base.y, width: base.w, height: base.h };
-                if let Some(plate) = btn.plate(rect) {
+                if let Some(mut plate) = btn.plate(rect) {
+                    // This pass's clone of the button a Tab step focused (its
+                    // registered rect, window coords, matches `refocus_rect`;
+                    // the view-pass tail hands it the focus): light its rim
+                    // now, since the carve is collected here, before that.
+                    let near = |a: f32, b: f32| (a - b).abs() < 0.5;
+                    let refocused = self
+                        .refocus_rect
+                        .is_some_and(|(fx, fy, fw, fh)| near(wx, fx) && near(wy, fy) && near(ww, fw) && near(wh, fh));
+                    if refocused {
+                        plate.tint = Some(cce_ui::widget::ControlPlate::focus_tint());
+                    }
                     self.page_control_reliefs.push(ControlCarve::Plate {
                         x: plate.rect.x,
                         y: plate.rect.y,
@@ -739,6 +750,23 @@ impl SystemInterface {
                 self.ui_context.register_widget(id, ptr);
             }
         }
+        // A Tab step's focus, handed to the fresh clone at the same rect (the
+        // page buttons above are per-rebuild allocations; see `focus_stepped`).
+        if let Some((fx, fy, fw, fh)) = self.refocus_rect.take() {
+            let near = |a: f32, b: f32| (a - b).abs() < 0.5;
+            let heir = self.ui_context.tree.iter_registered().find_map(|(id, ptr)| {
+                if ptr.is_null() {
+                    return None;
+                }
+                let w = unsafe { &*ptr };
+                let (x, y, ww, hh) = w.rect();
+                (w.focus_role() != cce_ui::widget::FocusRole::None && near(x, fx) && near(y, fy) && near(ww, fw) && near(hh, fh))
+                    .then_some(id)
+            });
+            if let Some(id) = heir {
+                self.ui_context.set_focused_id(id);
+            }
+        }
         self.needs_rebuild = false;
         self.laid_out_page = Some(self.app.current_page);
         self.last_scroll_y = self.scroll_y;