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}