authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 02:38:27-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 03:11:20-07:00
logba16a18feec9b6b0d5b03464b1e8cda9ded4a81e
tree9ecd9d41c836152bb8342c0b34c03c18fe6a873c
parent12d02ba99c1881032feaeb079d60181d0f066945
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: the wheel and trackpad scroll the interface as they scroll the page

Lists, menus, the sidebar, Options and the section tabs eased towards where the wheel sent them, so on GNOME, where the page follows each delta as it comes, the settings felt smooth-scrolled and out of step. Every scroller in the interface kit now moves by each wheel or trackpad delta at once, from the same deltas the page takes; only programmatic jumps, as scrolling to an index entry or a newly opened tab, still ease. Assisted-by: claude-opus-5.5

4 files changed, 34 insertions(+), 20 deletions(-)

crates/ui/src/lib.rs+4-2
......@@ -866,7 +866,7 @@ impl Ui {
866866 }
867867
868868 /// Scrolls `scroller`, a scrolling box, until `target` within it starts at its top, both
869 /// as last laid out; the wheel's easing carries it there.
869 /// as last laid out, easing there.
870870 pub fn scroll_to(&mut self, scroller: Id, target: Id) {
871871 let (Some(top), Some(start)) = (self.rect(target), self.rect(scroller)) else {
872872 return;
......@@ -1036,7 +1036,9 @@ impl Ui {
10361036 Some(id) => {
10371037 let state = self.states.entry(id).or_default();
10381038 let most = (state.content - (state.rect[3] - state.rect[1])).max(0.0);
1039 state.scroll_target = (state.scroll_target - delta[1]).clamp(0.0, most);
1039 // The wheel moves the content at once, as on the page; only jumps ease.
1040 state.scroll = (state.scroll - delta[1]).clamp(0.0, most);
1041 state.scroll_target = state.scroll;
10401042 self.signals.entry(id).or_default().events.push(event);
10411043 }
10421044 None => {}
crates/ui/src/list.rs+2-1
......@@ -132,7 +132,8 @@ pub fn list<R: Rows>(
132132
133133 for event in ui.signal(id).events {
134134 if let Event::Wheel(delta) = event {
135 state.target = (state.target - f64::from(delta[1])).clamp(0.0, most);
135 state.scroll = (state.scroll - f64::from(delta[1])).clamp(0.0, most);
136 state.target = state.scroll;
136137 }
137138 }
138139 let mut clicked = None;
crates/ui/src/shell.rs+12-2
......@@ -65,12 +65,18 @@ pub fn section_tabs(
6565 let wide = left + lean(height) + crate::SHADOW[0];
6666 let view = ui.rect(row).map_or(wide, |rect| rect[2] - rect[0]);
6767 let most = (wide - view).max(0.0);
68 let [scroll, shown] = [row.child("scroll"), row.child("shown")];
68 let [scroll, shown, shift] = [row.child("scroll"), row.child("shown"), row.child("offset")];
6969 let held_value = |ui: &Ui, id: Id| ui.states.get(&id).and_then(|state| state.tween);
7070 let mut target = held_value(ui, scroll).map_or(0.0, |[_, target]| target);
71 // The wheel moves the tabs at once from where they show, as on the page; only jumps ease.
72 let mut wheeled = false;
7173 for event in ui.signal(row).events {
7274 if let crate::Event::Wheel([x, y]) = event {
75 if !wheeled {
76 target = held_value(ui, shift).map_or(target, |[value, _]| value);
77 }
7378 target -= x + y;
79 wheeled = true;
7480 }
7581 }
7682 if held_value(ui, shown).is_none_or(|[_, was]| was != active as f32)
......@@ -91,7 +97,11 @@ pub fn section_tabs(
9197 Some(start)
9298 });
9399 let target = ui.hold(scroll, target.clamp(0.0, most));
94 let offset = ui.animate(row.child("offset"), target);
100 let offset = if wheeled {
101 ui.hold(shift, target)
102 } else {
103 ui.animate(shift, target)
104 };
95105 let fade = |cut: bool| if cut { EDGE } else { 0.0 };
96106 ui.open_as(
97107 row,
crates/ui/src/tests.rs+16-15
......@@ -542,10 +542,12 @@ fn wheel_scrolls_within_the_content_and_clips_children() {
542542 build(&mut ui);
543543 ui.event(Event::PointerMoved([50.0, 50.0]));
544544 ui.event(Event::Wheel([0.0, -1000.0]));
545 for _ in 0..60 {
546 build(&mut ui);
547 }
548 assert_eq!(ui.states[&list.unwrap()].scroll, 200.0);
545 build(&mut ui);
546 assert_eq!(
547 ui.states[&list.unwrap()].scroll,
548 200.0,
549 "at once, as on the page"
550 );
549551 assert_eq!(ui.rect(last.unwrap()), Some([0.0, 70.0, 100.0, 100.0]));
550552 let layers = ui.layers();
551553 assert!(matches!(
......@@ -1906,10 +1908,10 @@ fn lists_hold_the_selection_still_as_items_arrive_and_leave_above() {
19061908 list_frame(&mut ui, &rows, &mut selected, &[]);
19071909 rows = Keyed::new((20_000..20_050).chain(rows.keys.iter().copied()));
19081910 list_frame(&mut ui, &rows, &mut selected, &[]);
1909 let moving = row_top(&ui, 500).unwrap();
1910 assert!(
1911 moving < at && moving > at - 60.0,
1912 "the wheel eases on through arrivals"
1911 assert_eq!(
1912 row_top(&ui, 500),
1913 Some(at - 60.0),
1914 "the wheel moves the rows at once, and they hold through arrivals"
19131915 );
19141916 settle_list(&mut ui, &rows, &mut selected);
19151917 assert_eq!(row_top(&ui, 500), Some(at - 60.0));
......@@ -2950,16 +2952,15 @@ fn the_wheel_scrolls_overflowing_tabs_sideways_either_way_it_turns() {
29502952 let first = ui.rect(shell::tab_id(row, 0)).unwrap()[0];
29512953 ui.event(Event::PointerMoved([100.0, 14.0]));
29522954 ui.event(Event::Wheel([0.0, -40.0]));
2953 settle_tabs(&mut ui, 0);
2955 tab_row(&mut ui, 0);
2956 tab_row(&mut ui, 0);
29542957 let scrolled = ui.rect(shell::tab_id(row, 0)).unwrap()[0];
2955 assert!(
2956 (first - scrolled - 40.0).abs() < 0.5,
2957 "{first} to {scrolled}"
2958 );
2958 assert_eq!(first - scrolled, 40.0, "at once, as on the page");
29592959 ui.event(Event::Wheel([30.0, 0.0]));
2960 settle_tabs(&mut ui, 0);
2960 tab_row(&mut ui, 0);
2961 tab_row(&mut ui, 0);
29612962 let back = ui.rect(shell::tab_id(row, 0)).unwrap()[0];
2962 assert!((back - scrolled - 30.0).abs() < 0.5, "{scrolled} to {back}");
2963 assert_eq!(back - scrolled, 30.0);
29632964}
29642965
29652966#[test]