use std::time::{Duration, Instant};
use crate::tdom::node::NodeId;
use super::InputState;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollTarget {
Page,
Container(NodeId),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollIntoViewBlock {
Start,
Center,
End,
}
pub fn scroll_into_view_target(
rect: crate::layout::Rect,
block: ScrollIntoViewBlock,
viewport_rows: u32,
) -> u32 {
let (y, height) = (
u32::try_from(rect.y).unwrap_or(u32::MAX),
u32::try_from(rect.height).unwrap_or(u32::MAX),
);
match block {
ScrollIntoViewBlock::Start => y,
ScrollIntoViewBlock::Center => y.saturating_sub(viewport_rows.saturating_sub(height) / 2),
ScrollIntoViewBlock::End => y.saturating_add(height).saturating_sub(viewport_rows),
}
}
pub const ANIM_POLL_MS: u64 = 16;
const MIN_DURATION_MS: u64 = 80;
const MAX_DURATION_MS: u64 = 280;
const MS_PER_ROW: u64 = 3;
pub fn scroll_anim_duration(from: u32, to: u32) -> Duration {
let rows = u64::from(from.abs_diff(to));
Duration::from_millis((rows * MS_PER_ROW).clamp(MIN_DURATION_MS, MAX_DURATION_MS))
}
pub fn ease_out_cubic(t: f64) -> f64 {
let t = t.clamp(0.0, 1.0);
1.0 - (1.0 - t).powi(3)
}
pub fn step_target(base: u32, delta: i32, min: u32, max: u32) -> Option<u32> {
let next = if delta < 0 {
base.saturating_sub(delta.unsigned_abs())
} else {
base.saturating_add(u32::try_from(delta).unwrap_or(u32::MAX))
};
let next = next.clamp(min, max);
(next != base).then_some(next)
}
pub fn active_scroll_target(state: &InputState, kind: ScrollTarget) -> Option<u32> {
match &state.scroll_anim {
Some(anim) if anim.target() == kind => Some(anim.to()),
_ => None,
}
}
#[derive(Debug)]
pub struct ScrollAnim {
target: ScrollTarget,
from: u32,
to: u32,
start: Instant,
duration: Duration,
}
impl ScrollAnim {
pub fn start(target: ScrollTarget, from: u32, to: u32, now: Instant) -> Self {
Self {
target,
from,
to,
start: now,
duration: scroll_anim_duration(from, to),
}
}
pub fn target(&self) -> ScrollTarget {
self.target
}
pub fn to(&self) -> u32 {
self.to
}
pub fn position(&self, now: Instant) -> u32 {
let t =
now.saturating_duration_since(self.start).as_secs_f64() / self.duration.as_secs_f64();
let eased = ease_out_cubic(t);
let distance = self.to as i64 - self.from as i64;
(self.from as f64 + distance as f64 * eased)
.max(0.0)
.round() as u32
}
pub fn finished(&self, now: Instant) -> bool {
now.saturating_duration_since(self.start) >= self.duration
}
pub fn retarget(&mut self, to: u32, now: Instant) {
self.from = self.position(now);
self.to = to;
self.start = now;
self.duration = scroll_anim_duration(self.from, to);
}
}