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