Skip to main content

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