zng_wgt_scroll/
node.rs

1//! UI nodes used for building the scroll widget.
2//!
3
4use std::sync::Arc;
5
6use parking_lot::Mutex;
7use zng_app::{
8    access::ACCESS_SCROLL_EVENT,
9    view_process::raw_events::{RAW_MOUSE_INPUT_EVENT, RAW_MOUSE_MOVED_EVENT},
10};
11use zng_color::Rgba;
12use zng_ext_input::{
13    focus::{FOCUS, FOCUS_CHANGED_EVENT},
14    keyboard::{KEY_INPUT_EVENT, Key, KeyState},
15    mouse::{ButtonState, MOUSE_INPUT_EVENT, MOUSE_WHEEL_EVENT, MouseButton, MouseScrollDelta},
16    touch::{TOUCH_TRANSFORM_EVENT, TouchPhase},
17};
18use zng_wgt::prelude::{
19    gradient::{ExtendMode, RenderGradientStop},
20    *,
21};
22use zng_wgt_container::Container;
23use zng_wgt_layer::{AnchorMode, LAYERS, LayerIndex};
24
25use super::cmd::*;
26use super::scroll_properties::*;
27use super::scrollbar::Orientation;
28use super::types::*;
29
30/// The actual content presenter.
31pub fn viewport(child: impl UiNode, mode: impl IntoVar<ScrollMode>, child_align: impl IntoVar<Align>) -> impl UiNode {
32    let mode = mode.into_var();
33    let child_align = child_align.into_var();
34    let binding_key = FrameValueKey::new_unique();
35
36    let mut viewport_size = PxSize::zero();
37    let mut content_offset = PxVector::zero();
38    let mut content_scale = 1.fct();
39    let mut auto_hide_extra = PxSideOffsets::zero();
40    let mut last_render_offset = PxVector::zero();
41    let mut scroll_info = None;
42    let mut scroll_info = move || {
43        scroll_info
44            .get_or_insert_with(|| WIDGET.info().meta().get_clone(*SCROLL_INFO_ID).unwrap())
45            .clone()
46    };
47
48    match_node(child, move |child, op| match op {
49        UiNodeOp::Init => {
50            WIDGET
51                .sub_var_layout(&mode)
52                .sub_var_layout(&SCROLL_VERTICAL_OFFSET_VAR)
53                .sub_var_layout(&SCROLL_HORIZONTAL_OFFSET_VAR)
54                .sub_var_layout(&SCROLL_SCALE_VAR)
55                .sub_var_layout(&child_align);
56        }
57
58        UiNodeOp::Measure { wm, desired_size } => {
59            let constraints = LAYOUT.constraints();
60            if constraints.is_fill_max().all() {
61                *desired_size = constraints.fill_size();
62                child.delegated();
63                return;
64            }
65
66            let mode = mode.get();
67            let child_align = child_align.get();
68
69            let vp_unit = constraints.fill_size();
70            let has_fill_size = !vp_unit.is_empty() && constraints.max_size() == Some(vp_unit);
71            let define_vp_unit = has_fill_size && DEFINE_VIEWPORT_UNIT_VAR.get();
72
73            let mut content_size = LAYOUT.with_constraints(
74                {
75                    let mut c = constraints;
76                    if mode.contains(ScrollMode::VERTICAL) {
77                        c = c.with_unbounded_y().with_new_min_y(vp_unit.height);
78                    } else {
79                        c = c.with_new_min_y(Px(0));
80                        if has_fill_size {
81                            c = c.with_new_max_y(vp_unit.height);
82                        }
83                    }
84                    if mode.contains(ScrollMode::HORIZONTAL) {
85                        c = c.with_unbounded_x().with_new_min_x(vp_unit.width);
86                    } else {
87                        c = c.with_new_min_x(Px(0));
88                        if has_fill_size {
89                            c = c.with_new_max_x(vp_unit.width);
90                        }
91                    }
92
93                    child_align.child_constraints(c)
94                },
95                || {
96                    if define_vp_unit {
97                        LAYOUT.with_viewport(vp_unit, || child.measure(wm))
98                    } else {
99                        child.measure(wm)
100                    }
101                },
102            );
103
104            if mode.contains(ScrollMode::ZOOM) {
105                let scale = SCROLL_SCALE_VAR.get();
106                content_size.width *= scale;
107                content_size.height *= scale;
108            }
109
110            *desired_size = constraints.fill_size_or(content_size);
111        }
112        UiNodeOp::Layout { wl, final_size } => {
113            let mode = mode.get();
114            let child_align = child_align.get();
115
116            let constraints = LAYOUT.constraints();
117            let vp_unit = constraints.fill_size();
118
119            let has_fill_size = !vp_unit.is_empty() && constraints.max_size() == Some(vp_unit);
120            let define_vp_unit = has_fill_size && DEFINE_VIEWPORT_UNIT_VAR.get();
121
122            let joiner_size = scroll_info().joiner_size();
123
124            let mut content_size = LAYOUT.with_constraints(
125                {
126                    let mut c = constraints;
127                    if mode.contains(ScrollMode::VERTICAL) {
128                        // Align::FILL forces the min-size, because we have infinite space in scrollable dimensions.
129                        c = c.with_unbounded_y().with_new_min_y(vp_unit.height);
130                    } else {
131                        // If not scrollable Align::FILL works like normal `Container!` widgets.
132                        c = c.with_new_min_y(Px(0));
133                        if has_fill_size {
134                            c = c.with_new_max_y(vp_unit.height);
135                        }
136                    }
137                    if mode.contains(ScrollMode::HORIZONTAL) {
138                        c = c.with_unbounded_x().with_new_min_x(vp_unit.width);
139                    } else {
140                        c = c.with_new_min_x(Px(0));
141                        if has_fill_size {
142                            c = c.with_new_max_x(vp_unit.width);
143                        }
144                    }
145
146                    child_align.child_constraints(c)
147                },
148                || {
149                    if define_vp_unit {
150                        LAYOUT.with_viewport(vp_unit, || child.layout(wl))
151                    } else {
152                        child.layout(wl)
153                    }
154                },
155            );
156            if mode.contains(ScrollMode::ZOOM) {
157                content_scale = SCROLL_SCALE_VAR.get();
158                content_size.width *= content_scale;
159                content_size.height *= content_scale;
160            } else {
161                content_scale = 1.fct();
162            }
163
164            let vp_size = constraints.fill_size_or(content_size);
165            if viewport_size != vp_size {
166                viewport_size = vp_size;
167                SCROLL_VIEWPORT_SIZE_VAR.set(vp_size).unwrap();
168                WIDGET.render();
169            }
170
171            auto_hide_extra = LAYOUT.with_viewport(vp_size, || {
172                LAYOUT.with_constraints(PxConstraints2d::new_fill_size(vp_size), || {
173                    AUTO_HIDE_EXTRA_VAR.layout_dft(PxSideOffsets::new(vp_size.height, vp_size.width, vp_size.height, vp_size.width))
174                })
175            });
176            auto_hide_extra.top = auto_hide_extra.top.max(Px(0));
177            auto_hide_extra.right = auto_hide_extra.right.max(Px(0));
178            auto_hide_extra.bottom = auto_hide_extra.bottom.max(Px(0));
179            auto_hide_extra.left = auto_hide_extra.left.max(Px(0));
180
181            scroll_info().set_viewport_size(vp_size);
182
183            let align_offset = child_align.child_offset(content_size, viewport_size, LAYOUT.direction());
184
185            let mut ct_offset = PxVector::zero();
186
187            if mode.contains(ScrollMode::VERTICAL) && content_size.height > vp_size.height {
188                let v_offset = SCROLL_VERTICAL_OFFSET_VAR.get();
189                ct_offset.y = (viewport_size.height - content_size.height) * v_offset;
190            } else {
191                ct_offset.y = align_offset.y;
192            }
193            if mode.contains(ScrollMode::HORIZONTAL) && content_size.width > vp_size.width {
194                let h_offset = SCROLL_HORIZONTAL_OFFSET_VAR.get();
195                ct_offset.x = (viewport_size.width - content_size.width) * h_offset;
196            } else {
197                ct_offset.x = align_offset.x;
198            }
199
200            if ct_offset != content_offset {
201                content_offset = ct_offset;
202
203                // check if scrolled using only `render_update` to the end of the `auto_hide_extra` space.
204                let update_only_offset = (last_render_offset - content_offset).abs();
205                const OFFSET_EXTRA: Px = Px(20); // give a margin of error for widgets that render outside bounds.
206                let mut need_full_render = if update_only_offset.y < Px(0) {
207                    update_only_offset.y.abs() + OFFSET_EXTRA > auto_hide_extra.top
208                } else {
209                    update_only_offset.y + OFFSET_EXTRA > auto_hide_extra.bottom
210                };
211                if !need_full_render {
212                    need_full_render = if update_only_offset.x < Px(0) {
213                        update_only_offset.x.abs() + OFFSET_EXTRA > auto_hide_extra.left
214                    } else {
215                        update_only_offset.x + OFFSET_EXTRA > auto_hide_extra.right
216                    };
217                }
218
219                if need_full_render {
220                    // need to render more widgets, `auto_hide_extra` was reached using only `render_update`
221                    WIDGET.render();
222                } else {
223                    WIDGET.render_update();
224                }
225            }
226
227            let v_ratio = viewport_size.height.0 as f32 / content_size.height.0 as f32;
228            let h_ratio = viewport_size.width.0 as f32 / content_size.width.0 as f32;
229
230            SCROLL_VERTICAL_RATIO_VAR.set(v_ratio.fct()).unwrap();
231            SCROLL_HORIZONTAL_RATIO_VAR.set(h_ratio.fct()).unwrap();
232            SCROLL_CONTENT_SIZE_VAR.set(content_size).unwrap();
233
234            let full_size = viewport_size + joiner_size;
235
236            SCROLL_VERTICAL_CONTENT_OVERFLOWS_VAR
237                .set(mode.contains(ScrollMode::VERTICAL) && content_size.height > full_size.height)
238                .unwrap();
239            SCROLL_HORIZONTAL_CONTENT_OVERFLOWS_VAR
240                .set(mode.contains(ScrollMode::HORIZONTAL) && content_size.width > full_size.width)
241                .unwrap();
242
243            *final_size = viewport_size;
244
245            scroll_info().set_content(PxRect::new(content_offset.to_point(), content_size), content_scale);
246        }
247        UiNodeOp::Render { frame } => {
248            scroll_info().set_viewport_transform(*frame.transform());
249            last_render_offset = content_offset;
250
251            let mut culling_rect = PxBox::from_size(viewport_size);
252            culling_rect.min.y -= auto_hide_extra.top;
253            culling_rect.max.x += auto_hide_extra.right;
254            culling_rect.max.y += auto_hide_extra.bottom;
255            culling_rect.min.x -= auto_hide_extra.left;
256            let culling_rect = frame.transform().outer_transformed(culling_rect).unwrap_or(culling_rect).to_rect();
257
258            let transform = if content_scale != 1.fct() {
259                PxTransform::scale(content_scale.0, content_scale.0).then_translate(content_offset.cast())
260            } else {
261                content_offset.into()
262            };
263            frame.push_reference_frame(binding_key.into(), binding_key.bind(transform, true), true, false, |frame| {
264                frame.with_auto_hide_rect(culling_rect, |frame| {
265                    child.render(frame);
266                });
267            });
268        }
269        UiNodeOp::RenderUpdate { update } => {
270            scroll_info().set_viewport_transform(*update.transform());
271
272            let transform = if content_scale != 1.fct() {
273                PxTransform::scale(content_scale.0, content_scale.0).then_translate(content_offset.cast())
274            } else {
275                content_offset.into()
276            };
277            update.with_transform(binding_key.update(transform, true), false, |update| {
278                child.render_update(update);
279            });
280        }
281        _ => {}
282    })
283}
284
285/// Create a node that generates and presents the [vertical scrollbar].
286///
287/// [vertical scrollbar]: VERTICAL_SCROLLBAR_FN_VAR
288pub fn v_scrollbar_presenter() -> impl UiNode {
289    scrollbar_presenter(VERTICAL_SCROLLBAR_FN_VAR, Orientation::Vertical)
290}
291
292/// Create a node that generates and presents the [horizontal scrollbar].
293///
294/// [horizontal scrollbar]: HORIZONTAL_SCROLLBAR_FN_VAR
295pub fn h_scrollbar_presenter() -> impl UiNode {
296    scrollbar_presenter(HORIZONTAL_SCROLLBAR_FN_VAR, Orientation::Horizontal)
297}
298
299fn scrollbar_presenter(var: impl IntoVar<WidgetFn<ScrollBarArgs>>, orientation: Orientation) -> impl UiNode {
300    presenter(ScrollBarArgs::new(orientation), var)
301}
302
303/// Create a node that generates and presents the [scrollbar joiner].
304///
305/// [scrollbar joiner]: SCROLLBAR_JOINER_FN_VAR
306pub fn scrollbar_joiner_presenter() -> impl UiNode {
307    presenter((), SCROLLBAR_JOINER_FN_VAR)
308}
309
310/// Create a node that implements [`SCROLL_UP_CMD`], [`SCROLL_DOWN_CMD`],
311/// [`SCROLL_LEFT_CMD`] and [`SCROLL_RIGHT_CMD`] scoped on the widget.
312pub fn scroll_commands_node(child: impl UiNode) -> impl UiNode {
313    let mut up = CommandHandle::dummy();
314    let mut down = CommandHandle::dummy();
315    let mut left = CommandHandle::dummy();
316    let mut right = CommandHandle::dummy();
317
318    let mut layout_line = PxVector::zero();
319
320    match_node(child, move |child, op| match op {
321        UiNodeOp::Init => {
322            WIDGET
323                .sub_var_layout(&VERTICAL_LINE_UNIT_VAR)
324                .sub_var_layout(&HORIZONTAL_LINE_UNIT_VAR);
325
326            let scope = WIDGET.id();
327
328            up = SCROLL_UP_CMD.scoped(scope).subscribe(SCROLL.can_scroll_up().get());
329            down = SCROLL_DOWN_CMD.scoped(scope).subscribe(SCROLL.can_scroll_down().get());
330            left = SCROLL_LEFT_CMD.scoped(scope).subscribe(SCROLL.can_scroll_left().get());
331            right = SCROLL_RIGHT_CMD.scoped(scope).subscribe(SCROLL.can_scroll_right().get());
332        }
333        UiNodeOp::Deinit => {
334            child.deinit();
335
336            up = CommandHandle::dummy();
337            down = CommandHandle::dummy();
338            left = CommandHandle::dummy();
339            right = CommandHandle::dummy();
340        }
341        UiNodeOp::Update { updates } => {
342            child.update(updates);
343
344            if VERTICAL_LINE_UNIT_VAR.is_new() || HORIZONTAL_LINE_UNIT_VAR.is_new() {
345                WIDGET.layout();
346            }
347        }
348        UiNodeOp::Event { update } => {
349            child.event(update);
350
351            let scope = WIDGET.id();
352
353            if let Some(args) = SCROLL_UP_CMD.scoped(scope).on(update) {
354                args.handle_enabled(&up, |_| {
355                    let mut offset = -layout_line.y;
356                    let args = ScrollRequest::from_args(args).unwrap_or_default();
357                    if args.alternate {
358                        offset *= ALT_FACTOR_VAR.get();
359                    }
360                    SCROLL.scroll_vertical_clamp(ScrollFrom::VarTarget(offset), args.clamp.0, args.clamp.1);
361                });
362            } else if let Some(args) = SCROLL_DOWN_CMD.scoped(scope).on(update) {
363                args.handle_enabled(&down, |_| {
364                    let mut offset = layout_line.y;
365                    let args = ScrollRequest::from_args(args).unwrap_or_default();
366                    if args.alternate {
367                        offset *= ALT_FACTOR_VAR.get();
368                    }
369                    SCROLL.scroll_vertical_clamp(ScrollFrom::VarTarget(offset), args.clamp.0, args.clamp.1);
370                });
371            } else if let Some(args) = SCROLL_LEFT_CMD.scoped(scope).on(update) {
372                args.handle_enabled(&left, |_| {
373                    let mut offset = -layout_line.x;
374                    let args = ScrollRequest::from_args(args).unwrap_or_default();
375                    if args.alternate {
376                        offset *= ALT_FACTOR_VAR.get();
377                    }
378                    SCROLL.scroll_horizontal_clamp(ScrollFrom::VarTarget(offset), args.clamp.0, args.clamp.1);
379                });
380            } else if let Some(args) = SCROLL_RIGHT_CMD.scoped(scope).on(update) {
381                args.handle_enabled(&right, |_| {
382                    let mut offset = layout_line.x;
383                    let args = ScrollRequest::from_args(args).unwrap_or_default();
384                    if args.alternate {
385                        offset *= ALT_FACTOR_VAR.get();
386                    }
387                    SCROLL.scroll_horizontal_clamp(ScrollFrom::VarTarget(offset), args.clamp.0, args.clamp.1);
388                });
389            }
390        }
391        UiNodeOp::Layout { wl, final_size } => {
392            *final_size = child.layout(wl);
393
394            up.set_enabled(SCROLL.can_scroll_up().get());
395            down.set_enabled(SCROLL.can_scroll_down().get());
396            left.set_enabled(SCROLL.can_scroll_left().get());
397            right.set_enabled(SCROLL.can_scroll_right().get());
398
399            let viewport = SCROLL_VIEWPORT_SIZE_VAR.get();
400            LAYOUT.with_constraints(PxConstraints2d::new_fill_size(viewport), || {
401                layout_line = PxVector::new(
402                    HORIZONTAL_LINE_UNIT_VAR.layout_dft_x(Px(20)),
403                    VERTICAL_LINE_UNIT_VAR.layout_dft_y(Px(20)),
404                );
405            });
406        }
407        _ => {}
408    })
409}
410
411/// Create a node that implements [`PAGE_UP_CMD`], [`PAGE_DOWN_CMD`],
412/// [`PAGE_LEFT_CMD`] and [`PAGE_RIGHT_CMD`] scoped on the widget.
413pub fn page_commands_node(child: impl UiNode) -> impl UiNode {
414    let mut up = CommandHandle::dummy();
415    let mut down = CommandHandle::dummy();
416    let mut left = CommandHandle::dummy();
417    let mut right = CommandHandle::dummy();
418
419    let mut layout_page = PxVector::zero();
420
421    match_node(child, move |child, op| match op {
422        UiNodeOp::Init => {
423            WIDGET
424                .sub_var_layout(&VERTICAL_PAGE_UNIT_VAR)
425                .sub_var_layout(&HORIZONTAL_PAGE_UNIT_VAR);
426
427            let scope = WIDGET.id();
428
429            up = PAGE_UP_CMD.scoped(scope).subscribe(SCROLL.can_scroll_up().get());
430            down = PAGE_DOWN_CMD.scoped(scope).subscribe(SCROLL.can_scroll_down().get());
431            left = PAGE_LEFT_CMD.scoped(scope).subscribe(SCROLL.can_scroll_left().get());
432            right = PAGE_RIGHT_CMD.scoped(scope).subscribe(SCROLL.can_scroll_right().get());
433        }
434        UiNodeOp::Deinit => {
435            child.deinit();
436
437            up = CommandHandle::dummy();
438            down = CommandHandle::dummy();
439            left = CommandHandle::dummy();
440            right = CommandHandle::dummy();
441        }
442        UiNodeOp::Event { update } => {
443            child.event(update);
444
445            let scope = WIDGET.id();
446
447            if let Some(args) = PAGE_UP_CMD.scoped(scope).on(update) {
448                args.handle_enabled(&up, |_| {
449                    let mut offset = -layout_page.y;
450                    let args = ScrollRequest::from_args(args).unwrap_or_default();
451                    if args.alternate {
452                        offset *= ALT_FACTOR_VAR.get();
453                    }
454                    SCROLL.scroll_vertical_clamp(ScrollFrom::VarTarget(offset), args.clamp.0, args.clamp.1);
455                });
456            } else if let Some(args) = PAGE_DOWN_CMD.scoped(scope).on(update) {
457                args.handle_enabled(&down, |_| {
458                    let mut offset = layout_page.y;
459                    let args = ScrollRequest::from_args(args).unwrap_or_default();
460                    if args.alternate {
461                        offset *= ALT_FACTOR_VAR.get();
462                    }
463                    SCROLL.scroll_vertical_clamp(ScrollFrom::VarTarget(offset), args.clamp.0, args.clamp.1);
464                });
465            } else if let Some(args) = PAGE_LEFT_CMD.scoped(scope).on(update) {
466                args.handle_enabled(&left, |_| {
467                    let mut offset = -layout_page.x;
468                    let args = ScrollRequest::from_args(args).unwrap_or_default();
469                    if args.alternate {
470                        offset *= ALT_FACTOR_VAR.get();
471                    }
472                    SCROLL.scroll_horizontal_clamp(ScrollFrom::VarTarget(offset), args.clamp.0, args.clamp.1);
473                });
474            } else if let Some(args) = PAGE_RIGHT_CMD.scoped(scope).on(update) {
475                args.handle_enabled(&right, |_| {
476                    let mut offset = layout_page.x;
477                    let args = ScrollRequest::from_args(args).unwrap_or_default();
478                    if args.alternate {
479                        offset *= ALT_FACTOR_VAR.get();
480                    }
481                    SCROLL.scroll_horizontal_clamp(ScrollFrom::VarTarget(offset), args.clamp.0, args.clamp.1);
482                });
483            }
484        }
485        UiNodeOp::Layout { wl, final_size } => {
486            *final_size = child.layout(wl);
487
488            up.set_enabled(SCROLL.can_scroll_up().get());
489            down.set_enabled(SCROLL.can_scroll_down().get());
490            left.set_enabled(SCROLL.can_scroll_left().get());
491            right.set_enabled(SCROLL.can_scroll_right().get());
492
493            let viewport = SCROLL_VIEWPORT_SIZE_VAR.get();
494            LAYOUT.with_constraints(PxConstraints2d::new_fill_size(viewport), || {
495                layout_page = PxVector::new(
496                    HORIZONTAL_PAGE_UNIT_VAR.layout_dft_x(Px(20)),
497                    VERTICAL_PAGE_UNIT_VAR.layout_dft_y(Px(20)),
498                );
499            });
500        }
501        _ => {}
502    })
503}
504
505/// Create a node that implements [`SCROLL_TO_TOP_CMD`], [`SCROLL_TO_BOTTOM_CMD`],
506/// [`SCROLL_TO_LEFTMOST_CMD`] and [`SCROLL_TO_RIGHTMOST_CMD`] scoped on the widget.
507pub fn scroll_to_edge_commands_node(child: impl UiNode) -> impl UiNode {
508    let mut top = CommandHandle::dummy();
509    let mut bottom = CommandHandle::dummy();
510    let mut leftmost = CommandHandle::dummy();
511    let mut rightmost = CommandHandle::dummy();
512
513    match_node(child, move |child, op| match op {
514        UiNodeOp::Init => {
515            let scope = WIDGET.id();
516
517            top = SCROLL_TO_TOP_CMD.scoped(scope).subscribe(SCROLL.can_scroll_up().get());
518            bottom = SCROLL_TO_BOTTOM_CMD.scoped(scope).subscribe(SCROLL.can_scroll_down().get());
519            leftmost = SCROLL_TO_LEFTMOST_CMD.scoped(scope).subscribe(SCROLL.can_scroll_left().get());
520            rightmost = SCROLL_TO_RIGHTMOST_CMD.scoped(scope).subscribe(SCROLL.can_scroll_right().get());
521        }
522        UiNodeOp::Deinit => {
523            child.deinit();
524
525            top = CommandHandle::dummy();
526            bottom = CommandHandle::dummy();
527            leftmost = CommandHandle::dummy();
528            rightmost = CommandHandle::dummy();
529        }
530        UiNodeOp::Layout { .. } => {
531            top.set_enabled(SCROLL.can_scroll_up().get());
532            bottom.set_enabled(SCROLL.can_scroll_down().get());
533            leftmost.set_enabled(SCROLL.can_scroll_left().get());
534            rightmost.set_enabled(SCROLL.can_scroll_right().get());
535        }
536        UiNodeOp::Event { update } => {
537            child.event(update);
538
539            let scope = WIDGET.id();
540
541            if let Some(args) = SCROLL_TO_TOP_CMD.scoped(scope).on(update) {
542                args.handle_enabled(&top, |_| {
543                    SCROLL.chase_vertical(|_| 0.fct());
544                });
545            } else if let Some(args) = SCROLL_TO_BOTTOM_CMD.scoped(scope).on(update) {
546                args.handle_enabled(&bottom, |_| {
547                    SCROLL.chase_vertical(|_| 1.fct());
548                });
549            } else if let Some(args) = SCROLL_TO_LEFTMOST_CMD.scoped(scope).on(update) {
550                args.handle_enabled(&leftmost, |_| {
551                    SCROLL.chase_horizontal(|_| 0.fct());
552                });
553            } else if let Some(args) = SCROLL_TO_RIGHTMOST_CMD.scoped(scope).on(update) {
554                args.handle_enabled(&rightmost, |_| {
555                    SCROLL.chase_horizontal(|_| 1.fct());
556                });
557            }
558        }
559        _ => {}
560    })
561}
562
563/// Create a node that implements [`ZOOM_IN_CMD`], [`ZOOM_OUT_CMD`], [`ZOOM_TO_FIT_CMD`],
564/// and [`ZOOM_RESET_CMD`] scoped on the widget.
565pub fn zoom_commands_node(child: impl UiNode) -> impl UiNode {
566    let mut zoom_in = CommandHandle::dummy();
567    let mut zoom_out = CommandHandle::dummy();
568    let mut zoom_to_fit = CommandHandle::dummy();
569    let mut zoom_reset = CommandHandle::dummy();
570
571    let mut scale_delta = 0.fct();
572    let mut origin = Point::default();
573
574    fn fit_scale() -> Factor {
575        let scroll = WIDGET.info().scroll_info().unwrap();
576        let viewport = (scroll.viewport_size() + scroll.joiner_size()).to_f32(); // viewport without scrollbars
577        let content = scroll.content().size.to_f32() / scroll.zoom_scale();
578        (viewport.width / content.width).min(viewport.height / content.height).fct()
579    }
580
581    match_node(child, move |child, op| match op {
582        UiNodeOp::Init => {
583            let scope = WIDGET.id();
584
585            zoom_in = ZOOM_IN_CMD.scoped(scope).subscribe(SCROLL.can_zoom_in());
586            zoom_out = ZOOM_OUT_CMD.scoped(scope).subscribe(SCROLL.can_zoom_out());
587            zoom_to_fit = ZOOM_TO_FIT_CMD.scoped(scope).subscribe(true);
588            zoom_reset = ZOOM_RESET_CMD.scoped(scope).subscribe(true);
589        }
590        UiNodeOp::Deinit => {
591            child.deinit();
592
593            zoom_in = CommandHandle::dummy();
594            zoom_out = CommandHandle::dummy();
595            zoom_to_fit = CommandHandle::dummy();
596            zoom_reset = CommandHandle::dummy();
597        }
598        UiNodeOp::Event { update } => {
599            child.event(update);
600
601            let scope = WIDGET.id();
602
603            if let Some(args) = ZOOM_IN_CMD.scoped(scope).on(update) {
604                args.handle_enabled(&zoom_in, |args| {
605                    origin = args.param::<Point>().cloned().unwrap_or_default();
606                    scale_delta += ZOOM_WHEEL_UNIT_VAR.get();
607
608                    WIDGET.layout();
609                });
610            } else if let Some(args) = ZOOM_OUT_CMD.scoped(scope).on(update) {
611                args.handle_enabled(&zoom_out, |_| {
612                    origin = args.param::<Point>().cloned().unwrap_or_default();
613                    scale_delta -= ZOOM_WHEEL_UNIT_VAR.get();
614
615                    WIDGET.layout();
616                });
617            } else if let Some(args) = ZOOM_TO_FIT_CMD.scoped(scope).on(update) {
618                args.handle_enabled(&zoom_to_fit, |_| {
619                    let scale = fit_scale();
620                    SCROLL.chase_zoom(|_| scale);
621                });
622            } else if let Some(args) = ZOOM_RESET_CMD.scoped(scope).on(update) {
623                args.handle_enabled(&zoom_reset, |_| {
624                    SCROLL.chase_zoom(|_| 1.fct());
625                    scale_delta = 0.fct();
626                });
627            }
628        }
629        UiNodeOp::Layout { wl, final_size } => {
630            *final_size = child.layout(wl);
631
632            zoom_in.set_enabled(SCROLL.can_zoom_in());
633            zoom_out.set_enabled(SCROLL.can_zoom_out());
634            let scale = SCROLL.zoom_scale().get();
635            zoom_to_fit.set_enabled(scale != fit_scale());
636            zoom_reset.set_enabled(scale != 1.fct());
637
638            if scale_delta != 0.fct() {
639                let scroll_info = WIDGET.info().scroll_info().unwrap();
640                let viewport_size = scroll_info.viewport_size();
641
642                let default = PxPoint::new(
643                    Px(0),
644                    match LAYOUT.direction() {
645                        LayoutDirection::LTR => Px(0),
646                        LayoutDirection::RTL => viewport_size.width,
647                    },
648                );
649                let center_in_viewport =
650                    LAYOUT.with_constraints(PxConstraints2d::new_fill_size(viewport_size), || origin.layout_dft(default));
651
652                SCROLL.zoom(|f| f + scale_delta, center_in_viewport);
653                scale_delta = 0.fct();
654            }
655        }
656        _ => {}
657    })
658}
659
660/// Create a node that implements [`SCROLL_TO_CMD`] scoped on the widget and scroll to focused.
661pub fn scroll_to_node(child: impl UiNode) -> impl UiNode {
662    let mut _handle = CommandHandle::dummy();
663    let mut scroll_to = None;
664    let mut scroll_to_from_cmd = false;
665
666    match_node(child, move |child, op| match op {
667        UiNodeOp::Init => {
668            _handle = SCROLL_TO_CMD.scoped(WIDGET.id()).subscribe(true);
669            WIDGET.sub_event(&FOCUS_CHANGED_EVENT);
670        }
671        UiNodeOp::Deinit => {
672            _handle = CommandHandle::dummy();
673        }
674        UiNodeOp::Event { update } => {
675            let self_id = WIDGET.id();
676            if let Some(args) = FOCUS_CHANGED_EVENT.on(update) {
677                if let Some(path) = &args.new_focus {
678                    if (scroll_to.is_none() || !scroll_to_from_cmd)
679                        && path.contains(self_id)
680                        && path.widget_id() != self_id
681                        && !args.is_enabled_change()
682                        && !args.is_highlight_changed()
683                    {
684                        // focus move inside.
685                        if let Some(mode) = SCROLL_TO_FOCUSED_MODE_VAR.get() {
686                            // scroll_to_focused enabled
687
688                            let can_scroll_v = SCROLL.can_scroll_vertical().get();
689                            let can_scroll_h = SCROLL.can_scroll_horizontal().get();
690                            if can_scroll_v || can_scroll_h {
691                                // auto scroll if can scroll AND focus did not change by a click on the
692                                // Scroll! scope restoring focus back to a child AND the target is not already visible.
693
694                                let tree = WINDOW.info();
695                                if let Some(mut target) = tree.get(path.widget_id()) {
696                                    let mut is_focus_restore = false;
697
698                                    if args.prev_focus.as_ref().map(|p| p.widget_id()) == Some(self_id) {
699                                        // focus moved to child from Scroll! scope (self)
700
701                                        // Check if not caused by a click on a non-focusable child:
702                                        // - On a click in a non-focusable child the focus goes back to the Scroll!.
703                                        // - The Scroll! is a focus scope, it restores the focus to the previous focused child.
704                                        // - The clicked non-focusable becomes the `navigation_origin`.
705                                        // - We don't want to scroll back to the focusable child in this case.
706                                        if let Some(id) = FOCUS.navigation_origin().get() {
707                                            if let Some(origin) = tree.get(id) {
708                                                for a in origin.ancestors() {
709                                                    if a.id() == self_id {
710                                                        is_focus_restore = true;
711                                                        break;
712                                                    }
713                                                }
714                                            }
715                                        }
716                                    }
717
718                                    if !is_focus_restore {
719                                        for a in target.ancestors() {
720                                            if a.is_scroll() {
721                                                if a.id() == self_id {
722                                                    break;
723                                                } else {
724                                                    // actually focus move inside an inner scroll,
725                                                    // the inner-most scroll scrolls to the target,
726                                                    // the outer scrolls scroll to the child scroll.
727                                                    target = a;
728                                                }
729                                            }
730                                        }
731
732                                        // check if widget is not large and already visible.
733                                        let mut scroll = true;
734                                        let scroll_bounds = tree.get(self_id).unwrap().inner_bounds();
735                                        let target_bounds = target.inner_bounds();
736                                        if let Some(r) = scroll_bounds.intersection(&target_bounds) {
737                                            let is_large_visible_v =
738                                                can_scroll_v && r.height() > Px(20) && target_bounds.height() > scroll_bounds.height();
739                                            let is_large_visible_h =
740                                                can_scroll_h && r.width() > Px(20) && target_bounds.width() > scroll_bounds.width();
741
742                                            scroll = !is_large_visible_v && !is_large_visible_h;
743                                        }
744                                        if scroll {
745                                            scroll_to = Some((Rect::from(target_bounds), mode, None, false));
746                                            WIDGET.layout();
747                                        }
748                                    }
749                                }
750                            }
751                        }
752                    }
753                }
754            } else if let Some(args) = SCROLL_TO_CMD.scoped(self_id).on(update) {
755                // event send to us and enabled
756                if let Some(request) = ScrollToRequest::from_args(args) {
757                    // has unhandled request
758                    let tree = WINDOW.info();
759                    match request.target {
760                        ScrollToTarget::Descendant(target) => {
761                            if let Some(target) = tree.get(target) {
762                                // target exists
763                                if let Some(us) = target.ancestors().find(|w| w.id() == self_id) {
764                                    // target is descendant
765                                    if us.is_scroll() {
766                                        scroll_to = Some((Rect::from(target.inner_bounds()), request.mode, request.zoom, false));
767                                        scroll_to_from_cmd = true;
768                                        WIDGET.layout();
769
770                                        args.propagation().stop();
771                                    }
772                                }
773                            }
774                        }
775                        ScrollToTarget::Rect(rect) => {
776                            scroll_to = Some((rect, request.mode, request.zoom, true));
777                            scroll_to_from_cmd = true;
778                            WIDGET.layout();
779
780                            args.propagation().stop();
781                        }
782                    }
783                }
784            }
785        }
786        UiNodeOp::Layout { wl, final_size } => {
787            *final_size = child.layout(wl);
788
789            if let Some((bounds, mode, mut zoom, in_content)) = scroll_to.take() {
790                scroll_to_from_cmd = false;
791                let tree = WINDOW.info();
792                let us = tree.get(WIDGET.id()).unwrap();
793
794                if let Some(scroll_info) = us.scroll_info() {
795                    if let Some(s) = &mut zoom {
796                        *s = s.clamp(MIN_ZOOM_VAR.get(), MAX_ZOOM_VAR.get());
797                    }
798
799                    let rendered_content = scroll_info.content();
800
801                    let mut bounds = {
802                        let content = rendered_content;
803                        let mut rect = LAYOUT.with_constraints(PxConstraints2d::new_exact_size(content.size), || bounds.layout());
804                        if in_content {
805                            rect.origin += content.origin.to_vector();
806                        }
807                        rect
808                    };
809
810                    // remove viewport transform
811                    bounds = scroll_info
812                        .viewport_transform()
813                        .inverse()
814                        .and_then(|t| t.outer_transformed(bounds.to_box2d()))
815                        .map(|b| b.to_rect())
816                        .unwrap_or(bounds);
817
818                    let current_bounds = bounds;
819
820                    // remove offset
821                    let rendered_offset = rendered_content.origin.to_vector();
822                    bounds.origin -= rendered_offset;
823
824                    // replace scale
825                    let rendered_scale = SCROLL.rendered_zoom_scale();
826                    if let Some(s) = zoom {
827                        let s = s / rendered_scale;
828                        bounds.origin *= s;
829                        bounds.size *= s;
830                    }
831                    // target bounds is now in the content space at future scale
832
833                    let viewport_size = scroll_info.viewport_size();
834
835                    let mut offset = PxVector::splat(Px::MAX);
836
837                    match mode {
838                        ScrollToMode::Minimal { margin } => {
839                            // add minimal margin at new scale to target bounds
840                            let scaled_margin = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(bounds.size), || margin.layout());
841                            let bounds = inflate_margin(bounds, scaled_margin);
842
843                            // add minimal margin, at current scale to the current bounds
844                            let cur_margin = if zoom.is_some() {
845                                LAYOUT.with_constraints(PxConstraints2d::new_fill_size(current_bounds.size), || margin.layout())
846                            } else {
847                                scaled_margin
848                            };
849                            let current_bounds = inflate_margin(current_bounds, cur_margin);
850
851                            // vertical scroll
852                            if bounds.size.height < viewport_size.height {
853                                if current_bounds.origin.y < Px(0) {
854                                    // scroll up
855                                    offset.y = bounds.origin.y;
856                                } else if current_bounds.max_y() > viewport_size.height {
857                                    // scroll down
858                                    offset.y = bounds.max_y() - viewport_size.height;
859                                } else if zoom.is_some() {
860                                    // scale around center
861                                    let center_in_vp = current_bounds.center().y;
862                                    let center = bounds.center().y;
863                                    offset.y = center - center_in_vp;
864
865                                    // offset minimal if needed
866                                    let mut bounds_final = bounds;
867                                    bounds_final.origin.y -= offset.y;
868                                    if bounds_final.origin.y < Px(0) {
869                                        offset.y = bounds.origin.y;
870                                    } else if bounds_final.max_y() > viewport_size.height {
871                                        offset.y = bounds.max_y() - viewport_size.height;
872                                    }
873                                }
874                            } else {
875                                // center
876                                offset.y = viewport_size.height / Px(2) - bounds.center().y;
877                            };
878
879                            // horizontal scroll
880                            if bounds.size.width < viewport_size.width {
881                                if current_bounds.origin.x < Px(0) {
882                                    // scroll left
883                                    offset.x = bounds.origin.x;
884                                } else if current_bounds.max_x() > viewport_size.width {
885                                    // scroll right
886                                    offset.x = bounds.max_x() - viewport_size.width;
887                                } else if zoom.is_some() {
888                                    // scale around center
889                                    let center_in_vp = current_bounds.center().x;
890                                    let center = bounds.center().x;
891                                    offset.x = center - center_in_vp;
892
893                                    // offset minimal if needed
894                                    let mut bounds_final = bounds;
895                                    bounds_final.origin.x -= offset.x;
896                                    if bounds_final.origin.x < Px(0) {
897                                        offset.x = bounds.origin.x;
898                                    } else if bounds_final.max_x() > viewport_size.width {
899                                        offset.x = bounds.max_x() - viewport_size.width;
900                                    }
901                                }
902                            } else {
903                                // center
904                                offset.x = viewport_size.width / Px(2) - bounds.center().x;
905                            };
906                        }
907                        ScrollToMode::Center {
908                            widget_point,
909                            scroll_point,
910                        } => {
911                            // find the two points
912                            let default = (bounds.size / Px(2)).to_vector().to_point();
913                            let widget_point =
914                                LAYOUT.with_constraints(PxConstraints2d::new_fill_size(bounds.size), || widget_point.layout_dft(default));
915                            let default = (viewport_size / Px(2)).to_vector().to_point();
916                            let scroll_point =
917                                LAYOUT.with_constraints(PxConstraints2d::new_fill_size(viewport_size), || scroll_point.layout_dft(default));
918
919                            offset = (widget_point + bounds.origin.to_vector()) - scroll_point;
920                        }
921                    }
922
923                    // scroll range
924                    let mut content_size = SCROLL.content_size().get();
925                    if let Some(scale) = zoom {
926                        content_size *= scale / rendered_scale;
927                    }
928                    let max_scroll = content_size - viewport_size;
929
930                    // apply
931                    if let Some(scale) = zoom {
932                        SCROLL.chase_zoom(|_| scale);
933                    }
934                    if offset.y != Px::MAX && max_scroll.height > Px(0) {
935                        let offset_y = offset.y.0 as f32 / max_scroll.height.0 as f32;
936                        SCROLL.chase_vertical(|_| offset_y.fct());
937                    }
938                    if offset.x != Px::MAX && max_scroll.width > Px(0) {
939                        let offset_x = offset.x.0 as f32 / max_scroll.width.0 as f32;
940                        SCROLL.chase_horizontal(|_| offset_x.fct());
941                    }
942                }
943            }
944        }
945        _ => {}
946    })
947}
948fn inflate_margin(mut r: PxRect, margin: PxSideOffsets) -> PxRect {
949    r.origin.x -= margin.left;
950    r.origin.y -= margin.top;
951    r.size.width += margin.horizontal();
952    r.size.height += margin.vertical();
953    r
954}
955
956/// Create a node that implements scroll by touch gestures for the widget.
957pub fn scroll_touch_node(child: impl UiNode) -> impl UiNode {
958    let mut applied_offset = PxVector::zero();
959    match_node(child, move |child, op| match op {
960        UiNodeOp::Init => {
961            WIDGET.sub_event(&TOUCH_TRANSFORM_EVENT);
962        }
963        UiNodeOp::Event { update } => {
964            child.event(update);
965
966            if let Some(args) = TOUCH_TRANSFORM_EVENT.on_unhandled(update) {
967                let mut pending_translate = true;
968
969                if SCROLL.mode().get().contains(ScrollMode::ZOOM) {
970                    let f = args.scale();
971                    if f != 1.fct() {
972                        let center = WIDGET
973                            .info()
974                            .scroll_info()
975                            .unwrap()
976                            .viewport_transform()
977                            .inverse()
978                            .and_then(|t| t.transform_point_f32(args.latest_info.center))
979                            .unwrap_or(args.latest_info.center);
980
981                        SCROLL.zoom_touch(args.phase, f, center);
982                        pending_translate = false;
983                    }
984                }
985
986                if pending_translate {
987                    let new_offset = args.translation().cast::<Px>();
988                    let delta = new_offset - applied_offset;
989                    applied_offset = new_offset;
990
991                    if delta.y != Px(0) {
992                        SCROLL.scroll_vertical_touch(-delta.y);
993                    }
994                    if delta.x != Px(0) {
995                        SCROLL.scroll_horizontal_touch(-delta.x);
996                    }
997                }
998
999                match args.phase {
1000                    TouchPhase::Start => {}
1001                    TouchPhase::Move => {}
1002                    TouchPhase::End => {
1003                        applied_offset = PxVector::zero();
1004
1005                        let friction = Dip::new(1000);
1006                        let mode = SCROLL.mode().get();
1007                        if mode.contains(ScrollMode::VERTICAL) {
1008                            let (delta, duration) = args.translation_inertia_y(friction);
1009
1010                            if delta != Px(0) {
1011                                SCROLL.scroll_vertical_touch_inertia(-delta, duration);
1012                            }
1013                            SCROLL.clear_vertical_overscroll();
1014                        }
1015                        if mode.contains(ScrollMode::HORIZONTAL) {
1016                            let (delta, duration) = args.translation_inertia_x(friction);
1017                            if delta != Px(0) {
1018                                SCROLL.scroll_horizontal_touch_inertia(-delta, duration);
1019                            }
1020                            SCROLL.clear_horizontal_overscroll();
1021                        }
1022                    }
1023                    TouchPhase::Cancel => {
1024                        applied_offset = PxVector::zero();
1025
1026                        SCROLL.clear_vertical_overscroll();
1027                        SCROLL.clear_horizontal_overscroll();
1028                    }
1029                }
1030            }
1031        }
1032        _ => {}
1033    })
1034}
1035
1036/// Create a node that implements scroll-wheel handling for the widget.
1037pub fn scroll_wheel_node(child: impl UiNode) -> impl UiNode {
1038    let mut offset = Vector::zero();
1039    let mut scale_delta = 0.fct();
1040    let mut scale_position = DipPoint::zero();
1041
1042    match_node(child, move |child, op| match op {
1043        UiNodeOp::Init => {
1044            WIDGET.sub_event(&MOUSE_WHEEL_EVENT);
1045        }
1046        UiNodeOp::Event { update } => {
1047            child.event(update);
1048
1049            if let Some(args) = MOUSE_WHEEL_EVENT.on_unhandled(update) {
1050                if let Some(delta) = args.scroll_delta(ALT_FACTOR_VAR.get()) {
1051                    match delta {
1052                        MouseScrollDelta::LineDelta(x, y) => {
1053                            let scroll_x = if x > 0.0 {
1054                                SCROLL.can_scroll_left().get()
1055                            } else if x < 0.0 {
1056                                SCROLL.can_scroll_right().get()
1057                            } else {
1058                                false
1059                            };
1060                            let scroll_y = if y > 0.0 {
1061                                SCROLL.can_scroll_up().get()
1062                            } else if y < 0.0 {
1063                                SCROLL.can_scroll_down().get()
1064                            } else {
1065                                false
1066                            };
1067
1068                            if scroll_x || scroll_y {
1069                                args.propagation().stop();
1070
1071                                if scroll_x {
1072                                    offset.x -= HORIZONTAL_WHEEL_UNIT_VAR.get() * x.fct();
1073                                }
1074                                if scroll_y {
1075                                    offset.y -= VERTICAL_WHEEL_UNIT_VAR.get() * y.fct();
1076                                }
1077                            }
1078                        }
1079                        MouseScrollDelta::PixelDelta(x, y) => {
1080                            let scroll_x = if x > 0.0 {
1081                                SCROLL.can_scroll_left().get()
1082                            } else if x < 0.0 {
1083                                SCROLL.can_scroll_right().get()
1084                            } else {
1085                                false
1086                            };
1087                            let scroll_y = if y > 0.0 {
1088                                SCROLL.can_scroll_up().get()
1089                            } else if y < 0.0 {
1090                                SCROLL.can_scroll_down().get()
1091                            } else {
1092                                false
1093                            };
1094
1095                            if scroll_x || scroll_y {
1096                                args.propagation().stop();
1097
1098                                if scroll_x {
1099                                    offset.x -= x.px();
1100                                }
1101                                if scroll_y {
1102                                    offset.y -= y.px();
1103                                }
1104                            }
1105                        }
1106                    }
1107
1108                    WIDGET.layout();
1109                } else if let Some(delta) = args.zoom_delta() {
1110                    if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
1111                        return;
1112                    }
1113
1114                    let delta = match delta {
1115                        MouseScrollDelta::LineDelta(x, y) => {
1116                            if y.abs() > x.abs() {
1117                                ZOOM_WHEEL_UNIT_VAR.get() * y.fct()
1118                            } else {
1119                                ZOOM_WHEEL_UNIT_VAR.get() * x.fct()
1120                            }
1121                        }
1122                        MouseScrollDelta::PixelDelta(x, y) => {
1123                            if y.abs() > x.abs() {
1124                                // 1% per "pixel".
1125                                0.001.fct() * y.fct()
1126                            } else {
1127                                0.001.fct() * x.fct()
1128                            }
1129                        }
1130                    };
1131
1132                    let apply = if delta > 0.fct() {
1133                        SCROLL.can_zoom_in()
1134                    } else if delta < 0.fct() {
1135                        SCROLL.can_zoom_out()
1136                    } else {
1137                        false
1138                    };
1139
1140                    if apply {
1141                        scale_delta += delta;
1142                        scale_position = args.position;
1143                        WIDGET.layout();
1144                    }
1145                }
1146            }
1147        }
1148        UiNodeOp::Layout { wl, final_size } => {
1149            *final_size = child.layout(wl);
1150
1151            if offset != Vector::zero() {
1152                let viewport = SCROLL_VIEWPORT_SIZE_VAR.get();
1153
1154                LAYOUT.with_constraints(PxConstraints2d::new_fill_size(viewport), || {
1155                    let o = offset.layout_dft(viewport.to_vector());
1156                    offset = Vector::zero();
1157
1158                    if o.y != Px(0) {
1159                        SCROLL.scroll_vertical(ScrollFrom::VarTarget(o.y));
1160                    }
1161                    if o.x != Px(0) {
1162                        SCROLL.scroll_horizontal(ScrollFrom::VarTarget(o.x));
1163                    }
1164                });
1165            }
1166
1167            if scale_delta != 0.fct() {
1168                let scroll_info = WIDGET.info().scroll_info().unwrap();
1169                let default = scale_position.to_px(LAYOUT.scale_factor());
1170                let default = scroll_info
1171                    .viewport_transform()
1172                    .inverse()
1173                    .and_then(|t| t.transform_point(default))
1174                    .unwrap_or(default);
1175
1176                let viewport_size = scroll_info.viewport_size();
1177                let center_in_viewport = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(viewport_size), || {
1178                    ZOOM_WHEEL_ORIGIN_VAR.layout_dft(default)
1179                });
1180
1181                SCROLL.zoom(|f| f + scale_delta, center_in_viewport);
1182                scale_delta = 0.fct();
1183            }
1184        }
1185        _ => {}
1186    })
1187}
1188
1189/// Overscroll visual indicator.
1190pub fn overscroll_node(child: impl UiNode) -> impl UiNode {
1191    let mut v_rect = PxRect::zero();
1192    let mut v_center = PxPoint::zero();
1193    let mut v_radius_w = Px(0);
1194
1195    let mut h_rect = PxRect::zero();
1196    let mut h_center = PxPoint::zero();
1197    let mut h_radius_h = Px(0);
1198
1199    match_node(child, move |c, op| match op {
1200        UiNodeOp::Init => {
1201            WIDGET
1202                .sub_var_layout(&OVERSCROLL_VERTICAL_OFFSET_VAR)
1203                .sub_var_layout(&OVERSCROLL_HORIZONTAL_OFFSET_VAR);
1204        }
1205        UiNodeOp::Layout { final_size, wl } => {
1206            *final_size = c.layout(wl);
1207
1208            let mut new_v_rect = PxRect::zero();
1209            let v = OVERSCROLL_VERTICAL_OFFSET_VAR.get();
1210            if v < 0.fct() {
1211                // overscroll top
1212                new_v_rect.size = *final_size;
1213                new_v_rect.size.height *= v.abs() / 10.fct();
1214                v_center.y = Px(0);
1215            } else if v > 0.fct() {
1216                // overscroll bottom
1217                new_v_rect.size = *final_size;
1218                new_v_rect.size.height *= v.abs() / 10.fct();
1219                new_v_rect.origin.y = final_size.height - new_v_rect.size.height;
1220                v_center.y = new_v_rect.size.height;
1221            }
1222
1223            let mut new_h_rect = PxRect::zero();
1224            let h = OVERSCROLL_HORIZONTAL_OFFSET_VAR.get();
1225            if h < 0.fct() {
1226                // overscroll left
1227                new_h_rect.size = *final_size;
1228                new_h_rect.size.width *= h.abs() / 10.fct();
1229                h_center.x = Px(0);
1230            } else if h > 0.fct() {
1231                // overscroll right
1232                new_h_rect.size = *final_size;
1233                new_h_rect.size.width *= h.abs() / 10.fct();
1234                new_h_rect.origin.x = final_size.width - new_h_rect.size.width;
1235                h_center.x = new_h_rect.size.width;
1236            }
1237
1238            if new_v_rect != v_rect {
1239                v_rect = new_v_rect;
1240                // 50%
1241                v_center.x = v_rect.size.width / Px(2);
1242                // 110%
1243                let radius = v_center.x;
1244                v_radius_w = radius + radius * 0.1;
1245
1246                WIDGET.render();
1247            }
1248            if new_h_rect != h_rect {
1249                h_rect = new_h_rect;
1250                h_center.y = h_rect.size.height / Px(2);
1251                let radius = h_center.y;
1252                h_radius_h = radius + radius * 0.1;
1253                WIDGET.render();
1254            }
1255        }
1256        UiNodeOp::Render { frame } => {
1257            c.render(frame);
1258
1259            let stops = |color| {
1260                [
1261                    RenderGradientStop { offset: 0.0, color },
1262                    RenderGradientStop { offset: 0.99, color },
1263                    RenderGradientStop {
1264                        offset: 1.0,
1265                        color: {
1266                            let mut c = color;
1267                            c.alpha = 0.0;
1268                            c
1269                        },
1270                    },
1271                ]
1272            };
1273
1274            frame.with_auto_hit_test(false, |frame| {
1275                if !v_rect.size.is_empty() {
1276                    let mut color: Rgba = OVERSCROLL_COLOR_VAR.get();
1277                    color.alpha *= (OVERSCROLL_VERTICAL_OFFSET_VAR.get().abs().0).min(1.0);
1278                    let stops = stops(color);
1279
1280                    let mut radius = v_rect.size;
1281                    radius.width = v_radius_w;
1282                    frame.push_radial_gradient(
1283                        v_rect,
1284                        v_center,
1285                        radius,
1286                        &stops,
1287                        ExtendMode::Clamp.into(),
1288                        PxPoint::zero(),
1289                        v_rect.size,
1290                        PxSize::zero(),
1291                    );
1292                }
1293                if !h_rect.size.is_empty() {
1294                    let mut color: Rgba = OVERSCROLL_COLOR_VAR.get();
1295                    color.alpha *= (OVERSCROLL_HORIZONTAL_OFFSET_VAR.get().abs().0).min(1.0);
1296                    let stops = stops(color);
1297
1298                    let mut radius = h_rect.size;
1299                    radius.height = h_radius_h;
1300                    frame.push_radial_gradient(
1301                        h_rect,
1302                        h_center,
1303                        radius,
1304                        &stops,
1305                        ExtendMode::Clamp.into(),
1306                        PxPoint::zero(),
1307                        h_rect.size,
1308                        PxSize::zero(),
1309                    );
1310                }
1311            });
1312        }
1313        _ => {}
1314    })
1315}
1316
1317/// Create a node that converts [`ACCESS_SCROLL_EVENT`] to command requests.
1318///
1319/// [`ACCESS_SCROLL_EVENT`]: zng_app::access::ACCESS_SCROLL_EVENT
1320pub fn access_scroll_node(child: impl UiNode) -> impl UiNode {
1321    match_node(child, move |c, op| match op {
1322        UiNodeOp::Init => {
1323            WIDGET.sub_event(&ACCESS_SCROLL_EVENT);
1324        }
1325        UiNodeOp::Event { update } => {
1326            c.event(update);
1327
1328            if let Some(args) = ACCESS_SCROLL_EVENT.on_unhandled(update) {
1329                use zng_app::access::ScrollCmd::*;
1330
1331                let id = WIDGET.id();
1332                if args.widget_id == id {
1333                    match args.command {
1334                        PageUp => PAGE_UP_CMD.scoped(id).notify(),
1335                        PageDown => PAGE_DOWN_CMD.scoped(id).notify(),
1336                        PageLeft => PAGE_LEFT_CMD.scoped(id).notify(),
1337                        PageRight => PAGE_RIGHT_CMD.scoped(id).notify(),
1338                        ScrollToRect(rect) => SCROLL_TO_CMD.scoped(id).notify_param(Rect::from(rect)),
1339
1340                        ScrollTo => {
1341                            // parent scroll handles this
1342                            return;
1343                        }
1344                    }
1345                    args.propagation().stop();
1346                } else {
1347                    match args.command {
1348                        ScrollTo => super::cmd::scroll_to(args.widget_id, ScrollToMode::minimal(10)),
1349                        ScrollToRect(rect) => super::cmd::scroll_to(args.widget_id, ScrollToMode::minimal_rect(rect)),
1350                        _ => return,
1351                    }
1352                    args.propagation().stop();
1353                }
1354            }
1355        }
1356        _ => {}
1357    })
1358}
1359
1360/// Create a note that spawns the auto scroller on middle click and fulfill `AUTO_SCROLL_CMD` requests.
1361pub fn auto_scroll_node(child: impl UiNode) -> impl UiNode {
1362    let mut middle_handle = EventHandle::dummy();
1363    let mut cmd_handle = CommandHandle::dummy();
1364    let mut auto_scrolling = None::<(WidgetId, Arc<Mutex<DInstant>>)>;
1365    match_node(child, move |c, op| {
1366        enum Task {
1367            CheckEnable,
1368            Disable,
1369        }
1370        let mut task = None;
1371        match op {
1372            UiNodeOp::Init => {
1373                cmd_handle = AUTO_SCROLL_CMD
1374                    .scoped(WIDGET.id())
1375                    .subscribe(SCROLL.can_scroll_horizontal().get() || SCROLL.can_scroll_vertical().get());
1376                WIDGET.sub_var(&AUTO_SCROLL_VAR);
1377                task = Some(Task::CheckEnable);
1378            }
1379            UiNodeOp::Deinit => {
1380                task = Some(Task::Disable);
1381            }
1382            UiNodeOp::Update { .. } => {
1383                if AUTO_SCROLL_VAR.is_new() {
1384                    task = Some(Task::CheckEnable);
1385                }
1386            }
1387            UiNodeOp::Event { update } => {
1388                c.event(update);
1389
1390                if let Some(args) = MOUSE_INPUT_EVENT.on_unhandled(update) {
1391                    if args.is_mouse_down() && matches!(args.button, MouseButton::Middle) && AUTO_SCROLL_VAR.get() {
1392                        args.propagation().stop();
1393
1394                        let mut open = true;
1395                        if let Some((id, closed)) = auto_scrolling.take() {
1396                            let closed = *closed.lock();
1397                            if closed == DInstant::MAX {
1398                                LAYERS.remove(id);
1399                                open = false;
1400                            } else {
1401                                open = closed.elapsed() > 50.ms();
1402                            }
1403                        }
1404                        if open {
1405                            let (wgt, wgt_id, closed) = auto_scroller_wgt();
1406
1407                            let anchor = AnchorMode {
1408                                transform: zng_wgt_layer::AnchorTransform::CursorOnce {
1409                                    offset: zng_wgt_layer::AnchorOffset {
1410                                        place: Point::top_left(),
1411                                        origin: Point::center(),
1412                                    },
1413                                    include_touch: true,
1414                                    bounds: None,
1415                                },
1416                                min_size: zng_wgt_layer::AnchorSize::Unbounded,
1417                                max_size: zng_wgt_layer::AnchorSize::Window,
1418                                viewport_bound: true,
1419                                corner_radius: false,
1420                                visibility: true,
1421                                interactivity: false,
1422                            };
1423                            LAYERS.insert_anchored(LayerIndex::ADORNER, WIDGET.id(), anchor, wgt);
1424                            auto_scrolling = Some((wgt_id, closed));
1425                        }
1426                    }
1427                } else if let Some(args) = AUTO_SCROLL_CMD.scoped(WIDGET.id()).on_unhandled(update) {
1428                    if cmd_handle.is_enabled() {
1429                        args.propagation().stop();
1430
1431                        let acc = args.param::<DipVector>().copied().unwrap_or_else(DipVector::zero);
1432                        SCROLL.auto_scroll(acc)
1433                    }
1434                }
1435            }
1436            UiNodeOp::Layout { wl, final_size } => {
1437                *final_size = c.layout(wl);
1438                cmd_handle.set_enabled(SCROLL.can_scroll_horizontal().get() || SCROLL.can_scroll_vertical().get());
1439            }
1440            _ => {}
1441        }
1442
1443        while let Some(t) = task.take() {
1444            match t {
1445                Task::CheckEnable => {
1446                    if AUTO_SCROLL_VAR.get() {
1447                        if middle_handle.is_dummy() {
1448                            middle_handle = MOUSE_INPUT_EVENT.subscribe(WIDGET.id());
1449                        }
1450                    } else {
1451                        task = Some(Task::Disable);
1452                    }
1453                }
1454                Task::Disable => {
1455                    middle_handle = EventHandle::dummy();
1456                    if let Some((wgt_id, closed)) = auto_scrolling.take() {
1457                        if *closed.lock() == DInstant::MAX {
1458                            LAYERS.remove(wgt_id);
1459                        }
1460                    }
1461                }
1462            }
1463        }
1464    })
1465}
1466
1467fn auto_scroller_wgt() -> (impl UiNode, WidgetId, Arc<Mutex<DInstant>>) {
1468    let id = WidgetId::new_unique();
1469    let mut wgt = Container::widget_new();
1470    let closed = Arc::new(Mutex::new(DInstant::MAX));
1471    widget_set! {
1472        wgt;
1473        id;
1474        zng_wgt_input::focus::focusable = true;
1475        zng_wgt_input::focus::focus_on_init = true;
1476        zng_wgt_container::child = presenter(AutoScrollArgs {}, AUTO_SCROLL_INDICATOR_VAR);
1477    }
1478    wgt.widget_builder().push_build_action(clmv!(closed, |w| {
1479        w.push_intrinsic(
1480            NestGroup::EVENT,
1481            "auto_scroller_node",
1482            clmv!(closed, |c| auto_scroller_node(c, closed)),
1483        );
1484
1485        let mut ctx = LocalContext::capture_filtered(CaptureFilter::context_vars());
1486        let mut set = ContextValueSet::new();
1487        SCROLL.context_values_set(&mut set);
1488        ctx.extend(LocalContext::capture_filtered(CaptureFilter::Include(set)));
1489
1490        w.push_intrinsic(NestGroup::CONTEXT, "scroll-ctx", |c| with_context_blend(ctx, true, c));
1491    }));
1492
1493    (wgt.widget_build(), id, closed)
1494}
1495fn auto_scroller_node(child: impl UiNode, closed: Arc<Mutex<DInstant>>) -> impl UiNode {
1496    let mut requested_vel = DipVector::zero();
1497    match_node(child, move |_, op| match op {
1498        UiNodeOp::Init => {
1499            // widget is focusable and focus_on_init.
1500
1501            // RAW events to receive move outside widget without capturing pointer.
1502            WIDGET
1503                .sub_event(&RAW_MOUSE_MOVED_EVENT)
1504                .sub_event(&RAW_MOUSE_INPUT_EVENT)
1505                .sub_event(&FOCUS_CHANGED_EVENT);
1506
1507            requested_vel = DipVector::zero();
1508        }
1509        UiNodeOp::Deinit => {
1510            SCROLL.auto_scroll(DipVector::zero());
1511            *closed.lock() = INSTANT.now();
1512        }
1513        UiNodeOp::Event { update } => {
1514            if let Some(args) = RAW_MOUSE_MOVED_EVENT.on(update) {
1515                if args.window_id == WINDOW.id() {
1516                    let info = WIDGET.info();
1517                    let pos = args.position;
1518                    let bounds = info.inner_bounds().to_box2d().to_dip(info.tree().scale_factor());
1519                    let mut vel = DipVector::zero();
1520
1521                    let limit = Dip::new(400);
1522                    if pos.x < bounds.min.x {
1523                        if SCROLL.can_scroll_left().get() {
1524                            vel.x = (pos.x - bounds.min.x).max(-limit);
1525                        }
1526                    } else if pos.x > bounds.max.x && SCROLL.can_scroll_right().get() {
1527                        vel.x = (pos.x - bounds.max.x).min(limit);
1528                    }
1529                    if pos.y < bounds.min.y {
1530                        if SCROLL.can_scroll_up().get() {
1531                            vel.y = (pos.y - bounds.min.y).max(-limit);
1532                        }
1533                    } else if pos.y > bounds.max.y && SCROLL.can_scroll_down().get() {
1534                        vel.y = (pos.y - bounds.max.y).min(limit);
1535                    }
1536                    vel *= 6.fct();
1537
1538                    if vel != requested_vel {
1539                        SCROLL.auto_scroll(vel);
1540                        requested_vel = vel;
1541                    }
1542                }
1543            } else if let Some(args) = RAW_MOUSE_INPUT_EVENT.on(update) {
1544                if matches!((args.state, args.button), (ButtonState::Pressed, MouseButton::Middle)) {
1545                    args.propagation().stop();
1546                    LAYERS.remove(WIDGET.id());
1547                    SCROLL.auto_scroll(DipVector::zero());
1548                }
1549            } else if let Some(args) = KEY_INPUT_EVENT.on(update) {
1550                if matches!((args.state, &args.key), (KeyState::Pressed, Key::Escape)) {
1551                    args.propagation().stop();
1552                    LAYERS.remove(WIDGET.id());
1553                    SCROLL.auto_scroll(DipVector::zero());
1554                }
1555            } else if let Some(args) = FOCUS_CHANGED_EVENT.on(update) {
1556                if args.is_blur(WIDGET.id()) {
1557                    LAYERS.remove(WIDGET.id());
1558                    SCROLL.auto_scroll(DipVector::zero());
1559                }
1560            }
1561        }
1562        _ => {}
1563    })
1564}
1565
1566/// Renders a white circle with arrows that indicate what directions can be scrolled.
1567///
1568/// This is the default [`auto_scroll_indicator`].
1569///
1570/// [`auto_scroll_indicator`]: fn@crate::auto_scroll_indicator
1571pub fn default_auto_scroll_indicator() -> impl UiNode {
1572    match_node_leaf(|op| {
1573        match op {
1574            UiNodeOp::Init => {
1575                // vars used by SCROLL.can_scroll_*.
1576                WIDGET
1577                    .sub_var_render(&SCROLL_VIEWPORT_SIZE_VAR)
1578                    .sub_var_render(&SCROLL_CONTENT_SIZE_VAR)
1579                    .sub_var_render(&SCROLL_VERTICAL_OFFSET_VAR)
1580                    .sub_var_render(&SCROLL_HORIZONTAL_OFFSET_VAR);
1581            }
1582            UiNodeOp::Measure { desired_size, .. } => {
1583                *desired_size = PxSize::splat(Dip::new(40).to_px(LAYOUT.scale_factor()));
1584            }
1585            UiNodeOp::Layout { final_size, .. } => {
1586                *final_size = PxSize::splat(Dip::new(40).to_px(LAYOUT.scale_factor()));
1587            }
1588            UiNodeOp::Render { frame } => {
1589                let size = PxSize::splat(Dip::new(40).to_px(frame.scale_factor()));
1590                let corners = PxCornerRadius::new_all(size);
1591                // white circle
1592                frame.push_clip_rounded_rect(PxRect::from_size(size), corners, false, false, |frame| {
1593                    frame.push_color(PxRect::from_size(size), colors::WHITE.with_alpha(90.pct()).into());
1594                });
1595                // black border
1596                let widths = Dip::new(1).to_px(frame.scale_factor());
1597                frame.push_border(
1598                    PxRect::from_size(size),
1599                    PxSideOffsets::new_all_same(widths),
1600                    colors::BLACK.with_alpha(80.pct()).into(),
1601                    corners,
1602                );
1603                // black point middle
1604                let pt_size = PxSize::splat(Dip::new(4).to_px(frame.scale_factor()));
1605                frame.push_clip_rounded_rect(
1606                    PxRect::new((size / Px(2) - pt_size / Px(2)).to_vector().to_point(), pt_size),
1607                    PxCornerRadius::new_all(pt_size),
1608                    false,
1609                    false,
1610                    |frame| {
1611                        frame.push_color(PxRect::from_size(size), colors::BLACK.into());
1612                    },
1613                );
1614
1615                // arrow
1616                let ar_size = PxSize::splat(Dip::new(20).to_px(frame.scale_factor()));
1617                let ar_center = ar_size / Px(2);
1618
1619                // center circle
1620                let offset = (size / Px(2) - ar_center).to_vector();
1621
1622                // 45º with origin center
1623                let transform = Transform::new_translate(-ar_center.width, -ar_center.height)
1624                    .rotate(45.deg())
1625                    .translate(ar_center.width + offset.x, ar_center.height + offset.y)
1626                    .layout()
1627                    .into();
1628
1629                let widths = Dip::new(2).to_px(frame.scale_factor());
1630                let arrow_length = Dip::new(7).to_px(frame.scale_factor());
1631                let arrow_size = PxSize::splat(arrow_length);
1632
1633                let mut arrow = |clip| {
1634                    frame.push_reference_frame(SpatialFrameId::new_unique().into(), transform, false, false, |frame| {
1635                        frame.push_clip_rect(clip, false, false, |frame| {
1636                            frame.push_border(
1637                                PxRect::from_size(ar_size),
1638                                PxSideOffsets::new_all_same(widths),
1639                                colors::BLACK.with_alpha(80.pct()).into(),
1640                                PxCornerRadius::zero(),
1641                            );
1642                        });
1643                    });
1644                };
1645                if SCROLL.can_scroll_up().get() {
1646                    arrow(PxRect::from_size(arrow_size));
1647                }
1648                if SCROLL.can_scroll_right().get() {
1649                    arrow(PxRect::new(PxPoint::new(ar_size.width - arrow_length, Px(0)), arrow_size));
1650                }
1651                if SCROLL.can_scroll_down().get() {
1652                    arrow(PxRect::new(
1653                        PxPoint::new(ar_size.width - arrow_length, ar_size.height - arrow_length),
1654                        arrow_size,
1655                    ));
1656                }
1657                if SCROLL.can_scroll_left().get() {
1658                    arrow(PxRect::new(PxPoint::new(Px(0), ar_size.height - arrow_length), arrow_size));
1659                }
1660            }
1661            _ => (),
1662        }
1663    })
1664}