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