system settings
git clone https://git.lucas.co/cce-system-interface.git
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);
+ }
}