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