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