Skip to main content

zng_wgt_scroll/
cmd.rs

1//! Commands that control the scoped scroll widget.
2//!
3//! The scroll widget implements all of this commands scoped to its widget ID.
4
5use super::*;
6use zng_app::event::CommandParam;
7use zng_ext_window::WINDOWS;
8use zng_wgt::ICONS;
9
10command! {
11    /// Represents the **scroll up** by one [`v_line_unit`] action.
12    ///
13    /// # Parameter
14    ///
15    /// This command supports an optional parameter, it can be a [`bool`] that enables the alternate of the command
16    /// or a [`ScrollRequest`] that contains more configurations.
17    ///
18    /// [`v_line_unit`]: fn@crate::v_line_unit
19    pub static SCROLL_UP_CMD {
20        l10n!: true,
21        name: "Scroll Up",
22        info: "Scroll Up by one scroll unit",
23        shortcut: shortcut!(ArrowUp),
24        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
25    };
26
27    /// Represents the **scroll down** by one [`v_line_unit`] action.
28    ///
29    /// # Parameter
30    ///
31    /// This command supports an optional parameter, it can be a [`bool`] that enables the alternate of the command
32    /// or a [`ScrollRequest`] that contains more configurations.
33    ///
34    /// [`v_line_unit`]: fn@crate::v_line_unit
35    pub static SCROLL_DOWN_CMD {
36        l10n!: true,
37        name: "Scroll Down",
38        info: "Scroll Down by one scroll unit",
39        shortcut: shortcut!(ArrowDown),
40        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
41    };
42
43    /// Represents the **scroll left** by one [`h_line_unit`] action.
44    ///
45    /// # Parameter
46    ///
47    /// This command supports an optional parameter, it can be a [`bool`] that enables the alternate of the command
48    /// or a [`ScrollRequest`] that contains more configurations.
49    ///
50    /// [`h_line_unit`]: fn@crate::h_line_unit
51    pub static SCROLL_LEFT_CMD {
52        l10n!: true,
53        name: "Scroll Left",
54        info: "Scroll Left by one scroll unit",
55        shortcut: shortcut!(ArrowLeft),
56        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
57    };
58
59    /// Represents the **scroll right** by one [`h_line_unit`] action.
60    ///
61    /// # Parameter
62    ///
63    /// This command supports an optional parameter, it can be a [`bool`] that enables the alternate of the command
64    /// or a [`ScrollRequest`] that contains more configurations.
65    ///
66    /// [`h_line_unit`]: fn@crate::h_line_unit
67    pub static SCROLL_RIGHT_CMD {
68        l10n!: true,
69        name: "Scroll Right",
70        info: "Scroll Right by one scroll unit",
71        shortcut: shortcut!(ArrowRight),
72        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
73    };
74
75    /// Represents the **page up** by one [`v_page_unit`] action.
76    ///
77    /// # Parameter
78    ///
79    /// This command supports an optional parameter, it can be a [`bool`] that enables the alternate of the command
80    /// or a [`ScrollRequest`] that contains more configurations.
81    ///
82    /// [`name`]: CommandNameExt
83    /// [`info`]: CommandInfoExt
84    /// [`shortcut`]: CommandShortcutExt
85    /// [`v_page_unit`]: fn@crate::v_page_unit
86    pub static PAGE_UP_CMD {
87        l10n!: true,
88        name: "Page Up",
89        info: "Scroll Up by one page unit",
90        shortcut: shortcut!(PageUp),
91        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
92    };
93
94    /// Represents the **page down** by one [`v_page_unit`] action.
95    ///
96    /// # Parameter
97    ///
98    /// This command supports an optional parameter, it can be a [`bool`] that enables the alternate of the command
99    /// or a [`ScrollRequest`] that contains more configurations.
100    ///
101    /// [`v_page_unit`]: fn@crate::v_page_unit
102    pub static PAGE_DOWN_CMD {
103        l10n!: true,
104        name: "Page Down",
105        info: "Scroll down by one page unit",
106        shortcut: shortcut!(PageDown),
107        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
108    };
109
110    /// Represents the **page left** by one [`h_page_unit`] action.
111    ///
112    /// # Parameter
113    ///
114    /// This command supports an optional parameter, it can be a [`bool`] that enables the alternate of the command
115    /// or a [`ScrollRequest`] that contains more configurations.
116    ///
117    /// [`h_page_unit`]: fn@crate::h_page_unit
118    pub static PAGE_LEFT_CMD {
119        l10n!: true,
120        name: "Page Left",
121        info: "Scroll Left by one page unit",
122        shortcut: shortcut!(SHIFT + PageUp),
123        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
124    };
125
126    /// Represents the **page right** by one [`h_page_unit`] action.
127    ///
128    /// # Parameter
129    ///
130    /// This command supports an optional parameter, it can be a [`bool`] that enables the alternate of the command
131    /// or a [`ScrollRequest`] that contains more configurations.
132    ///
133    /// [`h_page_unit`]: fn@crate::h_page_unit
134    pub static PAGE_RIGHT_CMD {
135        l10n!: true,
136        name: "Page Right",
137        info: "Scroll Right by one page unit",
138        shortcut: shortcut!(SHIFT + PageDown),
139        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
140    };
141
142    /// Represents the **scroll to top** action.
143    pub static SCROLL_TO_TOP_CMD {
144        l10n!: true,
145        name: "Scroll to Top",
146        info: "Scroll up to the content top",
147        shortcut: [shortcut!(Home), shortcut!(CTRL + Home)],
148        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
149        icon: wgt_fn!(|_| ICONS.get(["scroll-top", "vertical-align-top"])),
150    };
151
152    /// Represents the **scroll to bottom** action.
153    pub static SCROLL_TO_BOTTOM_CMD {
154        l10n!: true,
155        name: "Scroll to Bottom",
156        info: "Scroll down to the content bottom.",
157        shortcut: [shortcut!(End), shortcut!(CTRL + End)],
158        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
159        icon: wgt_fn!(|_| ICONS.get(["scroll-bottom", "vertical-align-bottom"])),
160    };
161
162    /// Represents the **scroll to leftmost** action.
163    pub static SCROLL_TO_LEFTMOST_CMD {
164        l10n!: true,
165        name: "Scroll to Leftmost",
166        info: "Scroll left to the content left edge",
167        shortcut: [shortcut!(SHIFT + Home), shortcut!(CTRL | SHIFT + Home)],
168        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
169    };
170
171    /// Represents the **scroll to rightmost** action.
172    pub static SCROLL_TO_RIGHTMOST_CMD {
173        l10n!: true,
174        name: "Scroll to Rightmost",
175        info: "Scroll right to the content right edge",
176        shortcut: [shortcut!(SHIFT + End), shortcut!(CTRL | SHIFT + End)],
177        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
178    };
179
180    /// Represents the action of scrolling until a child widget is fully visible, the command can
181    /// also adjust the zoom scale.
182    ///
183    /// # Metadata
184    ///
185    /// This command initializes with no extra metadata.
186    ///
187    /// # Parameter
188    ///
189    /// This command requires a parameter to work, it can be a [`ScrollToRequest`] instance, or a
190    /// [`ScrollToTarget`], or the [`WidgetId`] of a descendant of the scroll, or a [`Rect`] resolved in the scrollable space.
191    ///
192    /// You can use the [`scroll_to`] function to invoke this command in all parent scrolls automatically.
193    ///
194    /// [`WidgetId`]: zng_wgt::prelude::WidgetId
195    /// [`Rect`]: zng_wgt::prelude::Rect
196    pub static SCROLL_TO_CMD;
197
198    /// Represents the **zoom in** action.
199    ///
200    /// # Parameter
201    ///
202    /// This commands accepts an optional [`Point`] parameter that defines the origin of the
203    /// scale transform, relative values are resolved in the viewport space. The default value
204    /// is *top-start*.
205    ///
206    /// [`Point`]: zng_wgt::prelude::Point
207    pub static ZOOM_IN_CMD {
208        l10n!: true,
209        name: "Zoom In",
210        shortcut: shortcut!(CTRL + '+'),
211        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
212        icon: wgt_fn!(|_| ICONS.get("zoom-in")),
213    };
214
215    /// Represents the **zoom out** action.
216    ///
217    /// # Parameter
218    ///
219    /// This commands accepts an optional [`Point`] parameter that defines the origin of the
220    /// scale transform, relative values are resolved in the viewport space. The default value
221    /// is *top-start*.
222    ///
223    /// [`Point`]: zng_wgt::prelude::Point
224    pub static ZOOM_OUT_CMD {
225        l10n!: true,
226        name: "Zoom Out",
227        shortcut: shortcut!(CTRL + '-'),
228        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
229        icon: wgt_fn!(|_| ICONS.get("zoom-out")),
230    };
231
232    /// Represents the **zoom to fit** action.
233    ///
234    /// The content is scaled to fit the viewport, the equivalent to `ImageFit::Contain`.
235    ///
236    /// # Parameter
237    ///
238    /// This command accepts an optional [`ZoomToFitRequest`] parameter with configuration.
239    pub static ZOOM_TO_FIT_CMD {
240        l10n!: true,
241        name: "Zoom to Fit",
242        shortcut: shortcut!(CTRL + '0'),
243        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
244        icon: wgt_fn!(|_| ICONS.get(["zoom-to-fit", "fit-screen"])),
245    };
246
247    /// Represents the **reset zoom** action.
248    ///
249    /// The content is scaled back to 100%, without adjusting the scroll.
250    pub static ZOOM_RESET_CMD {
251        l10n!: true,
252        name: "Reset Zoom",
253        shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
254    };
255
256    /// Represents the **auto scroll** toggle.
257    ///
258    /// # Parameter
259    ///
260    /// The parameter can be a [`DipVector`] that starts auto scrolling at the direction and velocity (dip/s). If
261    /// no parameter is provided the default speed is zero, which stops auto scrolling.
262    ///
263    /// [`DipVector`]: zng_wgt::prelude::DipVector
264    pub static AUTO_SCROLL_CMD;
265}
266
267/// Parameters for the [`ZOOM_TO_FIT_CMD`].
268///
269/// Also see the property [`zoom_to_fit_mode`].
270#[derive(Default, Debug, Clone, PartialEq)]
271#[non_exhaustive]
272pub struct ZoomToFitRequest {
273    /// Apply the change immediately, no easing/smooth animation.
274    pub skip_animation: bool,
275}
276impl ZoomToFitRequest {
277    /// Pack the request into a command parameter.
278    pub fn to_param(self) -> CommandParam {
279        CommandParam::new(self)
280    }
281
282    /// Extract a clone of the request from the command parameter if it is of a compatible type.
283    pub fn from_param(p: &CommandParam) -> Option<Self> {
284        p.downcast_ref::<Self>().cloned()
285    }
286}
287
288/// Parameters for the scroll and page commands.
289#[derive(Debug, Clone, PartialEq)]
290#[non_exhaustive]
291pub struct ScrollRequest {
292    /// If the [alt factor] should be applied to the base scroll unit when scrolling.
293    ///
294    /// [alt factor]: super::ALT_FACTOR_VAR
295    pub alternate: bool,
296    /// Only scroll within this inclusive range. The range is normalized `0.0..=1.0`, the default is `(f32::MIN, f32::MAX)`.
297    ///
298    /// Note that the commands are enabled and disabled for the full range, this parameter controls
299    /// the range for the request only.
300    pub clamp: (f32, f32),
301
302    /// Apply the change immediately, no easing/smooth animation.
303    pub skip_animation: bool,
304}
305impl Default for ScrollRequest {
306    fn default() -> Self {
307        Self {
308            alternate: Default::default(),
309            clamp: (f32::MIN, f32::MAX),
310            skip_animation: false,
311        }
312    }
313}
314impl ScrollRequest {
315    /// Pack the request into a command parameter.
316    pub fn to_param(self) -> CommandParam {
317        CommandParam::new(self)
318    }
319
320    /// Extract a clone of the request from the command parameter if it is of a compatible type.
321    pub fn from_param(p: &CommandParam) -> Option<Self> {
322        if let Some(req) = p.downcast_ref::<Self>() {
323            Some(req.clone())
324        } else {
325            p.downcast_ref::<bool>().map(|&alt| ScrollRequest {
326                alternate: alt,
327                ..Default::default()
328            })
329        }
330    }
331}
332impl_from_and_into_var! {
333    fn from(alternate: bool) -> ScrollRequest {
334        ScrollRequest {
335            alternate,
336            ..Default::default()
337        }
338    }
339}
340
341/// Target for the [`SCROLL_TO_CMD`].
342#[derive(Debug, Clone, PartialEq)]
343pub enum ScrollToTarget {
344    /// Widget (inner bounds) that will be scrolled into view.
345    Descendant(WidgetId),
346    /// Rectangle in the content space that will be scrolled into view.
347    Rect(Rect),
348}
349impl_from_and_into_var! {
350    fn from(widget_id: WidgetId) -> ScrollToTarget {
351        ScrollToTarget::Descendant(widget_id)
352    }
353    fn from(widget_id: &'static str) -> ScrollToTarget {
354        ScrollToTarget::Descendant(widget_id.into())
355    }
356    fn from(rect: Rect) -> ScrollToTarget {
357        ScrollToTarget::Rect(rect)
358    }
359}
360
361/// Parameters for the [`SCROLL_TO_CMD`].
362#[derive(Debug, Clone, PartialEq)]
363#[non_exhaustive]
364pub struct ScrollToRequest {
365    /// Area that will be scrolled into view.
366    pub target: ScrollToTarget,
367
368    /// How much the scroll position will change to showcase the target widget.
369    pub mode: ScrollToMode,
370
371    /// Optional zoom scale target.
372    ///
373    /// If set the offsets and scale will animate so that the `mode`
374    /// is fulfilled when this zoom factor is reached. If not set the scroll will happen in
375    /// the current zoom scale.
376    ///
377    /// Note that the viewport size can change due to a scrollbar visibility changing, this size
378    /// change is not accounted for when calculating minimal.
379    pub zoom: Option<Factor>,
380
381    /// If should scroll immediately to the target, no smooth animation.
382    pub skip_animation: bool,
383}
384impl ScrollToRequest {
385    /// New with target and mode.
386    pub fn new(target: impl Into<ScrollToTarget>, mode: impl Into<ScrollToMode>) -> Self {
387        Self {
388            target: target.into(),
389            mode: mode.into(),
390            zoom: None,
391            skip_animation: false,
392        }
393    }
394
395    /// Pack the request into a command parameter.
396    pub fn to_param(self) -> CommandParam {
397        CommandParam::new(self)
398    }
399
400    /// Extract a clone of the request from the command parameter if it is of a compatible type.
401    pub fn from_param(p: &CommandParam) -> Option<Self> {
402        if let Some(req) = p.downcast_ref::<Self>() {
403            Some(req.clone())
404        } else {
405            Some(ScrollToRequest {
406                target: if let Some(target) = p.downcast_ref::<ScrollToTarget>() {
407                    target.clone()
408                } else if let Some(target) = p.downcast_ref::<WidgetId>() {
409                    ScrollToTarget::Descendant(*target)
410                } else if let Some(target) = p.downcast_ref::<Rect>() {
411                    ScrollToTarget::Rect(target.clone())
412                } else {
413                    return None;
414                },
415                mode: ScrollToMode::default(),
416                zoom: None,
417                skip_animation: false,
418            })
419        }
420    }
421}
422
423/// Defines how much the [`SCROLL_TO_CMD`] will scroll to showcase the target widget.
424#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
425pub enum ScrollToMode {
426    /// Scroll will change only just enough so that the widget inner rect is fully visible with the optional
427    /// extra margin offsets.
428    Minimal {
429        /// Extra margin added so that the widget is touching the scroll edge.
430        margin: SideOffsets,
431    },
432    /// Scroll so that the point relative to the widget inner rectangle is at the same screen point on
433    /// the scroll viewport.
434    Center {
435        /// A point relative to the target widget inner size.
436        widget_point: Point,
437        /// A point relative to the scroll viewport.
438        scroll_point: Point,
439    },
440}
441impl ScrollToMode {
442    /// New [`Minimal`] mode.
443    ///
444    /// [`Minimal`]: Self::Minimal
445    pub fn minimal(margin: impl Into<SideOffsets>) -> Self {
446        ScrollToMode::Minimal { margin: margin.into() }
447    }
448
449    /// New [`Minimal`] mode.
450    ///
451    /// The minimal scroll needed so that `rect` in the content widget is fully visible.
452    ///
453    /// [`Minimal`]: Self::Minimal
454    pub fn minimal_rect(rect: impl Into<Rect>) -> Self {
455        let rect = rect.into();
456        ScrollToMode::Minimal {
457            margin: SideOffsets::new(
458                -rect.origin.y.clone(),
459                rect.origin.x.clone() + rect.size.width - 100.pct(),
460                rect.origin.y + rect.size.height - 100.pct(),
461                -rect.origin.x,
462            ),
463        }
464    }
465
466    /// New [`Center`] mode using the center points of widget and scroll.
467    ///
468    /// [`Center`]: Self::Center
469    pub fn center() -> Self {
470        Self::center_points(Point::center(), Point::center())
471    }
472
473    /// New [`Center`] mode.
474    ///
475    /// [`Center`]: Self::Center
476    pub fn center_points(widget_point: impl Into<Point>, scroll_point: impl Into<Point>) -> Self {
477        ScrollToMode::Center {
478            widget_point: widget_point.into(),
479            scroll_point: scroll_point.into(),
480        }
481    }
482}
483impl Default for ScrollToMode {
484    /// Minimal with margin 10.
485    fn default() -> Self {
486        Self::minimal(10)
487    }
488}
489impl_from_and_into_var! {
490    fn from(some: ScrollToMode) -> Option<ScrollToMode>;
491}
492
493/// Scroll all parent [`is_scroll`] widgets of `target` so that it becomes visible.
494///
495/// This function is a helper for searching for the `target` in all windows and sending [`SCROLL_TO_CMD`] for all required scroll widgets.
496/// Does nothing if the `target` is not found.
497///
498/// [`is_scroll`]: WidgetInfoExt::is_scroll
499pub fn scroll_to(target: impl ScrollToTargetProvider, mode: impl Into<ScrollToMode>) {
500    scroll_to_impl(target.find_target(), mode.into(), None)
501}
502
503/// Like [`scroll_to`], but also adjusts the zoom scale.
504pub fn scroll_to_zoom(target: impl ScrollToTargetProvider, mode: impl Into<ScrollToMode>, zoom: impl Into<Factor>) {
505    scroll_to_impl(target.find_target(), mode.into(), Some(zoom.into()))
506}
507
508fn scroll_to_impl(target: Option<WidgetInfo>, mode: ScrollToMode, zoom: Option<Factor>) {
509    if let Some(target) = target {
510        let mut t = target.id();
511        for a in target.ancestors() {
512            if a.is_scroll() {
513                SCROLL_TO_CMD.scoped(a.id()).notify_param(ScrollToRequest {
514                    target: ScrollToTarget::Descendant(t),
515                    mode: mode.clone(),
516                    zoom,
517                    skip_animation: false,
518                });
519                t = a.id();
520            }
521        }
522    }
523}
524
525/// Scroll at the direction and velocity (dip/sec) until the end or another auto scroll request.
526///
527/// Zero stops auto scrolling.
528pub fn auto_scroll(scroll_id: impl Into<WidgetId>, velocity: DipVector) {
529    auto_scroll_impl(scroll_id.into(), velocity)
530}
531fn auto_scroll_impl(scroll_id: WidgetId, vel: DipVector) {
532    AUTO_SCROLL_CMD.scoped(scroll_id).notify_param(vel);
533}
534
535/// Provides a target for scroll-to command methods.
536///
537/// Implemented for `"widget-id"`, `WidgetId` and `WidgetInfo`.
538pub trait ScrollToTargetProvider {
539    /// Find the target info.
540    fn find_target(self) -> Option<WidgetInfo>;
541}
542impl ScrollToTargetProvider for &'static str {
543    fn find_target(self) -> Option<WidgetInfo> {
544        WidgetId::named(self).find_target()
545    }
546}
547impl ScrollToTargetProvider for WidgetId {
548    fn find_target(self) -> Option<WidgetInfo> {
549        WINDOWS.widget_info(self)
550    }
551}
552impl ScrollToTargetProvider for WidgetInfo {
553    fn find_target(self) -> Option<WidgetInfo> {
554        Some(self)
555    }
556}