system settings
git clone https://git.lucas.co/cce-system-interface.git
fix: stop resurrecting fonts' unpainted "Other" boxes at their last rect
The four free-text boxes (borders/status/fuzzel/terminal) are painted
only when their section's dropdown is on "Other", but
register_extra_dispatch_roots registered all thirteen widgets by pointer
unconditionally and section_widgets reported them the same way.
This is the same invariant the sibling commit fixes on three other pages,
but it fails the other way round and is invisible to the usual oracle.
Registering by pointer bypasses the painted-this-frame check, so these
roots resolve: no "unregistered/stale root" line is ever printed. Fonts
measured 0 warnings while network and default-apps were emitting 65 and
81. What actually happens is worse than a dropped event — render_widget
is the only caller of a widget's layout(), so a box that stops being
painted keeps its last-drawn rect and keeps hit-testing there, as an
invisible live TextBox over whatever the reflow moved into that space.
It is also reached early: roots iterate reversed, so borders_box is
visited ahead of borders_menu and all of Preferred Fonts.
Both sites now carry the view's gate, named FONT_OTHER rather than
repeated as a bare `== 3` in twelve places — the drift between the three
statements of that gate is the whole bug.
Verified in the shadow that the working direction is untouched: with
Borders on "Other" the box paints, takes a click and shows its caret,
with zero stale-root drops; reverting to Monospace removes it and
reflows the sections back. section_widgets_gate_other_boxes fails
against the old code (borders: left 2, right 1).
Not confirmed at runtime: that the stale box actually swallows a click.
Both observables that would prove focus theft — PageDown page-switching
and the "/" search bar, each suppressed while a TextBox holds focus —
need key input, and injected keys do not reach a client in the shadow
session (a control press with nothing focused opens no search bar). The
mechanism is established by reading; the severity is inferred.
Co-Authored-By: Claude <[email protected]>
src/pages/fonts.rs | 111 +++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 95 insertions(+), 16 deletions(-)
diff --git a/src/pages/fonts.rs b/src/pages/fonts.rs
index 7b23bb3..11466f2 100644
--- a/src/pages/fonts.rs
+++ b/src/pages/fonts.rs
@@ -33,6 +33,18 @@ pub enum FontsMessage {
SetTerminalSize(u16),
}
+/// The "Other" entry in each font dropdown — the one selection whose section
+/// also paints a free-text box for a font name the presets don't cover.
+///
+/// Named because the gate now has to be stated in three places (the view, the
+/// dispatch-root report, and the manual registration) and a bare `== 3` drifting
+/// in one of them is exactly the bug this replaced. The boxes used to be
+/// registered by pointer unconditionally: an unpainted box kept its last-drawn
+/// rect, kept hit-testing there, and — because the registration *succeeded* —
+/// never tripped the `unregistered/stale root` warning that catches the same
+/// mistake on every other page.
+const FONT_OTHER: usize = 3;
+
#[derive(Clone)]
pub struct FontsState {
pub typeface_loaded: bool,
@@ -98,18 +110,44 @@ impl Default for FontsState {
impl AppPage for FontsState {
// Sections: [Preferred Fonts, Borders, Status Interface, Fuzzel, Terminal]
+ // Each section's free-text box is reported only when its menu is on "Other",
+ // mirroring the view below — the boxes are the only conditionally-painted
+ // widgets here, and the groups stay in the view's paint order either way.
fn section_widgets(&mut self) -> Vec<Vec<cce_ui::widget::WidgetId>> {
+ let mut borders = vec![self.borders_menu.id()];
+ if self.borders_menu.selected == FONT_OTHER {
+ borders.push(self.borders_box.id());
+ }
+ let mut status = vec![self.status_menu.id()];
+ if self.status_menu.selected == FONT_OTHER {
+ status.push(self.status_box.id());
+ }
+ let mut fuzzel = vec![self.fuzzel_menu.id()];
+ if self.fuzzel_menu.selected == FONT_OTHER {
+ fuzzel.push(self.fuzzel_box.id());
+ }
+ fuzzel.push(self.fuzzel_size_box.id());
+ let mut terminal = vec![self.terminal_menu.id()];
+ if self.terminal_menu.selected == FONT_OTHER {
+ terminal.push(self.terminal_box.id());
+ }
+ terminal.push(self.terminal_size_box.id());
vec![
vec![self.sans_box.id(), self.serif_box.id(), self.mono_box.id()],
- vec![self.borders_menu.id(), self.borders_box.id()],
- vec![self.status_menu.id(), self.status_box.id()],
- vec![self.fuzzel_menu.id(), self.fuzzel_box.id(), self.fuzzel_size_box.id()],
- vec![self.terminal_menu.id(), self.terminal_box.id(), self.terminal_size_box.id()],
+ borders,
+ status,
+ fuzzel,
+ terminal,
]
}
// Not every section widget passes through `render_widget`'s registration side
// effect (the menus draw custom); the id-rooted router needs them all resolvable.
+ //
+ // The four "Other" boxes carry the same gate as `section_widgets` above and
+ // the view below. Registering one the view did not paint does not merely
+ // waste a slot — it resurrects a widget at its last-drawn rect, ahead of the
+ // live widgets that now occupy that space in the dispatch order.
fn register_extra_dispatch_roots(&mut self, ctx: &mut cce_ui::context::UiContext) {
let (id, ptr) = (self.sans_box.id(), self.sans_box.as_ptr_mut());
ctx.register_widget(id, ptr);
@@ -119,22 +157,30 @@ impl AppPage for FontsState {
ctx.register_widget(id, ptr);
let (id, ptr) = (self.borders_menu.id(), self.borders_menu.as_ptr_mut());
ctx.register_widget(id, ptr);
- let (id, ptr) = (self.borders_box.id(), self.borders_box.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ if self.borders_menu.selected == FONT_OTHER {
+ let (id, ptr) = (self.borders_box.id(), self.borders_box.as_ptr_mut());
+ ctx.register_widget(id, ptr);
+ }
let (id, ptr) = (self.status_menu.id(), self.status_menu.as_ptr_mut());
ctx.register_widget(id, ptr);
- let (id, ptr) = (self.status_box.id(), self.status_box.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ if self.status_menu.selected == FONT_OTHER {
+ let (id, ptr) = (self.status_box.id(), self.status_box.as_ptr_mut());
+ ctx.register_widget(id, ptr);
+ }
let (id, ptr) = (self.fuzzel_menu.id(), self.fuzzel_menu.as_ptr_mut());
ctx.register_widget(id, ptr);
- let (id, ptr) = (self.fuzzel_box.id(), self.fuzzel_box.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ if self.fuzzel_menu.selected == FONT_OTHER {
+ let (id, ptr) = (self.fuzzel_box.id(), self.fuzzel_box.as_ptr_mut());
+ ctx.register_widget(id, ptr);
+ }
let (id, ptr) = (self.fuzzel_size_box.id(), self.fuzzel_size_box.as_ptr_mut());
ctx.register_widget(id, ptr);
let (id, ptr) = (self.terminal_menu.id(), self.terminal_menu.as_ptr_mut());
ctx.register_widget(id, ptr);
- let (id, ptr) = (self.terminal_box.id(), self.terminal_box.as_ptr_mut());
- ctx.register_widget(id, ptr);
+ if self.terminal_menu.selected == FONT_OTHER {
+ let (id, ptr) = (self.terminal_box.id(), self.terminal_box.as_ptr_mut());
+ ctx.register_widget(id, ptr);
+ }
let (id, ptr) = (self.terminal_size_box.id(), self.terminal_size_box.as_ptr_mut());
ctx.register_widget(id, ptr);
}
@@ -166,7 +212,7 @@ impl AppPage for FontsState {
let mut stack = sec.vstack(8.0);
let sec_w = stack.context.cw;
stack.add_widget(&mut self.borders_menu, sec_w - 28.0, 44.0, ctx);
- if self.borders_menu.selected == 3 {
+ if self.borders_menu.selected == FONT_OTHER {
stack.add_widget(&mut self.borders_box, sec_w - 28.0, 44.0, ctx);
}
});
@@ -175,7 +221,7 @@ impl AppPage for FontsState {
let mut stack = sec.vstack(8.0);
let sec_w = stack.context.cw;
stack.add_widget(&mut self.status_menu, sec_w - 28.0, 44.0, ctx);
- if self.status_menu.selected == 3 {
+ if self.status_menu.selected == FONT_OTHER {
stack.add_widget(&mut self.status_box, sec_w - 28.0, 44.0, ctx);
}
});
@@ -184,7 +230,7 @@ impl AppPage for FontsState {
let mut stack = sec.vstack(8.0);
let sec_w = stack.context.cw;
stack.add_widget(&mut self.fuzzel_menu, sec_w - 28.0, 44.0, ctx);
- if self.fuzzel_menu.selected == 3 {
+ if self.fuzzel_menu.selected == FONT_OTHER {
stack.add_widget(&mut self.fuzzel_box, sec_w - 28.0, 44.0, ctx);
}
stack.add_widget(&mut self.fuzzel_size_box, sec_w - 28.0, 44.0, ctx);
@@ -194,7 +240,7 @@ impl AppPage for FontsState {
let mut stack = sec.vstack(8.0);
let sec_w = stack.context.cw;
stack.add_widget(&mut self.terminal_menu, sec_w - 28.0, 44.0, ctx);
- if self.terminal_menu.selected == 3 {
+ if self.terminal_menu.selected == FONT_OTHER {
stack.add_widget(&mut self.terminal_box, sec_w - 28.0, 44.0, ctx);
}
stack.add_widget(&mut self.terminal_size_box, sec_w - 28.0, 44.0, ctx);
@@ -644,3 +690,36 @@ pub fn update(state: &mut FontsState, msg: FontsMessage) {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn section_widgets_gate_other_boxes() {
+ let mut st = FontsState::default();
+ // Presets selected: the four free-text boxes are not painted, so the
+ // menu is the section's only reported root (plus the size spinbox where
+ // the section has one).
+ let g = st.section_widgets();
+ assert_eq!(g[1].len(), 1, "borders: menu only");
+ assert_eq!(g[2].len(), 1, "status: menu only");
+ assert_eq!(g[3].len(), 2, "fuzzel: menu + size");
+ assert_eq!(g[4].len(), 2, "terminal: menu + size");
+
+ st.borders_menu.selected = FONT_OTHER;
+ st.fuzzel_menu.selected = FONT_OTHER;
+ let g = st.section_widgets();
+ assert_eq!(g[1].len(), 2, "borders: menu + box");
+ assert_eq!(g[2].len(), 1, "status untouched");
+ assert_eq!(g[3].len(), 3, "fuzzel: menu + box + size");
+ assert_eq!(g[4].len(), 2, "terminal untouched");
+ }
+
+ #[test]
+ fn section_group_count_matches_the_view() {
+ // The outer length picks each section's `sec_focused` index, so it has
+ // to track the five `add_section` calls in the view.
+ assert_eq!(FontsState::default().section_widgets().len(), 5);
+ }
+}