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, not scaled by zoom.
87    ///
88    /// Multiply by `SCROLL_SCALE_VAR` to get the content size.
89    pub(super) static SCROLL_CONTENT_ORIGINAL_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    ///
316    /// The size is scaled by the zoom scale.
317    pub fn content_size(&self) -> Var<PxSize> {
318        expr_var! {
319            *#{SCROLL_CONTENT_ORIGINAL_SIZE_VAR} * *#{SCROLL_SCALE_VAR}
320        }
321    }
322
323    /// Latest layout content size of the parent scroll. Not scaled by zoom.
324    pub fn content_original_size(&self) -> Var<PxSize> {
325        SCROLL_CONTENT_ORIGINAL_SIZE_VAR.read_only()
326    }
327
328    /// Applies the `delta` to the vertical offset.
329    ///
330    /// If smooth scrolling is enabled it is used to update the offset.
331    pub fn scroll_vertical(&self, delta: ScrollFrom) {
332        self.scroll_vertical_clamp(delta, f32::MIN, f32::MAX);
333    }
334
335    /// Applies the `delta` to the horizontal offset.
336    ///
337    /// If smooth scrolling is enabled the chase animation is created or updated by this call.
338    pub fn scroll_horizontal(&self, delta: ScrollFrom) {
339        self.scroll_horizontal_clamp(delta, f32::MIN, f32::MAX)
340    }
341
342    /// Applies the `delta` to the vertical offset, but clamps the final offset by the inclusive `min` and `max`.
343    ///
344    /// If smooth scrolling is enabled it is used to update the offset.
345    pub fn scroll_vertical_clamp(&self, delta: ScrollFrom, min: f32, max: f32) {
346        self.scroll_clamp(true, SCROLL_VERTICAL_OFFSET_VAR, delta, min, max)
347    }
348
349    /// Applies the `delta` to the horizontal offset, but clamps the final offset by the inclusive `min` and `max`.
350    ///
351    /// If smooth scrolling is enabled it is used to update the offset.
352    pub fn scroll_horizontal_clamp(&self, delta: ScrollFrom, min: f32, max: f32) {
353        self.scroll_clamp(false, SCROLL_HORIZONTAL_OFFSET_VAR, delta, min, max)
354    }
355    fn scroll_clamp(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, delta: ScrollFrom, min: f32, max: f32) {
356        let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
357        let content = (SCROLL_CONTENT_ORIGINAL_SIZE_VAR.get() * SCROLL_SCALE_VAR.get()).to_array()[vertical as usize];
358
359        let max_scroll = content - viewport;
360
361        if max_scroll <= 0 {
362            return;
363        }
364
365        match delta {
366            ScrollFrom::Var(a) => {
367                let amount = a.0 as f32 / max_scroll.0 as f32;
368                let f = scroll_offset_var.get();
369                SCROLL.chase(vertical, scroll_offset_var, |_| (f.0 + amount).clamp(min, max).fct());
370            }
371            ScrollFrom::VarTarget(a) => {
372                let amount = a.0 as f32 / max_scroll.0 as f32;
373                SCROLL.chase(vertical, scroll_offset_var, |f| (f.0 + amount).clamp(min, max).fct());
374            }
375            ScrollFrom::Rendered(a) => {
376                let amount = a.0 as f32 / max_scroll.0 as f32;
377                let f = SCROLL_CONFIG.get().rendered.load(Ordering::Relaxed).h;
378                SCROLL.chase(vertical, scroll_offset_var, |_| (f.0 + amount).clamp(min, max).fct());
379            }
380        }
381    }
382
383    /// Animate scroll at the direction and velocity (in DIPs per second).
384    pub fn auto_scroll(&self, velocity: DipVector) {
385        let viewport = SCROLL_VIEWPORT_SIZE_VAR.get();
386        let content = SCROLL_CONTENT_ORIGINAL_SIZE_VAR.get() * SCROLL_SCALE_VAR.get();
387        let max_scroll = content - viewport;
388
389        let velocity = velocity.to_px(WINDOW.info().scale_factor());
390
391        fn scroll(dimension: usize, velocity: Px, max_scroll: Px, offset_var: &ContextVar<Factor>) {
392            if velocity == 0 {
393                SCROLL_CONFIG.get().auto[dimension].lock().clone().stop();
394            } else {
395                let mut travel = max_scroll * offset_var.get();
396                let mut target = 0.0;
397                if velocity > Px(0) {
398                    travel = max_scroll - travel;
399                    target = 1.0;
400                }
401                let time = (travel.0 as f32 / velocity.0.abs() as f32).secs();
402
403                VARS.with_animation_controller(zng_var::animation::ForceAnimationController, || {
404                    let handle = offset_var.ease(target, time, easing::linear);
405                    mem::replace(&mut *SCROLL_CONFIG.get().auto[dimension].lock(), handle).stop();
406                });
407            }
408        }
409        scroll(0, velocity.x, max_scroll.width, &SCROLL_HORIZONTAL_OFFSET_VAR);
410        scroll(1, velocity.y, max_scroll.height, &SCROLL_VERTICAL_OFFSET_VAR);
411    }
412
413    /// Applies the `delta` to the vertical offset without smooth scrolling and
414    /// updates the vertical overscroll if it changes.
415    ///
416    /// This method is used to implement touch gesture scrolling, the delta is always [`ScrollFrom::Var`].
417    pub fn scroll_vertical_touch(&self, delta: Px) {
418        self.scroll_touch(true, SCROLL_VERTICAL_OFFSET_VAR, OVERSCROLL_VERTICAL_OFFSET_VAR, delta)
419    }
420
421    /// Applies the `delta` to the horizontal offset without smooth scrolling and
422    /// updates the horizontal overscroll if it changes.
423    ///
424    /// This method is used to implement touch gesture scrolling, the delta is always [`ScrollFrom::Var`].
425    pub fn scroll_horizontal_touch(&self, delta: Px) {
426        self.scroll_touch(false, SCROLL_HORIZONTAL_OFFSET_VAR, OVERSCROLL_HORIZONTAL_OFFSET_VAR, delta)
427    }
428
429    fn scroll_touch(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, overscroll_offset_var: ContextVar<Factor>, delta: Px) {
430        let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
431        let content = (SCROLL_CONTENT_ORIGINAL_SIZE_VAR.get() * SCROLL_SCALE_VAR.get()).to_array()[vertical as usize];
432
433        let max_scroll = content - viewport;
434        if max_scroll <= 0 {
435            return;
436        }
437
438        let delta = delta.0 as f32 / max_scroll.0 as f32;
439
440        let current = scroll_offset_var.get();
441        let mut next = current + delta.fct();
442        let mut overscroll = 0.fct();
443        if next > 1.fct() {
444            overscroll = next - 1.fct();
445            next = 1.fct();
446
447            let overscroll_px = overscroll * content.0.fct();
448            let overscroll_max = viewport.0.fct();
449            overscroll = overscroll_px.min(overscroll_max) / overscroll_max;
450        } else if next < 0.fct() {
451            overscroll = next;
452            next = 0.fct();
453
454            let overscroll_px = -overscroll * content.0.fct();
455            let overscroll_max = viewport.0.fct();
456            overscroll = -(overscroll_px.min(overscroll_max) / overscroll_max);
457        }
458
459        scroll_offset_var.set(next);
460        if overscroll != 0.fct() {
461            let new_handle = self.increment_overscroll(overscroll_offset_var, overscroll);
462
463            let config = SCROLL_CONFIG.get();
464            let mut handle = config.overscroll[vertical as usize].lock();
465            mem::replace(&mut *handle, new_handle).stop();
466        } else {
467            self.clear_horizontal_overscroll();
468        }
469    }
470
471    fn increment_overscroll(&self, overscroll: ContextVar<Factor>, delta: Factor) -> AnimationHandle {
472        enum State {
473            Increment,
474            ClearDelay,
475            Clear(Transition<Factor>),
476        }
477        let mut state = State::Increment;
478        overscroll.animate(move |a, o| match &mut state {
479            State::Increment => {
480                // set the increment and start delay to animation.
481                **o += delta;
482                **o = (*o).clamp(-1.fct(), 1.fct());
483
484                a.sleep(300.ms(), false);
485                state = State::ClearDelay;
486            }
487            State::ClearDelay => {
488                a.restart();
489                let t = Transition::new(**o, 0.fct());
490                state = State::Clear(t);
491            }
492            State::Clear(t) => {
493                let step = easing::linear(a.elapsed_stop(300.ms()));
494                o.set(t.sample(step));
495            }
496        })
497    }
498
499    /// Quick ease vertical overscroll to zero.
500    pub fn clear_vertical_overscroll(&self) {
501        self.clear_overscroll(true, OVERSCROLL_VERTICAL_OFFSET_VAR)
502    }
503
504    /// Quick ease horizontal overscroll to zero.
505    pub fn clear_horizontal_overscroll(&self) {
506        self.clear_overscroll(false, OVERSCROLL_HORIZONTAL_OFFSET_VAR)
507    }
508
509    fn clear_overscroll(&self, vertical: bool, overscroll_offset_var: ContextVar<Factor>) {
510        if overscroll_offset_var.get() != 0.fct() {
511            let new_handle = overscroll_offset_var.ease(0.fct(), 100.ms(), easing::linear);
512
513            let config = SCROLL_CONFIG.get();
514            let mut handle = config.overscroll[vertical as usize].lock();
515            mem::replace(&mut *handle, new_handle).stop();
516        }
517    }
518
519    /// Animates to `delta` over `duration`.
520    pub fn scroll_vertical_touch_inertia(&self, delta: Px, duration: Duration) {
521        self.scroll_touch_inertia(true, SCROLL_VERTICAL_OFFSET_VAR, OVERSCROLL_VERTICAL_OFFSET_VAR, delta, duration)
522    }
523
524    /// Animates to `delta` over `duration`.
525    pub fn scroll_horizontal_touch_inertia(&self, delta: Px, duration: Duration) {
526        self.scroll_touch_inertia(
527            false,
528            SCROLL_HORIZONTAL_OFFSET_VAR,
529            OVERSCROLL_HORIZONTAL_OFFSET_VAR,
530            delta,
531            duration,
532        )
533    }
534
535    fn scroll_touch_inertia(
536        &self,
537        vertical: bool,
538        scroll_offset_var: ContextVar<Factor>,
539        overscroll_offset_var: ContextVar<Factor>,
540        delta: Px,
541        duration: Duration,
542    ) {
543        let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
544        let content = (SCROLL_CONTENT_ORIGINAL_SIZE_VAR.get() * SCROLL_SCALE_VAR.get()).to_array()[vertical as usize];
545
546        let max_scroll = content - viewport;
547        if max_scroll <= 0 {
548            return;
549        }
550
551        let delta = delta.0 as f32 / max_scroll.0 as f32;
552
553        let current = scroll_offset_var.get();
554        let mut next = current + delta.fct();
555        let mut overscroll = 0.fct();
556        if next > 1.fct() {
557            overscroll = next - 1.fct();
558            next = 1.fct();
559
560            let overscroll_px = overscroll * content.0.fct();
561            let overscroll_max = viewport.0.fct();
562            overscroll = overscroll_px.min(overscroll_max) / overscroll_max;
563        } else if next < 0.fct() {
564            overscroll = next;
565            next = 0.fct();
566
567            let overscroll_px = -overscroll * content.0.fct();
568            let overscroll_max = viewport.0.fct();
569            overscroll = -(overscroll_px.min(overscroll_max) / overscroll_max);
570        }
571
572        let cfg = SCROLL_CONFIG.get();
573        let easing = |t| easing::ease_out(easing::quad, t);
574        *cfg.inertia[vertical as usize].lock() = if overscroll != 0.fct() {
575            let transition = Transition::new(current, next + overscroll);
576
577            let overscroll_var = overscroll_offset_var.current_context();
578            let overscroll_tr = Transition::new(overscroll, 0.fct());
579            let mut is_inertia_anim = true;
580
581            scroll_offset_var.animate(move |animation, value| {
582                if is_inertia_anim {
583                    // inertia ease animation
584                    let step = easing(animation.elapsed(duration));
585                    let v = transition.sample(step);
586
587                    if v < 0.fct() || v > 1.fct() {
588                        // follows the easing curve until cap, cuts out to overscroll indicator.
589                        value.set(v.clamp_range());
590                        animation.restart();
591                        is_inertia_anim = false;
592                        overscroll_var.set(overscroll_tr.from);
593                    } else {
594                        value.set(v);
595                    }
596                } else {
597                    // overscroll clear ease animation
598                    let step = easing::linear(animation.elapsed_stop(300.ms()));
599                    let v = overscroll_tr.sample(step);
600                    overscroll_var.set(v);
601                }
602            })
603        } else {
604            scroll_offset_var.ease(next, duration, easing)
605        };
606    }
607
608    /// Set the vertical offset to a new offset derived from the last, blending into the active smooth
609    /// scrolling chase animation, or starting a new one, or just setting the var if smooth scrolling is disabled.
610    pub fn chase_vertical(&self, modify_offset: impl FnOnce(Factor) -> Factor) {
611        self.chase(true, SCROLL_VERTICAL_OFFSET_VAR, modify_offset);
612    }
613
614    /// Set the horizontal offset to a new offset derived from the last set offset, blending into the active smooth
615    /// scrolling chase animation, or starting a new one, or just setting the var if smooth scrolling is disabled.
616    pub fn chase_horizontal(&self, modify_offset: impl FnOnce(Factor) -> Factor) {
617        self.chase(false, SCROLL_HORIZONTAL_OFFSET_VAR, modify_offset);
618    }
619
620    fn chase(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, modify_offset: impl FnOnce(Factor) -> Factor) {
621        let smooth = SMOOTH_SCROLLING_VAR.get();
622        let config = SCROLL_CONFIG.get();
623        let mut chase = config.chase[vertical as usize].lock();
624        match &mut *chase {
625            Some(t) => {
626                if smooth.is_disabled() {
627                    let t = modify_offset(*t.target()).clamp_range();
628                    scroll_offset_var.set(t);
629                    *chase = None;
630                } else {
631                    let easing = smooth.easing.clone();
632                    t.modify(|f| *f = modify_offset(*f).clamp_range(), smooth.duration, move |t| easing(t));
633                }
634            }
635            None => {
636                let t = modify_offset(scroll_offset_var.get()).clamp_range();
637                if smooth.is_disabled() {
638                    scroll_offset_var.set(t);
639                } else {
640                    let easing = smooth.easing.clone();
641                    let anim = scroll_offset_var.chase(t, smooth.duration, move |t| easing(t));
642                    *chase = Some(anim);
643                }
644            }
645        }
646    }
647
648    /// Set the zoom scale to a new scale derived from the last set scale, blending into the active
649    /// smooth scaling chase animation, or starting a new or, or just setting the var if smooth scrolling is disabled.
650    pub fn chase_zoom(&self, modify_scale: impl FnOnce(Factor) -> Factor) {
651        self.chase_zoom_impl(modify_scale);
652    }
653    fn chase_zoom_impl(&self, modify_scale: impl FnOnce(Factor) -> Factor) {
654        if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
655            return;
656        }
657
658        let smooth = SMOOTH_SCROLLING_VAR.get();
659        let config = SCROLL_CONFIG.get();
660        let mut zoom = config.zoom.lock();
661
662        let min = SCROLL.actual_min_zoom();
663        let max = super::MAX_ZOOM_VAR.get();
664
665        match &mut *zoom {
666            ZoomState::Chasing(t) => {
667                if smooth.is_disabled() {
668                    let next = modify_scale(*t.target()).clamp(min, max);
669                    SCROLL_SCALE_VAR.set(next);
670                    *zoom = ZoomState::None;
671                } else {
672                    let easing = smooth.easing.clone();
673                    t.modify(|f| *f = modify_scale(*f).clamp(min, max), smooth.duration, move |t| easing(t));
674                }
675            }
676            _ => {
677                let t = modify_scale(SCROLL_SCALE_VAR.get()).clamp(min, max);
678                if smooth.is_disabled() {
679                    SCROLL_SCALE_VAR.set(t);
680                } else {
681                    let easing = smooth.easing.clone();
682                    let anim = SCROLL_SCALE_VAR.chase(t, smooth.duration, move |t| easing(t));
683                    *zoom = ZoomState::Chasing(anim);
684                }
685            }
686        }
687    }
688
689    /// Zoom in or out keeping the `origin` point in the viewport aligned with the same point
690    /// in the content.
691    pub fn zoom(&self, modify_scale: impl FnOnce(Factor) -> Factor, origin: PxPoint) {
692        self.zoom_impl(modify_scale, origin);
693    }
694    fn zoom_impl(&self, modify_scale: impl FnOnce(Factor) -> Factor, center_in_viewport: PxPoint) {
695        if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
696            return;
697        }
698
699        let content = WIDGET.info().scroll_info().unwrap().content();
700        let mut center_in_content = -content.origin + center_in_viewport.to_vector();
701        let mut content_size = content.size;
702
703        let rendered_scale = SCROLL.rendered_zoom_scale();
704
705        SCROLL.chase_zoom(|f| {
706            let s = modify_scale(f);
707            let f = s / rendered_scale;
708            center_in_content *= f;
709            content_size *= f;
710            s
711        });
712
713        let viewport_size = SCROLL_VIEWPORT_SIZE_VAR.get();
714
715        // scroll so that new center_in_content is at the same center_in_viewport
716        let max_scroll = content_size - viewport_size;
717        let offset = center_in_content - center_in_viewport;
718
719        if offset.y != Px(0) && max_scroll.height > Px(0) {
720            let offset_y = offset.y.0 as f32 / max_scroll.height.0 as f32;
721            SCROLL.chase_vertical(|_| offset_y.fct());
722        }
723        if offset.x != Px(0) && max_scroll.width > Px(0) {
724            let offset_x = offset.x.0 as f32 / max_scroll.width.0 as f32;
725            SCROLL.chase_horizontal(|_| offset_x.fct());
726        }
727    }
728
729    /// Applies the `scale` to the current zoom scale without smooth scrolling and centered on the touch point.
730    pub fn zoom_touch(&self, phase: TouchPhase, scale: Factor, center_in_viewport: euclid::Point2D<f32, Px>) {
731        if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
732            return;
733        }
734
735        let cfg = SCROLL_CONFIG.get();
736
737        let rendered_scale = SCROLL.rendered_zoom_scale();
738
739        let start_scale;
740        let start_center;
741
742        let mut cfg = cfg.zoom.lock();
743
744        if let TouchPhase::Start = phase {
745            start_scale = rendered_scale;
746            start_center = center_in_viewport;
747
748            *cfg = ZoomState::TouchStart {
749                start_factor: start_scale,
750                start_center: center_in_viewport,
751                applied_offset: euclid::vec2(0.0, 0.0),
752            };
753        } else if let ZoomState::TouchStart {
754            start_factor: scale,
755            start_center: center_in_viewport,
756            ..
757        } = &*cfg
758        {
759            start_scale = *scale;
760            start_center = *center_in_viewport;
761        } else {
762            // touch canceled or not started correctly.
763            return;
764        }
765
766        // applied translate offset
767        let applied_offset = if let ZoomState::TouchStart { applied_offset, .. } = &mut *cfg {
768            applied_offset
769        } else {
770            unreachable!()
771        };
772
773        let translate_offset = start_center - center_in_viewport;
774        let translate_delta = translate_offset - *applied_offset;
775        *applied_offset = translate_offset;
776
777        let scroll_info = WIDGET.info().scroll_info().unwrap();
778        let content = scroll_info.content();
779        let mut center_in_content = -content.origin.cast::<f32>() + center_in_viewport.to_vector();
780        let mut content_size = content.size.cast::<f32>();
781
782        let scale = start_scale + (scale - 1.0.fct());
783
784        let min = SCROLL.actual_min_zoom();
785        let max = super::MAX_ZOOM_VAR.get();
786        let scale = scale.clamp(min, max);
787
788        let scale_transform = scale / rendered_scale;
789
790        center_in_content *= scale_transform;
791        content_size *= scale_transform;
792
793        let viewport_size = SCROLL_VIEWPORT_SIZE_VAR.get().cast::<f32>();
794
795        // scroll so that new center_in_content is at the same center_in_viewport
796        let max_scroll = content_size - viewport_size;
797        let zoom_offset = center_in_content - center_in_viewport;
798
799        let offset = zoom_offset + translate_delta;
800
801        SCROLL_SCALE_VAR.set(scale);
802
803        if offset.y != 0.0 && max_scroll.height > 0.0 {
804            let offset_y = offset.y / max_scroll.height;
805            SCROLL_VERTICAL_OFFSET_VAR.set(offset_y.clamp(0.0, 1.0));
806        }
807        if offset.x != 0.0 && max_scroll.width > 0.0 {
808            let offset_x = offset.x / max_scroll.width;
809            SCROLL_HORIZONTAL_OFFSET_VAR.set(offset_x.clamp(0.0, 1.0));
810        }
811    }
812
813    fn can_scroll(&self, predicate: impl Fn(PxSize, PxSize) -> bool + Send + Sync + 'static) -> Var<bool> {
814        expr_var! {
815            predicate(
816                *#{SCROLL_VIEWPORT_SIZE_VAR},
817                *#{SCROLL_CONTENT_ORIGINAL_SIZE_VAR} * *#{SCROLL_SCALE_VAR},
818            )
819        }
820    }
821
822    /// Gets a var that is `true` when the content height is greater then the viewport height.
823    pub fn can_scroll_vertical(&self) -> Var<bool> {
824        self.can_scroll(|vp, ct| ct.height > vp.height)
825    }
826
827    /// Gets a var that is `true` when the content width is greater then the viewport with.
828    pub fn can_scroll_horizontal(&self) -> Var<bool> {
829        self.can_scroll(|vp, ct| ct.width > vp.width)
830    }
831
832    fn can_scroll_v(&self, predicate: impl Fn(PxSize, PxSize, Factor) -> bool + Send + Sync + 'static) -> Var<bool> {
833        expr_var! {
834            predicate(
835                *#{SCROLL_VIEWPORT_SIZE_VAR},
836                *#{SCROLL_CONTENT_ORIGINAL_SIZE_VAR} * *#{SCROLL_SCALE_VAR},
837                *#{SCROLL_VERTICAL_OFFSET_VAR},
838            )
839        }
840    }
841
842    /// Gets a var that is `true` when the content height is greater then the viewport height and the vertical offset
843    /// is not at the maximum.
844    pub fn can_scroll_down(&self) -> Var<bool> {
845        self.can_scroll_v(|vp, ct, vo| ct.height > vp.height && 1.fct() > vo)
846    }
847
848    /// Gets a var that is `true` when the content height is greater then the viewport height and the vertical offset
849    /// is not at the minimum.
850    pub fn can_scroll_up(&self) -> Var<bool> {
851        self.can_scroll_v(|vp, ct, vo| ct.height > vp.height && 0.fct() < vo)
852    }
853
854    fn can_scroll_h(&self, predicate: impl Fn(PxSize, PxSize, Factor) -> bool + Send + Sync + 'static) -> Var<bool> {
855        expr_var! {
856            predicate(
857                *#{SCROLL_VIEWPORT_SIZE_VAR},
858                *#{SCROLL_CONTENT_ORIGINAL_SIZE_VAR} * *#{SCROLL_SCALE_VAR},
859                *#{SCROLL_HORIZONTAL_OFFSET_VAR},
860            )
861        }
862    }
863
864    /// Gets a var that is `true` when the content width is greater then the viewport width and the horizontal offset
865    /// is not at the minimum.
866    pub fn can_scroll_left(&self) -> Var<bool> {
867        self.can_scroll_h(|vp, ct, ho| ct.width > vp.width && 0.fct() < ho)
868    }
869
870    /// Gets a var that is `true` when the content width is greater then the viewport width and the horizontal offset
871    /// is not at the maximum.
872    pub fn can_scroll_right(&self) -> Var<bool> {
873        self.can_scroll_h(|vp, ct, ho| ct.width > vp.width && 1.fct() > ho)
874    }
875
876    /// Scroll the [`WIDGET`] into view.
877    ///
878    /// [`WIDGET`]: zng_wgt::prelude::WIDGET
879    pub fn scroll_to(&self, mode: impl Into<super::cmd::ScrollToMode>) {
880        cmd::scroll_to(WIDGET.info(), mode.into())
881    }
882
883    /// Scroll the [`WIDGET`] into view and adjusts the zoom scale.
884    ///
885    /// [`WIDGET`]: zng_wgt::prelude::WIDGET
886    pub fn scroll_to_zoom(&self, mode: impl Into<super::cmd::ScrollToMode>, zoom: impl Into<Factor>) {
887        cmd::scroll_to_zoom(WIDGET.info(), mode.into(), zoom.into())
888    }
889
890    /// Returns `true` if the content can be scaled and the current scale is less than the max.
891    pub fn can_zoom_in(&self) -> bool {
892        SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) && SCROLL_SCALE_VAR.get() < super::MAX_ZOOM_VAR.get()
893    }
894
895    /// Returns `true` if the content can be scaled and the current scale is more than the min.
896    pub fn can_zoom_out(&self) -> bool {
897        SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) && SCROLL_SCALE_VAR.get() > self.actual_min_zoom()
898    }
899
900    /// Compute the minimum scale allowed.
901    ///
902    /// This is the [`MIN_ZOOM_VAR`] unless it is set to < 0.01%, in that case it is the fit scale or 100% if
903    /// the content is smaller than the viewport.
904    ///
905    /// [`MIN_ZOOM_VAR`]: crate::MIN_ZOOM_VAR
906    pub fn actual_min_zoom(&self) -> Factor {
907        let min = super::MIN_ZOOM_VAR.get();
908        if min >= 0.01.pct().fct() {
909            return min;
910        }
911
912        let content;
913        let viewport;
914
915        if let Some(info) = WIDGET.try_info()
916            && let Some(info) = info.scroll_info()
917        {
918            // use more up-to-date values
919            content = info.content_original_size().cast::<f32>();
920            viewport = (info.viewport().size - info.joiner_size()).cast::<f32>();
921        } else {
922            content = SCROLL_CONTENT_ORIGINAL_SIZE_VAR.get().cast::<f32>();
923            viewport = SCROLL_VIEWPORT_SIZE_VAR.get().cast::<f32>(); // TODO missing JOINER_SIZE
924        }
925
926        if content.width < 0.1 || content.height < 0.1 {
927            return min;
928        }
929        (viewport.width / content.width)
930            .min(viewport.height / content.height)
931            .min(1.0)
932            .fct()
933    }
934}
935
936impl SCROLL {
937    /// Insert the context values used by `SCROLL` in the `set`.
938    ///
939    /// Capturing this set plus all context vars enables using all `SCROLL` methods outside the scroll.
940    pub fn context_values_set(&self, set: &mut ContextValueSet) {
941        set.insert(&SCROLL_CONFIG);
942    }
943}
944
945/// Scroll extensions for [`WidgetInfo`].
946///
947/// [`WidgetInfo`]: zng_wgt::prelude::WidgetInfo
948pub trait WidgetInfoExt {
949    /// Returns `true` if the widget is a [`Scroll!`](struct@super::Scroll).
950    fn is_scroll(&self) -> bool;
951
952    /// Returns a reference to the viewport bounds if the widget is a [`Scroll!`](struct@super::Scroll).
953    fn scroll_info(&self) -> Option<ScrollInfo>;
954
955    /// Gets the viewport bounds relative to the scroll widget inner bounds.
956    ///
957    /// The value is updated every layout and render, without requiring an info rebuild.
958    fn viewport(&self) -> Option<PxRect>;
959}
960impl WidgetInfoExt for WidgetInfo {
961    fn is_scroll(&self) -> bool {
962        self.meta().get(*SCROLL_INFO_ID).is_some()
963    }
964
965    fn scroll_info(&self) -> Option<ScrollInfo> {
966        self.meta().get(*SCROLL_INFO_ID).cloned()
967    }
968
969    fn viewport(&self) -> Option<PxRect> {
970        self.meta().get(*SCROLL_INFO_ID).map(|r| r.viewport())
971    }
972}
973
974#[derive(Debug)]
975struct ScrollData {
976    viewport_transform: PxTransform,
977    viewport_size: PxSize,
978    joiner_size: PxSize,
979    content_offset: PxPoint,
980    zoom_scale: Factor,
981    content_original_size: PxSize,
982}
983impl Default for ScrollData {
984    fn default() -> Self {
985        Self {
986            viewport_transform: Default::default(),
987            viewport_size: Default::default(),
988            joiner_size: Default::default(),
989            content_offset: Default::default(),
990            zoom_scale: 1.fct(),
991            content_original_size: Default::default(),
992        }
993    }
994}
995
996/// Shared reference to the viewport bounds of a scroll.
997#[derive(Clone, Default, Debug)]
998pub struct ScrollInfo(Arc<Mutex<ScrollData>>);
999impl ScrollInfo {
1000    /// Gets the viewport bounds in the window space.
1001    pub fn viewport(&self) -> PxRect {
1002        self.viewport_transform()
1003            .outer_transformed(PxBox::from_size(self.viewport_size()))
1004            .unwrap_or_default()
1005            .to_rect()
1006    }
1007
1008    /// Gets the layout size of the viewport.
1009    pub fn viewport_size(&self) -> PxSize {
1010        self.0.lock().viewport_size
1011    }
1012
1013    /// Gets the render transform of the viewport.
1014    pub fn viewport_transform(&self) -> PxTransform {
1015        self.0.lock().viewport_transform
1016    }
1017
1018    /// Gets the layout size of both scroll-bars.
1019    ///
1020    /// Joiner here is the corner joiner visual, it is sized by the width of the vertical bar and
1021    /// height of the horizontal bar.
1022    pub fn joiner_size(&self) -> PxSize {
1023        self.0.lock().joiner_size
1024    }
1025
1026    /// Latest content offset and size.
1027    ///
1028    /// This is the content bounds, scaled and in the viewport space.
1029    pub fn content(&self) -> PxRect {
1030        let m = self.0.lock();
1031        PxRect::new(m.content_offset, m.content_original_size * m.zoom_scale)
1032    }
1033
1034    /// Latest content size before it was scaled by the zoom scale.
1035    ///
1036    /// This value multiplied by `zoom_scale` is the same as the `content` size.
1037    pub fn content_original_size(&self) -> PxSize {
1038        self.0.lock().content_original_size
1039    }
1040
1041    /// Latest zoom scale.
1042    pub fn zoom_scale(&self) -> Factor {
1043        self.0.lock().zoom_scale
1044    }
1045
1046    pub(super) fn set_viewport_size(&self, size: PxSize) {
1047        self.0.lock().viewport_size = size;
1048    }
1049
1050    pub(super) fn set_viewport_transform(&self, transform: PxTransform) {
1051        self.0.lock().viewport_transform = transform;
1052    }
1053
1054    pub(super) fn set_joiner_size(&self, size: PxSize) {
1055        self.0.lock().joiner_size = size;
1056    }
1057
1058    pub(super) fn set_content(&self, content_offset: PxPoint, scale: Factor, content_original_size: PxSize) {
1059        let mut m = self.0.lock();
1060        m.content_offset = content_offset;
1061        m.zoom_scale = scale;
1062        m.content_original_size = content_original_size;
1063    }
1064}
1065
1066static_id! {
1067    pub(super) static ref SCROLL_INFO_ID: StateId<ScrollInfo>;
1068}
1069
1070/// Smooth scrolling config.
1071///
1072/// This config can be set by the [`smooth_scrolling`] property.
1073///
1074/// [`smooth_scrolling`]: fn@crate::smooth_scrolling
1075#[derive(Clone)]
1076pub struct SmoothScrolling {
1077    /// Chase transition duration.
1078    ///
1079    /// Default is `150.ms()`.
1080    pub duration: Duration,
1081    /// Chase transition easing function.
1082    ///
1083    /// Default is linear.
1084    pub easing: Arc<dyn Fn(EasingTime) -> EasingStep + Send + Sync>,
1085}
1086impl fmt::Debug for SmoothScrolling {
1087    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1088        f.debug_struct("SmoothScrolling")
1089            .field("duration", &self.duration)
1090            .finish_non_exhaustive()
1091    }
1092}
1093impl PartialEq for SmoothScrolling {
1094    fn eq(&self, other: &Self) -> bool {
1095        self.duration == other.duration && Arc::ptr_eq(&self.easing, &other.easing)
1096    }
1097}
1098impl Default for SmoothScrolling {
1099    fn default() -> Self {
1100        Self::new(150.ms(), easing::linear)
1101    }
1102}
1103impl SmoothScrolling {
1104    /// New custom smooth scrolling config.
1105    pub fn new(duration: Duration, easing: impl Fn(EasingTime) -> EasingStep + Send + Sync + 'static) -> Self {
1106        Self {
1107            duration,
1108            easing: Arc::new(easing),
1109        }
1110    }
1111
1112    /// No smooth scrolling, scroll position updates immediately.
1113    pub fn disabled() -> Self {
1114        Self::new(Duration::ZERO, easing::none)
1115    }
1116
1117    /// If this config represents [`disabled`].
1118    ///
1119    /// [`disabled`]: Self::disabled
1120    pub fn is_disabled(&self) -> bool {
1121        self.duration == Duration::ZERO
1122    }
1123}
1124impl_from_and_into_var! {
1125    /// Linear duration of smooth transition.
1126    fn from(duration: Duration) -> SmoothScrolling {
1127        SmoothScrolling {
1128            duration,
1129            ..Default::default()
1130        }
1131    }
1132
1133    /// Returns default config for `true`, [`disabled`] for `false`.
1134    ///
1135    /// [`disabled`]: SmoothScrolling::disabled
1136    fn from(enabled: bool) -> SmoothScrolling {
1137        if enabled {
1138            SmoothScrolling::default()
1139        } else {
1140            SmoothScrolling::disabled()
1141        }
1142    }
1143
1144    fn from<F: Fn(EasingTime) -> EasingStep + Send + Sync + 'static>((duration, easing): (Duration, F)) -> SmoothScrolling {
1145        SmoothScrolling::new(duration, easing)
1146    }
1147
1148    fn from((duration, easing): (Duration, easing::EasingFn)) -> SmoothScrolling {
1149        SmoothScrolling::new(duration, easing.ease_fn())
1150    }
1151}
1152
1153/// Arguments for the [`auto_scroll_indicator`] closure.
1154///
1155/// Empty struct, there are no args in the current release, this struct is declared so that if
1156/// args may be introduced in the future with minimal breaking changes.
1157///
1158/// Note that the [`SCROLL`] context is available during the icon closure call.
1159///
1160/// [`auto_scroll_indicator`]: fn@crate::auto_scroll_indicator
1161#[derive(Debug, Default, Clone, PartialEq)]
1162#[non_exhaustive]
1163pub struct AutoScrollArgs {}
1164
1165/// Defines how the scale is changed by the [`ZOOM_TO_FIT_CMD`].
1166///
1167/// See the [`zoom_to_fit_mode`] property for more details.
1168///
1169/// [`ZOOM_TO_FIT_CMD`]: crate::cmd::ZOOM_TO_FIT_CMD
1170/// [`zoom_to_fit_mode`]: fn@crate::zoom_to_fit_mode
1171#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1172pub enum ZoomToFitMode {
1173    /// The content is scaled down or up to fit the viewport.
1174    #[default]
1175    Contain,
1176    /// The content is only scaled down to fit the viewport. If the content is smaller them the viewport the scale is set to 100%.
1177    ScaleDown,
1178}