1use std::{fmt, mem, sync::Arc, time::Duration};
2
3use atomic::{Atomic, Ordering};
4use bitflags::bitflags;
5use parking_lot::Mutex;
6use zng_ext_input::touch::TouchPhase;
7use zng_var::{
8 VARS,
9 animation::{
10 AnimationHandle, ChaseAnimation, Transition,
11 easing::{self, EasingStep, EasingTime},
12 },
13};
14use zng_wgt::prelude::*;
15
16use super::{SMOOTH_SCROLLING_VAR, cmd};
17
18bitflags! {
19 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
24 #[serde(transparent)]
25 pub struct ScrollMode: u8 {
26 const NONE = 0;
28 const VERTICAL = 0b01;
30 const HORIZONTAL = 0b10;
32 const PAN = 0b11;
34 const ZOOM = 0b111;
37 }
38}
39impl_from_and_into_var! {
40 fn from(zoom: bool) -> ScrollMode {
45 if zoom { ScrollMode::ZOOM } else { ScrollMode::NONE }
46 }
47}
48
49context_var! {
50 pub(super) static SCROLL_VERTICAL_OFFSET_VAR: Factor = 0.fct();
54 pub(super) static SCROLL_HORIZONTAL_OFFSET_VAR: Factor = 0.fct();
58
59 pub(super) static OVERSCROLL_VERTICAL_OFFSET_VAR: Factor = 0.fct();
62
63 pub(super) static OVERSCROLL_HORIZONTAL_OFFSET_VAR: Factor = 0.fct();
66
67 pub(super) static SCROLL_VERTICAL_RATIO_VAR: Factor = 0.fct();
71
72 pub(super) static SCROLL_HORIZONTAL_RATIO_VAR: Factor = 0.fct();
76
77 pub(super) static SCROLL_VERTICAL_CONTENT_OVERFLOWS_VAR: bool = false;
79
80 pub(super) static SCROLL_HORIZONTAL_CONTENT_OVERFLOWS_VAR: bool = false;
82
83 pub(super) static SCROLL_VIEWPORT_SIZE_VAR: PxSize = PxSize::zero();
85
86 pub(super) static SCROLL_CONTENT_SIZE_VAR: PxSize = PxSize::zero();
90
91 pub(super) static SCROLL_SCALE_VAR: Factor = 1.fct();
93
94 pub(super) static SCROLL_MODE_VAR: ScrollMode = ScrollMode::empty();
96}
97
98context_local! {
99 static SCROLL_CONFIG: ScrollConfig = ScrollConfig::default();
100}
101
102#[derive(Debug, Clone, Copy, bytemuck::NoUninit)]
103#[repr(C)]
104struct RenderedOffsets {
105 h: Factor,
106 v: Factor,
107 z: Factor,
108}
109
110#[derive(Default, Debug)]
111enum ZoomState {
112 #[default]
113 None,
114 Chasing(ChaseAnimation<Factor>),
115 TouchStart {
116 start_factor: Factor,
117 start_center: euclid::Point2D<f32, Px>,
118 applied_offset: euclid::Vector2D<f32, Px>,
119 },
120}
121
122#[derive(Debug)]
123struct ScrollConfig {
124 id: Option<WidgetId>,
125 chase: [Mutex<Option<ChaseAnimation<Factor>>>; 2], zoom: Mutex<ZoomState>,
127
128 rendered: Atomic<RenderedOffsets>,
130
131 overscroll: [Mutex<AnimationHandle>; 2],
132 inertia: [Mutex<AnimationHandle>; 2],
133 auto: [Mutex<AnimationHandle>; 2],
134}
135impl Default for ScrollConfig {
136 fn default() -> Self {
137 Self {
138 id: Default::default(),
139 chase: Default::default(),
140 zoom: Default::default(),
141 rendered: Atomic::new(RenderedOffsets {
142 h: 0.fct(),
143 v: 0.fct(),
144 z: 0.fct(),
145 }),
146 overscroll: Default::default(),
147 inertia: Default::default(),
148 auto: Default::default(),
149 }
150 }
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
160pub enum ScrollFrom {
161 Var(Px),
167 VarTarget(Px),
174
175 Rendered(Px),
181}
182
183pub struct SCROLL;
185impl SCROLL {
186 pub fn try_id(&self) -> Option<WidgetId> {
188 SCROLL_CONFIG.get().id
189 }
190 pub fn id(&self) -> WidgetId {
196 self.try_id().expect("not inside scroll")
197 }
198
199 pub fn config_node(&self, child: impl IntoUiNode) -> UiNode {
203 let child = match_node(child, move |_, op| {
204 if let UiNodeOp::Render { .. } | UiNodeOp::RenderUpdate { .. } = op {
205 let h = SCROLL_HORIZONTAL_OFFSET_VAR.get();
206 let v = SCROLL_VERTICAL_OFFSET_VAR.get();
207 let z = SCROLL_SCALE_VAR.get();
208 SCROLL_CONFIG.get().rendered.store(RenderedOffsets { h, v, z }, Ordering::Relaxed);
209 }
210 });
211 with_context_local_init(child, &SCROLL_CONFIG, || ScrollConfig {
212 id: WIDGET.try_id(),
213 ..Default::default()
214 })
215 }
216
217 pub fn mode(&self) -> Var<ScrollMode> {
219 SCROLL_MODE_VAR.read_only()
220 }
221
222 pub fn vertical_offset(&self) -> ContextVar<Factor> {
230 SCROLL_VERTICAL_OFFSET_VAR
231 }
232
233 pub fn horizontal_offset(&self) -> ContextVar<Factor> {
241 SCROLL_HORIZONTAL_OFFSET_VAR
242 }
243
244 pub fn zoom_scale(&self) -> ContextVar<Factor> {
250 SCROLL_SCALE_VAR
251 }
252
253 pub fn rendered_offset(&self) -> Factor2d {
255 let cfg = SCROLL_CONFIG.get().rendered.load(Ordering::Relaxed);
256 Factor2d::new(cfg.h, cfg.v)
257 }
258
259 pub fn rendered_zoom_scale(&self) -> Factor {
261 SCROLL_CONFIG.get().rendered.load(Ordering::Relaxed).z
262 }
263
264 pub fn vertical_overscroll(&self) -> Var<Factor> {
272 OVERSCROLL_VERTICAL_OFFSET_VAR.read_only()
273 }
274
275 pub fn horizontal_overscroll(&self) -> Var<Factor> {
283 OVERSCROLL_HORIZONTAL_OFFSET_VAR.read_only()
284 }
285
286 pub fn vertical_ratio(&self) -> Var<Factor> {
290 SCROLL_VERTICAL_RATIO_VAR.read_only()
291 }
292 pub fn horizontal_ratio(&self) -> Var<Factor> {
296 SCROLL_HORIZONTAL_RATIO_VAR.read_only()
297 }
298
299 pub fn vertical_content_overflows(&self) -> Var<bool> {
301 SCROLL_VERTICAL_CONTENT_OVERFLOWS_VAR.read_only()
302 }
303
304 pub fn horizontal_content_overflows(&self) -> Var<bool> {
306 SCROLL_HORIZONTAL_CONTENT_OVERFLOWS_VAR.read_only()
307 }
308
309 pub fn viewport_size(&self) -> Var<PxSize> {
311 SCROLL_VIEWPORT_SIZE_VAR.read_only()
312 }
313
314 pub fn content_size(&self) -> Var<PxSize> {
316 SCROLL_CONTENT_SIZE_VAR.read_only()
317 }
318
319 pub fn scroll_vertical(&self, delta: ScrollFrom) {
323 self.scroll_vertical_clamp(delta, f32::MIN, f32::MAX);
324 }
325
326 pub fn scroll_horizontal(&self, delta: ScrollFrom) {
330 self.scroll_horizontal_clamp(delta, f32::MIN, f32::MAX)
331 }
332
333 pub fn scroll_vertical_clamp(&self, delta: ScrollFrom, min: f32, max: f32) {
337 self.scroll_clamp(true, SCROLL_VERTICAL_OFFSET_VAR, delta, min, max)
338 }
339
340 pub fn scroll_horizontal_clamp(&self, delta: ScrollFrom, min: f32, max: f32) {
344 self.scroll_clamp(false, SCROLL_HORIZONTAL_OFFSET_VAR, delta, min, max)
345 }
346 fn scroll_clamp(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, delta: ScrollFrom, min: f32, max: f32) {
347 let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
348 let content = SCROLL_CONTENT_SIZE_VAR.get().to_array()[vertical as usize];
349
350 let max_scroll = content - viewport;
351
352 if max_scroll <= 0 {
353 return;
354 }
355
356 match delta {
357 ScrollFrom::Var(a) => {
358 let amount = a.0 as f32 / max_scroll.0 as f32;
359 let f = scroll_offset_var.get();
360 SCROLL.chase(vertical, scroll_offset_var, |_| (f.0 + amount).clamp(min, max).fct());
361 }
362 ScrollFrom::VarTarget(a) => {
363 let amount = a.0 as f32 / max_scroll.0 as f32;
364 SCROLL.chase(vertical, scroll_offset_var, |f| (f.0 + amount).clamp(min, max).fct());
365 }
366 ScrollFrom::Rendered(a) => {
367 let amount = a.0 as f32 / max_scroll.0 as f32;
368 let f = SCROLL_CONFIG.get().rendered.load(Ordering::Relaxed).h;
369 SCROLL.chase(vertical, scroll_offset_var, |_| (f.0 + amount).clamp(min, max).fct());
370 }
371 }
372 }
373
374 pub fn auto_scroll(&self, velocity: DipVector) {
376 let viewport = SCROLL_VIEWPORT_SIZE_VAR.get();
377 let content = SCROLL_CONTENT_SIZE_VAR.get();
378 let max_scroll = content - viewport;
379
380 let velocity = velocity.to_px(WINDOW.info().scale_factor());
381
382 fn scroll(dimension: usize, velocity: Px, max_scroll: Px, offset_var: &ContextVar<Factor>) {
383 if velocity == 0 {
384 SCROLL_CONFIG.get().auto[dimension].lock().clone().stop();
385 } else {
386 let mut travel = max_scroll * offset_var.get();
387 let mut target = 0.0;
388 if velocity > Px(0) {
389 travel = max_scroll - travel;
390 target = 1.0;
391 }
392 let time = (travel.0 as f32 / velocity.0.abs() as f32).secs();
393
394 VARS.with_animation_controller(zng_var::animation::ForceAnimationController, || {
395 let handle = offset_var.ease(target, time, easing::linear);
396 mem::replace(&mut *SCROLL_CONFIG.get().auto[dimension].lock(), handle).stop();
397 });
398 }
399 }
400 scroll(0, velocity.x, max_scroll.width, &SCROLL_HORIZONTAL_OFFSET_VAR);
401 scroll(1, velocity.y, max_scroll.height, &SCROLL_VERTICAL_OFFSET_VAR);
402 }
403
404 pub fn scroll_vertical_touch(&self, delta: Px) {
409 self.scroll_touch(true, SCROLL_VERTICAL_OFFSET_VAR, OVERSCROLL_VERTICAL_OFFSET_VAR, delta)
410 }
411
412 pub fn scroll_horizontal_touch(&self, delta: Px) {
417 self.scroll_touch(false, SCROLL_HORIZONTAL_OFFSET_VAR, OVERSCROLL_HORIZONTAL_OFFSET_VAR, delta)
418 }
419
420 fn scroll_touch(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, overscroll_offset_var: ContextVar<Factor>, delta: Px) {
421 let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
422 let content = SCROLL_CONTENT_SIZE_VAR.get().to_array()[vertical as usize];
423
424 let max_scroll = content - viewport;
425 if max_scroll <= 0 {
426 return;
427 }
428
429 let delta = delta.0 as f32 / max_scroll.0 as f32;
430
431 let current = scroll_offset_var.get();
432 let mut next = current + delta.fct();
433 let mut overscroll = 0.fct();
434 if next > 1.fct() {
435 overscroll = next - 1.fct();
436 next = 1.fct();
437
438 let overscroll_px = overscroll * content.0.fct();
439 let overscroll_max = viewport.0.fct();
440 overscroll = overscroll_px.min(overscroll_max) / overscroll_max;
441 } else if next < 0.fct() {
442 overscroll = next;
443 next = 0.fct();
444
445 let overscroll_px = -overscroll * content.0.fct();
446 let overscroll_max = viewport.0.fct();
447 overscroll = -(overscroll_px.min(overscroll_max) / overscroll_max);
448 }
449
450 scroll_offset_var.set(next);
451 if overscroll != 0.fct() {
452 let new_handle = self.increment_overscroll(overscroll_offset_var, overscroll);
453
454 let config = SCROLL_CONFIG.get();
455 let mut handle = config.overscroll[vertical as usize].lock();
456 mem::replace(&mut *handle, new_handle).stop();
457 } else {
458 self.clear_horizontal_overscroll();
459 }
460 }
461
462 fn increment_overscroll(&self, overscroll: ContextVar<Factor>, delta: Factor) -> AnimationHandle {
463 enum State {
464 Increment,
465 ClearDelay,
466 Clear(Transition<Factor>),
467 }
468 let mut state = State::Increment;
469 overscroll.animate(move |a, o| match &mut state {
470 State::Increment => {
471 **o += delta;
473 **o = (*o).clamp(-1.fct(), 1.fct());
474
475 a.sleep(300.ms());
476 state = State::ClearDelay;
477 }
478 State::ClearDelay => {
479 a.restart();
480 let t = Transition::new(**o, 0.fct());
481 state = State::Clear(t);
482 }
483 State::Clear(t) => {
484 let step = easing::linear(a.elapsed_stop(300.ms()));
485 o.set(t.sample(step));
486 }
487 })
488 }
489
490 pub fn clear_vertical_overscroll(&self) {
492 self.clear_overscroll(true, OVERSCROLL_VERTICAL_OFFSET_VAR)
493 }
494
495 pub fn clear_horizontal_overscroll(&self) {
497 self.clear_overscroll(false, OVERSCROLL_HORIZONTAL_OFFSET_VAR)
498 }
499
500 fn clear_overscroll(&self, vertical: bool, overscroll_offset_var: ContextVar<Factor>) {
501 if overscroll_offset_var.get() != 0.fct() {
502 let new_handle = overscroll_offset_var.ease(0.fct(), 100.ms(), easing::linear);
503
504 let config = SCROLL_CONFIG.get();
505 let mut handle = config.overscroll[vertical as usize].lock();
506 mem::replace(&mut *handle, new_handle).stop();
507 }
508 }
509
510 pub fn scroll_vertical_touch_inertia(&self, delta: Px, duration: Duration) {
512 self.scroll_touch_inertia(true, SCROLL_VERTICAL_OFFSET_VAR, OVERSCROLL_VERTICAL_OFFSET_VAR, delta, duration)
513 }
514
515 pub fn scroll_horizontal_touch_inertia(&self, delta: Px, duration: Duration) {
517 self.scroll_touch_inertia(
518 false,
519 SCROLL_HORIZONTAL_OFFSET_VAR,
520 OVERSCROLL_HORIZONTAL_OFFSET_VAR,
521 delta,
522 duration,
523 )
524 }
525
526 fn scroll_touch_inertia(
527 &self,
528 vertical: bool,
529 scroll_offset_var: ContextVar<Factor>,
530 overscroll_offset_var: ContextVar<Factor>,
531 delta: Px,
532 duration: Duration,
533 ) {
534 let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
535 let content = SCROLL_CONTENT_SIZE_VAR.get().to_array()[vertical as usize];
536
537 let max_scroll = content - viewport;
538 if max_scroll <= 0 {
539 return;
540 }
541
542 let delta = delta.0 as f32 / max_scroll.0 as f32;
543
544 let current = scroll_offset_var.get();
545 let mut next = current + delta.fct();
546 let mut overscroll = 0.fct();
547 if next > 1.fct() {
548 overscroll = next - 1.fct();
549 next = 1.fct();
550
551 let overscroll_px = overscroll * content.0.fct();
552 let overscroll_max = viewport.0.fct();
553 overscroll = overscroll_px.min(overscroll_max) / overscroll_max;
554 } else if next < 0.fct() {
555 overscroll = next;
556 next = 0.fct();
557
558 let overscroll_px = -overscroll * content.0.fct();
559 let overscroll_max = viewport.0.fct();
560 overscroll = -(overscroll_px.min(overscroll_max) / overscroll_max);
561 }
562
563 let cfg = SCROLL_CONFIG.get();
564 let easing = |t| easing::ease_out(easing::quad, t);
565 *cfg.inertia[vertical as usize].lock() = if overscroll != 0.fct() {
566 let transition = Transition::new(current, next + overscroll);
567
568 let overscroll_var = overscroll_offset_var.current_context();
569 let overscroll_tr = Transition::new(overscroll, 0.fct());
570 let mut is_inertia_anim = true;
571
572 scroll_offset_var.animate(move |animation, value| {
573 if is_inertia_anim {
574 let step = easing(animation.elapsed(duration));
576 let v = transition.sample(step);
577
578 if v < 0.fct() || v > 1.fct() {
579 value.set(v.clamp_range());
581 animation.restart();
582 is_inertia_anim = false;
583 overscroll_var.set(overscroll_tr.from);
584 } else {
585 value.set(v);
586 }
587 } else {
588 let step = easing::linear(animation.elapsed_stop(300.ms()));
590 let v = overscroll_tr.sample(step);
591 overscroll_var.set(v);
592 }
593 })
594 } else {
595 scroll_offset_var.ease(next, duration, easing)
596 };
597 }
598
599 pub fn chase_vertical(&self, modify_offset: impl FnOnce(Factor) -> Factor) {
602 self.chase(true, SCROLL_VERTICAL_OFFSET_VAR, modify_offset);
603 }
604
605 pub fn chase_horizontal(&self, modify_offset: impl FnOnce(Factor) -> Factor) {
608 self.chase(false, SCROLL_HORIZONTAL_OFFSET_VAR, modify_offset);
609 }
610
611 fn chase(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, modify_offset: impl FnOnce(Factor) -> Factor) {
612 let smooth = SMOOTH_SCROLLING_VAR.get();
613 let config = SCROLL_CONFIG.get();
614 let mut chase = config.chase[vertical as usize].lock();
615 match &mut *chase {
616 Some(t) => {
617 if smooth.is_disabled() {
618 let t = modify_offset(*t.target()).clamp_range();
619 scroll_offset_var.set(t);
620 *chase = None;
621 } else {
622 let easing = smooth.easing.clone();
623 t.modify(|f| *f = modify_offset(*f).clamp_range(), smooth.duration, move |t| easing(t));
624 }
625 }
626 None => {
627 let t = modify_offset(scroll_offset_var.get()).clamp_range();
628 if smooth.is_disabled() {
629 scroll_offset_var.set(t);
630 } else {
631 let easing = smooth.easing.clone();
632 let anim = scroll_offset_var.chase(t, smooth.duration, move |t| easing(t));
633 *chase = Some(anim);
634 }
635 }
636 }
637 }
638
639 pub fn chase_zoom(&self, modify_scale: impl FnOnce(Factor) -> Factor) {
642 self.chase_zoom_impl(modify_scale);
643 }
644 fn chase_zoom_impl(&self, modify_scale: impl FnOnce(Factor) -> Factor) {
645 if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
646 return;
647 }
648
649 let smooth = SMOOTH_SCROLLING_VAR.get();
650 let config = SCROLL_CONFIG.get();
651 let mut zoom = config.zoom.lock();
652
653 let min = super::MIN_ZOOM_VAR.get();
654 let max = super::MAX_ZOOM_VAR.get();
655
656 match &mut *zoom {
657 ZoomState::Chasing(t) => {
658 if smooth.is_disabled() {
659 let next = modify_scale(*t.target()).clamp(min, max);
660 SCROLL_SCALE_VAR.set(next);
661 *zoom = ZoomState::None;
662 } else {
663 let easing = smooth.easing.clone();
664 t.modify(|f| *f = modify_scale(*f).clamp(min, max), smooth.duration, move |t| easing(t));
665 }
666 }
667 _ => {
668 let t = modify_scale(SCROLL_SCALE_VAR.get()).clamp(min, max);
669 if smooth.is_disabled() {
670 SCROLL_SCALE_VAR.set(t);
671 } else {
672 let easing = smooth.easing.clone();
673 let anim = SCROLL_SCALE_VAR.chase(t, smooth.duration, move |t| easing(t));
674 *zoom = ZoomState::Chasing(anim);
675 }
676 }
677 }
678 }
679
680 pub fn zoom(&self, modify_scale: impl FnOnce(Factor) -> Factor, origin: PxPoint) {
683 self.zoom_impl(modify_scale, origin);
684 }
685 fn zoom_impl(&self, modify_scale: impl FnOnce(Factor) -> Factor, center_in_viewport: PxPoint) {
686 if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
687 return;
688 }
689
690 let content = WIDGET.info().scroll_info().unwrap().content();
691 let mut center_in_content = -content.origin + center_in_viewport.to_vector();
692 let mut content_size = content.size;
693
694 let rendered_scale = SCROLL.rendered_zoom_scale();
695
696 SCROLL.chase_zoom(|f| {
697 let s = modify_scale(f);
698 let f = s / rendered_scale;
699 center_in_content *= f;
700 content_size *= f;
701 s
702 });
703
704 let viewport_size = SCROLL_VIEWPORT_SIZE_VAR.get();
705
706 let max_scroll = content_size - viewport_size;
708 let offset = center_in_content - center_in_viewport;
709
710 if offset.y != Px(0) && max_scroll.height > Px(0) {
711 let offset_y = offset.y.0 as f32 / max_scroll.height.0 as f32;
712 SCROLL.chase_vertical(|_| offset_y.fct());
713 }
714 if offset.x != Px(0) && max_scroll.width > Px(0) {
715 let offset_x = offset.x.0 as f32 / max_scroll.width.0 as f32;
716 SCROLL.chase_horizontal(|_| offset_x.fct());
717 }
718 }
719
720 pub fn zoom_touch(&self, phase: TouchPhase, scale: Factor, center_in_viewport: euclid::Point2D<f32, Px>) {
722 if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
723 return;
724 }
725
726 let cfg = SCROLL_CONFIG.get();
727
728 let rendered_scale = SCROLL.rendered_zoom_scale();
729
730 let start_scale;
731 let start_center;
732
733 let mut cfg = cfg.zoom.lock();
734
735 if let TouchPhase::Start = phase {
736 start_scale = rendered_scale;
737 start_center = center_in_viewport;
738
739 *cfg = ZoomState::TouchStart {
740 start_factor: start_scale,
741 start_center: center_in_viewport,
742 applied_offset: euclid::vec2(0.0, 0.0),
743 };
744 } else if let ZoomState::TouchStart {
745 start_factor: scale,
746 start_center: center_in_viewport,
747 ..
748 } = &*cfg
749 {
750 start_scale = *scale;
751 start_center = *center_in_viewport;
752 } else {
753 return;
755 }
756
757 let applied_offset = if let ZoomState::TouchStart { applied_offset, .. } = &mut *cfg {
759 applied_offset
760 } else {
761 unreachable!()
762 };
763
764 let scale = start_scale + (scale - 1.0.fct());
765
766 let min = super::MIN_ZOOM_VAR.get();
767 let max = super::MAX_ZOOM_VAR.get();
768 let scale = scale.clamp(min, max);
769
770 let translate_offset = start_center - center_in_viewport;
771 let translate_delta = translate_offset - *applied_offset;
772 *applied_offset = translate_offset;
773
774 let content = WIDGET.info().scroll_info().unwrap().content();
775 let mut center_in_content = -content.origin.cast::<f32>() + center_in_viewport.to_vector();
776 let mut content_size = content.size.cast::<f32>();
777
778 let scale_transform = scale / rendered_scale;
779
780 center_in_content *= scale_transform;
781 content_size *= scale_transform;
782
783 let viewport_size = SCROLL_VIEWPORT_SIZE_VAR.get().cast::<f32>();
784
785 let max_scroll = content_size - viewport_size;
787 let zoom_offset = center_in_content - center_in_viewport;
788
789 let offset = zoom_offset + translate_delta;
790
791 SCROLL_SCALE_VAR.set(scale);
792
793 if offset.y != 0.0 && max_scroll.height > 0.0 {
794 let offset_y = offset.y / max_scroll.height;
795 SCROLL_VERTICAL_OFFSET_VAR.set(offset_y.clamp(0.0, 1.0));
796 }
797 if offset.x != 0.0 && max_scroll.width > 0.0 {
798 let offset_x = offset.x / max_scroll.width;
799 SCROLL_HORIZONTAL_OFFSET_VAR.set(offset_x.clamp(0.0, 1.0));
800 }
801 }
802
803 fn can_scroll(&self, predicate: impl Fn(PxSize, PxSize) -> bool + Send + Sync + 'static) -> Var<bool> {
804 merge_var!(SCROLL_VIEWPORT_SIZE_VAR, SCROLL_CONTENT_SIZE_VAR, move |&vp, &ct| predicate(vp, ct))
805 }
806
807 pub fn can_scroll_vertical(&self) -> Var<bool> {
809 self.can_scroll(|vp, ct| ct.height > vp.height)
810 }
811
812 pub fn can_scroll_horizontal(&self) -> Var<bool> {
814 self.can_scroll(|vp, ct| ct.width > vp.width)
815 }
816
817 fn can_scroll_v(&self, predicate: impl Fn(PxSize, PxSize, Factor) -> bool + Send + Sync + 'static) -> Var<bool> {
818 merge_var!(
819 SCROLL_VIEWPORT_SIZE_VAR,
820 SCROLL_CONTENT_SIZE_VAR,
821 SCROLL_VERTICAL_OFFSET_VAR,
822 move |&vp, &ct, &vo| predicate(vp, ct, vo)
823 )
824 }
825
826 pub fn can_scroll_down(&self) -> Var<bool> {
829 self.can_scroll_v(|vp, ct, vo| ct.height > vp.height && 1.fct() > vo)
830 }
831
832 pub fn can_scroll_up(&self) -> Var<bool> {
835 self.can_scroll_v(|vp, ct, vo| ct.height > vp.height && 0.fct() < vo)
836 }
837
838 fn can_scroll_h(&self, predicate: impl Fn(PxSize, PxSize, Factor) -> bool + Send + Sync + 'static) -> Var<bool> {
839 merge_var!(
840 SCROLL_VIEWPORT_SIZE_VAR,
841 SCROLL_CONTENT_SIZE_VAR,
842 SCROLL_HORIZONTAL_OFFSET_VAR,
843 move |&vp, &ct, &ho| predicate(vp, ct, ho)
844 )
845 }
846
847 pub fn can_scroll_left(&self) -> Var<bool> {
850 self.can_scroll_h(|vp, ct, ho| ct.width > vp.width && 0.fct() < ho)
851 }
852
853 pub fn can_scroll_right(&self) -> Var<bool> {
856 self.can_scroll_h(|vp, ct, ho| ct.width > vp.width && 1.fct() > ho)
857 }
858
859 pub fn scroll_to(&self, mode: impl Into<super::cmd::ScrollToMode>) {
863 cmd::scroll_to(WIDGET.info(), mode.into())
864 }
865
866 pub fn scroll_to_zoom(&self, mode: impl Into<super::cmd::ScrollToMode>, zoom: impl Into<Factor>) {
870 cmd::scroll_to_zoom(WIDGET.info(), mode.into(), zoom.into())
871 }
872
873 pub fn can_zoom_in(&self) -> bool {
875 SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) && SCROLL_SCALE_VAR.get() < super::MAX_ZOOM_VAR.get()
876 }
877
878 pub fn can_zoom_out(&self) -> bool {
880 SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) && SCROLL_SCALE_VAR.get() > super::MIN_ZOOM_VAR.get()
881 }
882}
883
884impl SCROLL {
885 pub fn context_values_set(&self, set: &mut ContextValueSet) {
889 set.insert(&SCROLL_CONFIG);
890 }
891}
892
893pub trait WidgetInfoExt {
897 fn is_scroll(&self) -> bool;
899
900 fn scroll_info(&self) -> Option<ScrollInfo>;
902
903 fn viewport(&self) -> Option<PxRect>;
907}
908impl WidgetInfoExt for WidgetInfo {
909 fn is_scroll(&self) -> bool {
910 self.meta().get(*SCROLL_INFO_ID).is_some()
911 }
912
913 fn scroll_info(&self) -> Option<ScrollInfo> {
914 self.meta().get(*SCROLL_INFO_ID).cloned()
915 }
916
917 fn viewport(&self) -> Option<PxRect> {
918 self.meta().get(*SCROLL_INFO_ID).map(|r| r.viewport())
919 }
920}
921
922#[derive(Debug)]
923struct ScrollData {
924 viewport_transform: PxTransform,
925 viewport_size: PxSize,
926 joiner_size: PxSize,
927 content: PxRect,
928 zoom_scale: Factor,
929}
930impl Default for ScrollData {
931 fn default() -> Self {
932 Self {
933 viewport_transform: Default::default(),
934 viewport_size: Default::default(),
935 joiner_size: Default::default(),
936 content: Default::default(),
937 zoom_scale: 1.fct(),
938 }
939 }
940}
941
942#[derive(Clone, Default, Debug)]
944pub struct ScrollInfo(Arc<Mutex<ScrollData>>);
945impl ScrollInfo {
946 pub fn viewport(&self) -> PxRect {
948 self.viewport_transform()
949 .outer_transformed(PxBox::from_size(self.viewport_size()))
950 .unwrap_or_default()
951 .to_rect()
952 }
953
954 pub fn viewport_size(&self) -> PxSize {
956 self.0.lock().viewport_size
957 }
958
959 pub fn viewport_transform(&self) -> PxTransform {
961 self.0.lock().viewport_transform
962 }
963
964 pub fn joiner_size(&self) -> PxSize {
969 self.0.lock().joiner_size
970 }
971
972 pub fn content(&self) -> PxRect {
976 self.0.lock().content
977 }
978
979 pub fn zoom_scale(&self) -> Factor {
981 self.0.lock().zoom_scale
982 }
983
984 pub(super) fn set_viewport_size(&self, size: PxSize) {
985 self.0.lock().viewport_size = size;
986 }
987
988 pub(super) fn set_viewport_transform(&self, transform: PxTransform) {
989 self.0.lock().viewport_transform = transform;
990 }
991
992 pub(super) fn set_joiner_size(&self, size: PxSize) {
993 self.0.lock().joiner_size = size;
994 }
995
996 pub(super) fn set_content(&self, content: PxRect, scale: Factor) {
997 let mut m = self.0.lock();
998 m.content = content;
999 m.zoom_scale = scale;
1000 }
1001}
1002
1003static_id! {
1004 pub(super) static ref SCROLL_INFO_ID: StateId<ScrollInfo>;
1005}
1006
1007#[derive(Clone)]
1013pub struct SmoothScrolling {
1014 pub duration: Duration,
1018 pub easing: Arc<dyn Fn(EasingTime) -> EasingStep + Send + Sync>,
1022}
1023impl fmt::Debug for SmoothScrolling {
1024 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1025 f.debug_struct("SmoothScrolling")
1026 .field("duration", &self.duration)
1027 .finish_non_exhaustive()
1028 }
1029}
1030impl PartialEq for SmoothScrolling {
1031 fn eq(&self, other: &Self) -> bool {
1032 self.duration == other.duration && Arc::ptr_eq(&self.easing, &other.easing)
1033 }
1034}
1035impl Default for SmoothScrolling {
1036 fn default() -> Self {
1037 Self::new(150.ms(), easing::linear)
1038 }
1039}
1040impl SmoothScrolling {
1041 pub fn new(duration: Duration, easing: impl Fn(EasingTime) -> EasingStep + Send + Sync + 'static) -> Self {
1043 Self {
1044 duration,
1045 easing: Arc::new(easing),
1046 }
1047 }
1048
1049 pub fn disabled() -> Self {
1051 Self::new(Duration::ZERO, easing::none)
1052 }
1053
1054 pub fn is_disabled(&self) -> bool {
1058 self.duration == Duration::ZERO
1059 }
1060}
1061impl_from_and_into_var! {
1062 fn from(duration: Duration) -> SmoothScrolling {
1064 SmoothScrolling {
1065 duration,
1066 ..Default::default()
1067 }
1068 }
1069
1070 fn from(enabled: bool) -> SmoothScrolling {
1074 if enabled {
1075 SmoothScrolling::default()
1076 } else {
1077 SmoothScrolling::disabled()
1078 }
1079 }
1080
1081 fn from<F: Fn(EasingTime) -> EasingStep + Send + Sync + 'static>((duration, easing): (Duration, F)) -> SmoothScrolling {
1082 SmoothScrolling::new(duration, easing)
1083 }
1084
1085 fn from((duration, easing): (Duration, easing::EasingFn)) -> SmoothScrolling {
1086 SmoothScrolling::new(duration, easing.ease_fn())
1087 }
1088}
1089
1090#[derive(Debug, Default, Clone, PartialEq)]
1099#[non_exhaustive]
1100pub struct AutoScrollArgs {}
1101
1102#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1109pub enum ZoomToFitMode {
1110 #[default]
1112 Contain,
1113 ScaleDown,
1115}