use crate::application::Task;
use alloc::vec::Vec;
use embassy_time::{Duration, Timer};
use embedded_graphics::prelude::*;
pub const SCROLL_THRESHOLD: i32 = 8;
pub const FRICTION: f32 = 0.95;
pub const MIN_FLING_V: f32 = 20.0;
pub const MAX_FLING_V: f32 = 4000.0;
pub const RUBBER_C: f32 = 0.55;
pub const SPRING_K: f32 = 0.25;
pub const TICK_MS: u64 = 16;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ScrollDirection {
Vertical,
Horizontal,
Both,
None,
}
impl ScrollDirection {
#[must_use]
pub fn scrolls_y(self) -> bool {
matches!(self, ScrollDirection::Vertical | ScrollDirection::Both)
}
#[must_use]
pub fn scrolls_x(self) -> bool {
matches!(self, ScrollDirection::Horizontal | ScrollDirection::Both)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ScrollbarMode {
Off,
On,
Auto,
Active,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum SnapMode {
None,
Start,
Center,
End,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum GesturePhase {
Idle,
Pressing,
Dragging,
Flinging,
Springing,
}
#[derive(Copy, Clone, Debug)]
pub struct ScrollState {
pub offset: Point,
pub accum: (f32, f32),
pub phase: GesturePhase,
pub press_origin: Point,
pub offset_at_press: Point,
pub last_point: Point,
pub velocity: (f32, f32),
pub last_sample_point: Point,
pub last_sample_ms: u64,
pub spring_target: Point,
pub max_offset: Point,
pub content: Size,
pub viewport: Size,
}
impl Default for ScrollState {
fn default() -> Self {
Self::new()
}
}
impl ScrollState {
#[must_use]
pub const fn new() -> Self {
Self {
offset: Point::new(0, 0),
accum: (0.0, 0.0),
phase: GesturePhase::Idle,
press_origin: Point::new(0, 0),
offset_at_press: Point::new(0, 0),
last_point: Point::new(0, 0),
velocity: (0.0, 0.0),
last_sample_point: Point::new(0, 0),
last_sample_ms: 0,
spring_target: Point::new(0, 0),
max_offset: Point::new(0, 0),
content: Size::new(0, 0),
viewport: Size::new(0, 0),
}
}
#[must_use]
pub fn is_animating(&self) -> bool {
matches!(self.phase, GesturePhase::Flinging | GesturePhase::Springing)
}
#[must_use]
pub fn is_active(&self) -> bool {
self.phase != GesturePhase::Idle
}
fn cache_geometry(&mut self, content: Size, viewport: Size) {
self.content = content;
self.viewport = viewport;
self.max_offset = Point::new(
(content.width as i32 - viewport.width as i32).max(0),
(content.height as i32 - viewport.height as i32).max(0),
);
}
#[must_use]
pub fn clamp_offset(&self, offset: Point) -> Point {
Point::new(
offset.x.clamp(0, self.max_offset.x),
offset.y.clamp(0, self.max_offset.y),
)
}
#[must_use]
pub fn rubber_band(&self, offset: Point) -> Point {
Point::new(
rubber_axis(offset.x, self.max_offset.x, self.viewport.width),
rubber_axis(offset.y, self.max_offset.y, self.viewport.height),
)
}
#[must_use]
pub fn nearest_snap(&self, offset: Point, snap: SnapMode, snap_lines: &[i32]) -> Point {
let clamped = self.clamp_offset(offset);
if snap == SnapMode::None || snap_lines.is_empty() {
return clamped;
}
if self.max_offset.y > 0 {
let target = nearest(clamped.y, snap_lines).clamp(0, self.max_offset.y);
Point::new(clamped.x, target)
} else if self.max_offset.x > 0 {
let target = nearest(clamped.x, snap_lines).clamp(0, self.max_offset.x);
Point::new(target, clamped.y)
} else {
clamped
}
}
pub fn apply(&mut self, msg: ScrollMsg, now_ms: u64) {
match msg {
ScrollMsg::Press {
point,
content,
viewport,
} => self.on_press(point, content, viewport, now_ms),
ScrollMsg::DragTo {
point,
content,
viewport,
} => self.on_move(point, content, viewport, now_ms),
ScrollMsg::Release {
point,
content,
viewport,
snap_lines,
} => self.on_release(point, content, viewport, &snap_lines, now_ms),
}
}
fn on_press(&mut self, point: Point, content: Size, viewport: Size, now_ms: u64) {
self.cache_geometry(content, viewport);
self.phase = GesturePhase::Pressing;
self.press_origin = point;
self.offset_at_press = self.clamp_offset(self.offset);
self.offset = self.offset_at_press;
self.last_point = point;
self.velocity = (0.0, 0.0);
self.accum = (0.0, 0.0);
self.last_sample_point = point;
self.last_sample_ms = now_ms;
}
fn on_move(&mut self, point: Point, content: Size, viewport: Size, now_ms: u64) {
self.cache_geometry(content, viewport);
if self.phase == GesturePhase::Idle {
self.on_press(point, content, viewport, now_ms);
return;
}
self.phase = GesturePhase::Dragging;
let raw = Point::new(
self.offset_at_press.x - (point.x - self.press_origin.x),
self.offset_at_press.y - (point.y - self.press_origin.y),
);
self.offset = self.rubber_band(raw);
let dt = now_ms.saturating_sub(self.last_sample_ms);
if dt > 0 {
let dx = (point.x - self.last_sample_point.x) as f32;
let dy = (point.y - self.last_sample_point.y) as f32;
let s = 1000.0 / dt as f32;
self.velocity = (clamp_v(-dx * s), clamp_v(-dy * s));
self.last_sample_point = point;
self.last_sample_ms = now_ms;
}
self.last_point = point;
}
fn on_release(
&mut self,
point: Point,
content: Size,
viewport: Size,
snap_lines: &[i32],
now_ms: u64,
) {
self.cache_geometry(content, viewport);
let _ = (point, now_ms);
self.accum = (0.0, 0.0);
let past_edge = self.offset != self.clamp_offset(self.offset);
let fast = self.velocity.0.abs() >= MIN_FLING_V || self.velocity.1.abs() >= MIN_FLING_V;
if !past_edge && fast {
self.phase = GesturePhase::Flinging;
} else {
self.spring_target =
self.nearest_snap(self.offset, snap_mode_from(snap_lines), snap_lines);
self.phase = GesturePhase::Springing;
}
}
pub fn tick(&mut self, dt_ms: u32, snap: SnapMode, snap_lines: &[i32]) {
let dt = (dt_ms.max(1) as f32) / 1000.0;
match self.phase {
GesturePhase::Flinging => {
self.accum.0 += self.velocity.0 * dt;
self.accum.1 += self.velocity.1 * dt;
let step = Point::new(self.accum.0 as i32, self.accum.1 as i32);
self.accum.0 -= step.x as f32;
self.accum.1 -= step.y as f32;
self.offset += step;
self.velocity.0 *= FRICTION;
self.velocity.1 *= FRICTION;
let past_edge = self.offset != self.clamp_offset(self.offset);
let slow =
self.velocity.0.abs() < MIN_FLING_V && self.velocity.1.abs() < MIN_FLING_V;
if past_edge || slow {
self.spring_target = self.nearest_snap(self.offset, snap, snap_lines);
self.velocity = (0.0, 0.0);
self.accum = (0.0, 0.0);
self.phase = GesturePhase::Springing;
}
}
GesturePhase::Springing => {
let dx = (self.spring_target.x - self.offset.x) as f32;
let dy = (self.spring_target.y - self.offset.y) as f32;
if dx.abs() < 1.0 && dy.abs() < 1.0 {
self.offset = self.spring_target;
self.accum = (0.0, 0.0);
self.velocity = (0.0, 0.0);
self.phase = GesturePhase::Idle;
} else {
self.accum.0 += dx * SPRING_K;
self.accum.1 += dy * SPRING_K;
let step = Point::new(self.accum.0 as i32, self.accum.1 as i32);
self.accum.0 -= step.x as f32;
self.accum.1 -= step.y as f32;
let step = Point::new(nudge(step.x, dx), nudge(step.y, dy));
self.offset += step;
}
}
_ => {}
}
}
}
#[derive(Clone, Debug)]
pub enum ScrollMsg {
Press {
point: Point,
content: Size,
viewport: Size,
},
DragTo {
point: Point,
content: Size,
viewport: Size,
},
Release {
point: Point,
content: Size,
viewport: Size,
snap_lines: Vec<i32>,
},
}
#[must_use]
pub fn tick_task<M: 'static>(msg: M) -> Task<M> {
Task::perform(async move {
Timer::after(Duration::from_millis(TICK_MS)).await;
Some(msg)
})
}
fn clamp_v(v: f32) -> f32 {
v.clamp(-MAX_FLING_V, MAX_FLING_V)
}
fn rubber_axis(value: i32, max: i32, dim: u32) -> i32 {
if value < 0 {
-resist(-value, dim)
} else if value > max {
max + resist(value - max, dim)
} else {
value
}
}
fn resist(overshoot: i32, dim: u32) -> i32 {
let o = overshoot as f32;
let d = (dim.max(1)) as f32;
(o * d / (d + o * RUBBER_C)) as i32
}
fn nearest(value: i32, lines: &[i32]) -> i32 {
let mut best = lines[0];
let mut best_d = (value - best).abs();
for &l in &lines[1..] {
let d = (value - l).abs();
if d < best_d {
best_d = d;
best = l;
}
}
best
}
fn nudge(step: i32, delta: f32) -> i32 {
if step != 0 {
step
} else if delta > 0.5 {
1
} else if delta < -0.5 {
-1
} else {
0
}
}
fn snap_mode_from(snap_lines: &[i32]) -> SnapMode {
if snap_lines.is_empty() {
SnapMode::None
} else {
SnapMode::Start
}
}