From 1e49511fcf16d9cd48a71b4005391c88f2e02399 Mon Sep 17 00:00:00 2001 From: Frederic Laing Date: Tue, 30 Jun 2026 21:39:17 +0200 Subject: [PATCH] fix: accumulate fractional wheel-line scroll deltas --- src/terminal_box.rs | 67 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/src/terminal_box.rs b/src/terminal_box.rs index 91c37c8..92947e6 100644 --- a/src/terminal_box.rs +++ b/src/terminal_box.rs @@ -1553,16 +1553,22 @@ where } else { match delta { ScrollDelta::Lines { x: _, y } => { - //TODO: this adjustment is just a guess! + // High-resolution wheels deliver a notch as many + // fractional deltas; accumulate them so nothing + // below one whole line is lost (see + // accumulate_wheel_lines). state.scroll_pixels = 0.0; - let lines = (-y * 6.0) as i32; + let (lines, remainder) = + accumulate_wheel_lines(*y, state.scroll_lines); + state.scroll_lines = remainder; if lines != 0 { - terminal.scroll(TerminalScroll::Delta(-lines)); + terminal.scroll(TerminalScroll::Delta(lines)); } shell.capture_event(); } ScrollDelta::Pixels { x: _, y } => { //TODO: this adjustment is just a guess! + state.scroll_lines = 0.0; state.scroll_pixels -= y * 6.0; let mut lines = 0; let metrics = terminal.with_buffer(|buffer| buffer.metrics()); @@ -1728,6 +1734,26 @@ fn update_active_regex_match( } } +/// Whole lines scrolled per unit of wheel `Lines` delta. +//TODO: this adjustment is just a guess! +const SCROLL_LINE_MULTIPLIER: f32 = 6.0; + +/// Convert a wheel `Lines` delta into whole lines to scroll, accumulating the +/// fractional remainder across events. +/// +/// High-resolution wheels deliver a single physical notch as many fractional +/// deltas (e.g. `y = 0.125`). Scaling by [`SCROLL_LINE_MULTIPLIER`] and +/// truncating per event discards anything below one whole line, so most of +/// those events would scroll nothing. Carrying the remainder forward means the +/// fractions add up and a full notch scrolls the intended amount. +/// +/// Returns `(whole_lines, new_accumulator)`. +fn accumulate_wheel_lines(y: f32, accumulator: f32) -> (i32, f32) { + let total = accumulator + y * SCROLL_LINE_MULTIPLIER; + let lines = total.trunc() as i32; + (lines, total - lines as f32) +} + fn shade(color: cosmic_text::Color, is_focused: bool) -> cosmic_text::Color { if is_focused { color @@ -1929,6 +1955,7 @@ pub struct State { dragging: Option, is_focused: bool, scroll_pixels: f32, + scroll_lines: f32, scrollbar_rect: Cell>, autoscroll: DragAutoscroll, preedit: Option, @@ -1943,6 +1970,7 @@ impl State { dragging: None, is_focused: false, scroll_pixels: 0.0, + scroll_lines: 0.0, scrollbar_rect: Cell::new(Rectangle::default()), autoscroll: DragAutoscroll::new(AUTOSCROLL_INTERVAL), preedit: None, @@ -2022,7 +2050,38 @@ fn ss3(code: &str, modifiers: u8) -> Option> { #[cfg(test)] mod tests { - use super::{EdgeScrollDirection, edge_scroll_adjustment}; + use super::{EdgeScrollDirection, accumulate_wheel_lines, edge_scroll_adjustment}; + + #[test] + fn wheel_lines_single_fractional_event_keeps_remainder() { + // A high-res wheel notch arrives as fractional deltas. A lone 0.125 + // event is less than one whole line, but must not be discarded. + let (lines, remainder) = accumulate_wheel_lines(0.125, 0.0); + assert_eq!(lines, 0); + assert!((remainder - 0.75).abs() < f32::EPSILON); + } + + #[test] + fn wheel_lines_accumulate_into_whole_lines() { + // Eight 0.125 deltas make up one physical notch and must scroll the + // full SCROLL_LINE_MULTIPLIER (6) lines, not be lost to truncation. + let mut accumulator = 0.0; + let mut total = 0; + for _ in 0..8 { + let (lines, remainder) = accumulate_wheel_lines(0.125, accumulator); + accumulator = remainder; + total += lines; + } + assert_eq!(total, 6); + } + + #[test] + fn wheel_lines_preserve_direction() { + let (down, _) = accumulate_wheel_lines(0.25, 0.0); + let (up, _) = accumulate_wheel_lines(-0.25, 0.0); + assert_eq!(down, 1); + assert_eq!(up, -1); + } const BUFFER_HEIGHT: f32 = 200.0; const CELL_HEIGHT: f32 = 20.0;