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
135 ignore_next_scroll_to_focused: Mutex<Option<DInstant>>,
136}
137impl Default for ScrollConfig {
138 fn default() -> Self {
139 Self {
140 id: Default::default(),
141 chase: Default::default(),
142 zoom: Default::default(),
143 rendered: Atomic::new(RenderedOffsets {
144 h: 0.fct(),
145 v: 0.fct(),
146 z: 0.fct(),
147 }),
148 overscroll: Default::default(),
149 inertia: Default::default(),
150 auto: Default::default(),
151 ignore_next_scroll_to_focused: Default::default(),
152 }
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
163pub enum ScrollFrom {
164 Var(Px),
170 VarTarget(Px),
177
178 Rendered(Px),
184}
185
186pub struct SCROLL;
188impl SCROLL {
189 pub fn try_id(&self) -> Option<WidgetId> {
191 SCROLL_CONFIG.get().id
192 }
193 pub fn id(&self) -> WidgetId {
199 self.try_id().expect("not inside scroll")
200 }
201
202 pub fn config_node(&self, child: impl IntoUiNode) -> UiNode {
206 let child = match_node(child, move |_, op| {
207 if let UiNodeOp::Render { .. } | UiNodeOp::RenderUpdate { .. } = op {
208 let h = SCROLL_HORIZONTAL_OFFSET_VAR.get();
209 let v = SCROLL_VERTICAL_OFFSET_VAR.get();
210 let z = SCROLL_SCALE_VAR.get();
211 SCROLL_CONFIG.get().rendered.store(RenderedOffsets { h, v, z }, Ordering::Relaxed);
212 }
213 });
214 with_context_local_init(child, &SCROLL_CONFIG, || ScrollConfig {
215 id: WIDGET.try_id(),
216 ..Default::default()
217 })
218 }
219
220 pub fn mode(&self) -> Var<ScrollMode> {
222 SCROLL_MODE_VAR.read_only()
223 }
224
225 pub fn vertical_offset(&self) -> ContextVar<Factor> {
233 SCROLL_VERTICAL_OFFSET_VAR
234 }
235
236 pub fn horizontal_offset(&self) -> ContextVar<Factor> {
244 SCROLL_HORIZONTAL_OFFSET_VAR
245 }
246
247 pub fn zoom_scale(&self) -> ContextVar<Factor> {
253 SCROLL_SCALE_VAR
254 }
255
256 pub fn rendered_offset(&self) -> Factor2d {
258 let cfg = SCROLL_CONFIG.get().rendered.load(Ordering::Relaxed);
259 Factor2d::new(cfg.h, cfg.v)
260 }
261
262 pub fn rendered_zoom_scale(&self) -> Factor {
264 SCROLL_CONFIG.get().rendered.load(Ordering::Relaxed).z
265 }
266
267 pub fn vertical_overscroll(&self) -> Var<Factor> {
275 OVERSCROLL_VERTICAL_OFFSET_VAR.read_only()
276 }
277
278 pub fn horizontal_overscroll(&self) -> Var<Factor> {
286 OVERSCROLL_HORIZONTAL_OFFSET_VAR.read_only()
287 }
288
289 pub fn vertical_ratio(&self) -> Var<Factor> {
293 SCROLL_VERTICAL_RATIO_VAR.read_only()
294 }
295 pub fn horizontal_ratio(&self) -> Var<Factor> {
299 SCROLL_HORIZONTAL_RATIO_VAR.read_only()
300 }
301
302 pub fn vertical_content_overflows(&self) -> Var<bool> {
304 SCROLL_VERTICAL_CONTENT_OVERFLOWS_VAR.read_only()
305 }
306
307 pub fn horizontal_content_overflows(&self) -> Var<bool> {
309 SCROLL_HORIZONTAL_CONTENT_OVERFLOWS_VAR.read_only()
310 }
311
312 pub fn viewport_size(&self) -> Var<PxSize> {
314 SCROLL_VIEWPORT_SIZE_VAR.read_only()
315 }
316
317 pub fn content_size(&self) -> Var<PxSize> {
321 expr_var! {
322 *#{SCROLL_CONTENT_ORIGINAL_SIZE_VAR} * *#{SCROLL_SCALE_VAR}
323 }
324 }
325
326 pub fn content_original_size(&self) -> Var<PxSize> {
328 SCROLL_CONTENT_ORIGINAL_SIZE_VAR.read_only()
329 }
330
331 pub fn scroll_vertical(&self, delta: ScrollFrom) {
335 self.scroll_vertical_clamp(delta, f32::MIN, f32::MAX);
336 }
337
338 pub fn scroll_horizontal(&self, delta: ScrollFrom) {
342 self.scroll_horizontal_clamp(delta, f32::MIN, f32::MAX)
343 }
344
345 pub fn scroll_vertical_clamp(&self, delta: ScrollFrom, min: f32, max: f32) {
349 self.scroll_clamp(true, SCROLL_VERTICAL_OFFSET_VAR, delta, min, max)
350 }
351
352 pub fn scroll_horizontal_clamp(&self, delta: ScrollFrom, min: f32, max: f32) {
356 self.scroll_clamp(false, SCROLL_HORIZONTAL_OFFSET_VAR, delta, min, max)
357 }
358 fn scroll_clamp(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, delta: ScrollFrom, min: f32, max: f32) {
359 let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
360 let content = (SCROLL_CONTENT_ORIGINAL_SIZE_VAR.get() * SCROLL_SCALE_VAR.get()).to_array()[vertical as usize];
361
362 let max_scroll = content - viewport;
363
364 if max_scroll <= 0 {
365 return;
366 }
367
368 match delta {
369 ScrollFrom::Var(a) => {
370 let amount = a.0 as f32 / max_scroll.0 as f32;
371 let f = scroll_offset_var.get();
372 SCROLL.chase(vertical, scroll_offset_var, |_| (f.0 + amount).clamp(min, max).fct());
373 }
374 ScrollFrom::VarTarget(a) => {
375 let amount = a.0 as f32 / max_scroll.0 as f32;
376 SCROLL.chase(vertical, scroll_offset_var, |f| (f.0 + amount).clamp(min, max).fct());
377 }
378 ScrollFrom::Rendered(a) => {
379 let amount = a.0 as f32 / max_scroll.0 as f32;
380 let f = SCROLL_CONFIG.get().rendered.load(Ordering::Relaxed).h;
381 SCROLL.chase(vertical, scroll_offset_var, |_| (f.0 + amount).clamp(min, max).fct());
382 }
383 }
384 }
385
386 pub fn auto_scroll(&self, velocity: DipVector) {
388 let viewport = SCROLL_VIEWPORT_SIZE_VAR.get();
389 let content = SCROLL_CONTENT_ORIGINAL_SIZE_VAR.get() * SCROLL_SCALE_VAR.get();
390 let max_scroll = content - viewport;
391
392 let velocity = velocity.to_px(WINDOW.info().scale_factor());
393
394 fn scroll(dimension: usize, velocity: Px, max_scroll: Px, offset_var: &ContextVar<Factor>) {
395 if velocity == 0 {
396 SCROLL_CONFIG.get().auto[dimension].lock().clone().stop();
397 } else {
398 let mut travel = max_scroll * offset_var.get();
399 let mut target = 0.0;
400 if velocity > Px(0) {
401 travel = max_scroll - travel;
402 target = 1.0;
403 }
404 let time = (travel.0 as f32 / velocity.0.abs() as f32).secs();
405
406 VARS.with_animation_controller(zng_var::animation::ForceAnimationController, || {
407 let handle = offset_var.ease(target, time, easing::linear);
408 mem::replace(&mut *SCROLL_CONFIG.get().auto[dimension].lock(), handle).stop();
409 });
410 }
411 }
412 scroll(0, velocity.x, max_scroll.width, &SCROLL_HORIZONTAL_OFFSET_VAR);
413 scroll(1, velocity.y, max_scroll.height, &SCROLL_VERTICAL_OFFSET_VAR);
414 }
415
416 pub fn scroll_vertical_touch(&self, delta: Px) {
421 self.scroll_touch(true, SCROLL_VERTICAL_OFFSET_VAR, OVERSCROLL_VERTICAL_OFFSET_VAR, delta)
422 }
423
424 pub fn scroll_horizontal_touch(&self, delta: Px) {
429 self.scroll_touch(false, SCROLL_HORIZONTAL_OFFSET_VAR, OVERSCROLL_HORIZONTAL_OFFSET_VAR, delta)
430 }
431
432 fn scroll_touch(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, overscroll_offset_var: ContextVar<Factor>, delta: Px) {
433 let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
434 let content = (SCROLL_CONTENT_ORIGINAL_SIZE_VAR.get() * SCROLL_SCALE_VAR.get()).to_array()[vertical as usize];
435
436 let max_scroll = content - viewport;
437 if max_scroll <= 0 {
438 return;
439 }
440
441 let delta = delta.0 as f32 / max_scroll.0 as f32;
442
443 let current = scroll_offset_var.get();
444 let mut next = current + delta.fct();
445 let mut overscroll = 0.fct();
446 if next > 1.fct() {
447 overscroll = next - 1.fct();
448 next = 1.fct();
449
450 let overscroll_px = overscroll * content.0.fct();
451 let overscroll_max = viewport.0.fct();
452 overscroll = overscroll_px.min(overscroll_max) / overscroll_max;
453 } else if next < 0.fct() {
454 overscroll = next;
455 next = 0.fct();
456
457 let overscroll_px = -overscroll * content.0.fct();
458 let overscroll_max = viewport.0.fct();
459 overscroll = -(overscroll_px.min(overscroll_max) / overscroll_max);
460 }
461
462 scroll_offset_var.set(next);
463 if overscroll != 0.fct() {
464 let new_handle = self.increment_overscroll(overscroll_offset_var, overscroll);
465
466 let config = SCROLL_CONFIG.get();
467 let mut handle = config.overscroll[vertical as usize].lock();
468 mem::replace(&mut *handle, new_handle).stop();
469 } else {
470 self.clear_horizontal_overscroll();
471 }
472 }
473
474 fn increment_overscroll(&self, overscroll: ContextVar<Factor>, delta: Factor) -> AnimationHandle {
475 enum State {
476 Increment,
477 ClearDelay,
478 Clear(Transition<Factor>),
479 }
480 let mut state = State::Increment;
481 overscroll.animate(move |a, o| match &mut state {
482 State::Increment => {
483 **o += delta;
485 **o = (*o).clamp(-1.fct(), 1.fct());
486
487 a.sleep(300.ms(), false);
488 state = State::ClearDelay;
489 }
490 State::ClearDelay => {
491 a.restart();
492 let t = Transition::new(**o, 0.fct());
493 state = State::Clear(t);
494 }
495 State::Clear(t) => {
496 let step = easing::linear(a.elapsed_stop(300.ms()));
497 o.set(t.sample(step));
498 }
499 })
500 }
501
502 pub fn clear_vertical_overscroll(&self) {
504 self.clear_overscroll(true, OVERSCROLL_VERTICAL_OFFSET_VAR)
505 }
506
507 pub fn clear_horizontal_overscroll(&self) {
509 self.clear_overscroll(false, OVERSCROLL_HORIZONTAL_OFFSET_VAR)
510 }
511
512 fn clear_overscroll(&self, vertical: bool, overscroll_offset_var: ContextVar<Factor>) {
513 if overscroll_offset_var.get() != 0.fct() {
514 let new_handle = overscroll_offset_var.ease(0.fct(), 100.ms(), easing::linear);
515
516 let config = SCROLL_CONFIG.get();
517 let mut handle = config.overscroll[vertical as usize].lock();
518 mem::replace(&mut *handle, new_handle).stop();
519 }
520 }
521
522 pub fn scroll_vertical_touch_inertia(&self, delta: Px, duration: Duration) {
524 self.scroll_touch_inertia(true, SCROLL_VERTICAL_OFFSET_VAR, OVERSCROLL_VERTICAL_OFFSET_VAR, delta, duration)
525 }
526
527 pub fn scroll_horizontal_touch_inertia(&self, delta: Px, duration: Duration) {
529 self.scroll_touch_inertia(
530 false,
531 SCROLL_HORIZONTAL_OFFSET_VAR,
532 OVERSCROLL_HORIZONTAL_OFFSET_VAR,
533 delta,
534 duration,
535 )
536 }
537
538 fn scroll_touch_inertia(
539 &self,
540 vertical: bool,
541 scroll_offset_var: ContextVar<Factor>,
542 overscroll_offset_var: ContextVar<Factor>,
543 delta: Px,
544 duration: Duration,
545 ) {
546 let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
547 let content = (SCROLL_CONTENT_ORIGINAL_SIZE_VAR.get() * SCROLL_SCALE_VAR.get()).to_array()[vertical as usize];
548
549 let max_scroll = content - viewport;
550 if max_scroll <= 0 {
551 return;
552 }
553
554 let delta = delta.0 as f32 / max_scroll.0 as f32;
555
556 let current = scroll_offset_var.get();
557 let mut next = current + delta.fct();
558 let mut overscroll = 0.fct();
559 if next > 1.fct() {
560 overscroll = next - 1.fct();
561 next = 1.fct();
562
563 let overscroll_px = overscroll * content.0.fct();
564 let overscroll_max = viewport.0.fct();
565 overscroll = overscroll_px.min(overscroll_max) / overscroll_max;
566 } else if next < 0.fct() {
567 overscroll = next;
568 next = 0.fct();
569
570 let overscroll_px = -overscroll * content.0.fct();
571 let overscroll_max = viewport.0.fct();
572 overscroll = -(overscroll_px.min(overscroll_max) / overscroll_max);
573 }
574
575 let cfg = SCROLL_CONFIG.get();
576 let easing = |t| easing::ease_out(easing::quad, t);
577 *cfg.inertia[vertical as usize].lock() = if overscroll != 0.fct() {
578 let transition = Transition::new(current, next + overscroll);
579
580 let overscroll_var = overscroll_offset_var.current_context();
581 let overscroll_tr = Transition::new(overscroll, 0.fct());
582 let mut is_inertia_anim = true;
583
584 scroll_offset_var.animate(move |animation, value| {
585 if is_inertia_anim {
586 let step = easing(animation.elapsed(duration));
588 let v = transition.sample(step);
589
590 if v < 0.fct() || v > 1.fct() {
591 value.set(v.clamp_range());
593 animation.restart();
594 is_inertia_anim = false;
595 overscroll_var.set(overscroll_tr.from);
596 } else {
597 value.set(v);
598 }
599 } else {
600 let step = easing::linear(animation.elapsed_stop(300.ms()));
602 let v = overscroll_tr.sample(step);
603 overscroll_var.set(v);
604 }
605 })
606 } else {
607 scroll_offset_var.ease(next, duration, easing)
608 };
609 }
610
611 pub fn chase_vertical(&self, modify_offset: impl FnOnce(Factor) -> Factor) {
614 self.chase(true, SCROLL_VERTICAL_OFFSET_VAR, modify_offset);
615 }
616
617 pub fn chase_horizontal(&self, modify_offset: impl FnOnce(Factor) -> Factor) {
620 self.chase(false, SCROLL_HORIZONTAL_OFFSET_VAR, modify_offset);
621 }
622
623 fn chase(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, modify_offset: impl FnOnce(Factor) -> Factor) {
624 let smooth = SMOOTH_SCROLLING_VAR.get();
625 let config = SCROLL_CONFIG.get();
626 let mut chase = config.chase[vertical as usize].lock();
627 match &mut *chase {
628 Some(t) => {
629 if smooth.is_disabled() {
630 let t = modify_offset(*t.target()).clamp_range();
631 scroll_offset_var.set(t);
632 *chase = None;
633 } else {
634 let easing = smooth.easing.clone();
635 t.modify(|f| *f = modify_offset(*f).clamp_range(), smooth.duration, move |t| easing(t));
636 }
637 }
638 None => {
639 let t = modify_offset(scroll_offset_var.get()).clamp_range();
640 if smooth.is_disabled() {
641 scroll_offset_var.set(t);
642 } else {
643 let easing = smooth.easing.clone();
644 let anim = scroll_offset_var.chase(t, smooth.duration, move |t| easing(t));
645 *chase = Some(anim);
646 }
647 }
648 }
649 }
650
651 pub fn chase_zoom(&self, modify_scale: impl FnOnce(Factor) -> Factor) {
654 self.chase_zoom_impl(modify_scale);
655 }
656 fn chase_zoom_impl(&self, modify_scale: impl FnOnce(Factor) -> Factor) {
657 if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
658 return;
659 }
660
661 let smooth = SMOOTH_SCROLLING_VAR.get();
662 let config = SCROLL_CONFIG.get();
663 let mut zoom = config.zoom.lock();
664
665 let min = SCROLL.actual_min_zoom();
666 let max = super::MAX_ZOOM_VAR.get();
667
668 match &mut *zoom {
669 ZoomState::Chasing(t) => {
670 if smooth.is_disabled() {
671 let next = modify_scale(*t.target()).clamp(min, max);
672 SCROLL_SCALE_VAR.set(next);
673 *zoom = ZoomState::None;
674 } else {
675 let easing = smooth.easing.clone();
676 t.modify(|f| *f = modify_scale(*f).clamp(min, max), smooth.duration, move |t| easing(t));
677 }
678 }
679 _ => {
680 let t = modify_scale(SCROLL_SCALE_VAR.get()).clamp(min, max);
681 if smooth.is_disabled() {
682 SCROLL_SCALE_VAR.set(t);
683 } else {
684 let easing = smooth.easing.clone();
685 let anim = SCROLL_SCALE_VAR.chase(t, smooth.duration, move |t| easing(t));
686 *zoom = ZoomState::Chasing(anim);
687 }
688 }
689 }
690 }
691
692 pub fn zoom(&self, modify_scale: impl FnOnce(Factor) -> Factor, origin: PxPoint) {
695 self.zoom_impl(modify_scale, origin);
696 }
697 fn zoom_impl(&self, modify_scale: impl FnOnce(Factor) -> Factor, center_in_viewport: PxPoint) {
698 if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
699 return;
700 }
701
702 let content = WIDGET.info().scroll_info().unwrap().content();
703 let mut center_in_content = -content.origin + center_in_viewport.to_vector();
704 let mut content_size = content.size;
705
706 let rendered_scale = SCROLL.rendered_zoom_scale();
707
708 SCROLL.chase_zoom(|f| {
709 let s = modify_scale(f);
710 let f = s / rendered_scale;
711 center_in_content *= f;
712 content_size *= f;
713 s
714 });
715
716 let viewport_size = SCROLL_VIEWPORT_SIZE_VAR.get();
717
718 let max_scroll = content_size - viewport_size;
720 let offset = center_in_content - center_in_viewport;
721
722 if offset.y != Px(0) && max_scroll.height > Px(0) {
723 let offset_y = offset.y.0 as f32 / max_scroll.height.0 as f32;
724 SCROLL.chase_vertical(|_| offset_y.fct());
725 }
726 if offset.x != Px(0) && max_scroll.width > Px(0) {
727 let offset_x = offset.x.0 as f32 / max_scroll.width.0 as f32;
728 SCROLL.chase_horizontal(|_| offset_x.fct());
729 }
730 }
731
732 pub fn zoom_touch(&self, phase: TouchPhase, scale: Factor, center_in_viewport: euclid::Point2D<f32, Px>) {
734 if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
735 return;
736 }
737
738 let cfg = SCROLL_CONFIG.get();
739
740 let rendered_scale = SCROLL.rendered_zoom_scale();
741
742 let start_scale;
743 let start_center;
744
745 let mut cfg = cfg.zoom.lock();
746
747 if let TouchPhase::Start = phase {
748 start_scale = rendered_scale;
749 start_center = center_in_viewport;
750
751 *cfg = ZoomState::TouchStart {
752 start_factor: start_scale,
753 start_center: center_in_viewport,
754 applied_offset: euclid::vec2(0.0, 0.0),
755 };
756 } else if let ZoomState::TouchStart {
757 start_factor: scale,
758 start_center: center_in_viewport,
759 ..
760 } = &*cfg
761 {
762 start_scale = *scale;
763 start_center = *center_in_viewport;
764 } else {
765 return;
767 }
768
769 let applied_offset = if let ZoomState::TouchStart { applied_offset, .. } = &mut *cfg {
771 applied_offset
772 } else {
773 unreachable!()
774 };
775
776 let translate_offset = start_center - center_in_viewport;
777 let translate_delta = translate_offset - *applied_offset;
778 *applied_offset = translate_offset;
779
780 let scroll_info = WIDGET.info().scroll_info().unwrap();
781 let content = scroll_info.content();
782 let mut center_in_content = -content.origin.cast::<f32>() + center_in_viewport.to_vector();
783 let mut content_size = content.size.cast::<f32>();
784
785 let scale = start_scale + (scale - 1.0.fct());
786
787 let min = SCROLL.actual_min_zoom();
788 let max = super::MAX_ZOOM_VAR.get();
789 let scale = scale.clamp(min, max);
790
791 let scale_transform = scale / rendered_scale;
792
793 center_in_content *= scale_transform;
794 content_size *= scale_transform;
795
796 let viewport_size = SCROLL_VIEWPORT_SIZE_VAR.get().cast::<f32>();
797
798 let max_scroll = content_size - viewport_size;
800 let zoom_offset = center_in_content - center_in_viewport;
801
802 let offset = zoom_offset + translate_delta;
803
804 SCROLL_SCALE_VAR.set(scale);
805
806 if offset.y != 0.0 && max_scroll.height > 0.0 {
807 let offset_y = offset.y / max_scroll.height;
808 SCROLL_VERTICAL_OFFSET_VAR.set(offset_y.clamp(0.0, 1.0));
809 }
810 if offset.x != 0.0 && max_scroll.width > 0.0 {
811 let offset_x = offset.x / max_scroll.width;
812 SCROLL_HORIZONTAL_OFFSET_VAR.set(offset_x.clamp(0.0, 1.0));
813 }
814 }
815
816 fn can_scroll(&self, predicate: impl Fn(PxSize, PxSize) -> bool + Send + Sync + 'static) -> Var<bool> {
817 expr_var! {
818 predicate(
819 *#{SCROLL_VIEWPORT_SIZE_VAR},
820 *#{SCROLL_CONTENT_ORIGINAL_SIZE_VAR} * *#{SCROLL_SCALE_VAR},
821 )
822 }
823 }
824
825 pub fn can_scroll_vertical(&self) -> Var<bool> {
827 self.can_scroll(|vp, ct| ct.height > vp.height)
828 }
829
830 pub fn can_scroll_horizontal(&self) -> Var<bool> {
832 self.can_scroll(|vp, ct| ct.width > vp.width)
833 }
834
835 fn can_scroll_v(&self, predicate: impl Fn(PxSize, PxSize, Factor) -> bool + Send + Sync + 'static) -> Var<bool> {
836 expr_var! {
837 predicate(
838 *#{SCROLL_VIEWPORT_SIZE_VAR},
839 *#{SCROLL_CONTENT_ORIGINAL_SIZE_VAR} * *#{SCROLL_SCALE_VAR},
840 *#{SCROLL_VERTICAL_OFFSET_VAR},
841 )
842 }
843 }
844
845 pub fn can_scroll_down(&self) -> Var<bool> {
848 self.can_scroll_v(|vp, ct, vo| ct.height > vp.height && 1.fct() > vo)
849 }
850
851 pub fn can_scroll_up(&self) -> Var<bool> {
854 self.can_scroll_v(|vp, ct, vo| ct.height > vp.height && 0.fct() < vo)
855 }
856
857 fn can_scroll_h(&self, predicate: impl Fn(PxSize, PxSize, Factor) -> bool + Send + Sync + 'static) -> Var<bool> {
858 expr_var! {
859 predicate(
860 *#{SCROLL_VIEWPORT_SIZE_VAR},
861 *#{SCROLL_CONTENT_ORIGINAL_SIZE_VAR} * *#{SCROLL_SCALE_VAR},
862 *#{SCROLL_HORIZONTAL_OFFSET_VAR},
863 )
864 }
865 }
866
867 pub fn can_scroll_left(&self) -> Var<bool> {
870 self.can_scroll_h(|vp, ct, ho| ct.width > vp.width && 0.fct() < ho)
871 }
872
873 pub fn can_scroll_right(&self) -> Var<bool> {
876 self.can_scroll_h(|vp, ct, ho| ct.width > vp.width && 1.fct() > ho)
877 }
878
879 pub fn scroll_to(&self, mode: impl Into<super::cmd::ScrollToMode>) {
883 cmd::scroll_to(WIDGET.info(), mode.into())
884 }
885
886 pub fn scroll_to_zoom(&self, mode: impl Into<super::cmd::ScrollToMode>, zoom: impl Into<Factor>) {
890 cmd::scroll_to_zoom(WIDGET.info(), mode.into(), zoom.into())
891 }
892
893 pub fn can_zoom_in(&self) -> bool {
895 SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) && SCROLL_SCALE_VAR.get() < super::MAX_ZOOM_VAR.get()
896 }
897
898 pub fn can_zoom_out(&self) -> bool {
900 SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) && SCROLL_SCALE_VAR.get() > self.actual_min_zoom()
901 }
902
903 pub fn actual_min_zoom(&self) -> Factor {
910 let min = super::MIN_ZOOM_VAR.get();
911 if min >= 0.01.pct().fct() {
912 return min;
913 }
914
915 let content;
916 let viewport;
917
918 if let Some(info) = WIDGET.try_info()
919 && let Some(info) = info.scroll_info()
920 {
921 content = info.content_original_size().cast::<f32>();
923 viewport = (info.viewport().size - info.joiner_size()).cast::<f32>();
924 } else {
925 content = SCROLL_CONTENT_ORIGINAL_SIZE_VAR.get().cast::<f32>();
926 viewport = SCROLL_VIEWPORT_SIZE_VAR.get().cast::<f32>();
927 }
928
929 if content.width < 0.1 || content.height < 0.1 {
930 return min;
931 }
932 (viewport.width / content.width)
933 .min(viewport.height / content.height)
934 .min(1.0)
935 .fct()
936 }
937
938 pub fn ignore_next_scroll_to_focused(&self) {
940 *SCROLL_CONFIG.get().ignore_next_scroll_to_focused.lock() = Some(INSTANT.now());
941 }
942
943 pub(crate) fn take_ignore_next_scroll_to_focused(&self) -> bool {
944 match SCROLL_CONFIG.get().ignore_next_scroll_to_focused.lock().take() {
945 Some(t) => t.elapsed() < Duration::from_millis(50),
946 None => false,
947 }
948 }
949}
950
951impl SCROLL {
952 pub fn context_values_set(&self, set: &mut ContextValueSet) {
956 set.insert(&SCROLL_CONFIG);
957 }
958}
959
960pub trait WidgetInfoExt {
964 fn is_scroll(&self) -> bool;
966
967 fn scroll_info(&self) -> Option<ScrollInfo>;
969
970 fn viewport(&self) -> Option<PxRect>;
974}
975impl WidgetInfoExt for WidgetInfo {
976 fn is_scroll(&self) -> bool {
977 self.meta().get(*SCROLL_INFO_ID).is_some()
978 }
979
980 fn scroll_info(&self) -> Option<ScrollInfo> {
981 self.meta().get(*SCROLL_INFO_ID).cloned()
982 }
983
984 fn viewport(&self) -> Option<PxRect> {
985 self.meta().get(*SCROLL_INFO_ID).map(|r| r.viewport())
986 }
987}
988
989#[derive(Debug)]
990struct ScrollData {
991 viewport_transform: PxTransform,
992 viewport_size: PxSize,
993 joiner_size: PxSize,
994 content_offset: PxPoint,
995 zoom_scale: Factor,
996 content_original_size: PxSize,
997}
998impl Default for ScrollData {
999 fn default() -> Self {
1000 Self {
1001 viewport_transform: Default::default(),
1002 viewport_size: Default::default(),
1003 joiner_size: Default::default(),
1004 content_offset: Default::default(),
1005 zoom_scale: 1.fct(),
1006 content_original_size: Default::default(),
1007 }
1008 }
1009}
1010
1011#[derive(Clone, Default, Debug)]
1013pub struct ScrollInfo(Arc<Mutex<ScrollData>>);
1014impl ScrollInfo {
1015 pub fn viewport(&self) -> PxRect {
1017 self.viewport_transform()
1018 .outer_transformed(PxBox::from_size(self.viewport_size()))
1019 .unwrap_or_default()
1020 .to_rect()
1021 }
1022
1023 pub fn viewport_size(&self) -> PxSize {
1025 self.0.lock().viewport_size
1026 }
1027
1028 pub fn viewport_transform(&self) -> PxTransform {
1030 self.0.lock().viewport_transform
1031 }
1032
1033 pub fn joiner_size(&self) -> PxSize {
1038 self.0.lock().joiner_size
1039 }
1040
1041 pub fn content(&self) -> PxRect {
1045 let m = self.0.lock();
1046 PxRect::new(m.content_offset, m.content_original_size * m.zoom_scale)
1047 }
1048
1049 pub fn content_original_size(&self) -> PxSize {
1053 self.0.lock().content_original_size
1054 }
1055
1056 pub fn zoom_scale(&self) -> Factor {
1058 self.0.lock().zoom_scale
1059 }
1060
1061 pub(super) fn set_viewport_size(&self, size: PxSize) {
1062 self.0.lock().viewport_size = size;
1063 }
1064
1065 pub(super) fn set_viewport_transform(&self, transform: PxTransform) {
1066 self.0.lock().viewport_transform = transform;
1067 }
1068
1069 pub(super) fn set_joiner_size(&self, size: PxSize) {
1070 self.0.lock().joiner_size = size;
1071 }
1072
1073 pub(super) fn set_content(&self, content_offset: PxPoint, scale: Factor, content_original_size: PxSize) {
1074 let mut m = self.0.lock();
1075 m.content_offset = content_offset;
1076 m.zoom_scale = scale;
1077 m.content_original_size = content_original_size;
1078 }
1079}
1080
1081static_id! {
1082 pub(super) static ref SCROLL_INFO_ID: StateId<ScrollInfo>;
1083}
1084
1085#[derive(Clone)]
1091pub struct SmoothScrolling {
1092 pub duration: Duration,
1096 pub easing: Arc<dyn Fn(EasingTime) -> EasingStep + Send + Sync>,
1100}
1101impl fmt::Debug for SmoothScrolling {
1102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1103 f.debug_struct("SmoothScrolling")
1104 .field("duration", &self.duration)
1105 .finish_non_exhaustive()
1106 }
1107}
1108impl PartialEq for SmoothScrolling {
1109 fn eq(&self, other: &Self) -> bool {
1110 self.duration == other.duration && Arc::ptr_eq(&self.easing, &other.easing)
1111 }
1112}
1113impl Default for SmoothScrolling {
1114 fn default() -> Self {
1115 Self::new(150.ms(), easing::linear)
1116 }
1117}
1118impl SmoothScrolling {
1119 pub fn new(duration: Duration, easing: impl Fn(EasingTime) -> EasingStep + Send + Sync + 'static) -> Self {
1121 Self {
1122 duration,
1123 easing: Arc::new(easing),
1124 }
1125 }
1126
1127 pub fn disabled() -> Self {
1129 Self::new(Duration::ZERO, easing::none)
1130 }
1131
1132 pub fn is_disabled(&self) -> bool {
1136 self.duration == Duration::ZERO
1137 }
1138}
1139impl_from_and_into_var! {
1140 fn from(duration: Duration) -> SmoothScrolling {
1142 SmoothScrolling {
1143 duration,
1144 ..Default::default()
1145 }
1146 }
1147
1148 fn from(enabled: bool) -> SmoothScrolling {
1152 if enabled {
1153 SmoothScrolling::default()
1154 } else {
1155 SmoothScrolling::disabled()
1156 }
1157 }
1158
1159 fn from<F: Fn(EasingTime) -> EasingStep + Send + Sync + 'static>((duration, easing): (Duration, F)) -> SmoothScrolling {
1160 SmoothScrolling::new(duration, easing)
1161 }
1162
1163 fn from((duration, easing): (Duration, easing::EasingFn)) -> SmoothScrolling {
1164 SmoothScrolling::new(duration, easing.ease_fn())
1165 }
1166}
1167
1168#[derive(Debug, Default, Clone, PartialEq)]
1177#[non_exhaustive]
1178pub struct AutoScrollArgs {}
1179
1180#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1187pub enum ZoomToFitMode {
1188 #[default]
1190 Contain,
1191 ScaleDown,
1193}