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}