diff --git a/src/lib.rs b/src/lib.rs index a180c224..7e61730b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,6 +108,8 @@ pub mod task; pub mod theme; +pub mod scroll; + #[doc(inline)] pub use theme::{Theme, style}; diff --git a/src/scroll.rs b/src/scroll.rs new file mode 100644 index 00000000..b6d42378 --- /dev/null +++ b/src/scroll.rs @@ -0,0 +1,112 @@ +use iced::Task; +use iced::mouse::ScrollDelta; +use std::time::{Duration, Instant}; + +// Number of scroll pixels before changing workspace +const SCROLL_PIXELS: f32 = 24.0; + +// Timeout for scroll accumulation; older partial scroll is dropped +const SCROLL_TIMEOUT: Duration = Duration::from_millis(100); + +/// A scroll delta with discrete integer deltas +#[derive(Debug, Default, Clone, Copy)] +pub struct DiscreteScrollDelta { + pub x: isize, + pub y: isize, +} + +/// Helper for accumulating and converting pixel/line scrolls into and integer +/// delta between discrete options. +#[derive(Debug, Default)] +pub struct DiscreteScrollState { + x: Scroll, + y: Scroll, + rate_limit: Option, +} + +impl DiscreteScrollState { + /// Set a rate limit. If set, a call to `update()` will only not produce + /// values other than 1, -1, or 0 and a non-zero return value will not + /// occur more frequently than this duration. + pub fn rate_limit(mut self, rate_limit: Option) -> Self { + self.rate_limit = rate_limit; + self + } + + /// Reset, clearing any acculuated scroll events that haven't been + /// converted to discrete events yet. + pub fn reset(&mut self) { + self.x.reset(); + self.y.reset(); + } + + /// Accumulate delta with a timer + pub fn update(&mut self, delta: ScrollDelta) -> DiscreteScrollDelta { + let (x, y) = match delta { + ScrollDelta::Pixels { x, y } => (x / SCROLL_PIXELS, y / SCROLL_PIXELS), + ScrollDelta::Lines { x, y } => (x, y), + }; + + DiscreteScrollDelta { + x: self.x.update(x, self.rate_limit), + y: self.y.update(y, self.rate_limit), + } + } +} + +/// Scroll over a single axis +#[derive(Debug, Default)] +struct Scroll { + scroll: Option<(f32, Instant)>, + last_discrete: Option, +} + +impl Scroll { + fn reset(&mut self) { + *self = Default::default(); + } + + fn update(&mut self, delta: f32, rate_limit: Option) -> isize { + if delta == 0. { + // If delta is 0, scroll is on other axis; clear accumulated scroll + self.reset(); + 0 + } else { + let previous_scroll = if let Some((scroll, last_scroll_time)) = self.scroll { + if last_scroll_time.elapsed() > SCROLL_TIMEOUT { + 0. + } else { + scroll + } + } else { + 0. + }; + + let scroll = previous_scroll + delta; + + if self + .last_discrete + .is_some_and(|time| time.elapsed() < rate_limit.unwrap_or(Duration::ZERO)) + { + // If rate limit is hit, continute accumulating, but don't return + // a discrete event yet. + self.scroll = Some((scroll, Instant::now())); + 0 + } else { + // Return integer part of scroll, and keep remainder + self.scroll = Some((scroll.fract(), Instant::now())); + let mut discrete = scroll.trunc() as isize; + if discrete != 0 { + self.last_discrete = Some(Instant::now()); + } + if rate_limit.is_some() { + // If we are rate limiting, don't return multiple discrete events + // at once; drop extras. + discrete.signum() + } else { + discrete + } + } + } + } +}