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

commit6fee3ffdcbb0a369132853b9fa1a7c27819734be
parenta9bfa91293
authorLucas Galante <[email protected]>
date2026-08-22 12:34
fix: gate three more pages' section_widgets on what the view painted

d13a901 fixed this on packages/services/storage and the sweep stopped
there. network, default-apps and bluetooth still reported their dispatch
roots unconditionally while painting them behind a gate, so every pointer
move over the page dropped one event per unpainted root and ctrl-nav
descended into a widget that was not on screen.

Measured in the shadow session, launching straight onto each page (the
--page arg) so the burst of pointer motion lands inside the load window
rather than after it has closed:

  network       65 drops   wifi_toggle, behind "Loading WiFi interfaces..."
  default-apps  81 drops   all nine category dropdowns, during the scan

Bluetooth is the one that does not heal on its own. Its gate is
loaded && installed && service_active, and the middle two are steady
states rather than a load window — on a host without bluez the page's
only ctrl-nav target is dead for the life of the process. This host has
bluez, so the regime had to be entered deliberately: `installed` is
decided by whether bluetoothctl can be spawned, so launching the app with
PATH=/var/empty puts it in the absent branch without touching any
packages. 114 drops after one burst of motion, 174 after a second burst
seconds later — still climbing, where a load-gate bug would have stopped.
All three now measure 0 under the same harness.

Each page gets the section_widgets_mirror_* test power.rs already had.
They are real regression guards: re-introducing the gate as `if false`
fails network's with left [[WidgetId(4)]], right [[]].

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

 src/pages/bluetooth.rs    | 31 +++++++++++++++++++++++++++++++
 src/pages/default_apps.rs | 17 +++++++++++++++++
 src/pages/network.rs      | 17 +++++++++++++++++
 3 files changed, 65 insertions(+)

diff --git a/src/pages/bluetooth.rs b/src/pages/bluetooth.rs
index 2c47c86..1d6a736 100644
--- a/src/pages/bluetooth.rs
+++ b/src/pages/bluetooth.rs
@@ -297,7 +297,16 @@ pub fn update(state: &mut BluetoothState, msg: BluetoothMessage) {
 
 impl crate::pages::AppPage for BluetoothState {
     // Sections: [Bluetooth]
+    // The gate mirrors view()'s branch chain exactly (the d13a901 lesson):
+    // `toggle` is painted only in the innermost `else`, so reporting it from any
+    // earlier branch is a dead root. Unlike a load gate this one does not close
+    // on its own — `!installed` and `!service_active` are steady states, so on a
+    // host without bluez the page's only ctrl-nav target stays dead for the life
+    // of the process and every pointer move over the page drops an event.
     fn section_widgets(&mut self) -> Vec<Vec<cce_ui::widget::WidgetId>> {
+        if !self.loaded || !self.installed || !self.service_active {
+            return vec![Vec::new()];
+        }
         vec![vec![self.toggle.id()]]
     }
 
@@ -321,3 +330,25 @@ impl crate::pages::AppPage for BluetoothState {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::pages::AppPage;
+
+    #[test]
+    fn section_widgets_mirror_branch_chain() {
+        let mut st = BluetoothState::default();
+        // Each of the three early branches paints a message (and maybe a plain
+        // PageContent button) but never `toggle` — the widget lives only in the
+        // innermost `else`. Unlike a load gate, the middle two are steady
+        // states: a host without bluez sits in one of them forever.
+        assert_eq!(st.section_widgets(), vec![Vec::new()], "not loaded");
+        st.loaded = true;
+        assert_eq!(st.section_widgets(), vec![Vec::new()], "bluez absent");
+        st.installed = true;
+        assert_eq!(st.section_widgets(), vec![Vec::new()], "service stopped");
+        st.service_active = true;
+        assert_eq!(st.section_widgets()[0].len(), 1, "toggle painted");
+    }
+}
diff --git a/src/pages/default_apps.rs b/src/pages/default_apps.rs
index a7c0c57..07d25bc 100644
--- a/src/pages/default_apps.rs
+++ b/src/pages/default_apps.rs
@@ -385,7 +385,13 @@ fn fetch_terminal_category(apps: &HashMap<String, DesktopApp>) -> CategoryInfo {
 
 impl crate::pages::AppPage for DefaultAppsState {
     // Sections: [Default Apps]
+    // Mirrors the view's load gate (d13a901): the category dropdowns are added
+    // to the stack only in the `else` of `if !state.loaded`, so reporting them
+    // during the application scan is one dead root per category.
     fn section_widgets(&mut self) -> Vec<Vec<cce_ui::widget::WidgetId>> {
+        if !self.loaded {
+            return vec![Vec::new()];
+        }
         vec![self.categories.iter().map(|c| c.dropdown.id()).collect()]
     }
 
@@ -416,6 +422,17 @@ impl crate::pages::AppPage for DefaultAppsState {
 mod tests {
     use super::*;
 
+    #[test]
+    fn section_widgets_mirror_load_gate() {
+        use crate::pages::AppPage;
+        let mut st = DefaultAppsState::default();
+        // Still scanning: the dropdowns are not in the stack yet, so none of
+        // them may be reported as a dispatch root.
+        assert_eq!(st.section_widgets(), vec![Vec::new()]);
+        st.loaded = true;
+        assert_eq!(st.section_widgets()[0].len(), st.categories.len());
+    }
+
     #[test]
     fn parse_desktop_file_basics() {
         let dir = std::env::temp_dir().join("cce-da-test");
diff --git a/src/pages/network.rs b/src/pages/network.rs
index 94db000..db68eb8 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -267,7 +267,13 @@ impl NetworkState {
 
 impl crate::pages::AppPage for NetworkState {
     // Sections: [WiFi]
+    // Mirrors the view's load gate (d13a901): `wifi_toggle` is painted only in
+    // the `else` of `if !state.loaded`, so reporting it while the page still
+    // reads "Loading WiFi interfaces..." is a dead root.
     fn section_widgets(&mut self) -> Vec<Vec<cce_ui::widget::WidgetId>> {
+        if !self.loaded {
+            return vec![Vec::new()];
+        }
         vec![vec![self.wifi_toggle.id()]]
     }
 
@@ -347,4 +353,15 @@ mod tests {
         let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, false, &mut layout, &mut cce_ui::context::UiContext::new());
         assert!(!pc.rects.is_empty() || !pc.texts.is_empty() || !pc.buttons.is_empty());
     }
+
+    #[test]
+    fn section_widgets_mirror_load_gate() {
+        use crate::pages::AppPage;
+        let mut st = NetworkState::default();
+        // Not loaded: the view paints only "Loading WiFi interfaces...", so
+        // reporting the toggle would be a root nothing registered this frame.
+        assert_eq!(st.section_widgets(), vec![Vec::new()]);
+        st.loaded = true;
+        assert_eq!(st.section_widgets()[0].len(), 1);
+    }
 }