Skip to main content

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