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