From ba16a18feec9b6b0d5b03464b1e8cda9ded4a81e Mon Sep 17 00:00:00 2001 From: clover caruso Date: Fri, 2 Oct 2026 02:38:27 -0700 Subject: [PATCH] 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 --- crates/ui/src/lib.rs | 6 ++++-- crates/ui/src/list.rs | 3 ++- crates/ui/src/shell.rs | 14 ++++++++++++-- crates/ui/src/tests.rs | 31 ++++++++++++++++--------------- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index c8d6ac4f0d910d8a7df9b2d2f44984fd5a9a1a83..433bc139cfc273e9d086473eabb55cb94ae579ed 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -866,7 +866,7 @@ impl Ui { } /// Scrolls `scroller`, a scrolling box, until `target` within it starts at its top, both - /// as last laid out; the wheel's easing carries it there. + /// as last laid out, easing there. pub fn scroll_to(&mut self, scroller: Id, target: Id) { let (Some(top), Some(start)) = (self.rect(target), self.rect(scroller)) else { return; @@ -1036,7 +1036,9 @@ impl Ui { Some(id) => { let state = self.states.entry(id).or_default(); let most = (state.content - (state.rect[3] - state.rect[1])).max(0.0); - state.scroll_target = (state.scroll_target - delta[1]).clamp(0.0, most); + // The wheel moves the content at once, as on the page; only jumps ease. + state.scroll = (state.scroll - delta[1]).clamp(0.0, most); + state.scroll_target = state.scroll; self.signals.entry(id).or_default().events.push(event); } None => {} diff --git a/crates/ui/src/list.rs b/crates/ui/src/list.rs index f2239326690c3c4c58d91304f11c15aa5b336f40..93c5c4ed2e14468f984adf3fb2d9d4c405bd1145 100644 --- a/crates/ui/src/list.rs +++ b/crates/ui/src/list.rs @@ -132,7 +132,8 @@ pub fn list( for event in ui.signal(id).events { if let Event::Wheel(delta) = event { - state.target = (state.target - f64::from(delta[1])).clamp(0.0, most); + state.scroll = (state.scroll - f64::from(delta[1])).clamp(0.0, most); + state.target = state.scroll; } } let mut clicked = None; diff --git a/crates/ui/src/shell.rs b/crates/ui/src/shell.rs index 8cee9f9eaf614fbd0ab377f0f51125e6e5a3af1f..e530d0523391bf391f5b4f7a41557d2c379d1533 100644 --- a/crates/ui/src/shell.rs +++ b/crates/ui/src/shell.rs @@ -65,12 +65,18 @@ pub fn section_tabs( let wide = left + lean(height) + crate::SHADOW[0]; let view = ui.rect(row).map_or(wide, |rect| rect[2] - rect[0]); let most = (wide - view).max(0.0); - let [scroll, shown] = [row.child("scroll"), row.child("shown")]; + let [scroll, shown, shift] = [row.child("scroll"), row.child("shown"), row.child("offset")]; let held_value = |ui: &Ui, id: Id| ui.states.get(&id).and_then(|state| state.tween); let mut target = held_value(ui, scroll).map_or(0.0, |[_, target]| target); + // The wheel moves the tabs at once from where they show, as on the page; only jumps ease. + let mut wheeled = false; for event in ui.signal(row).events { if let crate::Event::Wheel([x, y]) = event { + if !wheeled { + target = held_value(ui, shift).map_or(target, |[value, _]| value); + } target -= x + y; + wheeled = true; } } if held_value(ui, shown).is_none_or(|[_, was]| was != active as f32) @@ -91,7 +97,11 @@ pub fn section_tabs( Some(start) }); let target = ui.hold(scroll, target.clamp(0.0, most)); - let offset = ui.animate(row.child("offset"), target); + let offset = if wheeled { + ui.hold(shift, target) + } else { + ui.animate(shift, target) + }; let fade = |cut: bool| if cut { EDGE } else { 0.0 }; ui.open_as( row, diff --git a/crates/ui/src/tests.rs b/crates/ui/src/tests.rs index 3bc93204a6bb413a7b51552742e7562a42105736..97f317480f7bc253eba8e4aff47b2cd2cbf40fb3 100644 --- a/crates/ui/src/tests.rs +++ b/crates/ui/src/tests.rs @@ -542,10 +542,12 @@ fn wheel_scrolls_within_the_content_and_clips_children() { build(&mut ui); ui.event(Event::PointerMoved([50.0, 50.0])); ui.event(Event::Wheel([0.0, -1000.0])); - for _ in 0..60 { - build(&mut ui); - } - assert_eq!(ui.states[&list.unwrap()].scroll, 200.0); + build(&mut ui); + assert_eq!( + ui.states[&list.unwrap()].scroll, + 200.0, + "at once, as on the page" + ); assert_eq!(ui.rect(last.unwrap()), Some([0.0, 70.0, 100.0, 100.0])); let layers = ui.layers(); assert!(matches!( @@ -1906,10 +1908,10 @@ fn lists_hold_the_selection_still_as_items_arrive_and_leave_above() { list_frame(&mut ui, &rows, &mut selected, &[]); rows = Keyed::new((20_000..20_050).chain(rows.keys.iter().copied())); list_frame(&mut ui, &rows, &mut selected, &[]); - let moving = row_top(&ui, 500).unwrap(); - assert!( - moving < at && moving > at - 60.0, - "the wheel eases on through arrivals" + assert_eq!( + row_top(&ui, 500), + Some(at - 60.0), + "the wheel moves the rows at once, and they hold through arrivals" ); settle_list(&mut ui, &rows, &mut selected); 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() { let first = ui.rect(shell::tab_id(row, 0)).unwrap()[0]; ui.event(Event::PointerMoved([100.0, 14.0])); ui.event(Event::Wheel([0.0, -40.0])); - settle_tabs(&mut ui, 0); + tab_row(&mut ui, 0); + tab_row(&mut ui, 0); let scrolled = ui.rect(shell::tab_id(row, 0)).unwrap()[0]; - assert!( - (first - scrolled - 40.0).abs() < 0.5, - "{first} to {scrolled}" - ); + assert_eq!(first - scrolled, 40.0, "at once, as on the page"); ui.event(Event::Wheel([30.0, 0.0])); - settle_tabs(&mut ui, 0); + tab_row(&mut ui, 0); + tab_row(&mut ui, 0); let back = ui.rect(shell::tab_id(row, 0)).unwrap()[0]; - assert!((back - scrolled - 30.0).abs() < 0.5, "{scrolled} to {back}"); + assert_eq!(back - scrolled, 30.0); } #[test] -- 2.54.0