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