use crate::runtime::scrollbar::state;
use std::time::{Duration, Instant};
use rdom_core::NodeId;
use crate::TuiDom;
use crate::layout::ScrollBehavior;
use crate::node::TuiNodeExt;
use crate::runtime::scrollbar::{max_offsets, write_offsets};
pub const SMOOTH_SCROLL_DURATION: Duration = Duration::from_millis(250);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ScrollBehaviorOption {
#[default]
Auto,
Instant,
Smooth,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct ScrollToOptions {
pub left: Option<i32>,
pub top: Option<i32>,
pub behavior: ScrollBehaviorOption,
}
impl ScrollToOptions {
pub fn new() -> Self {
Self::default()
}
pub fn left(mut self, left: i32) -> Self {
self.left = Some(left);
self
}
pub fn top(mut self, top: i32) -> Self {
self.top = Some(top);
self
}
pub fn behavior(mut self, behavior: ScrollBehaviorOption) -> Self {
self.behavior = behavior;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScrollLogicalPosition {
Start,
Center,
End,
Nearest,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct ScrollIntoViewOptions {
pub behavior: ScrollBehaviorOption,
pub block: ScrollLogicalPosition,
pub inline: ScrollLogicalPosition,
}
impl Default for ScrollIntoViewOptions {
fn default() -> Self {
Self {
behavior: ScrollBehaviorOption::Auto,
block: ScrollLogicalPosition::Start,
inline: ScrollLogicalPosition::Nearest,
}
}
}
impl ScrollIntoViewOptions {
pub fn new() -> Self {
Self::default()
}
pub fn behavior(mut self, behavior: ScrollBehaviorOption) -> Self {
self.behavior = behavior;
self
}
pub fn block(mut self, block: ScrollLogicalPosition) -> Self {
self.block = block;
self
}
pub fn inline(mut self, inline: ScrollLogicalPosition) -> Self {
self.inline = inline;
self
}
}
impl From<bool> for ScrollIntoViewOptions {
fn from(align_to_top: bool) -> Self {
let block = if align_to_top {
ScrollLogicalPosition::Start
} else {
ScrollLogicalPosition::End
};
Self::new().block(block)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SmoothScroll {
from: (usize, usize),
to: (usize, usize),
start: Option<Instant>,
}
pub(crate) fn perform_scroll(
dom: &mut TuiDom,
element: NodeId,
x: i32,
y: i32,
behavior: ScrollBehaviorOption,
) {
abort(dom, element);
if !is_smooth(dom, element, behavior) {
write_offsets(dom, element, x, y);
return;
}
let Some((max_x, max_y)) = max_offsets(dom, element) else {
return;
};
let to = (x.clamp(0, max_x) as usize, y.clamp(0, max_y) as usize);
let mut node = dom.node_mut(element);
let Some(ext) = node.ext_mut() else {
return;
};
let from = (ext.scroll_x, ext.scroll_y);
if from != to {
state::set_smooth(
ext,
Some(SmoothScroll {
from,
to,
start: None,
}),
);
}
}
pub(crate) fn abort(dom: &mut TuiDom, element: NodeId) {
if let Some(ext) = dom.node_mut(element).ext_mut() {
state::set_smooth(ext, None);
}
}
pub(crate) fn destination(dom: &TuiDom, element: NodeId) -> (usize, usize) {
match dom.node(element).tui_ext() {
Some(ext) => state::smooth(ext).map_or((ext.scroll_x, ext.scroll_y), |s| s.to),
None => (0, 0),
}
}
fn is_smooth(dom: &TuiDom, element: NodeId, behavior: ScrollBehaviorOption) -> bool {
match behavior {
ScrollBehaviorOption::Instant => false,
ScrollBehaviorOption::Smooth => true,
ScrollBehaviorOption::Auto => dom
.node(element)
.computed()
.is_some_and(|c| c.scroll_behavior == ScrollBehavior::Smooth),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct StepOutcome {
pub moved: bool,
pub active: bool,
}
pub(crate) fn step_all(dom: &mut TuiDom, now: Instant) -> StepOutcome {
let mut outcome = StepOutcome::default();
for element in in_flight(dom) {
step(dom, element, now, &mut outcome);
}
outcome
}
fn in_flight(dom: &TuiDom) -> Vec<NodeId> {
let mut found = Vec::new();
let mut stack = vec![dom.root()];
while let Some(id) = stack.pop() {
let node = dom.node(id);
if node.tui_ext().is_some_and(|e| state::smooth(e).is_some()) {
found.push(id);
}
let first = stack.len();
stack.extend(node.child_nodes().map(|c| c.id()));
stack[first..].reverse();
}
found
}
fn step(dom: &mut TuiDom, element: NodeId, now: Instant, outcome: &mut StepOutcome) {
let (x, y) = {
let mut node = dom.node_mut(element);
let Some(ext) = node.ext_mut() else {
return;
};
let Some(mut anim) = state::smooth(ext) else {
return;
};
let start = *anim.start.get_or_insert(now);
let t = now.saturating_duration_since(start).as_secs_f64()
/ SMOOTH_SCROLL_DURATION.as_secs_f64();
if t >= 1.0 {
state::set_smooth(ext, None);
anim.to
} else {
state::set_smooth(ext, Some(anim));
outcome.active = true;
let p = ease_out(t);
(
lerp(anim.from.0, anim.to.0, p),
lerp(anim.from.1, anim.to.1, p),
)
}
};
outcome.moved |= write_offsets(dom, element, x as i32, y as i32);
}
fn ease_out(t: f64) -> f64 {
1.0 - (1.0 - t).powi(3)
}
fn lerp(from: usize, to: usize, p: f64) -> usize {
(from as f64 + (to as f64 - from as f64) * p).round() as usize
}
#[cfg(test)]
mod tests;