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