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