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}