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