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}