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