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