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_ORIGINAL_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> {
318 expr_var! {
319 *#{SCROLL_CONTENT_ORIGINAL_SIZE_VAR} * *#{SCROLL_SCALE_VAR}
320 }
321 }
322
323 pub fn content_original_size(&self) -> Var<PxSize> {
325 SCROLL_CONTENT_ORIGINAL_SIZE_VAR.read_only()
326 }
327
328 pub fn scroll_vertical(&self, delta: ScrollFrom) {
332 self.scroll_vertical_clamp(delta, f32::MIN, f32::MAX);
333 }
334
335 pub fn scroll_horizontal(&self, delta: ScrollFrom) {
339 self.scroll_horizontal_clamp(delta, f32::MIN, f32::MAX)
340 }
341
342 pub fn scroll_vertical_clamp(&self, delta: ScrollFrom, min: f32, max: f32) {
346 self.scroll_clamp(true, SCROLL_VERTICAL_OFFSET_VAR, delta, min, max)
347 }
348
349 pub fn scroll_horizontal_clamp(&self, delta: ScrollFrom, min: f32, max: f32) {
353 self.scroll_clamp(false, SCROLL_HORIZONTAL_OFFSET_VAR, delta, min, max)
354 }
355 fn scroll_clamp(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, delta: ScrollFrom, min: f32, max: f32) {
356 let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
357 let content = (SCROLL_CONTENT_ORIGINAL_SIZE_VAR.get() * SCROLL_SCALE_VAR.get()).to_array()[vertical as usize];
358
359 let max_scroll = content - viewport;
360
361 if max_scroll <= 0 {
362 return;
363 }
364
365 match delta {
366 ScrollFrom::Var(a) => {
367 let amount = a.0 as f32 / max_scroll.0 as f32;
368 let f = scroll_offset_var.get();
369 SCROLL.chase(vertical, scroll_offset_var, |_| (f.0 + amount).clamp(min, max).fct());
370 }
371 ScrollFrom::VarTarget(a) => {
372 let amount = a.0 as f32 / max_scroll.0 as f32;
373 SCROLL.chase(vertical, scroll_offset_var, |f| (f.0 + amount).clamp(min, max).fct());
374 }
375 ScrollFrom::Rendered(a) => {
376 let amount = a.0 as f32 / max_scroll.0 as f32;
377 let f = SCROLL_CONFIG.get().rendered.load(Ordering::Relaxed).h;
378 SCROLL.chase(vertical, scroll_offset_var, |_| (f.0 + amount).clamp(min, max).fct());
379 }
380 }
381 }
382
383 pub fn auto_scroll(&self, velocity: DipVector) {
385 let viewport = SCROLL_VIEWPORT_SIZE_VAR.get();
386 let content = SCROLL_CONTENT_ORIGINAL_SIZE_VAR.get() * SCROLL_SCALE_VAR.get();
387 let max_scroll = content - viewport;
388
389 let velocity = velocity.to_px(WINDOW.info().scale_factor());
390
391 fn scroll(dimension: usize, velocity: Px, max_scroll: Px, offset_var: &ContextVar<Factor>) {
392 if velocity == 0 {
393 SCROLL_CONFIG.get().auto[dimension].lock().clone().stop();
394 } else {
395 let mut travel = max_scroll * offset_var.get();
396 let mut target = 0.0;
397 if velocity > Px(0) {
398 travel = max_scroll - travel;
399 target = 1.0;
400 }
401 let time = (travel.0 as f32 / velocity.0.abs() as f32).secs();
402
403 VARS.with_animation_controller(zng_var::animation::ForceAnimationController, || {
404 let handle = offset_var.ease(target, time, easing::linear);
405 mem::replace(&mut *SCROLL_CONFIG.get().auto[dimension].lock(), handle).stop();
406 });
407 }
408 }
409 scroll(0, velocity.x, max_scroll.width, &SCROLL_HORIZONTAL_OFFSET_VAR);
410 scroll(1, velocity.y, max_scroll.height, &SCROLL_VERTICAL_OFFSET_VAR);
411 }
412
413 pub fn scroll_vertical_touch(&self, delta: Px) {
418 self.scroll_touch(true, SCROLL_VERTICAL_OFFSET_VAR, OVERSCROLL_VERTICAL_OFFSET_VAR, delta)
419 }
420
421 pub fn scroll_horizontal_touch(&self, delta: Px) {
426 self.scroll_touch(false, SCROLL_HORIZONTAL_OFFSET_VAR, OVERSCROLL_HORIZONTAL_OFFSET_VAR, delta)
427 }
428
429 fn scroll_touch(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, overscroll_offset_var: ContextVar<Factor>, delta: Px) {
430 let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
431 let content = (SCROLL_CONTENT_ORIGINAL_SIZE_VAR.get() * SCROLL_SCALE_VAR.get()).to_array()[vertical as usize];
432
433 let max_scroll = content - viewport;
434 if max_scroll <= 0 {
435 return;
436 }
437
438 let delta = delta.0 as f32 / max_scroll.0 as f32;
439
440 let current = scroll_offset_var.get();
441 let mut next = current + delta.fct();
442 let mut overscroll = 0.fct();
443 if next > 1.fct() {
444 overscroll = next - 1.fct();
445 next = 1.fct();
446
447 let overscroll_px = overscroll * content.0.fct();
448 let overscroll_max = viewport.0.fct();
449 overscroll = overscroll_px.min(overscroll_max) / overscroll_max;
450 } else if next < 0.fct() {
451 overscroll = next;
452 next = 0.fct();
453
454 let overscroll_px = -overscroll * content.0.fct();
455 let overscroll_max = viewport.0.fct();
456 overscroll = -(overscroll_px.min(overscroll_max) / overscroll_max);
457 }
458
459 scroll_offset_var.set(next);
460 if overscroll != 0.fct() {
461 let new_handle = self.increment_overscroll(overscroll_offset_var, overscroll);
462
463 let config = SCROLL_CONFIG.get();
464 let mut handle = config.overscroll[vertical as usize].lock();
465 mem::replace(&mut *handle, new_handle).stop();
466 } else {
467 self.clear_horizontal_overscroll();
468 }
469 }
470
471 fn increment_overscroll(&self, overscroll: ContextVar<Factor>, delta: Factor) -> AnimationHandle {
472 enum State {
473 Increment,
474 ClearDelay,
475 Clear(Transition<Factor>),
476 }
477 let mut state = State::Increment;
478 overscroll.animate(move |a, o| match &mut state {
479 State::Increment => {
480 **o += delta;
482 **o = (*o).clamp(-1.fct(), 1.fct());
483
484 a.sleep(300.ms(), false);
485 state = State::ClearDelay;
486 }
487 State::ClearDelay => {
488 a.restart();
489 let t = Transition::new(**o, 0.fct());
490 state = State::Clear(t);
491 }
492 State::Clear(t) => {
493 let step = easing::linear(a.elapsed_stop(300.ms()));
494 o.set(t.sample(step));
495 }
496 })
497 }
498
499 pub fn clear_vertical_overscroll(&self) {
501 self.clear_overscroll(true, OVERSCROLL_VERTICAL_OFFSET_VAR)
502 }
503
504 pub fn clear_horizontal_overscroll(&self) {
506 self.clear_overscroll(false, OVERSCROLL_HORIZONTAL_OFFSET_VAR)
507 }
508
509 fn clear_overscroll(&self, vertical: bool, overscroll_offset_var: ContextVar<Factor>) {
510 if overscroll_offset_var.get() != 0.fct() {
511 let new_handle = overscroll_offset_var.ease(0.fct(), 100.ms(), easing::linear);
512
513 let config = SCROLL_CONFIG.get();
514 let mut handle = config.overscroll[vertical as usize].lock();
515 mem::replace(&mut *handle, new_handle).stop();
516 }
517 }
518
519 pub fn scroll_vertical_touch_inertia(&self, delta: Px, duration: Duration) {
521 self.scroll_touch_inertia(true, SCROLL_VERTICAL_OFFSET_VAR, OVERSCROLL_VERTICAL_OFFSET_VAR, delta, duration)
522 }
523
524 pub fn scroll_horizontal_touch_inertia(&self, delta: Px, duration: Duration) {
526 self.scroll_touch_inertia(
527 false,
528 SCROLL_HORIZONTAL_OFFSET_VAR,
529 OVERSCROLL_HORIZONTAL_OFFSET_VAR,
530 delta,
531 duration,
532 )
533 }
534
535 fn scroll_touch_inertia(
536 &self,
537 vertical: bool,
538 scroll_offset_var: ContextVar<Factor>,
539 overscroll_offset_var: ContextVar<Factor>,
540 delta: Px,
541 duration: Duration,
542 ) {
543 let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
544 let content = (SCROLL_CONTENT_ORIGINAL_SIZE_VAR.get() * SCROLL_SCALE_VAR.get()).to_array()[vertical as usize];
545
546 let max_scroll = content - viewport;
547 if max_scroll <= 0 {
548 return;
549 }
550
551 let delta = delta.0 as f32 / max_scroll.0 as f32;
552
553 let current = scroll_offset_var.get();
554 let mut next = current + delta.fct();
555 let mut overscroll = 0.fct();
556 if next > 1.fct() {
557 overscroll = next - 1.fct();
558 next = 1.fct();
559
560 let overscroll_px = overscroll * content.0.fct();
561 let overscroll_max = viewport.0.fct();
562 overscroll = overscroll_px.min(overscroll_max) / overscroll_max;
563 } else if next < 0.fct() {
564 overscroll = next;
565 next = 0.fct();
566
567 let overscroll_px = -overscroll * content.0.fct();
568 let overscroll_max = viewport.0.fct();
569 overscroll = -(overscroll_px.min(overscroll_max) / overscroll_max);
570 }
571
572 let cfg = SCROLL_CONFIG.get();
573 let easing = |t| easing::ease_out(easing::quad, t);
574 *cfg.inertia[vertical as usize].lock() = if overscroll != 0.fct() {
575 let transition = Transition::new(current, next + overscroll);
576
577 let overscroll_var = overscroll_offset_var.current_context();
578 let overscroll_tr = Transition::new(overscroll, 0.fct());
579 let mut is_inertia_anim = true;
580
581 scroll_offset_var.animate(move |animation, value| {
582 if is_inertia_anim {
583 let step = easing(animation.elapsed(duration));
585 let v = transition.sample(step);
586
587 if v < 0.fct() || v > 1.fct() {
588 value.set(v.clamp_range());
590 animation.restart();
591 is_inertia_anim = false;
592 overscroll_var.set(overscroll_tr.from);
593 } else {
594 value.set(v);
595 }
596 } else {
597 let step = easing::linear(animation.elapsed_stop(300.ms()));
599 let v = overscroll_tr.sample(step);
600 overscroll_var.set(v);
601 }
602 })
603 } else {
604 scroll_offset_var.ease(next, duration, easing)
605 };
606 }
607
608 pub fn chase_vertical(&self, modify_offset: impl FnOnce(Factor) -> Factor) {
611 self.chase(true, SCROLL_VERTICAL_OFFSET_VAR, modify_offset);
612 }
613
614 pub fn chase_horizontal(&self, modify_offset: impl FnOnce(Factor) -> Factor) {
617 self.chase(false, SCROLL_HORIZONTAL_OFFSET_VAR, modify_offset);
618 }
619
620 fn chase(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, modify_offset: impl FnOnce(Factor) -> Factor) {
621 let smooth = SMOOTH_SCROLLING_VAR.get();
622 let config = SCROLL_CONFIG.get();
623 let mut chase = config.chase[vertical as usize].lock();
624 match &mut *chase {
625 Some(t) => {
626 if smooth.is_disabled() {
627 let t = modify_offset(*t.target()).clamp_range();
628 scroll_offset_var.set(t);
629 *chase = None;
630 } else {
631 let easing = smooth.easing.clone();
632 t.modify(|f| *f = modify_offset(*f).clamp_range(), smooth.duration, move |t| easing(t));
633 }
634 }
635 None => {
636 let t = modify_offset(scroll_offset_var.get()).clamp_range();
637 if smooth.is_disabled() {
638 scroll_offset_var.set(t);
639 } else {
640 let easing = smooth.easing.clone();
641 let anim = scroll_offset_var.chase(t, smooth.duration, move |t| easing(t));
642 *chase = Some(anim);
643 }
644 }
645 }
646 }
647
648 pub fn chase_zoom(&self, modify_scale: impl FnOnce(Factor) -> Factor) {
651 self.chase_zoom_impl(modify_scale);
652 }
653 fn chase_zoom_impl(&self, modify_scale: impl FnOnce(Factor) -> Factor) {
654 if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
655 return;
656 }
657
658 let smooth = SMOOTH_SCROLLING_VAR.get();
659 let config = SCROLL_CONFIG.get();
660 let mut zoom = config.zoom.lock();
661
662 let min = SCROLL.actual_min_zoom();
663 let max = super::MAX_ZOOM_VAR.get();
664
665 match &mut *zoom {
666 ZoomState::Chasing(t) => {
667 if smooth.is_disabled() {
668 let next = modify_scale(*t.target()).clamp(min, max);
669 SCROLL_SCALE_VAR.set(next);
670 *zoom = ZoomState::None;
671 } else {
672 let easing = smooth.easing.clone();
673 t.modify(|f| *f = modify_scale(*f).clamp(min, max), smooth.duration, move |t| easing(t));
674 }
675 }
676 _ => {
677 let t = modify_scale(SCROLL_SCALE_VAR.get()).clamp(min, max);
678 if smooth.is_disabled() {
679 SCROLL_SCALE_VAR.set(t);
680 } else {
681 let easing = smooth.easing.clone();
682 let anim = SCROLL_SCALE_VAR.chase(t, smooth.duration, move |t| easing(t));
683 *zoom = ZoomState::Chasing(anim);
684 }
685 }
686 }
687 }
688
689 pub fn zoom(&self, modify_scale: impl FnOnce(Factor) -> Factor, origin: PxPoint) {
692 self.zoom_impl(modify_scale, origin);
693 }
694 fn zoom_impl(&self, modify_scale: impl FnOnce(Factor) -> Factor, center_in_viewport: PxPoint) {
695 if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
696 return;
697 }
698
699 let content = WIDGET.info().scroll_info().unwrap().content();
700 let mut center_in_content = -content.origin + center_in_viewport.to_vector();
701 let mut content_size = content.size;
702
703 let rendered_scale = SCROLL.rendered_zoom_scale();
704
705 SCROLL.chase_zoom(|f| {
706 let s = modify_scale(f);
707 let f = s / rendered_scale;
708 center_in_content *= f;
709 content_size *= f;
710 s
711 });
712
713 let viewport_size = SCROLL_VIEWPORT_SIZE_VAR.get();
714
715 let max_scroll = content_size - viewport_size;
717 let offset = center_in_content - center_in_viewport;
718
719 if offset.y != Px(0) && max_scroll.height > Px(0) {
720 let offset_y = offset.y.0 as f32 / max_scroll.height.0 as f32;
721 SCROLL.chase_vertical(|_| offset_y.fct());
722 }
723 if offset.x != Px(0) && max_scroll.width > Px(0) {
724 let offset_x = offset.x.0 as f32 / max_scroll.width.0 as f32;
725 SCROLL.chase_horizontal(|_| offset_x.fct());
726 }
727 }
728
729 pub fn zoom_touch(&self, phase: TouchPhase, scale: Factor, center_in_viewport: euclid::Point2D<f32, Px>) {
731 if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
732 return;
733 }
734
735 let cfg = SCROLL_CONFIG.get();
736
737 let rendered_scale = SCROLL.rendered_zoom_scale();
738
739 let start_scale;
740 let start_center;
741
742 let mut cfg = cfg.zoom.lock();
743
744 if let TouchPhase::Start = phase {
745 start_scale = rendered_scale;
746 start_center = center_in_viewport;
747
748 *cfg = ZoomState::TouchStart {
749 start_factor: start_scale,
750 start_center: center_in_viewport,
751 applied_offset: euclid::vec2(0.0, 0.0),
752 };
753 } else if let ZoomState::TouchStart {
754 start_factor: scale,
755 start_center: center_in_viewport,
756 ..
757 } = &*cfg
758 {
759 start_scale = *scale;
760 start_center = *center_in_viewport;
761 } else {
762 return;
764 }
765
766 let applied_offset = if let ZoomState::TouchStart { applied_offset, .. } = &mut *cfg {
768 applied_offset
769 } else {
770 unreachable!()
771 };
772
773 let translate_offset = start_center - center_in_viewport;
774 let translate_delta = translate_offset - *applied_offset;
775 *applied_offset = translate_offset;
776
777 let scroll_info = WIDGET.info().scroll_info().unwrap();
778 let content = scroll_info.content();
779 let mut center_in_content = -content.origin.cast::<f32>() + center_in_viewport.to_vector();
780 let mut content_size = content.size.cast::<f32>();
781
782 let scale = start_scale + (scale - 1.0.fct());
783
784 let min = SCROLL.actual_min_zoom();
785 let max = super::MAX_ZOOM_VAR.get();
786 let scale = scale.clamp(min, max);
787
788 let scale_transform = scale / rendered_scale;
789
790 center_in_content *= scale_transform;
791 content_size *= scale_transform;
792
793 let viewport_size = SCROLL_VIEWPORT_SIZE_VAR.get().cast::<f32>();
794
795 let max_scroll = content_size - viewport_size;
797 let zoom_offset = center_in_content - center_in_viewport;
798
799 let offset = zoom_offset + translate_delta;
800
801 SCROLL_SCALE_VAR.set(scale);
802
803 if offset.y != 0.0 && max_scroll.height > 0.0 {
804 let offset_y = offset.y / max_scroll.height;
805 SCROLL_VERTICAL_OFFSET_VAR.set(offset_y.clamp(0.0, 1.0));
806 }
807 if offset.x != 0.0 && max_scroll.width > 0.0 {
808 let offset_x = offset.x / max_scroll.width;
809 SCROLL_HORIZONTAL_OFFSET_VAR.set(offset_x.clamp(0.0, 1.0));
810 }
811 }
812
813 fn can_scroll(&self, predicate: impl Fn(PxSize, PxSize) -> bool + Send + Sync + 'static) -> Var<bool> {
814 expr_var! {
815 predicate(
816 *#{SCROLL_VIEWPORT_SIZE_VAR},
817 *#{SCROLL_CONTENT_ORIGINAL_SIZE_VAR} * *#{SCROLL_SCALE_VAR},
818 )
819 }
820 }
821
822 pub fn can_scroll_vertical(&self) -> Var<bool> {
824 self.can_scroll(|vp, ct| ct.height > vp.height)
825 }
826
827 pub fn can_scroll_horizontal(&self) -> Var<bool> {
829 self.can_scroll(|vp, ct| ct.width > vp.width)
830 }
831
832 fn can_scroll_v(&self, predicate: impl Fn(PxSize, PxSize, Factor) -> bool + Send + Sync + 'static) -> Var<bool> {
833 expr_var! {
834 predicate(
835 *#{SCROLL_VIEWPORT_SIZE_VAR},
836 *#{SCROLL_CONTENT_ORIGINAL_SIZE_VAR} * *#{SCROLL_SCALE_VAR},
837 *#{SCROLL_VERTICAL_OFFSET_VAR},
838 )
839 }
840 }
841
842 pub fn can_scroll_down(&self) -> Var<bool> {
845 self.can_scroll_v(|vp, ct, vo| ct.height > vp.height && 1.fct() > vo)
846 }
847
848 pub fn can_scroll_up(&self) -> Var<bool> {
851 self.can_scroll_v(|vp, ct, vo| ct.height > vp.height && 0.fct() < vo)
852 }
853
854 fn can_scroll_h(&self, predicate: impl Fn(PxSize, PxSize, Factor) -> bool + Send + Sync + 'static) -> Var<bool> {
855 expr_var! {
856 predicate(
857 *#{SCROLL_VIEWPORT_SIZE_VAR},
858 *#{SCROLL_CONTENT_ORIGINAL_SIZE_VAR} * *#{SCROLL_SCALE_VAR},
859 *#{SCROLL_HORIZONTAL_OFFSET_VAR},
860 )
861 }
862 }
863
864 pub fn can_scroll_left(&self) -> Var<bool> {
867 self.can_scroll_h(|vp, ct, ho| ct.width > vp.width && 0.fct() < ho)
868 }
869
870 pub fn can_scroll_right(&self) -> Var<bool> {
873 self.can_scroll_h(|vp, ct, ho| ct.width > vp.width && 1.fct() > ho)
874 }
875
876 pub fn scroll_to(&self, mode: impl Into<super::cmd::ScrollToMode>) {
880 cmd::scroll_to(WIDGET.info(), mode.into())
881 }
882
883 pub fn scroll_to_zoom(&self, mode: impl Into<super::cmd::ScrollToMode>, zoom: impl Into<Factor>) {
887 cmd::scroll_to_zoom(WIDGET.info(), mode.into(), zoom.into())
888 }
889
890 pub fn can_zoom_in(&self) -> bool {
892 SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) && SCROLL_SCALE_VAR.get() < super::MAX_ZOOM_VAR.get()
893 }
894
895 pub fn can_zoom_out(&self) -> bool {
897 SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) && SCROLL_SCALE_VAR.get() > self.actual_min_zoom()
898 }
899
900 pub fn actual_min_zoom(&self) -> Factor {
907 let min = super::MIN_ZOOM_VAR.get();
908 if min >= 0.01.pct().fct() {
909 return min;
910 }
911
912 let content;
913 let viewport;
914
915 if let Some(info) = WIDGET.try_info()
916 && let Some(info) = info.scroll_info()
917 {
918 content = info.content_original_size().cast::<f32>();
920 viewport = (info.viewport().size - info.joiner_size()).cast::<f32>();
921 } else {
922 content = SCROLL_CONTENT_ORIGINAL_SIZE_VAR.get().cast::<f32>();
923 viewport = SCROLL_VIEWPORT_SIZE_VAR.get().cast::<f32>(); }
925
926 if content.width < 0.1 || content.height < 0.1 {
927 return min;
928 }
929 (viewport.width / content.width)
930 .min(viewport.height / content.height)
931 .min(1.0)
932 .fct()
933 }
934}
935
936impl SCROLL {
937 pub fn context_values_set(&self, set: &mut ContextValueSet) {
941 set.insert(&SCROLL_CONFIG);
942 }
943}
944
945pub trait WidgetInfoExt {
949 fn is_scroll(&self) -> bool;
951
952 fn scroll_info(&self) -> Option<ScrollInfo>;
954
955 fn viewport(&self) -> Option<PxRect>;
959}
960impl WidgetInfoExt for WidgetInfo {
961 fn is_scroll(&self) -> bool {
962 self.meta().get(*SCROLL_INFO_ID).is_some()
963 }
964
965 fn scroll_info(&self) -> Option<ScrollInfo> {
966 self.meta().get(*SCROLL_INFO_ID).cloned()
967 }
968
969 fn viewport(&self) -> Option<PxRect> {
970 self.meta().get(*SCROLL_INFO_ID).map(|r| r.viewport())
971 }
972}
973
974#[derive(Debug)]
975struct ScrollData {
976 viewport_transform: PxTransform,
977 viewport_size: PxSize,
978 joiner_size: PxSize,
979 content_offset: PxPoint,
980 zoom_scale: Factor,
981 content_original_size: PxSize,
982}
983impl Default for ScrollData {
984 fn default() -> Self {
985 Self {
986 viewport_transform: Default::default(),
987 viewport_size: Default::default(),
988 joiner_size: Default::default(),
989 content_offset: Default::default(),
990 zoom_scale: 1.fct(),
991 content_original_size: Default::default(),
992 }
993 }
994}
995
996#[derive(Clone, Default, Debug)]
998pub struct ScrollInfo(Arc<Mutex<ScrollData>>);
999impl ScrollInfo {
1000 pub fn viewport(&self) -> PxRect {
1002 self.viewport_transform()
1003 .outer_transformed(PxBox::from_size(self.viewport_size()))
1004 .unwrap_or_default()
1005 .to_rect()
1006 }
1007
1008 pub fn viewport_size(&self) -> PxSize {
1010 self.0.lock().viewport_size
1011 }
1012
1013 pub fn viewport_transform(&self) -> PxTransform {
1015 self.0.lock().viewport_transform
1016 }
1017
1018 pub fn joiner_size(&self) -> PxSize {
1023 self.0.lock().joiner_size
1024 }
1025
1026 pub fn content(&self) -> PxRect {
1030 let m = self.0.lock();
1031 PxRect::new(m.content_offset, m.content_original_size * m.zoom_scale)
1032 }
1033
1034 pub fn content_original_size(&self) -> PxSize {
1038 self.0.lock().content_original_size
1039 }
1040
1041 pub fn zoom_scale(&self) -> Factor {
1043 self.0.lock().zoom_scale
1044 }
1045
1046 pub(super) fn set_viewport_size(&self, size: PxSize) {
1047 self.0.lock().viewport_size = size;
1048 }
1049
1050 pub(super) fn set_viewport_transform(&self, transform: PxTransform) {
1051 self.0.lock().viewport_transform = transform;
1052 }
1053
1054 pub(super) fn set_joiner_size(&self, size: PxSize) {
1055 self.0.lock().joiner_size = size;
1056 }
1057
1058 pub(super) fn set_content(&self, content_offset: PxPoint, scale: Factor, content_original_size: PxSize) {
1059 let mut m = self.0.lock();
1060 m.content_offset = content_offset;
1061 m.zoom_scale = scale;
1062 m.content_original_size = content_original_size;
1063 }
1064}
1065
1066static_id! {
1067 pub(super) static ref SCROLL_INFO_ID: StateId<ScrollInfo>;
1068}
1069
1070#[derive(Clone)]
1076pub struct SmoothScrolling {
1077 pub duration: Duration,
1081 pub easing: Arc<dyn Fn(EasingTime) -> EasingStep + Send + Sync>,
1085}
1086impl fmt::Debug for SmoothScrolling {
1087 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1088 f.debug_struct("SmoothScrolling")
1089 .field("duration", &self.duration)
1090 .finish_non_exhaustive()
1091 }
1092}
1093impl PartialEq for SmoothScrolling {
1094 fn eq(&self, other: &Self) -> bool {
1095 self.duration == other.duration && Arc::ptr_eq(&self.easing, &other.easing)
1096 }
1097}
1098impl Default for SmoothScrolling {
1099 fn default() -> Self {
1100 Self::new(150.ms(), easing::linear)
1101 }
1102}
1103impl SmoothScrolling {
1104 pub fn new(duration: Duration, easing: impl Fn(EasingTime) -> EasingStep + Send + Sync + 'static) -> Self {
1106 Self {
1107 duration,
1108 easing: Arc::new(easing),
1109 }
1110 }
1111
1112 pub fn disabled() -> Self {
1114 Self::new(Duration::ZERO, easing::none)
1115 }
1116
1117 pub fn is_disabled(&self) -> bool {
1121 self.duration == Duration::ZERO
1122 }
1123}
1124impl_from_and_into_var! {
1125 fn from(duration: Duration) -> SmoothScrolling {
1127 SmoothScrolling {
1128 duration,
1129 ..Default::default()
1130 }
1131 }
1132
1133 fn from(enabled: bool) -> SmoothScrolling {
1137 if enabled {
1138 SmoothScrolling::default()
1139 } else {
1140 SmoothScrolling::disabled()
1141 }
1142 }
1143
1144 fn from<F: Fn(EasingTime) -> EasingStep + Send + Sync + 'static>((duration, easing): (Duration, F)) -> SmoothScrolling {
1145 SmoothScrolling::new(duration, easing)
1146 }
1147
1148 fn from((duration, easing): (Duration, easing::EasingFn)) -> SmoothScrolling {
1149 SmoothScrolling::new(duration, easing.ease_fn())
1150 }
1151}
1152
1153#[derive(Debug, Default, Clone, PartialEq)]
1162#[non_exhaustive]
1163pub struct AutoScrollArgs {}
1164
1165#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1172pub enum ZoomToFitMode {
1173 #[default]
1175 Contain,
1176 ScaleDown,
1178}