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 {
411                    let target = p.downcast_ref::<Rect>()?;
412                    ScrollToTarget::Rect(target.clone())
413                },
414                mode: ScrollToMode::default(),
415                zoom: None,
416                skip_animation: false,
417            })
418        }
419    }
420}
421
422/// Defines how much the [`SCROLL_TO_CMD`] will scroll to showcase the target widget.
423#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
424pub enum ScrollToMode {
425    /// Scroll will change only just enough so that the widget inner rect is fully visible with the optional
426    /// extra margin offsets.
427    Minimal {
428        /// Extra margin added so that the widget is touching the scroll edge.
429        margin: SideOffsets,
430    },
431    /// Scroll so that the point relative to the widget inner rectangle is at the same screen point on
432    /// the scroll viewport.
433    Center {
434        /// A point relative to the target widget inner size.
435        widget_point: Point,
436        /// A point relative to the scroll viewport.
437        scroll_point: Point,
438    },
439}
440impl ScrollToMode {
441    /// New [`Minimal`] mode.
442    ///
443    /// [`Minimal`]: Self::Minimal
444    pub fn minimal(margin: impl Into<SideOffsets>) -> Self {
445        ScrollToMode::Minimal { margin: margin.into() }
446    }
447
448    /// New [`Minimal`] mode.
449    ///
450    /// The minimal scroll needed so that `rect` in the content widget is fully visible.
451    ///
452    /// [`Minimal`]: Self::Minimal
453    pub fn minimal_rect(rect: impl Into<Rect>) -> Self {
454        let rect = rect.into();
455        ScrollToMode::Minimal {
456            margin: SideOffsets::new(
457                -rect.origin.y.clone(),
458                rect.origin.x.clone() + rect.size.width - 100.pct(),
459                rect.origin.y + rect.size.height - 100.pct(),
460                -rect.origin.x,
461            ),
462        }
463    }
464
465    /// New [`Center`] mode using the center points of widget and scroll.
466    ///
467    /// [`Center`]: Self::Center
468    pub fn center() -> Self {
469        Self::center_points(Point::center(), Point::center())
470    }
471
472    /// New [`Center`] mode.
473    ///
474    /// [`Center`]: Self::Center
475    pub fn center_points(widget_point: impl Into<Point>, scroll_point: impl Into<Point>) -> Self {
476        ScrollToMode::Center {
477            widget_point: widget_point.into(),
478            scroll_point: scroll_point.into(),
479        }
480    }
481}
482impl Default for ScrollToMode {
483    /// Minimal with margin 10.
484    fn default() -> Self {
485        Self::minimal(10)
486    }
487}
488impl_from_and_into_var! {
489    fn from(some: ScrollToMode) -> Option<ScrollToMode>;
490}
491
492/// Scroll all parent [`is_scroll`] widgets of `target` so that it becomes visible.
493///
494/// This function is a helper for searching for the `target` in all windows and sending [`SCROLL_TO_CMD`] for all required scroll widgets.
495/// Does nothing if the `target` is not found.
496///
497/// [`is_scroll`]: WidgetInfoExt::is_scroll
498pub fn scroll_to(target: impl ScrollToTargetProvider, mode: impl Into<ScrollToMode>) {
499    scroll_to_impl(target.find_target(), mode.into(), None)
500}
501
502/// Like [`scroll_to`], but also adjusts the zoom scale.
503pub fn scroll_to_zoom(target: impl ScrollToTargetProvider, mode: impl Into<ScrollToMode>, zoom: impl Into<Factor>) {
504    scroll_to_impl(target.find_target(), mode.into(), Some(zoom.into()))
505}
506
507fn scroll_to_impl(target: Option<WidgetInfo>, mode: ScrollToMode, zoom: Option<Factor>) {
508    if let Some(target) = target {
509        let mut t = target.id();
510        for a in target.ancestors() {
511            if a.is_scroll() {
512                SCROLL_TO_CMD.scoped(a.id()).notify_param(ScrollToRequest {
513                    target: ScrollToTarget::Descendant(t),
514                    mode: mode.clone(),
515                    zoom,
516                    skip_animation: false,
517                });
518                t = a.id();
519            }
520        }
521    }
522}
523
524/// Scroll at the direction and velocity (dip/sec) until the end or another auto scroll request.
525///
526/// Zero stops auto scrolling.
527pub fn auto_scroll(scroll_id: impl Into<WidgetId>, velocity: DipVector) {
528    auto_scroll_impl(scroll_id.into(), velocity)
529}
530fn auto_scroll_impl(scroll_id: WidgetId, vel: DipVector) {
531    AUTO_SCROLL_CMD.scoped(scroll_id).notify_param(vel);
532}
533
534/// Provides a target for scroll-to command methods.
535///
536/// Implemented for `"widget-id"`, `WidgetId` and `WidgetInfo`.
537pub trait ScrollToTargetProvider {
538    /// Find the target info.
539    fn find_target(self) -> Option<WidgetInfo>;
540}
541impl ScrollToTargetProvider for &'static str {
542    fn find_target(self) -> Option<WidgetInfo> {
543        WidgetId::named(self).find_target()
544    }
545}
546impl ScrollToTargetProvider for WidgetId {
547    fn find_target(self) -> Option<WidgetInfo> {
548        WINDOWS.widget_info(self)
549    }
550}
551impl ScrollToTargetProvider for WidgetInfo {
552    fn find_target(self) -> Option<WidgetInfo> {
553        Some(self)
554    }
555}