system settings
git clone https://git.lucas.co/cce-system-interface.git
page scroll: glide and coast through ScrollMotion; tick the page lists
Two things the toolkit's smooth scrolling (cce-ui@0f84843) needed here.
The whole-page fallback scroll is now driven by a ScrollMotion: the wheel
moves the target (or, for a trackpad finger, the offset itself) and a new
tick_page_scroll in tick_internal carries the drawn offset there each frame.
The in-place geometry shift the fast path did is factored into
shift_page_to(new_y), which both paths use, so a glide still never rebuilds
the layout; scroll_y stays the drawn value and the scrollbar thumb follows
it. Arrow keys ride the same glide as wheel notches, Home/End glide to their
target. Thumb drags, the search jump and the clamp are adopted by reconcile.
The pages' inner ScrollRegions were never ticked, and a region's tick is now
what moves its offset after a wheel — without it the list would only move
its target and appear frozen. AppPage gains a default-false tick(dt) that
the six list pages (accounts, network, packages, processes, services,
timers) implement over their regions; the host pumps the current page and
re-lays it out when it reports movement.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/input_handler.rs | 92 +++++++++++++++++++++++++++++---------------------
src/main.rs | 32 ++++++++++++++++++
src/pages/accounts.rs | 4 +++
src/pages/mod.rs | 9 +++++
src/pages/network.rs | 4 +++
src/pages/packages.rs | 4 +++
src/pages/processes.rs | 4 +++
src/pages/services.rs | 4 +++
src/pages/timers.rs | 4 +++
9 files changed, 119 insertions(+), 38 deletions(-)
diff --git a/src/input_handler.rs b/src/input_handler.rs
index 0a6d003..7c67798 100644
--- a/src/input_handler.rs
+++ b/src/input_handler.rs
@@ -304,33 +304,14 @@ impl SystemInterface {
return true;
}
- let scroll_speed = 24.0;
- let dy = match delta {
- cce_ui::widget::MouseScrollDelta::LineDelta(_, y) => -y * scroll_speed,
- cce_ui::widget::MouseScrollDelta::PixelDelta(pos) => -pos.y as f32,
- };
- let old_scroll = self.scroll_y;
- self.scroll_y = (self.scroll_y + dy).max(0.0).min(self.max_scroll_y);
- if (self.scroll_y - old_scroll).abs() > 0.01 {
- let actual_dy = self.scroll_y - old_scroll;
- for w in &mut self.widgets[self.scrollable_widgets_start_idx..] {
- w.y -= actual_dy;
- }
- for (_, _, _, ty, _, _, bounds) in &mut self.texts[self.scrollable_text_items_start_idx..] {
- *ty -= actual_dy;
- if let Some(ref mut b) = bounds {
- b[1] -= actual_dy;
- b[3] -= actual_dy;
- }
- }
- for (btn, _) in &mut self.page_buttons[self.scrollable_buttons_start_idx..] {
- btn.base_mut().y -= actual_dy;
- }
- self.last_scroll_y = self.scroll_y;
- // The fast path skips the rebuild, so feed the scrollbar here:
- // sync the thumb and raise the bar from behind the window plate
- // (display_list emits it fresh each frame from this state).
- self.page_scroll_bar.scroll_y = self.scroll_y;
+ // The whole-page scroll: a wheel notch moves the motion's target
+ // and `tick_page_scroll` glides the page there; a trackpad finger
+ // moves it now. Either way the bar raises in the same frame.
+ use cce_ui::widget::{Bounds, LINE_PX};
+ self.page_scroll_motion.reconcile(0.0, self.scroll_y);
+ let moved = self.page_scroll_motion.apply(delta, (LINE_PX, LINE_PX), Bounds::max(0.0), Bounds::max(self.max_scroll_y));
+ if moved {
+ self.shift_page_to(self.page_scroll_motion.y.pos());
self.page_scroll_bar.on_scroll();
return true;
}
@@ -338,6 +319,35 @@ impl SystemInterface {
false
}
+ /// Move the page to `new_scroll_y` WITHOUT a rebuild — the wheel fast
+ /// path: the cached widget/text/button geometry shifts in place by the
+ /// delta from the current drawn offset, and the scrollbar thumb follows
+ /// (display_list emits the bar fresh each frame from this state). False
+ /// when the offset did not actually change.
+ pub(crate) fn shift_page_to(&mut self, new_scroll_y: f32) -> bool {
+ let actual_dy = new_scroll_y - self.scroll_y;
+ if actual_dy.abs() <= 0.01 {
+ return false;
+ }
+ self.scroll_y = new_scroll_y;
+ for w in &mut self.widgets[self.scrollable_widgets_start_idx..] {
+ w.y -= actual_dy;
+ }
+ for (_, _, _, ty, _, _, bounds) in &mut self.texts[self.scrollable_text_items_start_idx..] {
+ *ty -= actual_dy;
+ if let Some(ref mut b) = bounds {
+ b[1] -= actual_dy;
+ b[3] -= actual_dy;
+ }
+ }
+ for (btn, _) in &mut self.page_buttons[self.scrollable_buttons_start_idx..] {
+ btn.base_mut().y -= actual_dy;
+ }
+ self.last_scroll_y = self.scroll_y;
+ self.page_scroll_bar.scroll_y = self.scroll_y;
+ true
+ }
+
/// The current page's event-dispatch roots (Phase 6w — SectionContainer dissolved):
/// the pages' widgets themselves, flattened in the legacy propagate order (sections
/// last-to-first, and within a section the container children were visited in
@@ -655,18 +665,24 @@ impl SystemInterface {
&& self.cursor_y <= ry + rh
};
if over_page {
- use cce_ui::widget::{Key, NamedKey};
- let old_scroll = self.scroll_y;
- match &event.logical_key {
- Key::Named(NamedKey::ArrowDown) => self.scroll_y = (self.scroll_y + 24.0).min(self.max_scroll_y),
- Key::Named(NamedKey::ArrowUp) => self.scroll_y = (self.scroll_y - 24.0).max(0.0),
- Key::Named(NamedKey::Home) => self.scroll_y = 0.0,
- Key::Named(NamedKey::End) => self.scroll_y = self.max_scroll_y,
- _ => {}
- }
- if (self.scroll_y - old_scroll).abs() > 0.01 {
+ use cce_ui::widget::{Bounds, Key, NamedKey, LINE_PX};
+ // Arrow steps ride the same glide as wheel notches (a held
+ // key accumulates into one motion); Home/End glide to the
+ // absolute target. `tick_page_scroll` carries the page there.
+ let s = cce_ui::widget::scroll_motion::scroll_settings();
+ let b = Bounds::max(self.max_scroll_y);
+ self.page_scroll_motion.reconcile(0.0, self.scroll_y);
+ let moved = match &event.logical_key {
+ Key::Named(NamedKey::ArrowDown) => self.page_scroll_motion.y.wheel(LINE_PX, b, &s),
+ Key::Named(NamedKey::ArrowUp) => self.page_scroll_motion.y.wheel(-LINE_PX, b, &s),
+ Key::Named(NamedKey::Home) => self.page_scroll_motion.y.scroll_to(0.0, b, &s),
+ Key::Named(NamedKey::End) => self.page_scroll_motion.y.scroll_to(self.max_scroll_y, b, &s),
+ _ => false,
+ };
+ if moved {
+ // With smoothing off the axis jumped: land the page now.
+ self.shift_page_to(self.page_scroll_motion.y.pos());
// Keyboard scrolling raises the bar like the wheel does.
- self.page_scroll_bar.scroll_y = self.scroll_y;
self.page_scroll_bar.on_scroll();
self.needs_rebuild = true;
key_handled = true;
diff --git a/src/main.rs b/src/main.rs
index 2e293d0..b9abe16 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -84,7 +84,12 @@ struct SystemInterface {
width: u32,
height: u32,
needs_rebuild: bool,
+ /// The page's DRAWN offset — `page_scroll_motion` glides it (wheel) or
+ /// coasts it (trackpad flick) by shifting the cached geometry in place;
+ /// direct writes (thumb drag, keyboard, search jump, clamp) are adopted
+ /// by the motion on its next step.
scroll_y: f32,
+ page_scroll_motion: cce_ui::widget::ScrollMotion,
max_scroll_y: f32,
scrollable_widgets_start_idx: usize,
scrollable_text_items_start_idx: usize,
@@ -221,6 +226,7 @@ impl cce_ui::engine::Application for SystemInterface {
height: 680,
needs_rebuild: true,
scroll_y: 0.0,
+ page_scroll_motion: cce_ui::widget::ScrollMotion::new(),
max_scroll_y: 0.0,
scrollable_widgets_start_idx: 0,
scrollable_text_items_start_idx: 0,
@@ -633,6 +639,17 @@ impl SystemInterface {
if self.page_scroll_bar.tick_activity(dt) {
needs_redraw = true;
}
+ // The page's own wheel glide / flick coast: shifts the cached
+ // geometry like the wheel fast path, no rebuild.
+ if self.tick_page_scroll(dt) {
+ needs_redraw = true;
+ }
+ // The current page's inner lists (their glide/coast lives in the
+ // region's tick): a moved list re-lays the page out.
+ if self.app.get_current_page_mut().tick(dt) {
+ needs_redraw = true;
+ self.needs_rebuild = true;
+ }
if self.ui_context.tick(dt) {
needs_redraw = true;
self.needs_rebuild = true;
@@ -641,6 +658,21 @@ impl SystemInterface {
needs_redraw
}
+ /// Advance the page's wheel glide / flick coast; true while the offset is
+ /// moving, so the frame loop keeps drawing until it settles.
+ fn tick_page_scroll(&mut self, dt: f32) -> bool {
+ use cce_ui::widget::Bounds;
+ self.page_scroll_motion.reconcile(0.0, self.scroll_y);
+ if !self.page_scroll_motion.is_animating() {
+ return false;
+ }
+ let moved = self.page_scroll_motion.tick(dt, Bounds::max(0.0), Bounds::max(self.max_scroll_y));
+ if moved {
+ self.shift_page_to(self.page_scroll_motion.y.pos());
+ }
+ moved || self.page_scroll_motion.is_animating()
+ }
+
fn poll_background_updates(&mut self) {
use pages::*;
while let Ok(s) = self.rx_audio.try_recv() {
diff --git a/src/pages/accounts.rs b/src/pages/accounts.rs
index a979c83..75dec5a 100644
--- a/src/pages/accounts.rs
+++ b/src/pages/accounts.rs
@@ -1129,6 +1129,10 @@ impl crate::pages::AppPage for AccountsState {
fn handle_key_input(&mut self, event: &cce_ui::widget::KeyEvent) -> bool {
self.list_visible() && self.list.keyboard(event)
}
+
+ fn tick(&mut self, dt: f32) -> bool {
+ self.list.tick(dt)
+ }
}
#[cfg(test)]
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index d7bb0bd..eaeb621 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -142,6 +142,15 @@ pub trait AppPage {
fn handle_key_input(&mut self, _event: &cce_ui::widget::KeyEvent) -> bool {
false
}
+
+ /// Per-frame upkeep for the dissolved inner lists. A `ScrollRegion`'s
+ /// wheel only moves its target; its `tick` is what glides (wheel) or
+ /// coasts (trackpad flick) the drawn offset there — a page hosting one
+ /// must pump it here or the list freezes after the first notch. True =
+ /// the page's geometry changed (the host re-lays it out).
+ fn tick(&mut self, _dt: f32) -> bool {
+ false
+ }
}
diff --git a/src/pages/network.rs b/src/pages/network.rs
index 4191b18..d1512f3 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -325,6 +325,10 @@ impl crate::pages::AppPage for NetworkState {
fn handle_key_input(&mut self, event: &cce_ui::widget::KeyEvent) -> bool {
self.wifi_list_visible() && self.wifi_list.keyboard(event)
}
+
+ fn tick(&mut self, dt: f32) -> bool {
+ self.wifi_list.tick(dt)
+ }
}
#[cfg(test)]
diff --git a/src/pages/packages.rs b/src/pages/packages.rs
index e07ec6e..648c5a7 100644
--- a/src/pages/packages.rs
+++ b/src/pages/packages.rs
@@ -838,6 +838,10 @@ impl crate::pages::AppPage for PackagesState {
fn handle_key_input(&mut self, event: &cce_ui::widget::KeyEvent) -> bool {
self.loaded && self.active_list().keyboard(event)
}
+
+ fn tick(&mut self, dt: f32) -> bool {
+ self.installed_list.tick(dt) | self.updates_list.tick(dt)
+ }
}
#[cfg(test)]
diff --git a/src/pages/processes.rs b/src/pages/processes.rs
index 8168ccc..0a4a6a2 100644
--- a/src/pages/processes.rs
+++ b/src/pages/processes.rs
@@ -360,6 +360,10 @@ impl crate::pages::AppPage for ProcessesState {
fn handle_key_input(&mut self, event: &cce_ui::widget::KeyEvent) -> bool {
self.loaded && self.cpu_list.keyboard(event)
}
+
+ fn tick(&mut self, dt: f32) -> bool {
+ self.cpu_list.tick(dt)
+ }
}
#[cfg(test)]
diff --git a/src/pages/services.rs b/src/pages/services.rs
index 9293f50..070b46b 100644
--- a/src/pages/services.rs
+++ b/src/pages/services.rs
@@ -525,6 +525,10 @@ impl crate::pages::AppPage for ServicesState {
fn handle_key_input(&mut self, event: &cce_ui::widget::KeyEvent) -> bool {
self.loaded && self.list.keyboard(event)
}
+
+ fn tick(&mut self, dt: f32) -> bool {
+ self.list.tick(dt)
+ }
}
#[cfg(test)]
diff --git a/src/pages/timers.rs b/src/pages/timers.rs
index b0b4803..c8eb4b5 100644
--- a/src/pages/timers.rs
+++ b/src/pages/timers.rs
@@ -738,6 +738,10 @@ impl crate::pages::AppPage for TimersState {
fn handle_key_input(&mut self, event: &cce_ui::widget::KeyEvent) -> bool {
self.loaded && self.list.keyboard(event)
}
+
+ fn tick(&mut self, dt: f32) -> bool {
+ self.list.tick(dt)
+ }
}
#[cfg(test)]