zng_wgt_scroll/
types.rs

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    ReadOnlyContextVar, 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    /// What dimensions are scrollable in a widget.
20    ///
21    /// If a dimension is scrollable the content can be any size in that dimension, if the size
22    /// is more then available scrolling is enabled for that dimension.
23    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
24    #[serde(transparent)]
25    pub struct ScrollMode: u8 {
26        /// Content size is constrained by the viewport and is not scrollable.
27        const NONE = 0;
28        /// Content can be any height and scrolls vertically if overflow height.
29        const VERTICAL = 0b01;
30        /// Content can be any width and scrolls horizontally if overflow width.
31        const HORIZONTAL = 0b10;
32        /// Content can be any size and scrolls if overflow.
33        const PAN = 0b11;
34        /// Content can be any size and scrolls if overflow (`PAN`) and also can be scaled
35        /// up and down by zoom commands and gestures.
36        const ZOOM = 0b111;
37    }
38}
39impl_from_and_into_var! {
40    /// Returns [`ZOOM`] for `true` and [`NONE`] for `false`.
41    ///
42    /// [`ZOOM`]: ScrollMode::ZOOM
43    /// [`NONE`]: ScrollMode::NONE
44    fn from(zoom: bool) -> ScrollMode {
45        if zoom {
46            ScrollMode::ZOOM
47        } else {
48            ScrollMode::NONE
49        }
50    }
51}
52
53context_var! {
54    /// Vertical offset of the parent scroll.
55    ///
56    /// The value is a percentage of `content.height - viewport.height`.
57    pub(super) static SCROLL_VERTICAL_OFFSET_VAR: Factor = 0.fct();
58    /// Horizontal offset of the parent scroll.
59    ///
60    /// The value is a percentage of `content.width - viewport.width`.
61    pub(super) static SCROLL_HORIZONTAL_OFFSET_VAR: Factor = 0.fct();
62
63    /// Extra vertical offset requested that could not be fulfilled because [`SCROLL_VERTICAL_OFFSET_VAR`]
64    /// is already at `0.fct()` or `1.fct()`.
65    pub(super) static OVERSCROLL_VERTICAL_OFFSET_VAR: Factor = 0.fct();
66
67    /// Extra horizontal offset requested that could not be fulfilled because [`SCROLL_HORIZONTAL_OFFSET_VAR`]
68    /// is already at `0.fct()` or `1.fct()`.
69    pub(super) static OVERSCROLL_HORIZONTAL_OFFSET_VAR: Factor = 0.fct();
70
71    /// Ratio of the scroll parent viewport height to its content.
72    ///
73    /// The value is `viewport.height / content.height`.
74    pub(super) static SCROLL_VERTICAL_RATIO_VAR: Factor = 0.fct();
75
76    /// Ratio of the scroll parent viewport width to its content.
77    ///
78    /// The value is `viewport.width / content.width`.
79    pub(super) static SCROLL_HORIZONTAL_RATIO_VAR: Factor = 0.fct();
80
81    /// If the vertical scrollbar should be visible.
82    pub(super) static SCROLL_VERTICAL_CONTENT_OVERFLOWS_VAR: bool = false;
83
84    /// If the horizontal scrollbar should be visible.
85    pub(super) static SCROLL_HORIZONTAL_CONTENT_OVERFLOWS_VAR: bool = false;
86
87    /// Latest computed viewport size of the parent scroll.
88    pub(super) static SCROLL_VIEWPORT_SIZE_VAR: PxSize = PxSize::zero();
89
90    /// Latest computed content size of the parent scroll.
91    ///
92    /// The size is scaled if zoom is set.
93    pub(super) static SCROLL_CONTENT_SIZE_VAR: PxSize = PxSize::zero();
94
95    /// Zoom scaling of the parent scroll.
96    pub(super) static SCROLL_SCALE_VAR: Factor = 1.fct();
97
98    /// Scroll mode.
99    pub(super) static SCROLL_MODE_VAR: ScrollMode = ScrollMode::empty();
100}
101
102context_local! {
103    static SCROLL_CONFIG: ScrollConfig = ScrollConfig::default();
104}
105
106#[derive(Debug, Clone, Copy, bytemuck::NoUninit)]
107#[repr(C)]
108struct RenderedOffsets {
109    h: Factor,
110    v: Factor,
111    z: Factor,
112}
113
114#[derive(Default, Debug)]
115enum ZoomState {
116    #[default]
117    None,
118    Chasing(ChaseAnimation<Factor>),
119    TouchStart {
120        start_factor: Factor,
121        start_center: euclid::Point2D<f32, Px>,
122        applied_offset: euclid::Vector2D<f32, Px>,
123    },
124}
125
126#[derive(Debug)]
127struct ScrollConfig {
128    id: Option<WidgetId>,
129    chase: [Mutex<Option<ChaseAnimation<Factor>>>; 2], // [horizontal, vertical]
130    zoom: Mutex<ZoomState>,
131
132    // last rendered horizontal, vertical offsets.
133    rendered: Atomic<RenderedOffsets>,
134
135    overscroll: [Mutex<AnimationHandle>; 2],
136    inertia: [Mutex<AnimationHandle>; 2],
137    auto: [Mutex<AnimationHandle>; 2],
138}
139impl Default for ScrollConfig {
140    fn default() -> Self {
141        Self {
142            id: Default::default(),
143            chase: Default::default(),
144            zoom: Default::default(),
145            rendered: Atomic::new(RenderedOffsets {
146                h: 0.fct(),
147                v: 0.fct(),
148                z: 0.fct(),
149            }),
150            overscroll: Default::default(),
151            inertia: Default::default(),
152            auto: Default::default(),
153        }
154    }
155}
156
157/// Defines a scroll delta and to what value source it is applied.
158///
159/// Scrolling can get out of sync depending on what moment and source the current scroll is read,
160/// the offset vars can be multiple frames ahead as update cycles have higher priority than render,
161/// some scrolling operations also target the value the smooth scrolling animation is animating too,
162/// this enum lets you specify from what scroll offset a delta must be computed.
163#[derive(Debug, Clone, Copy, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
164pub enum ScrollFrom {
165    /// Scroll amount added to the offset var current value, if smooth scrolling is enabled this
166    /// can be a partial value different from `VarTarget`.
167    ///
168    /// Operations that compute a scroll delta from the offset var must use this variant otherwise they
169    /// will overshoot.
170    Var(Px),
171    /// Scroll amount added to the value the offset var is animating too.
172    ///
173    /// Operations that accumulate a delta (line-up/down) must use this variant otherwise they will
174    /// undershoot.
175    ///
176    /// This is the same as `Var` if smooth scrolling is disabled.
177    VarTarget(Px),
178
179    /// Scroll amount added to the offset already rendered, this can be different from the offset var as multiple
180    /// events and updates can happen before a pending render is applied.
181    ///
182    /// Operations that compute a scroll offset from widget bounds info must use this variant otherwise they
183    /// will overshoot.
184    Rendered(Px),
185}
186
187/// Controls the parent scroll.
188pub struct SCROLL;
189impl SCROLL {
190    /// Gets the ID of the scroll ancestor represented by the [`SCROLL`].
191    pub fn try_id(&self) -> Option<WidgetId> {
192        SCROLL_CONFIG.get().id
193    }
194    /// Gets the ID of the scroll ancestor represented by the [`SCROLL`].
195    ///
196    /// # Panics
197    ///
198    /// Panics if not inside a scroll.
199    pub fn id(&self) -> WidgetId {
200        self.try_id().expect("not inside scroll")
201    }
202
203    /// New node that holds data for the [`SCROLL`] context.
204    ///
205    /// Scroll implementers must add this node to their context.
206    pub fn config_node(&self, child: impl UiNode) -> impl UiNode {
207        let child = match_node(child, move |_, op| {
208            if let UiNodeOp::Render { .. } | UiNodeOp::RenderUpdate { .. } = op {
209                let h = SCROLL_HORIZONTAL_OFFSET_VAR.get();
210                let v = SCROLL_VERTICAL_OFFSET_VAR.get();
211                let z = SCROLL_SCALE_VAR.get();
212                SCROLL_CONFIG.get().rendered.store(RenderedOffsets { h, v, z }, Ordering::Relaxed);
213            }
214        });
215        with_context_local_init(child, &SCROLL_CONFIG, || ScrollConfig {
216            id: WIDGET.try_id(),
217            ..Default::default()
218        })
219    }
220
221    /// Scroll mode of the parent scroll.
222    pub fn mode(&self) -> ReadOnlyContextVar<ScrollMode> {
223        SCROLL_MODE_VAR.read_only()
224    }
225
226    /// Vertical offset of the parent scroll.
227    ///
228    /// The value is a percentage of `content.height - viewport.height`.
229    ///
230    /// This variable is usually read-write, but you should avoid modifying it directly as
231    /// direct assign as the value is not validated and does not participate in smooths scrolling.
232    /// Prefer the scroll methods of this service to scroll.
233    pub fn vertical_offset(&self) -> ContextVar<Factor> {
234        SCROLL_VERTICAL_OFFSET_VAR
235    }
236
237    /// Horizontal offset of the parent scroll.
238    ///
239    /// The value is a percentage of `content.width - viewport.width`.
240    ///
241    /// This variable is usually read-write, but you should avoid modifying it directly as
242    /// direct assign as the value is not validated and does not participate in smooths scrolling.
243    /// Prefer the scroll methods of this service to scroll.
244    pub fn horizontal_offset(&self) -> ContextVar<Factor> {
245        SCROLL_HORIZONTAL_OFFSET_VAR
246    }
247
248    /// Zoom scale factor of the parent scroll.
249    ///
250    /// This variable is usually read-write, but you should avoid modifying it directly as
251    /// direct assign as the value is not validated and does not participate in smooths scrolling.
252    /// Prefer the zoom methods of this service to change scale.
253    pub fn zoom_scale(&self) -> ContextVar<Factor> {
254        SCROLL_SCALE_VAR
255    }
256
257    /// Latest rendered offset.
258    pub fn rendered_offset(&self) -> Factor2d {
259        let cfg = SCROLL_CONFIG.get().rendered.load(Ordering::Relaxed);
260        Factor2d::new(cfg.h, cfg.v)
261    }
262
263    /// Latest rendered zoom scale factor.
264    pub fn rendered_zoom_scale(&self) -> Factor {
265        SCROLL_CONFIG.get().rendered.load(Ordering::Relaxed).z
266    }
267
268    /// Extra vertical offset, requested by touch gesture, that could not be fulfilled because [`vertical_offset`]
269    /// is already at `0.fct()` or `1.fct()`.
270    ///
271    /// The factor is between in the `-1.0..=1.0` range and represents the overscroll offset in pixels divided
272    /// by the viewport width.
273    ///
274    /// [`vertical_offset`]: Self::vertical_offset
275    pub fn vertical_overscroll(&self) -> ReadOnlyContextVar<Factor> {
276        OVERSCROLL_VERTICAL_OFFSET_VAR.read_only()
277    }
278
279    /// Extra horizontal offset requested that could not be fulfilled because [`horizontal_offset`]
280    /// is already at `0.fct()` or `1.fct()`.
281    ///
282    /// The factor is between in the `-1.0..=1.0` range and represents the overscroll offset in pixels divided
283    /// by the viewport width.
284    ///
285    /// [`horizontal_offset`]: Self::horizontal_offset
286    pub fn horizontal_overscroll(&self) -> ReadOnlyContextVar<Factor> {
287        OVERSCROLL_HORIZONTAL_OFFSET_VAR.read_only()
288    }
289
290    /// Ratio of the scroll parent viewport height to its content.
291    ///
292    /// The value is `viewport.height / content.height`.
293    pub fn vertical_ratio(&self) -> ReadOnlyContextVar<Factor> {
294        SCROLL_VERTICAL_RATIO_VAR.read_only()
295    }
296    /// Ratio of the scroll parent viewport width to its content.
297    ///
298    /// The value is `viewport.width / content.width`.
299    pub fn horizontal_ratio(&self) -> ReadOnlyContextVar<Factor> {
300        SCROLL_HORIZONTAL_RATIO_VAR.read_only()
301    }
302
303    /// If the vertical scrollbar should be visible.
304    pub fn vertical_content_overflows(&self) -> ReadOnlyContextVar<bool> {
305        SCROLL_VERTICAL_CONTENT_OVERFLOWS_VAR.read_only()
306    }
307
308    /// If the horizontal scrollbar should be visible.
309    pub fn horizontal_content_overflows(&self) -> ReadOnlyContextVar<bool> {
310        SCROLL_HORIZONTAL_CONTENT_OVERFLOWS_VAR.read_only()
311    }
312
313    /// Latest computed viewport size of the parent scroll.
314    pub fn viewport_size(&self) -> ReadOnlyContextVar<PxSize> {
315        SCROLL_VIEWPORT_SIZE_VAR.read_only()
316    }
317
318    /// Latest computed content size of the parent scroll.
319    pub fn content_size(&self) -> ReadOnlyContextVar<PxSize> {
320        SCROLL_CONTENT_SIZE_VAR.read_only()
321    }
322
323    /// Applies the `delta` to the vertical offset.
324    ///
325    /// If smooth scrolling is enabled it is used to update the offset.
326    pub fn scroll_vertical(&self, delta: ScrollFrom) {
327        self.scroll_vertical_clamp(delta, f32::MIN, f32::MAX);
328    }
329
330    /// Applies the `delta` to the horizontal offset.
331    ///
332    /// If smooth scrolling is enabled the chase animation is created or updated by this call.
333    pub fn scroll_horizontal(&self, delta: ScrollFrom) {
334        self.scroll_horizontal_clamp(delta, f32::MIN, f32::MAX)
335    }
336
337    /// Applies the `delta` to the vertical offset, but clamps the final offset by the inclusive `min` and `max`.
338    ///
339    /// If smooth scrolling is enabled it is used to update the offset.
340    pub fn scroll_vertical_clamp(&self, delta: ScrollFrom, min: f32, max: f32) {
341        self.scroll_clamp(true, SCROLL_VERTICAL_OFFSET_VAR, delta, min, max)
342    }
343
344    /// Applies the `delta` to the horizontal offset, but clamps the final offset by the inclusive `min` and `max`.
345    ///
346    /// If smooth scrolling is enabled it is used to update the offset.
347    pub fn scroll_horizontal_clamp(&self, delta: ScrollFrom, min: f32, max: f32) {
348        self.scroll_clamp(false, SCROLL_HORIZONTAL_OFFSET_VAR, delta, min, max)
349    }
350    fn scroll_clamp(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, delta: ScrollFrom, min: f32, max: f32) {
351        let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
352        let content = SCROLL_CONTENT_SIZE_VAR.get().to_array()[vertical as usize];
353
354        let max_scroll = content - viewport;
355
356        if max_scroll <= 0 {
357            return;
358        }
359
360        match delta {
361            ScrollFrom::Var(a) => {
362                let amount = a.0 as f32 / max_scroll.0 as f32;
363                let f = scroll_offset_var.get();
364                SCROLL.chase(vertical, scroll_offset_var, |_| (f.0 + amount).clamp(min, max).fct());
365            }
366            ScrollFrom::VarTarget(a) => {
367                let amount = a.0 as f32 / max_scroll.0 as f32;
368                SCROLL.chase(vertical, scroll_offset_var, |f| (f.0 + amount).clamp(min, max).fct());
369            }
370            ScrollFrom::Rendered(a) => {
371                let amount = a.0 as f32 / max_scroll.0 as f32;
372                let f = SCROLL_CONFIG.get().rendered.load(Ordering::Relaxed).h;
373                SCROLL.chase(vertical, scroll_offset_var, |_| (f.0 + amount).clamp(min, max).fct());
374            }
375        }
376    }
377
378    /// Animate scroll at the direction and velocity (in DIPs per second).
379    pub fn auto_scroll(&self, velocity: DipVector) {
380        let viewport = SCROLL_VIEWPORT_SIZE_VAR.get();
381        let content = SCROLL_CONTENT_SIZE_VAR.get();
382        let max_scroll = content - viewport;
383
384        let velocity = velocity.to_px(WINDOW.info().scale_factor());
385
386        fn scroll(dimension: usize, velocity: Px, max_scroll: Px, offset_var: &ContextVar<Factor>) {
387            if velocity == 0 {
388                SCROLL_CONFIG.get().auto[dimension].lock().clone().stop();
389            } else {
390                let mut travel = max_scroll * offset_var.get();
391                let mut target = 0.0;
392                if velocity > Px(0) {
393                    travel = max_scroll - travel;
394                    target = 1.0;
395                }
396                let time = (travel.0 as f32 / velocity.0.abs() as f32).secs();
397
398                VARS.with_animation_controller(zng_var::animation::ForceAnimationController, || {
399                    let handle = offset_var.ease(target, time, easing::linear);
400                    mem::replace(&mut *SCROLL_CONFIG.get().auto[dimension].lock(), handle).stop();
401                });
402            }
403        }
404        scroll(0, velocity.x, max_scroll.width, &SCROLL_HORIZONTAL_OFFSET_VAR);
405        scroll(1, velocity.y, max_scroll.height, &SCROLL_VERTICAL_OFFSET_VAR);
406    }
407
408    /// Applies the `delta` to the vertical offset without smooth scrolling and
409    /// updates the vertical overscroll if it changes.
410    ///
411    /// This method is used to implement touch gesture scrolling, the delta is always [`ScrollFrom::Var`].
412    pub fn scroll_vertical_touch(&self, delta: Px) {
413        self.scroll_touch(true, SCROLL_VERTICAL_OFFSET_VAR, OVERSCROLL_VERTICAL_OFFSET_VAR, delta)
414    }
415
416    /// Applies the `delta` to the horizontal offset without smooth scrolling and
417    /// updates the horizontal overscroll if it changes.
418    ///
419    /// This method is used to implement touch gesture scrolling, the delta is always [`ScrollFrom::Var`].
420    pub fn scroll_horizontal_touch(&self, delta: Px) {
421        self.scroll_touch(false, SCROLL_HORIZONTAL_OFFSET_VAR, OVERSCROLL_HORIZONTAL_OFFSET_VAR, delta)
422    }
423
424    fn scroll_touch(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, overscroll_offset_var: ContextVar<Factor>, delta: Px) {
425        let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
426        let content = SCROLL_CONTENT_SIZE_VAR.get().to_array()[vertical as usize];
427
428        let max_scroll = content - viewport;
429        if max_scroll <= 0 {
430            return;
431        }
432
433        let delta = delta.0 as f32 / max_scroll.0 as f32;
434
435        let current = scroll_offset_var.get();
436        let mut next = current + delta.fct();
437        let mut overscroll = 0.fct();
438        if next > 1.fct() {
439            overscroll = next - 1.fct();
440            next = 1.fct();
441
442            let overscroll_px = overscroll * content.0.fct();
443            let overscroll_max = viewport.0.fct();
444            overscroll = overscroll_px.min(overscroll_max) / overscroll_max;
445        } else if next < 0.fct() {
446            overscroll = next;
447            next = 0.fct();
448
449            let overscroll_px = -overscroll * content.0.fct();
450            let overscroll_max = viewport.0.fct();
451            overscroll = -(overscroll_px.min(overscroll_max) / overscroll_max);
452        }
453
454        let _ = scroll_offset_var.set(next);
455        if overscroll != 0.fct() {
456            let new_handle = self.increment_overscroll(overscroll_offset_var, overscroll);
457
458            let config = SCROLL_CONFIG.get();
459            let mut handle = config.overscroll[vertical as usize].lock();
460            mem::replace(&mut *handle, new_handle).stop();
461        } else {
462            self.clear_horizontal_overscroll();
463        }
464    }
465
466    fn increment_overscroll(&self, overscroll: ContextVar<Factor>, delta: Factor) -> AnimationHandle {
467        enum State {
468            Increment,
469            ClearDelay,
470            Clear(Transition<Factor>),
471        }
472        let mut state = State::Increment;
473        overscroll.animate(move |a, o| match &mut state {
474            State::Increment => {
475                // set the increment and start delay to animation.
476                *o.to_mut() += delta;
477                *o.to_mut() = (*o).clamp((-1).fct(), 1.fct());
478
479                a.sleep(300.ms());
480                state = State::ClearDelay;
481            }
482            State::ClearDelay => {
483                a.restart();
484                let t = Transition::new(**o, 0.fct());
485                state = State::Clear(t);
486            }
487            State::Clear(t) => {
488                let step = easing::linear(a.elapsed_stop(300.ms()));
489                o.set(t.sample(step));
490            }
491        })
492    }
493
494    /// Quick ease vertical overscroll to zero.
495    pub fn clear_vertical_overscroll(&self) {
496        self.clear_overscroll(true, OVERSCROLL_VERTICAL_OFFSET_VAR)
497    }
498
499    /// Quick ease horizontal overscroll to zero.
500    pub fn clear_horizontal_overscroll(&self) {
501        self.clear_overscroll(false, OVERSCROLL_HORIZONTAL_OFFSET_VAR)
502    }
503
504    fn clear_overscroll(&self, vertical: bool, overscroll_offset_var: ContextVar<Factor>) {
505        if overscroll_offset_var.get() != 0.fct() {
506            let new_handle = overscroll_offset_var.ease(0.fct(), 100.ms(), easing::linear);
507
508            let config = SCROLL_CONFIG.get();
509            let mut handle = config.overscroll[vertical as usize].lock();
510            mem::replace(&mut *handle, new_handle).stop();
511        }
512    }
513
514    /// Animates to `delta` over `duration`.
515    pub fn scroll_vertical_touch_inertia(&self, delta: Px, duration: Duration) {
516        self.scroll_touch_inertia(true, SCROLL_VERTICAL_OFFSET_VAR, OVERSCROLL_VERTICAL_OFFSET_VAR, delta, duration)
517    }
518
519    /// Animates to `delta` over `duration`.
520    pub fn scroll_horizontal_touch_inertia(&self, delta: Px, duration: Duration) {
521        self.scroll_touch_inertia(
522            false,
523            SCROLL_HORIZONTAL_OFFSET_VAR,
524            OVERSCROLL_HORIZONTAL_OFFSET_VAR,
525            delta,
526            duration,
527        )
528    }
529
530    fn scroll_touch_inertia(
531        &self,
532        vertical: bool,
533        scroll_offset_var: ContextVar<Factor>,
534        overscroll_offset_var: ContextVar<Factor>,
535        delta: Px,
536        duration: Duration,
537    ) {
538        let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
539        let content = SCROLL_CONTENT_SIZE_VAR.get().to_array()[vertical as usize];
540
541        let max_scroll = content - viewport;
542        if max_scroll <= 0 {
543            return;
544        }
545
546        let delta = delta.0 as f32 / max_scroll.0 as f32;
547
548        let current = scroll_offset_var.get();
549        let mut next = current + delta.fct();
550        let mut overscroll = 0.fct();
551        if next > 1.fct() {
552            overscroll = next - 1.fct();
553            next = 1.fct();
554
555            let overscroll_px = overscroll * content.0.fct();
556            let overscroll_max = viewport.0.fct();
557            overscroll = overscroll_px.min(overscroll_max) / overscroll_max;
558        } else if next < 0.fct() {
559            overscroll = next;
560            next = 0.fct();
561
562            let overscroll_px = -overscroll * content.0.fct();
563            let overscroll_max = viewport.0.fct();
564            overscroll = -(overscroll_px.min(overscroll_max) / overscroll_max);
565        }
566
567        let cfg = SCROLL_CONFIG.get();
568        let easing = |t| easing::ease_out(easing::quad, t);
569        *cfg.inertia[vertical as usize].lock() = if overscroll != 0.fct() {
570            let transition = Transition::new(current, next + overscroll);
571
572            let overscroll_var = overscroll_offset_var.actual_var();
573            let overscroll_tr = Transition::new(overscroll, 0.fct());
574            let mut is_inertia_anim = true;
575
576            scroll_offset_var.animate(move |animation, value| {
577                if is_inertia_anim {
578                    // inertia ease animation
579                    let step = easing(animation.elapsed(duration));
580                    let v = transition.sample(step);
581
582                    if v < 0.fct() || v > 1.fct() {
583                        // follows the easing curve until cap, cuts out to overscroll indicator.
584                        value.set(v.clamp_range());
585                        animation.restart();
586                        is_inertia_anim = false;
587                        let _ = overscroll_var.set(overscroll_tr.from);
588                    } else {
589                        value.set(v);
590                    }
591                } else {
592                    // overscroll clear ease animation
593                    let step = easing::linear(animation.elapsed_stop(300.ms()));
594                    let v = overscroll_tr.sample(step);
595                    let _ = overscroll_var.set(v);
596                }
597            })
598        } else {
599            scroll_offset_var.ease(next, duration, easing)
600        };
601    }
602
603    /// Set the vertical offset to a new offset derived from the last, blending into the active smooth
604    /// scrolling chase animation, or starting a new one, or just setting the var if smooth scrolling is disabled.
605    pub fn chase_vertical(&self, modify_offset: impl FnOnce(Factor) -> Factor) {
606        #[cfg(feature = "dyn_closure")]
607        let modify_offset: Box<dyn FnOnce(Factor) -> Factor> = Box::new(modify_offset);
608        self.chase(true, SCROLL_VERTICAL_OFFSET_VAR, modify_offset);
609    }
610
611    /// Set the horizontal offset to a new offset derived from the last set offset, blending into the active smooth
612    /// scrolling chase animation, or starting a new one, or just setting the var if smooth scrolling is disabled.
613    pub fn chase_horizontal(&self, modify_offset: impl FnOnce(Factor) -> Factor) {
614        #[cfg(feature = "dyn_closure")]
615        let modify_offset: Box<dyn FnOnce(Factor) -> Factor> = Box::new(modify_offset);
616        self.chase(false, SCROLL_HORIZONTAL_OFFSET_VAR, modify_offset);
617    }
618
619    fn chase(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, modify_offset: impl FnOnce(Factor) -> Factor) {
620        let smooth = SMOOTH_SCROLLING_VAR.get();
621        let config = SCROLL_CONFIG.get();
622        let mut chase = config.chase[vertical as usize].lock();
623        match &mut *chase {
624            Some(t) => {
625                if smooth.is_disabled() {
626                    let t = modify_offset(*t.target()).clamp_range();
627                    let _ = scroll_offset_var.set(t);
628                    *chase = None;
629                } else {
630                    let easing = smooth.easing.clone();
631                    t.modify(|f| *f = modify_offset(*f).clamp_range(), smooth.duration, move |t| easing(t));
632                }
633            }
634            None => {
635                let t = modify_offset(scroll_offset_var.get()).clamp_range();
636                if smooth.is_disabled() {
637                    let _ = scroll_offset_var.set(t);
638                } else {
639                    let easing = smooth.easing.clone();
640                    let anim = scroll_offset_var.chase(t, smooth.duration, move |t| easing(t));
641                    *chase = Some(anim);
642                }
643            }
644        }
645    }
646
647    /// Set the zoom scale to a new scale derived from the last set scale, blending into the active
648    /// smooth scaling chase animation, or starting a new or, or just setting the var if smooth scrolling is disabled.
649    pub fn chase_zoom(&self, modify_scale: impl FnOnce(Factor) -> Factor) {
650        #[cfg(feature = "dyn_closure")]
651        let modify_scale: Box<dyn FnOnce(Factor) -> Factor> = Box::new(modify_scale);
652        self.chase_zoom_impl(modify_scale);
653    }
654    fn chase_zoom_impl(&self, modify_scale: impl FnOnce(Factor) -> Factor) {
655        if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
656            return;
657        }
658
659        let smooth = SMOOTH_SCROLLING_VAR.get();
660        let config = SCROLL_CONFIG.get();
661        let mut zoom = config.zoom.lock();
662
663        let min = super::MIN_ZOOM_VAR.get();
664        let max = super::MAX_ZOOM_VAR.get();
665
666        match &mut *zoom {
667            ZoomState::Chasing(t) => {
668                if smooth.is_disabled() {
669                    let next = modify_scale(*t.target()).clamp(min, max);
670                    let _ = SCROLL_SCALE_VAR.set(next);
671                    *zoom = ZoomState::None;
672                } else {
673                    let easing = smooth.easing.clone();
674                    t.modify(|f| *f = modify_scale(*f).clamp(min, max), smooth.duration, move |t| easing(t));
675                }
676            }
677            _ => {
678                let t = modify_scale(SCROLL_SCALE_VAR.get()).clamp(min, max);
679                if smooth.is_disabled() {
680                    let _ = SCROLL_SCALE_VAR.set(t);
681                } else {
682                    let easing = smooth.easing.clone();
683                    let anim = SCROLL_SCALE_VAR.chase(t, smooth.duration, move |t| easing(t));
684                    *zoom = ZoomState::Chasing(anim);
685                }
686            }
687        }
688    }
689
690    /// Zoom in or out keeping the `origin` point in the viewport aligned with the same point
691    /// in the content.
692    pub fn zoom(&self, modify_scale: impl FnOnce(Factor) -> Factor, origin: PxPoint) {
693        #[cfg(feature = "dyn_closure")]
694        let modify_scale: Box<dyn FnOnce(Factor) -> Factor> = Box::new(modify_scale);
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        // scroll so that new center_in_content is at the same center_in_viewport
719        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    /// Applies the `scale` to the current zoom scale without smooth scrolling and centered on the touch point.
733    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            // touch canceled or not started correctly.
766            return;
767        }
768
769        // applied translate offset
770        let applied_offset = if let ZoomState::TouchStart { applied_offset, .. } = &mut *cfg {
771            applied_offset
772        } else {
773            unreachable!()
774        };
775
776        let scale = start_scale + (scale - 1.0.fct());
777
778        let min = super::MIN_ZOOM_VAR.get();
779        let max = super::MAX_ZOOM_VAR.get();
780        let scale = scale.clamp(min, max);
781
782        let translate_offset = start_center - center_in_viewport;
783        let translate_delta = translate_offset - *applied_offset;
784        *applied_offset = translate_offset;
785
786        let content = WIDGET.info().scroll_info().unwrap().content();
787        let mut center_in_content = -content.origin.cast::<f32>() + center_in_viewport.to_vector();
788        let mut content_size = content.size.cast::<f32>();
789
790        let scale_transform = scale / rendered_scale;
791
792        center_in_content *= scale_transform;
793        content_size *= scale_transform;
794
795        let viewport_size = SCROLL_VIEWPORT_SIZE_VAR.get().cast::<f32>();
796
797        // scroll so that new center_in_content is at the same center_in_viewport
798        let max_scroll = content_size - viewport_size;
799        let zoom_offset = center_in_content - center_in_viewport;
800
801        let offset = zoom_offset + translate_delta;
802
803        let _ = SCROLL_SCALE_VAR.set(scale);
804
805        if offset.y != 0.0 && max_scroll.height > 0.0 {
806            let offset_y = offset.y / max_scroll.height;
807            let _ = SCROLL_VERTICAL_OFFSET_VAR.set(offset_y.clamp(0.0, 1.0));
808        }
809        if offset.x != 0.0 && max_scroll.width > 0.0 {
810            let offset_x = offset.x / max_scroll.width;
811            let _ = SCROLL_HORIZONTAL_OFFSET_VAR.set(offset_x.clamp(0.0, 1.0));
812        }
813    }
814
815    fn can_scroll(&self, predicate: impl Fn(PxSize, PxSize) -> bool + Send + Sync + 'static) -> impl Var<bool> {
816        merge_var!(SCROLL_VIEWPORT_SIZE_VAR, SCROLL_CONTENT_SIZE_VAR, move |&vp, &ct| predicate(vp, ct))
817    }
818
819    /// Gets a var that is `true` when the content height is greater then the viewport height.
820    pub fn can_scroll_vertical(&self) -> impl Var<bool> {
821        self.can_scroll(|vp, ct| ct.height > vp.height)
822    }
823
824    /// Gets a var that is `true` when the content width is greater then the viewport with.
825    pub fn can_scroll_horizontal(&self) -> impl Var<bool> {
826        self.can_scroll(|vp, ct| ct.width > vp.width)
827    }
828
829    fn can_scroll_v(&self, predicate: impl Fn(PxSize, PxSize, Factor) -> bool + Send + Sync + 'static) -> impl Var<bool> {
830        merge_var!(
831            SCROLL_VIEWPORT_SIZE_VAR,
832            SCROLL_CONTENT_SIZE_VAR,
833            SCROLL_VERTICAL_OFFSET_VAR,
834            move |&vp, &ct, &vo| predicate(vp, ct, vo)
835        )
836    }
837
838    /// Gets a var that is `true` when the content height is greater then the viewport height and the vertical offset
839    /// is not at the maximum.
840    pub fn can_scroll_down(&self) -> impl Var<bool> {
841        self.can_scroll_v(|vp, ct, vo| ct.height > vp.height && 1.fct() > vo)
842    }
843
844    /// Gets a var that is `true` when the content height is greater then the viewport height and the vertical offset
845    /// is not at the minimum.
846    pub fn can_scroll_up(&self) -> impl Var<bool> {
847        self.can_scroll_v(|vp, ct, vo| ct.height > vp.height && 0.fct() < vo)
848    }
849
850    fn can_scroll_h(&self, predicate: impl Fn(PxSize, PxSize, Factor) -> bool + Send + Sync + 'static) -> impl Var<bool> {
851        merge_var!(
852            SCROLL_VIEWPORT_SIZE_VAR,
853            SCROLL_CONTENT_SIZE_VAR,
854            SCROLL_HORIZONTAL_OFFSET_VAR,
855            move |&vp, &ct, &ho| predicate(vp, ct, ho)
856        )
857    }
858
859    /// Gets a var that is `true` when the content width is greater then the viewport width and the horizontal offset
860    /// is not at the minimum.
861    pub fn can_scroll_left(&self) -> impl Var<bool> {
862        self.can_scroll_h(|vp, ct, ho| ct.width > vp.width && 0.fct() < ho)
863    }
864
865    /// Gets a var that is `true` when the content width is greater then the viewport width and the horizontal offset
866    /// is not at the maximum.
867    pub fn can_scroll_right(&self) -> impl Var<bool> {
868        self.can_scroll_h(|vp, ct, ho| ct.width > vp.width && 1.fct() > ho)
869    }
870
871    /// Scroll the [`WIDGET`] into view.
872    ///
873    /// [`WIDGET`]: zng_wgt::prelude::WIDGET
874    pub fn scroll_to(&self, mode: impl Into<super::cmd::ScrollToMode>) {
875        cmd::scroll_to(WIDGET.info(), mode.into())
876    }
877
878    /// Scroll the [`WIDGET`] into view and adjusts the zoom scale.
879    ///
880    /// [`WIDGET`]: zng_wgt::prelude::WIDGET
881    pub fn scroll_to_zoom(&self, mode: impl Into<super::cmd::ScrollToMode>, zoom: impl Into<Factor>) {
882        cmd::scroll_to_zoom(WIDGET.info(), mode.into(), zoom.into())
883    }
884
885    /// Returns `true` if the content can be scaled and the current scale is less than the max.
886    pub fn can_zoom_in(&self) -> bool {
887        SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) && SCROLL_SCALE_VAR.get() < super::MAX_ZOOM_VAR.get()
888    }
889
890    /// Returns `true` if the content can be scaled and the current scale is more than the min.
891    pub fn can_zoom_out(&self) -> bool {
892        SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) && SCROLL_SCALE_VAR.get() > super::MIN_ZOOM_VAR.get()
893    }
894}
895
896impl SCROLL {
897    /// Insert the context values used by `SCROLL` in the `set`.
898    ///
899    /// Capturing this set plus all context vars enables using all `SCROLL` methods outside the scroll.
900    pub fn context_values_set(&self, set: &mut ContextValueSet) {
901        set.insert(&SCROLL_CONFIG);
902    }
903}
904
905/// Scroll extensions for [`WidgetInfo`].
906///
907/// [`WidgetInfo`]: zng_wgt::prelude::WidgetInfo
908pub trait WidgetInfoExt {
909    /// Returns `true` if the widget is a [`Scroll!`](struct@super::Scroll).
910    fn is_scroll(&self) -> bool;
911
912    /// Returns a reference to the viewport bounds if the widget is a [`Scroll!`](struct@super::Scroll).
913    fn scroll_info(&self) -> Option<ScrollInfo>;
914
915    /// Gets the viewport bounds relative to the scroll widget inner bounds.
916    ///
917    /// The value is updated every layout and render, without requiring an info rebuild.
918    fn viewport(&self) -> Option<PxRect>;
919}
920impl WidgetInfoExt for WidgetInfo {
921    fn is_scroll(&self) -> bool {
922        self.meta().get(*SCROLL_INFO_ID).is_some()
923    }
924
925    fn scroll_info(&self) -> Option<ScrollInfo> {
926        self.meta().get(*SCROLL_INFO_ID).cloned()
927    }
928
929    fn viewport(&self) -> Option<PxRect> {
930        self.meta().get(*SCROLL_INFO_ID).map(|r| r.viewport())
931    }
932}
933
934#[derive(Debug)]
935struct ScrollData {
936    viewport_transform: PxTransform,
937    viewport_size: PxSize,
938    joiner_size: PxSize,
939    content: PxRect,
940    zoom_scale: Factor,
941}
942impl Default for ScrollData {
943    fn default() -> Self {
944        Self {
945            viewport_transform: Default::default(),
946            viewport_size: Default::default(),
947            joiner_size: Default::default(),
948            content: Default::default(),
949            zoom_scale: 1.fct(),
950        }
951    }
952}
953
954/// Shared reference to the viewport bounds of a scroll.
955#[derive(Clone, Default, Debug)]
956pub struct ScrollInfo(Arc<Mutex<ScrollData>>);
957impl ScrollInfo {
958    /// Gets the viewport bounds in the window space.
959    pub fn viewport(&self) -> PxRect {
960        self.viewport_transform()
961            .outer_transformed(PxBox::from_size(self.viewport_size()))
962            .unwrap_or_default()
963            .to_rect()
964    }
965
966    /// Gets the layout size of the viewport.
967    pub fn viewport_size(&self) -> PxSize {
968        self.0.lock().viewport_size
969    }
970
971    /// Gets the render transform of the viewport.
972    pub fn viewport_transform(&self) -> PxTransform {
973        self.0.lock().viewport_transform
974    }
975
976    /// Gets the layout size of both scroll-bars.
977    ///
978    /// Joiner here is the corner joiner visual, it is sized by the width of the vertical bar and
979    /// height of the horizontal bar.
980    pub fn joiner_size(&self) -> PxSize {
981        self.0.lock().joiner_size
982    }
983
984    /// Latest content offset and size.
985    ///
986    /// This is the content bounds, scaled and in the viewport space.
987    pub fn content(&self) -> PxRect {
988        self.0.lock().content
989    }
990
991    /// Latest zoom scale.
992    pub fn zoom_scale(&self) -> Factor {
993        self.0.lock().zoom_scale
994    }
995
996    pub(super) fn set_viewport_size(&self, size: PxSize) {
997        self.0.lock().viewport_size = size;
998    }
999
1000    pub(super) fn set_viewport_transform(&self, transform: PxTransform) {
1001        self.0.lock().viewport_transform = transform;
1002    }
1003
1004    pub(super) fn set_joiner_size(&self, size: PxSize) {
1005        self.0.lock().joiner_size = size;
1006    }
1007
1008    pub(super) fn set_content(&self, content: PxRect, scale: Factor) {
1009        let mut m = self.0.lock();
1010        m.content = content;
1011        m.zoom_scale = scale;
1012    }
1013}
1014
1015static_id! {
1016    pub(super) static ref SCROLL_INFO_ID: StateId<ScrollInfo>;
1017}
1018
1019/// Smooth scrolling config.
1020///
1021/// This config can be set by the [`smooth_scrolling`] property.
1022///
1023/// [`smooth_scrolling`]: fn@crate::smooth_scrolling
1024#[derive(Clone)]
1025pub struct SmoothScrolling {
1026    /// Chase transition duration.
1027    ///
1028    /// Default is `150.ms()`.
1029    pub duration: Duration,
1030    /// Chase transition easing function.
1031    ///
1032    /// Default is linear.
1033    pub easing: Arc<dyn Fn(EasingTime) -> EasingStep + Send + Sync>,
1034}
1035impl fmt::Debug for SmoothScrolling {
1036    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1037        f.debug_struct("SmoothScrolling")
1038            .field("duration", &self.duration)
1039            .finish_non_exhaustive()
1040    }
1041}
1042impl PartialEq for SmoothScrolling {
1043    fn eq(&self, other: &Self) -> bool {
1044        self.duration == other.duration && Arc::ptr_eq(&self.easing, &other.easing)
1045    }
1046}
1047impl Default for SmoothScrolling {
1048    fn default() -> Self {
1049        Self::new(150.ms(), easing::linear)
1050    }
1051}
1052impl SmoothScrolling {
1053    /// New custom smooth scrolling config.
1054    pub fn new(duration: Duration, easing: impl Fn(EasingTime) -> EasingStep + Send + Sync + 'static) -> Self {
1055        Self {
1056            duration,
1057            easing: Arc::new(easing),
1058        }
1059    }
1060
1061    /// No smooth scrolling, scroll position updates immediately.
1062    pub fn disabled() -> Self {
1063        Self::new(Duration::ZERO, easing::none)
1064    }
1065
1066    /// If this config represents [`disabled`].
1067    ///
1068    /// [`disabled`]: Self::disabled
1069    pub fn is_disabled(&self) -> bool {
1070        self.duration == Duration::ZERO
1071    }
1072}
1073impl_from_and_into_var! {
1074    /// Linear duration of smooth transition.
1075    fn from(duration: Duration) -> SmoothScrolling {
1076        SmoothScrolling {
1077            duration,
1078            ..Default::default()
1079        }
1080    }
1081
1082    /// Returns default config for `true`, [`disabled`] for `false`.
1083    ///
1084    /// [`disabled`]: SmoothScrolling::disabled
1085    fn from(enabled: bool) -> SmoothScrolling {
1086        if enabled {
1087            SmoothScrolling::default()
1088        } else {
1089            SmoothScrolling::disabled()
1090        }
1091    }
1092
1093    fn from<F: Fn(EasingTime) -> EasingStep + Send + Sync + 'static>((duration, easing): (Duration, F)) -> SmoothScrolling {
1094        SmoothScrolling::new(duration, easing)
1095    }
1096
1097    fn from((duration, easing): (Duration, easing::EasingFn)) -> SmoothScrolling {
1098        SmoothScrolling::new(duration, easing.ease_fn())
1099    }
1100}
1101
1102/// Arguments for the [`auto_scroll_indicator`] closure.
1103///
1104/// Empty struct, there are no args in the current release, this struct is declared so that if
1105/// args may be introduced in the future with minimal breaking changes.
1106///
1107/// Note that the [`SCROLL`] context is available during the icon closure call.
1108///
1109/// [`auto_scroll_indicator`]: fn@crate::auto_scroll_indicator
1110#[derive(Debug, Default, Clone, PartialEq)]
1111pub struct AutoScrollArgs {}