1use 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
30pub 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 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 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 let update_only_offset = (last_render_offset - content_offset).abs();
197 const OFFSET_EXTRA: Px = Px(20); 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 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
273pub fn v_scrollbar_presenter() -> UiNode {
277 scrollbar_presenter(VERTICAL_SCROLLBAR_FN_VAR, Orientation::Vertical)
278}
279
280pub 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
291pub 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
308pub 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
429pub 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
546pub 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
611pub 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(); 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
730pub 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 if !SCROLL.take_ignore_next_scroll_to_focused()
764 && let Some(mode) = SCROLL_TO_FOCUSED_MODE_VAR.get()
765 {
766 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 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 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 target = a;
808 }
809 }
810 }
811
812 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 if let Some(request) = args.param::<ScrollToRequest>().cloned() {
838 args.propagation.stop();
839
840 let tree = WINDOW.info();
842 match request.target {
843 ScrollToTarget::Descendant(target) => {
844 if let Some(target) = tree.get(target) {
845 if let Some(us) = target.ancestors().find(|w| w.id() == self_id) {
847 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 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 let rendered_offset = rendered_content.origin.to_vector();
911 bounds.origin -= rendered_offset;
912
913 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 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 let scaled_margin = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(bounds.size), || margin.layout());
930 let bounds = inflate_margin(bounds, scaled_margin);
931
932 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 if bounds.size.height < viewport_size.height {
942 if current_bounds.origin.y < Px(0) {
943 offset.y = bounds.origin.y;
945 } else if current_bounds.max_y() > viewport_size.height {
946 offset.y = bounds.max_y() - viewport_size.height;
948 } else if zoom.is_some() {
949 let center_in_vp = current_bounds.center().y;
951 let center = bounds.center().y;
952 offset.y = center - center_in_vp;
953
954 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 offset.y = viewport_size.height / Px(2) - bounds.center().y;
966 };
967
968 if bounds.size.width < viewport_size.width {
970 if current_bounds.origin.x < Px(0) {
971 offset.x = bounds.origin.x;
973 } else if current_bounds.max_x() > viewport_size.width {
974 offset.x = bounds.max_x() - viewport_size.width;
976 } else if zoom.is_some() {
977 let center_in_vp = current_bounds.center().x;
979 let center = bounds.center().x;
980 offset.x = center - center_in_vp;
981
982 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 offset.x = viewport_size.width / Px(2) - bounds.center().x;
994 };
995 }
996 ScrollToMode::Center {
997 widget_point,
998 scroll_point,
999 } => {
1000 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 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 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
1050pub 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
1130pub 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 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
1286pub 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 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 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 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 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 v_center.x = v_rect.size.width / Px(2);
1339 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
1414pub 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 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
1458pub 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 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
1686pub fn default_auto_scroll_indicator() -> UiNode {
1692 match_node_leaf(|op| {
1693 match op {
1694 UiNodeOp::Init => {
1695 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 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 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 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 let ar_size = PxSize::splat(Dip::new(20).to_px(frame.scale_factor()));
1738 let ar_center = ar_size / Px(2);
1739
1740 let offset = (size / Px(2) - ar_center).to_vector();
1742
1743 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}