freya_components/scrollviews/
use_scroll_controller.rs1use std::time::Instant;
2
3use freya_core::prelude::*;
4use torin::{
5 geometry::{
6 Point2D,
7 Size2D,
8 Vector2D,
9 },
10 prelude::Direction,
11};
12
13use crate::scrollviews::shared::get_corrected_scroll_position;
14
15#[derive(Default, PartialEq, Eq)]
17pub enum ScrollPosition {
18 #[default]
19 Start,
20 End,
21}
22
23impl ScrollPosition {
24 fn offset(&self) -> i32 {
26 match self {
27 Self::Start => 0,
28 Self::End => ScrollController::END,
29 }
30 }
31}
32
33#[derive(Default)]
35pub struct ScrollConfig {
36 pub default_vertical_position: ScrollPosition,
38 pub default_horizontal_position: ScrollPosition,
40}
41
42#[derive(PartialEq, Clone, Copy)]
132pub struct ScrollController {
133 pub(crate) scroll: State<(i32, i32)>,
134 bounds: State<Option<(Size2D, Size2D)>>,
135 pub(crate) damp: State<SmoothDamp>,
136 pub(crate) drag: State<Drag>,
137 pub(crate) task: State<Option<TaskHandle>>,
138}
139
140impl From<ScrollController> for (i32, i32) {
141 fn from(val: ScrollController) -> Self {
143 *val.scroll.read()
144 }
145}
146
147impl ScrollController {
148 const END: i32 = i32::MIN;
150
151 pub fn new(x: i32, y: i32) -> Self {
153 Self {
154 scroll: State::create((x, y)),
155 bounds: State::create(None),
156 damp: State::create(SmoothDamp::new()),
157 drag: State::create(Drag::default()),
158 task: State::create(None),
159 }
160 }
161
162 pub(crate) fn apply_layout(&mut self, content_size: Size2D, viewport_size: Size2D) {
164 self.bounds.set(Some((content_size, viewport_size)));
165
166 let (x, y) = *self.scroll.peek();
167 self.scroll_to_x(x);
168 self.scroll_to_y(y);
169 }
170
171 pub(crate) fn position(self) -> Point2D {
172 let (x, y): (i32, i32) = self.into();
173 Point2D::new(x as f32, y as f32)
174 }
175
176 pub fn scroll_to_x(&mut self, to: i32) -> bool {
178 let to = self.bounded_position(to, Direction::Horizontal);
179 let changed = self.scroll.peek().0 != to;
180 if changed {
181 self.scroll.write().0 = to;
182 }
183 changed
184 }
185
186 pub fn scroll_to_y(&mut self, to: i32) -> bool {
188 let to = self.bounded_position(to, Direction::Vertical);
189 let changed = self.scroll.peek().1 != to;
190 if changed {
191 self.scroll.write().1 = to;
192 }
193 changed
194 }
195
196 fn bounded_position(&self, position: i32, direction: Direction) -> i32 {
197 let Some((content_size, viewport_size)) = *self.bounds.read() else {
198 return position;
199 };
200
201 let (content_size, viewport_size) = match direction {
202 Direction::Horizontal => (content_size.width, viewport_size.width),
203 Direction::Vertical => (content_size.height, viewport_size.height),
204 };
205 get_corrected_scroll_position(content_size, viewport_size, position as f32) as i32
206 }
207
208 pub fn scroll_to(&mut self, scroll_position: ScrollPosition, scroll_direction: Direction) {
210 let to = scroll_position.offset();
211 match scroll_direction {
212 Direction::Vertical => self.scroll_to_y(to),
213 Direction::Horizontal => self.scroll_to_x(to),
214 };
215 }
216}
217
218pub fn use_scroll_controller(config: impl FnOnce() -> ScrollConfig) -> ScrollController {
220 use_hook(|| {
221 let config = config();
222
223 ScrollController::new(
224 config.default_horizontal_position.offset(),
225 config.default_vertical_position.offset(),
226 )
227 })
228}
229
230const SETTLE_DISTANCE: f32 = 0.5;
232const SETTLE_SPEED: f32 = 20.0;
234
235const FLING_MIN_SPEED: f32 = 50.0;
237
238pub(crate) trait ScrollFeel {
240 fn scroll_smoothing_time(&self) -> f32;
242 fn scroll_fling_time(&self) -> f32;
244}
245
246impl ScrollFeel for TargetPlatform {
247 fn scroll_smoothing_time(&self) -> f32 {
248 if self.is_mobile() { 0.1 } else { 0.06 }
249 }
250
251 fn scroll_fling_time(&self) -> f32 {
252 if self.is_mobile() { 0.35 } else { 0.5 }
253 }
254}
255
256#[derive(Clone, Copy, Default)]
258pub(crate) struct SmoothDamp {
259 position: Point2D,
260 velocity: Vector2D,
261 smooth_time: f32,
262}
263
264impl SmoothDamp {
265 pub(crate) fn new() -> Self {
266 Self {
267 position: Point2D::zero(),
268 velocity: Vector2D::zero(),
269 smooth_time: TargetPlatform::Unknown.scroll_smoothing_time(),
270 }
271 }
272
273 fn advance(&mut self, target: Point2D, elapsed_seconds: f32) -> bool {
275 let omega = 2.0 / self.smooth_time;
276 let decay = (-omega * elapsed_seconds).exp();
277 let change = self.position - target;
278 let linear_term = (self.velocity + change * omega) * elapsed_seconds;
279
280 let velocity = (self.velocity - linear_term * omega) * decay;
281 let position = target + (change + linear_term) * decay;
282
283 if (target - position).length() < SETTLE_DISTANCE && velocity.length() < SETTLE_SPEED {
284 self.position = target;
285 self.velocity = Vector2D::zero();
286 return true;
287 }
288
289 self.position = position;
290 self.velocity = velocity;
291 false
292 }
293}
294
295#[derive(Clone, Copy)]
297pub(crate) struct Drag {
298 velocity: Vector2D,
299 last_move: Instant,
300}
301
302impl Default for Drag {
303 fn default() -> Self {
304 Self {
305 velocity: Vector2D::zero(),
306 last_move: Instant::now(),
307 }
308 }
309}
310
311impl Drag {
312 fn track(&mut self, delta: Vector2D) {
313 let now = Instant::now();
314 let elapsed_seconds = now.duration_since(self.last_move).as_secs_f32();
315 if elapsed_seconds > 0.0 {
316 self.velocity = self.velocity.lerp(-delta / elapsed_seconds, 0.5);
317 }
318 self.last_move = now;
319 }
320}
321
322impl ScrollController {
324 pub fn animated_position(&self, target: Point2D) -> Point2D {
326 if self.task.read().is_some() {
327 self.damp.read().position
328 } else {
329 target
330 }
331 }
332
333 pub fn animate_from(&mut self, current: Point2D) {
335 self.start(current, None, TargetPlatform::get().scroll_smoothing_time());
336 }
337
338 fn fling_from(&mut self, current: Point2D, velocity: Vector2D) {
340 self.start(
341 current,
342 Some(velocity),
343 TargetPlatform::get().scroll_fling_time(),
344 );
345 }
346
347 fn start(&mut self, current: Point2D, velocity: Option<Vector2D>, smooth_time: f32) {
348 let is_animating = self.task.read().is_some();
349 {
350 let mut damp = self.damp.write();
351 damp.smooth_time = smooth_time;
352 if let Some(velocity) = velocity {
353 damp.velocity = velocity;
354 }
355 if !is_animating {
356 damp.position = current;
357 }
358 }
359 if is_animating {
360 return;
361 }
362
363 let ticker = RenderingTicker::get();
364 let platform = Platform::get().current_window();
365 let animation_clock = AnimationClock::get();
366 let scroll_controller = *self;
367 let mut damp = self.damp;
368 let mut task = self.task;
369
370 let animation_task = spawn(async move {
371 platform.send(UserEvent::RequestRedraw);
372 let mut previous_frame = Instant::now();
373
374 loop {
375 ticker.tick().await;
376
377 let elapsed_seconds = animation_clock
378 .correct_elapsed_duration(previous_frame.elapsed())
379 .as_secs_f32();
380 previous_frame = Instant::now();
381
382 let target = scroll_controller.position();
383 if damp.write().advance(target, elapsed_seconds) {
384 break;
385 }
386
387 platform.send(UserEvent::RequestRedraw);
388 }
389
390 task.write().take();
391 });
392 task.write().replace(animation_task);
393 }
394
395 pub fn begin_drag(&mut self) {
397 let caught_velocity = self.stop();
398 self.drag.set(Drag {
399 velocity: caught_velocity,
400 last_move: Instant::now(),
401 });
402 }
403
404 pub fn drag(&mut self, delta: Vector2D) {
406 self.stop();
407 self.drag.write().track(delta);
408 }
409
410 pub fn release_drag(&mut self, from: Point2D, content: Size2D, viewport: Size2D) {
412 let velocity = self.drag.peek().velocity;
413 if velocity.length() < FLING_MIN_SPEED {
414 return;
415 }
416
417 let projected = from + velocity * TargetPlatform::get().scroll_fling_time();
418 let target_x = get_corrected_scroll_position(content.width, viewport.width, projected.x);
419 let target_y = get_corrected_scroll_position(content.height, viewport.height, projected.y);
420
421 self.fling_from(from, velocity);
422 self.scroll_to_x(target_x as i32);
423 self.scroll_to_y(target_y as i32);
424 }
425
426 pub fn stop(&mut self) -> Vector2D {
428 let task = self.task.write().take();
429 if let Some(task) = task {
430 task.cancel();
431
432 let position = self.damp.peek().position.to_i32();
433 self.scroll_to_x(position.x);
434 self.scroll_to_y(position.y);
435 }
436
437 let velocity = self.damp.peek().velocity;
438 self.damp.write().velocity = Vector2D::zero();
439 velocity
440 }
441}