use std::time::{
Duration,
Instant,
};
use freya_core::prelude::*;
use torin::{
geometry::{
Point2D,
Size2D,
Vector2D,
},
prelude::Direction,
};
use crate::scrollviews::shared::get_corrected_scroll_position;
#[derive(Default, PartialEq, Eq)]
pub enum ScrollPosition {
#[default]
Start,
End,
}
impl ScrollPosition {
fn offset(&self) -> i32 {
match self {
Self::Start => 0,
Self::End => ScrollController::END,
}
}
}
#[derive(Default)]
pub struct ScrollConfig {
pub default_vertical_position: ScrollPosition,
pub default_horizontal_position: ScrollPosition,
}
#[derive(PartialEq, Clone, Copy)]
pub struct ScrollController {
pub(crate) scroll: State<(i32, i32)>,
pub bounds: State<Option<(Size2D, Size2D)>>,
pub(crate) damp: State<SmoothDamp>,
pub(crate) drag: State<Drag>,
pub(crate) task: State<Option<TaskHandle>>,
}
impl From<ScrollController> for (i32, i32) {
fn from(val: ScrollController) -> Self {
*val.scroll.read()
}
}
impl ScrollController {
const END: i32 = i32::MIN;
pub fn new(x: i32, y: i32) -> Self {
Self {
scroll: State::create((x, y)),
bounds: State::create(None),
damp: State::create(SmoothDamp::new()),
drag: State::create(Drag::default()),
task: State::create(None),
}
}
pub fn apply_layout(&mut self, content_size: Size2D, viewport_size: Size2D) {
self.bounds.set(Some((content_size, viewport_size)));
let (x, y) = *self.scroll.peek();
self.scroll_to_x(x);
self.scroll_to_y(y);
}
pub fn position(self) -> Point2D {
let (x, y): (i32, i32) = self.into();
Point2D::new(x as f32, y as f32)
}
pub fn scroll_to_x(&mut self, to: i32) -> bool {
let to = self.bounded_position(to, Direction::Horizontal);
let changed = self.scroll.peek().0 != to;
if changed {
self.scroll.write().0 = to;
}
changed
}
pub fn scroll_to_y(&mut self, to: i32) -> bool {
let to = self.bounded_position(to, Direction::Vertical);
let changed = self.scroll.peek().1 != to;
if changed {
self.scroll.write().1 = to;
}
changed
}
fn bounded_position(&self, position: i32, direction: Direction) -> i32 {
let Some((content_size, viewport_size)) = *self.bounds.read() else {
return position;
};
let (content_size, viewport_size) = match direction {
Direction::Horizontal => (content_size.width, viewport_size.width),
Direction::Vertical => (content_size.height, viewport_size.height),
};
get_corrected_scroll_position(content_size, viewport_size, position as f32) as i32
}
pub fn scroll_to(&mut self, scroll_position: ScrollPosition, scroll_direction: Direction) {
let to = scroll_position.offset();
match scroll_direction {
Direction::Vertical => self.scroll_to_y(to),
Direction::Horizontal => self.scroll_to_x(to),
};
}
}
pub fn use_scroll_controller(config: impl FnOnce() -> ScrollConfig) -> ScrollController {
use_hook(|| {
let config = config();
ScrollController::new(
config.default_horizontal_position.offset(),
config.default_vertical_position.offset(),
)
})
}
const SETTLE_DISTANCE: f32 = 0.5;
const SETTLE_SPEED: f32 = 20.0;
const FLING_MIN_SPEED: f32 = 50.0;
const FLING_RELEASE_TIMEOUT: Duration = Duration::from_millis(100);
pub(crate) trait ScrollFeel {
fn scroll_smoothing_time(&self) -> f32;
fn scroll_fling_time(&self) -> f32;
}
impl ScrollFeel for TargetPlatform {
fn scroll_smoothing_time(&self) -> f32 {
if self.is_mobile() { 0.1 } else { 0.06 }
}
fn scroll_fling_time(&self) -> f32 {
if self.is_mobile() { 0.35 } else { 0.5 }
}
}
#[derive(Clone, Copy, Default)]
pub(crate) struct SmoothDamp {
position: Point2D,
velocity: Vector2D,
smooth_time: f32,
}
impl SmoothDamp {
pub(crate) fn new() -> Self {
Self {
position: Point2D::zero(),
velocity: Vector2D::zero(),
smooth_time: TargetPlatform::Unknown.scroll_smoothing_time(),
}
}
fn advance(&mut self, target: Point2D, elapsed_seconds: f32) -> bool {
let omega = 2.0 / self.smooth_time;
let decay = (-omega * elapsed_seconds).exp();
let change = self.position - target;
let linear_term = (self.velocity + change * omega) * elapsed_seconds;
let velocity = (self.velocity - linear_term * omega) * decay;
let position = target + (change + linear_term) * decay;
if (target - position).length() < SETTLE_DISTANCE && velocity.length() < SETTLE_SPEED {
self.position = target;
self.velocity = Vector2D::zero();
return true;
}
self.position = position;
self.velocity = velocity;
false
}
}
#[derive(Clone, Copy)]
pub(crate) struct Drag {
velocity: Vector2D,
last_move: Instant,
}
impl Default for Drag {
fn default() -> Self {
Self {
velocity: Vector2D::zero(),
last_move: Instant::now(),
}
}
}
impl Drag {
fn track(&mut self, delta: Vector2D) {
let now = Instant::now();
let elapsed_seconds = now.duration_since(self.last_move).as_secs_f32();
if elapsed_seconds > 0.0 {
self.velocity = self.velocity.lerp(-delta / elapsed_seconds, 0.5);
}
self.last_move = now;
}
}
impl ScrollController {
pub fn animated_position(&self, target: Point2D) -> Point2D {
if self.task.read().is_some() {
self.damp.read().position
} else {
target
}
}
pub fn animate_from(&mut self, current: Point2D) {
self.start(current, None, TargetPlatform::get().scroll_smoothing_time());
}
fn fling_from(&mut self, current: Point2D, velocity: Vector2D) {
self.start(
current,
Some(velocity),
TargetPlatform::get().scroll_fling_time(),
);
}
fn start(&mut self, current: Point2D, velocity: Option<Vector2D>, smooth_time: f32) {
let is_animating = self.task.read().is_some();
{
let mut damp = self.damp.write();
damp.smooth_time = smooth_time;
if let Some(velocity) = velocity {
damp.velocity = velocity;
}
if !is_animating {
damp.position = current;
}
}
if is_animating {
return;
}
let ticker = RenderingTicker::get();
let platform = Platform::get().current_window();
let animation_clock = AnimationClock::get();
let scroll_controller = *self;
let mut damp = self.damp;
let mut task = self.task;
let animation_task = spawn(async move {
platform.send(UserEvent::RequestRedraw);
let mut previous_frame = Instant::now();
loop {
ticker.tick().await;
let elapsed_seconds = animation_clock
.correct_elapsed_duration(previous_frame.elapsed())
.as_secs_f32();
previous_frame = Instant::now();
let target = scroll_controller.position();
if damp.write().advance(target, elapsed_seconds) {
break;
}
platform.send(UserEvent::RequestRedraw);
}
task.write().take();
});
task.write().replace(animation_task);
}
pub(crate) fn scroll_gesture_phase(&mut self, phase: TouchPhase, delta: Vector2D) -> bool {
match phase {
TouchPhase::Started => {
self.stop();
self.drag.set(Drag::default());
}
TouchPhase::Moved => self.drag.write().track(-delta),
TouchPhase::Ended => {
let bounds = *self.bounds.peek();
if self.drag.peek().last_move.elapsed() < FLING_RELEASE_TIMEOUT
&& let Some((content, viewport)) = bounds
{
self.release_drag(self.animated_position(self.position()), content, viewport);
}
self.drag.set(Drag::default());
return true;
}
TouchPhase::Cancelled => {
self.stop();
self.drag.set(Drag::default());
return true;
}
}
false
}
pub fn begin_drag(&mut self) {
let caught_velocity = self.stop();
self.drag.set(Drag {
velocity: caught_velocity,
last_move: Instant::now(),
});
}
pub fn drag(&mut self, delta: Vector2D) {
self.stop();
self.drag.write().track(delta);
}
pub fn release_drag(&mut self, from: Point2D, content: Size2D, viewport: Size2D) {
let velocity = self.drag.peek().velocity;
if velocity.length() < FLING_MIN_SPEED {
return;
}
let projected = from + velocity * TargetPlatform::get().scroll_fling_time();
let target_x = get_corrected_scroll_position(content.width, viewport.width, projected.x);
let target_y = get_corrected_scroll_position(content.height, viewport.height, projected.y);
self.fling_from(from, velocity);
self.scroll_to_x(target_x as i32);
self.scroll_to_y(target_y as i32);
}
pub fn stop(&mut self) -> Vector2D {
let task = self.task.write().take();
if let Some(task) = task {
task.cancel();
let position = self.damp.peek().position.to_i32();
self.scroll_to_x(position.x);
self.scroll_to_y(position.y);
}
let velocity = self.damp.peek().velocity;
self.damp.write().velocity = Vector2D::zero();
velocity
}
}