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