system settings
git clone https://git.lucas.co/cce-system-interface.git
fix: dispatch roots follow row virtualization, so clicks hit the row on screen
`render_widget` is the only caller of a row's `layout()`, so a row scrolled out of
the viewport keeps the rect from the last frame it was drawn. But the row lists
re-registered and reported EVERY row as a dispatch root, and `dispatch_page_event`
takes the first root that hit-tests true in index order — so a stale low-index row
reliably beat the row actually painted at those coordinates.
Live repro on Packages (1359 rows): scroll ~25 notches, then jump the list, then
click the top visible row. It read "fontforge"; the app selected "appstream" — the
package that had been at the top of the list at the previous scroll position. Hover
highlight was wrong the same way.
Both `extra_dispatch_roots` and `register_extra_dispatch_roots` now filter on
`get_item_draw_y`, the same predicate the view's paint loop virtualizes on, so the
root set is exactly the set that had its rect refreshed this frame. Applied to
packages, services and timers — all three carried the identical register-all loop.
(Services only mis-highlighted, since its item clicks are never drained.)
Verified live after the fix: the same scroll-jump-click sequence selects fontforge.
Co-Authored-By: Claude Opus 5 <[email protected]>
src/pages/packages.rs | 31 +++++++++++++++++++++++--------
src/pages/services.rs | 17 +++++++++++++++--
src/pages/timers.rs | 17 +++++++++++++++--
3 files changed, 53 insertions(+), 12 deletions(-)
diff --git a/src/pages/packages.rs b/src/pages/packages.rs
index 86cbcc9..b36bb4b 100644
--- a/src/pages/packages.rs
+++ b/src/pages/packages.rs
@@ -772,19 +772,34 @@ impl crate::pages::AppPage for PackagesState {
}
}
+ // Both root methods below filter by `get_item_draw_y`, the SAME predicate the view's
+ // paint loop uses to virtualize rows. Only a row that was drawn had `render_widget`
+ // refresh its rect; a scrolled-out row keeps the rect from the last frame it was
+ // visible, and since `dispatch_page_event` takes the first root that hit-tests true
+ // in index order, an unfiltered list let a stale low-index row swallow clicks meant
+ // for the row actually on screen (click "fontforge", select "appstream").
fn extra_dispatch_roots(&mut self) -> Vec<cce_ui::widget::WidgetId> {
- match self.active_tab {
- PackageTab::Installed => self.installed_items.iter().map(|i| i.id()).collect(),
- PackageTab::Updates => self.updates_items.iter().map(|i| i.id()).collect(),
- }
+ let (list, items) = match self.active_tab {
+ PackageTab::Installed => (&self.installed_list, &self.installed_items),
+ PackageTab::Updates => (&self.updates_list, &self.updates_items),
+ };
+ items
+ .iter()
+ .enumerate()
+ .filter(|(idx, _)| list.get_item_draw_y(*idx, 4.0).is_some())
+ .map(|(_, i)| i.id())
+ .collect()
}
fn register_extra_dispatch_roots(&mut self, ctx: &mut cce_ui::context::UiContext) {
- let items = match self.active_tab {
- PackageTab::Installed => &mut self.installed_items,
- PackageTab::Updates => &mut self.updates_items,
+ let (list, items) = match self.active_tab {
+ PackageTab::Installed => (&self.installed_list, &mut self.installed_items),
+ PackageTab::Updates => (&self.updates_list, &mut self.updates_items),
};
- for i in items.iter_mut() {
+ for (idx, i) in items.iter_mut().enumerate() {
+ if list.get_item_draw_y(idx, 4.0).is_none() {
+ continue;
+ }
let (id, ptr) = (i.id(), i.as_ptr_mut());
ctx.register_widget(id, ptr);
}
diff --git a/src/pages/services.rs b/src/pages/services.rs
index fc2d03b..fbd4be1 100644
--- a/src/pages/services.rs
+++ b/src/pages/services.rs
@@ -391,12 +391,25 @@ impl crate::pages::AppPage for ServicesState {
fn propagate_widget_changes(&mut self, _actions: &mut Vec<crate::app::AppAction>) {}
+ // Filtered by `get_item_draw_y`, the same predicate the view's paint loop virtualizes
+ // on — a scrolled-out row keeps its last-drawn rect and would otherwise win the
+ // hit-test against the row actually on screen. See the note in packages.rs.
fn extra_dispatch_roots(&mut self) -> Vec<cce_ui::widget::WidgetId> {
- self.items.iter().map(|i| i.id()).collect()
+ let (list, items) = (&self.list, &self.items);
+ items
+ .iter()
+ .enumerate()
+ .filter(|(idx, _)| list.get_item_draw_y(*idx, 4.0).is_some())
+ .map(|(_, i)| i.id())
+ .collect()
}
fn register_extra_dispatch_roots(&mut self, ctx: &mut cce_ui::context::UiContext) {
- for i in self.items.iter_mut() {
+ let (list, items) = (&self.list, &mut self.items);
+ for (idx, i) in items.iter_mut().enumerate() {
+ if list.get_item_draw_y(idx, 4.0).is_none() {
+ continue;
+ }
let (id, ptr) = (i.id(), i.as_ptr_mut());
ctx.register_widget(id, ptr);
}
diff --git a/src/pages/timers.rs b/src/pages/timers.rs
index d68ed37..adbb8e3 100644
--- a/src/pages/timers.rs
+++ b/src/pages/timers.rs
@@ -689,12 +689,25 @@ impl crate::pages::AppPage for TimersState {
fn propagate_widget_changes(&mut self, _actions: &mut Vec<crate::app::AppAction>) {}
+ // Filtered by `get_item_draw_y`, the same predicate the view's paint loop virtualizes
+ // on — a scrolled-out row keeps its last-drawn rect and would otherwise win the
+ // hit-test against the row actually on screen. See the note in packages.rs.
fn extra_dispatch_roots(&mut self) -> Vec<cce_ui::widget::WidgetId> {
- self.items.iter().map(|i| i.id()).collect()
+ let (list, items) = (&self.list, &self.items);
+ items
+ .iter()
+ .enumerate()
+ .filter(|(idx, _)| list.get_item_draw_y(*idx, 4.0).is_some())
+ .map(|(_, i)| i.id())
+ .collect()
}
fn register_extra_dispatch_roots(&mut self, ctx: &mut cce_ui::context::UiContext) {
- for i in self.items.iter_mut() {
+ let (list, items) = (&self.list, &mut self.items);
+ for (idx, i) in items.iter_mut().enumerate() {
+ if list.get_item_draw_y(idx, 4.0).is_none() {
+ continue;
+ }
let (id, ptr) = (i.id(), i.as_ptr_mut());
ctx.register_widget(id, ptr);
}