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::{CommandArgs, 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    /// Extract a clone of the request from [`CommandArgs::param`] if it is set to a compatible type and
288    /// stop-propagation was not requested for the event.
289    ///
290    /// [`CommandArgs::param`]: zng_app::event::CommandArgs
291    #[deprecated = "use `CommandArgs::param`"]
292    pub fn from_args(args: &CommandArgs) -> Option<Self> {
293        if let Some(p) = &args.param {
294            if args.propagation.is_stopped() { None } else { Self::from_param(p) }
295        } else {
296            None
297        }
298    }
299}
300
301/// Parameters for the scroll and page commands.
302#[derive(Debug, Clone, PartialEq)]
303#[non_exhaustive]
304pub struct ScrollRequest {
305    /// If the [alt factor] should be applied to the base scroll unit when scrolling.
306    ///
307    /// [alt factor]: super::ALT_FACTOR_VAR
308    pub alternate: bool,
309    /// Only scroll within this inclusive range. The range is normalized `0.0..=1.0`, the default is `(f32::MIN, f32::MAX)`.
310    ///
311    /// Note that the commands are enabled and disabled for the full range, this parameter controls
312    /// the range for the request only.
313    pub clamp: (f32, f32),
314
315    /// Apply the change immediately, no easing/smooth animation.
316    pub skip_animation: bool,
317}
318impl Default for ScrollRequest {
319    fn default() -> Self {
320        Self {
321            alternate: Default::default(),
322            clamp: (f32::MIN, f32::MAX),
323            skip_animation: false,
324        }
325    }
326}
327impl ScrollRequest {
328    /// Pack the request into a command parameter.
329    pub fn to_param(self) -> CommandParam {
330        CommandParam::new(self)
331    }
332
333    /// Extract a clone of the request from the command parameter if it is of a compatible type.
334    pub fn from_param(p: &CommandParam) -> Option<Self> {
335        if let Some(req) = p.downcast_ref::<Self>() {
336            Some(req.clone())
337        } else {
338            p.downcast_ref::<bool>().map(|&alt| ScrollRequest {
339                alternate: alt,
340                ..Default::default()
341            })
342        }
343    }
344
345    /// Extract a clone of the request from [`CommandArgs::param`] if it is set to a compatible type and
346    /// stop-propagation was not requested for the event.
347    ///
348    /// [`CommandArgs::param`]: zng_app::event::CommandArgs
349    #[deprecated = "use `CommandArgs::param`"]
350    pub fn from_args(args: &CommandArgs) -> Option<Self> {
351        if let Some(p) = &args.param {
352            if args.propagation.is_stopped() { None } else { Self::from_param(p) }
353        } else {
354            None
355        }
356    }
357}
358impl_from_and_into_var! {
359    fn from(alternate: bool) -> ScrollRequest {
360        ScrollRequest {
361            alternate,
362            ..Default::default()
363        }
364    }
365}
366
367/// Target for the [`SCROLL_TO_CMD`].
368#[derive(Debug, Clone, PartialEq)]
369pub enum ScrollToTarget {
370    /// Widget (inner bounds) that will be scrolled into view.
371    Descendant(WidgetId),
372    /// Rectangle in the content space that will be scrolled into view.
373    Rect(Rect),
374}
375impl_from_and_into_var! {
376    fn from(widget_id: WidgetId) -> ScrollToTarget {
377        ScrollToTarget::Descendant(widget_id)
378    }
379    fn from(widget_id: &'static str) -> ScrollToTarget {
380        ScrollToTarget::Descendant(widget_id.into())
381    }
382    fn from(rect: Rect) -> ScrollToTarget {
383        ScrollToTarget::Rect(rect)
384    }
385}
386
387/// Parameters for the [`SCROLL_TO_CMD`].
388#[derive(Debug, Clone, PartialEq)]
389#[non_exhaustive]
390pub struct ScrollToRequest {
391    /// Area that will be scrolled into view.
392    pub target: ScrollToTarget,
393
394    /// How much the scroll position will change to showcase the target widget.
395    pub mode: ScrollToMode,
396
397    /// Optional zoom scale target.
398    ///
399    /// If set the offsets and scale will animate so that the `mode`
400    /// is fulfilled when this zoom factor is reached. If not set the scroll will happen in
401    /// the current zoom scale.
402    ///
403    /// Note that the viewport size can change due to a scrollbar visibility changing, this size
404    /// change is not accounted for when calculating minimal.
405    pub zoom: Option<Factor>,
406
407    /// If should scroll immediately to the target, no smooth animation.
408    pub skip_animation: bool,
409}
410impl ScrollToRequest {
411    /// New with target and mode.
412    pub fn new(target: impl Into<ScrollToTarget>, mode: impl Into<ScrollToMode>) -> Self {
413        Self {
414            target: target.into(),
415            mode: mode.into(),
416            zoom: None,
417            skip_animation: false,
418        }
419    }
420
421    /// Pack the request into a command parameter.
422    pub fn to_param(self) -> CommandParam {
423        CommandParam::new(self)
424    }
425
426    /// Extract a clone of the request from the command parameter if it is of a compatible type.
427    pub fn from_param(p: &CommandParam) -> Option<Self> {
428        if let Some(req) = p.downcast_ref::<Self>() {
429            Some(req.clone())
430        } else {
431            Some(ScrollToRequest {
432                target: if let Some(target) = p.downcast_ref::<ScrollToTarget>() {
433                    target.clone()
434                } else if let Some(target) = p.downcast_ref::<WidgetId>() {
435                    ScrollToTarget::Descendant(*target)
436                } else if let Some(target) = p.downcast_ref::<Rect>() {
437                    ScrollToTarget::Rect(target.clone())
438                } else {
439                    return None;
440                },
441                mode: ScrollToMode::default(),
442                zoom: None,
443                skip_animation: false,
444            })
445        }
446    }
447
448    /// Extract a clone of the request from [`CommandArgs::param`] if it is set to a compatible type and
449    /// stop-propagation was not requested for the event and the command was enabled when it was send.
450    ///
451    /// [`CommandArgs::param`]: zng_app::event::CommandArgs
452    #[deprecated = "use `CommandArgs::param`"]
453    pub fn from_args(args: &CommandArgs) -> Option<Self> {
454        if let Some(p) = &args.param {
455            if !args.enabled || args.propagation.is_stopped() {
456                None
457            } else {
458                Self::from_param(p)
459            }
460        } else {
461            None
462        }
463    }
464}
465
466/// Defines how much the [`SCROLL_TO_CMD`] will scroll to showcase the target widget.
467#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
468pub enum ScrollToMode {
469    /// Scroll will change only just enough so that the widget inner rect is fully visible with the optional
470    /// extra margin offsets.
471    Minimal {
472        /// Extra margin added so that the widget is touching the scroll edge.
473        margin: SideOffsets,
474    },
475    /// Scroll so that the point relative to the widget inner rectangle is at the same screen point on
476    /// the scroll viewport.
477    Center {
478        /// A point relative to the target widget inner size.
479        widget_point: Point,
480        /// A point relative to the scroll viewport.
481        scroll_point: Point,
482    },
483}
484impl ScrollToMode {
485    /// New [`Minimal`] mode.
486    ///
487    /// [`Minimal`]: Self::Minimal
488    pub fn minimal(margin: impl Into<SideOffsets>) -> Self {
489        ScrollToMode::Minimal { margin: margin.into() }
490    }
491
492    /// New [`Minimal`] mode.
493    ///
494    /// The minimal scroll needed so that `rect` in the content widget is fully visible.
495    ///
496    /// [`Minimal`]: Self::Minimal
497    pub fn minimal_rect(rect: impl Into<Rect>) -> Self {
498        let rect = rect.into();
499        ScrollToMode::Minimal {
500            margin: SideOffsets::new(
501                -rect.origin.y.clone(),
502                rect.origin.x.clone() + rect.size.width - 100.pct(),
503                rect.origin.y + rect.size.height - 100.pct(),
504                -rect.origin.x,
505            ),
506        }
507    }
508
509    /// New [`Center`] mode using the center points of widget and scroll.
510    ///
511    /// [`Center`]: Self::Center
512    pub fn center() -> Self {
513        Self::center_points(Point::center(), Point::center())
514    }
515
516    /// New [`Center`] mode.
517    ///
518    /// [`Center`]: Self::Center
519    pub fn center_points(widget_point: impl Into<Point>, scroll_point: impl Into<Point>) -> Self {
520        ScrollToMode::Center {
521            widget_point: widget_point.into(),
522            scroll_point: scroll_point.into(),
523        }
524    }
525}
526impl Default for ScrollToMode {
527    /// Minimal with margin 10.
528    fn default() -> Self {
529        Self::minimal(10)
530    }
531}
532impl_from_and_into_var! {
533    fn from(some: ScrollToMode) -> Option<ScrollToMode>;
534}
535
536/// Scroll all parent [`is_scroll`] widgets of `target` so that it becomes visible.
537///
538/// This function is a helper for searching for the `target` in all windows and sending [`SCROLL_TO_CMD`] for all required scroll widgets.
539/// Does nothing if the `target` is not found.
540///
541/// [`is_scroll`]: WidgetInfoExt::is_scroll
542pub fn scroll_to(target: impl ScrollToTargetProvider, mode: impl Into<ScrollToMode>) {
543    scroll_to_impl(target.find_target(), mode.into(), None)
544}
545
546/// Like [`scroll_to`], but also adjusts the zoom scale.
547pub fn scroll_to_zoom(target: impl ScrollToTargetProvider, mode: impl Into<ScrollToMode>, zoom: impl Into<Factor>) {
548    scroll_to_impl(target.find_target(), mode.into(), Some(zoom.into()))
549}
550
551fn scroll_to_impl(target: Option<WidgetInfo>, mode: ScrollToMode, zoom: Option<Factor>) {
552    if let Some(target) = target {
553        let mut t = target.id();
554        for a in target.ancestors() {
555            if a.is_scroll() {
556                SCROLL_TO_CMD.scoped(a.id()).notify_param(ScrollToRequest {
557                    target: ScrollToTarget::Descendant(t),
558                    mode: mode.clone(),
559                    zoom,
560                    skip_animation: false,
561                });
562                t = a.id();
563            }
564        }
565    }
566}
567
568/// Scroll at the direction and velocity (dip/sec) until the end or another auto scroll request.
569///
570/// Zero stops auto scrolling.
571pub fn auto_scroll(scroll_id: impl Into<WidgetId>, velocity: DipVector) {
572    auto_scroll_impl(scroll_id.into(), velocity)
573}
574fn auto_scroll_impl(scroll_id: WidgetId, vel: DipVector) {
575    AUTO_SCROLL_CMD.scoped(scroll_id).notify_param(vel);
576}
577
578/// Provides a target for scroll-to command methods.
579///
580/// Implemented for `"widget-id"`, `WidgetId` and `WidgetInfo`.
581pub trait ScrollToTargetProvider {
582    /// Find the target info.
583    fn find_target(self) -> Option<WidgetInfo>;
584}
585impl ScrollToTargetProvider for &'static str {
586    fn find_target(self) -> Option<WidgetInfo> {
587        WidgetId::named(self).find_target()
588    }
589}
590impl ScrollToTargetProvider for WidgetId {
591    fn find_target(self) -> Option<WidgetInfo> {
592        WINDOWS.widget_info(self)
593    }
594}
595impl ScrollToTargetProvider for WidgetInfo {
596    fn find_target(self) -> Option<WidgetInfo> {
597        Some(self)
598    }
599}