1use alloc::vec::Vec;
24
25use azul_core::{
26 dom::{DomId, DomNodeId, NodeId},
27 geom::{LogicalPosition, LogicalRect, LogicalSize},
28 task::{Duration, Instant},
29};
30
31use crate::{
32 managers::scroll_state::ScrollManager,
33 solver3::getters::{get_overflow_x, get_overflow_y},
34 window::DomLayoutResult,
35};
36
37pub use azul_core::events::{ScrollIntoViewBehavior, ScrollIntoViewOptions, ScrollLogicalPosition};
39
40const SCROLL_DELTA_THRESHOLD: f32 = 0.5;
42const SMOOTH_SCROLL_DURATION_MS: u64 = 300;
44
45#[derive(Copy, Debug, Clone)]
47pub struct ScrollAdjustment {
48 pub scroll_container_dom_id: DomId,
50 pub scroll_container_node_id: NodeId,
52 pub delta: LogicalPosition,
54 pub behavior: ScrollIntoViewBehavior,
56}
57
58#[derive(Debug, Clone)]
60struct ScrollableAncestor {
61 dom_id: DomId,
62 node_id: NodeId,
63 visible_rect: LogicalRect,
65 scroll_x: bool,
67 scroll_y: bool,
69}
70
71#[allow(clippy::needless_pass_by_value)]
96pub(crate) fn scroll_rect_into_view(
97 target_rect: LogicalRect,
98 target_dom_id: DomId,
99 target_node_id: NodeId,
100 layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
101 scroll_manager: &mut ScrollManager,
102 options: ScrollIntoViewOptions,
103 now: Instant,
104) -> Vec<ScrollAdjustment> {
105 let mut adjustments = Vec::new();
106
107 let scroll_ancestors = find_scrollable_ancestors(
109 target_dom_id,
110 target_node_id,
111 layout_results,
112 scroll_manager,
113 );
114
115 if scroll_ancestors.is_empty() {
116 return adjustments;
117 }
118
119 let mut current_rect = target_rect;
121
122 for ancestor in scroll_ancestors {
123 let delta = calculate_scroll_delta(
125 current_rect,
126 ancestor.visible_rect,
127 options.block,
128 options.inline_axis,
129 ancestor.scroll_x,
130 ancestor.scroll_y,
131 );
132
133 if delta.x.abs() > SCROLL_DELTA_THRESHOLD || delta.y.abs() > SCROLL_DELTA_THRESHOLD {
135 let behavior = resolve_scroll_behavior(
137 options.behavior,
138 ancestor.dom_id,
139 ancestor.node_id,
140 layout_results,
141 );
142
143 apply_scroll_adjustment(
145 scroll_manager,
146 ancestor.dom_id,
147 ancestor.node_id,
148 delta,
149 behavior,
150 now.clone(),
151 );
152
153 adjustments.push(ScrollAdjustment {
154 scroll_container_dom_id: ancestor.dom_id,
155 scroll_container_node_id: ancestor.node_id,
156 delta,
157 behavior,
158 });
159
160 current_rect.origin.x -= delta.x;
162 current_rect.origin.y -= delta.y;
163 }
164 }
165
166 adjustments
167}
168
169pub fn scroll_node_into_view(
178 node_id: DomNodeId,
179 layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
180 scroll_manager: &mut ScrollManager,
181 options: ScrollIntoViewOptions,
182 now: Instant,
183) -> Vec<ScrollAdjustment> {
184 let Some(target_rect) = get_node_rect(node_id, layout_results) else {
186 return Vec::new();
187 };
188
189 let Some(internal_node_id) = node_id.node.into_crate_internal() else {
190 return Vec::new();
191 };
192
193 scroll_rect_into_view(
195 target_rect,
196 node_id.dom,
197 internal_node_id,
198 layout_results,
199 scroll_manager,
200 options,
201 now,
202 )
203}
204
205pub fn scroll_cursor_into_view(
210 cursor_rect: LogicalRect,
211 node_id: DomNodeId,
212 layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
213 scroll_manager: &mut ScrollManager,
214 options: ScrollIntoViewOptions,
215 now: Instant,
216) -> Vec<ScrollAdjustment> {
217 let Some(node_rect) = get_node_rect(node_id, layout_results) else {
219 return Vec::new();
220 };
221
222 let absolute_cursor_rect = LogicalRect {
224 origin: LogicalPosition {
225 x: node_rect.origin.x + cursor_rect.origin.x,
226 y: node_rect.origin.y + cursor_rect.origin.y,
227 },
228 size: cursor_rect.size,
229 };
230
231 let Some(internal_node_id) = node_id.node.into_crate_internal() else {
232 return Vec::new();
233 };
234
235 scroll_rect_into_view(
237 absolute_cursor_rect,
238 node_id.dom,
239 internal_node_id,
240 layout_results,
241 scroll_manager,
242 options,
243 now,
244 )
245}
246
247fn find_scrollable_ancestors(
255 dom_id: DomId,
256 node_id: NodeId,
257 layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
258 scroll_manager: &ScrollManager,
259) -> Vec<ScrollableAncestor> {
260 let mut ancestors = Vec::new();
261
262 let Some(layout_result) = layout_results.get(&dom_id) else {
263 return ancestors;
264 };
265
266 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
267
268 let mut current = node_hierarchy.get(node_id).and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id);
270
271 while let Some(current_node_id) = current {
272 if let Some(ancestor) = check_if_scrollable(
274 dom_id,
275 current_node_id,
276 layout_result,
277 scroll_manager,
278 ) {
279 ancestors.push(ancestor);
280 }
281
282 current = node_hierarchy.get(current_node_id).and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id);
284 }
285
286 ancestors
287}
288
289fn check_if_scrollable(
291 dom_id: DomId,
292 node_id: NodeId,
293 layout_result: &DomLayoutResult,
294 scroll_manager: &ScrollManager,
295) -> Option<ScrollableAncestor> {
296 let styled_nodes = layout_result.styled_dom.styled_nodes.as_container();
297 let styled_node = styled_nodes.get(node_id)?;
298
299 let overflow_x = get_overflow_x(
300 &layout_result.styled_dom,
301 node_id,
302 &styled_node.styled_node_state,
303 );
304 let overflow_y = get_overflow_y(
305 &layout_result.styled_dom,
306 node_id,
307 &styled_node.styled_node_state,
308 );
309
310 let scroll_x = overflow_x.is_scroll();
311 let scroll_y = overflow_y.is_scroll();
312
313 if !scroll_x && !scroll_y {
315 return None;
316 }
317
318 let scroll_state = scroll_manager.get_scroll_state(dom_id, node_id)?;
321
322 let effective_width = scroll_state.virtual_scroll_size.map_or(scroll_state.content_rect.size.width, |s| s.width);
324 let effective_height = scroll_state.virtual_scroll_size.map_or(scroll_state.content_rect.size.height, |s| s.height);
325 let has_overflow_x = effective_width > scroll_state.container_rect.size.width;
326 let has_overflow_y = effective_height > scroll_state.container_rect.size.height;
327
328 if !has_overflow_x && !has_overflow_y {
329 return None;
330 }
331
332 let visible_rect = LogicalRect {
334 origin: LogicalPosition {
335 x: scroll_state.container_rect.origin.x + scroll_state.current_offset.x,
336 y: scroll_state.container_rect.origin.y + scroll_state.current_offset.y,
337 },
338 size: scroll_state.container_rect.size,
339 };
340
341 Some(ScrollableAncestor {
342 dom_id,
343 node_id,
344 visible_rect,
345 scroll_x: scroll_x && has_overflow_x,
346 scroll_y: scroll_y && has_overflow_y,
347 })
348}
349
350#[allow(clippy::similar_names)] fn calculate_scroll_delta(
353 target: LogicalRect,
354 container: LogicalRect,
355 block: ScrollLogicalPosition,
356 inline: ScrollLogicalPosition,
357 scroll_x_enabled: bool,
358 scroll_y_enabled: bool,
359) -> LogicalPosition {
360 LogicalPosition {
361 x: if scroll_x_enabled {
362 calculate_axis_delta(
363 target.origin.x,
364 target.size.width,
365 container.origin.x,
366 container.size.width,
367 inline,
368 )
369 } else {
370 0.0
371 },
372 y: if scroll_y_enabled {
373 calculate_axis_delta(
374 target.origin.y,
375 target.size.height,
376 container.origin.y,
377 container.size.height,
378 block,
379 )
380 } else {
381 0.0
382 },
383 }
384}
385
386#[must_use] pub fn calculate_axis_delta(
388 target_start: f32,
389 target_size: f32,
390 container_start: f32,
391 container_size: f32,
392 position: ScrollLogicalPosition,
393) -> f32 {
394 let target_end = target_start + target_size;
395 let container_end = container_start + container_size;
396
397 match position {
398 ScrollLogicalPosition::Start => {
399 target_start - container_start
401 }
402 ScrollLogicalPosition::End => {
403 target_end - container_end
405 }
406 ScrollLogicalPosition::Center => {
407 let target_center = target_start + target_size / 2.0;
409 let container_center = container_start + container_size / 2.0;
410 target_center - container_center
411 }
412 ScrollLogicalPosition::Nearest => {
413 if target_start < container_start {
415 target_start - container_start
417 } else if target_end > container_end {
418 if target_size <= container_size {
420 target_end - container_end
422 } else {
423 target_start - container_start
425 }
426 } else {
427 0.0
429 }
430 }
431 }
432}
433
434const fn resolve_scroll_behavior(
437 requested: ScrollIntoViewBehavior,
438 _dom_id: DomId,
439 _node_id: NodeId,
440 _layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
441) -> ScrollIntoViewBehavior {
442 match requested {
443 ScrollIntoViewBehavior::Auto => {
444 ScrollIntoViewBehavior::Instant
447 }
448 other => other,
449 }
450}
451
452fn apply_scroll_adjustment(
454 scroll_manager: &mut ScrollManager,
455 dom_id: DomId,
456 node_id: NodeId,
457 delta: LogicalPosition,
458 behavior: ScrollIntoViewBehavior,
459 now: Instant,
460) {
461 use azul_core::events::EasingFunction;
462 use azul_core::task::SystemTimeDiff;
463
464 let current = scroll_manager
465 .get_current_offset(dom_id, node_id)
466 .unwrap_or_default();
467
468 let new_position = LogicalPosition {
469 x: current.x + delta.x,
470 y: current.y + delta.y,
471 };
472
473 match behavior {
474 ScrollIntoViewBehavior::Instant | ScrollIntoViewBehavior::Auto => {
475 scroll_manager.set_scroll_position(dom_id, node_id, new_position, now);
476 }
477 ScrollIntoViewBehavior::Smooth => {
478 let duration = Duration::System(SystemTimeDiff::from_millis(SMOOTH_SCROLL_DURATION_MS));
480 scroll_manager.scroll_to(
481 dom_id,
482 node_id,
483 new_position,
484 duration,
485 EasingFunction::EaseOut,
486 now,
487 );
488 }
489 }
490}
491
492fn get_node_rect(
494 node_id: DomNodeId,
495 layout_results: &alloc::collections::BTreeMap<DomId, DomLayoutResult>,
496) -> Option<LogicalRect> {
497 let layout_result = layout_results.get(&node_id.dom)?;
498 let nid = node_id.node.into_crate_internal()?;
499
500 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&nid)?;
502 let layout_index = *layout_indices.first()?;
503 let position = *layout_result.calculated_positions.get(layout_index)?;
504
505 let layout_node = layout_result.layout_tree.get(layout_index)?;
507 let size = layout_node.used_size?;
508
509 Some(LogicalRect::new(position, size))
510}
511
512#[cfg(test)]
513mod autotest_generated {
514 use alloc::collections::BTreeMap;
515 use std::collections::HashMap;
516
517 use azul_core::{
518 dom::{Dom, FormattingContext, IdOrClass},
519 styled_dom::{NodeHierarchyItemId, StyledDom},
520 };
521
522 use super::*;
523 use crate::solver3::{
524 display_list::DisplayList,
525 geometry::PackedBoxProps,
526 layout_tree::{LayoutNodeHot, LayoutTree},
527 };
528
529 const OUTER: usize = 1;
536 const INNER: usize = 2;
537 const TARGET: usize = 3;
538 const OUT_OF_RANGE: usize = 9999;
540
541 const SCROLL_CSS: &str = ".outer { overflow-x: scroll; overflow-y: scroll; } .inner { \
542 overflow-x: scroll; overflow-y: scroll; }";
543 const X_ONLY_CSS: &str = ".inner { overflow-x: scroll; }";
544 const NO_CSS: &str = "";
545
546 fn dom_id(inner: usize) -> DomId {
547 DomId { inner }
548 }
549
550 fn nid(index: usize) -> NodeId {
551 NodeId::new(index)
552 }
553
554 fn dnid(dom: usize, index: usize) -> DomNodeId {
555 DomNodeId {
556 dom: dom_id(dom),
557 node: NodeHierarchyItemId::from_crate_internal(Some(nid(index))),
558 }
559 }
560
561 fn null_dnid(dom: usize) -> DomNodeId {
563 DomNodeId {
564 dom: dom_id(dom),
565 node: NodeHierarchyItemId::NONE,
566 }
567 }
568
569 fn pos(x: f32, y: f32) -> LogicalPosition {
570 LogicalPosition::new(x, y)
571 }
572
573 fn size(width: f32, height: f32) -> LogicalSize {
574 LogicalSize::new(width, height)
575 }
576
577 fn rect(x: f32, y: f32, width: f32, height: f32) -> LogicalRect {
578 LogicalRect::new(pos(x, y), size(width, height))
579 }
580
581 fn close(a: f32, b: f32) -> bool {
582 (a - b).abs() < 1e-3
583 }
584
585 fn now() -> Instant {
586 Instant::now()
587 }
588
589 fn opts(
590 block: ScrollLogicalPosition,
591 inline_axis: ScrollLogicalPosition,
592 behavior: ScrollIntoViewBehavior,
593 ) -> ScrollIntoViewOptions {
594 ScrollIntoViewOptions {
595 block,
596 inline_axis,
597 behavior,
598 }
599 }
600
601 fn div_with_class(class: &str) -> Dom {
602 Dom::create_div().with_ids_and_classes(vec![IdOrClass::Class(class.into())].into())
603 }
604
605 fn chain_dom(css_str: &str) -> StyledDom {
609 let mut dom = Dom::create_body().with_child(
610 div_with_class("outer")
611 .with_child(div_with_class("inner").with_child(div_with_class("target"))),
612 );
613 let (css, _warnings) = azul_css::parser2::new_from_str(css_str);
614 StyledDom::create(&mut dom, css)
615 }
616
617 fn empty_layout_tree() -> LayoutTree {
618 LayoutTree {
619 nodes: Vec::new(),
620 warm: Vec::new(),
621 cold: Vec::new(),
622 root: 0,
623 dom_to_layout: BTreeMap::new(),
624 children_arena: Vec::new(),
625 children_offsets: Vec::new(),
626 subtree_needs_intrinsic: Vec::new(),
627 }
628 }
629
630 fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
633 DomLayoutResult {
634 styled_dom,
635 layout_tree: empty_layout_tree(),
636 calculated_positions: Vec::new(),
637 viewport: LogicalRect::zero(),
638 display_list: DisplayList::default(),
639 scroll_ids: HashMap::new(),
640 scroll_id_to_node_id: HashMap::new(),
641 }
642 }
643
644 fn layout_result_with_boxes(
647 styled_dom: StyledDom,
648 boxes: &[(usize, LogicalPosition, Option<LogicalSize>)],
649 ) -> DomLayoutResult {
650 let mut lr = layout_result(styled_dom);
651 for (layout_index, (node_index, position, used_size)) in boxes.iter().enumerate() {
652 lr.layout_tree
653 .dom_to_layout
654 .insert(nid(*node_index), vec![layout_index]);
655 lr.layout_tree.nodes.push(LayoutNodeHot {
656 box_props: PackedBoxProps::default(),
657 dom_node_id: Some(nid(*node_index)),
658 used_size: *used_size,
659 formatting_context: FormattingContext::Block {
660 establishes_new_context: false,
661 },
662 parent: None,
663 });
664 lr.calculated_positions.push(*position);
665 }
666 lr
667 }
668
669 fn window(lr: DomLayoutResult) -> BTreeMap<DomId, DomLayoutResult> {
670 let mut map = BTreeMap::new();
671 map.insert(dom_id(0), lr);
672 map
673 }
674
675 fn register(sm: &mut ScrollManager, node: usize, container: LogicalRect, content: LogicalSize) {
676 sm.register_or_update_scroll_node(
677 dom_id(0),
678 nid(node),
679 container,
680 content,
681 now(),
682 16.0,
683 8.0,
684 false,
685 false,
686 );
687 }
688
689 fn inner_only() -> (BTreeMap<DomId, DomLayoutResult>, ScrollManager) {
694 let layout_results = window(layout_result(chain_dom(SCROLL_CSS)));
695 let mut sm = ScrollManager::new();
696 register(
697 &mut sm,
698 INNER,
699 rect(0.0, 0.0, 100.0, 100.0),
700 size(100.0, 1000.0),
701 );
702 (layout_results, sm)
703 }
704
705 #[test]
710 fn axis_delta_all_zero_inputs_are_zero_for_every_position() {
711 for position in [
712 ScrollLogicalPosition::Start,
713 ScrollLogicalPosition::Center,
714 ScrollLogicalPosition::End,
715 ScrollLogicalPosition::Nearest,
716 ] {
717 let delta = calculate_axis_delta(0.0, 0.0, 0.0, 0.0, position);
718 assert!(close(delta, 0.0), "{position:?} on all-zero input");
719 }
720 }
721
722 #[test]
723 fn axis_delta_start_aligns_target_start_with_container_start() {
724 assert!(close(
725 calculate_axis_delta(100.0, 50.0, 20.0, 30.0, ScrollLogicalPosition::Start),
726 80.0
727 ));
728 }
729
730 #[test]
731 fn axis_delta_end_aligns_target_end_with_container_end() {
732 assert!(close(
734 calculate_axis_delta(100.0, 50.0, 0.0, 30.0, ScrollLogicalPosition::End),
735 120.0
736 ));
737 }
738
739 #[test]
740 fn axis_delta_center_aligns_midpoints() {
741 assert!(close(
743 calculate_axis_delta(100.0, 40.0, 0.0, 100.0, ScrollLogicalPosition::Center),
744 70.0
745 ));
746 }
747
748 #[test]
749 fn axis_delta_center_of_zero_sized_target_is_offset_of_container_center() {
750 assert!(close(
751 calculate_axis_delta(0.0, 0.0, 0.0, 100.0, ScrollLogicalPosition::Center),
752 -50.0
753 ));
754 }
755
756 #[test]
757 fn axis_delta_nearest_leaves_fully_visible_target_alone() {
758 assert!(close(
760 calculate_axis_delta(10.0, 20.0, 0.0, 100.0, ScrollLogicalPosition::Nearest),
761 0.0
762 ));
763 }
764
765 #[test]
766 fn axis_delta_nearest_scrolls_back_for_target_before_container() {
767 assert!(close(
768 calculate_axis_delta(-40.0, 20.0, 0.0, 100.0, ScrollLogicalPosition::Nearest),
769 -40.0
770 ));
771 }
772
773 #[test]
774 fn axis_delta_nearest_aligns_end_when_target_fits() {
775 assert!(close(
777 calculate_axis_delta(150.0, 20.0, 0.0, 100.0, ScrollLogicalPosition::Nearest),
778 70.0
779 ));
780 }
781
782 #[test]
783 fn axis_delta_nearest_aligns_start_when_target_does_not_fit() {
784 assert!(close(
786 calculate_axis_delta(150.0, 300.0, 0.0, 100.0, ScrollLogicalPosition::Nearest),
787 150.0
788 ));
789 }
790
791 #[test]
792 fn axis_delta_handles_negative_coordinates_deterministically() {
793 assert!(close(
795 calculate_axis_delta(-200.0, 10.0, -100.0, 50.0, ScrollLogicalPosition::Start),
796 -100.0
797 ));
798 assert!(close(
799 calculate_axis_delta(-200.0, 10.0, -100.0, 50.0, ScrollLogicalPosition::End),
800 -140.0
801 ));
802 assert!(close(
803 calculate_axis_delta(-200.0, 10.0, -100.0, 50.0, ScrollLogicalPosition::Nearest),
804 -100.0
805 ));
806 }
807
808 #[test]
809 fn axis_delta_nearest_never_scrolls_the_wrong_way() {
810 let (container_start, container_size) = (100.0f32, 50.0f32);
811 for target_start in [-1000.0f32, 0.0, 99.0, 100.0, 120.0, 149.0, 150.0, 1000.0] {
812 for target_size in [0.0f32, 10.0, 50.0, 60.0] {
813 let delta = calculate_axis_delta(
814 target_start,
815 target_size,
816 container_start,
817 container_size,
818 ScrollLogicalPosition::Nearest,
819 );
820 let start_align = target_start - container_start;
821 let end_align = (target_start + target_size) - (container_start + container_size);
822 let fully_visible = target_start >= container_start
823 && (target_start + target_size) <= (container_start + container_size);
824
825 assert!(
828 close(delta, 0.0) || close(delta, start_align) || close(delta, end_align),
829 "delta {delta} for target {target_start}+{target_size}"
830 );
831 if fully_visible {
832 assert!(
833 close(delta, 0.0),
834 "visible target {target_start}+{target_size} must not scroll"
835 );
836 }
837 if target_start < container_start {
838 assert!(delta <= 0.0, "delta {delta} scrolled the wrong way");
840 }
841 }
842 }
843 }
844
845 #[test]
846 fn axis_delta_nan_target_start_does_not_panic() {
847 for position in [
849 ScrollLogicalPosition::Start,
850 ScrollLogicalPosition::End,
851 ScrollLogicalPosition::Center,
852 ] {
853 let delta = calculate_axis_delta(f32::NAN, 10.0, 0.0, 100.0, position);
854 assert!(delta.is_nan(), "{position:?} should propagate NaN, got {delta}");
855 }
856 }
857
858 #[test]
859 fn axis_delta_nan_target_start_is_zero_for_nearest() {
860 let delta = calculate_axis_delta(f32::NAN, 10.0, 0.0, 100.0, ScrollLogicalPosition::Nearest);
864 assert_eq!(delta, 0.0);
865 }
866
867 #[test]
868 fn axis_delta_nan_container_does_not_panic() {
869 for position in [
870 ScrollLogicalPosition::Start,
871 ScrollLogicalPosition::End,
872 ScrollLogicalPosition::Center,
873 ScrollLogicalPosition::Nearest,
874 ] {
875 let delta = calculate_axis_delta(0.0, 10.0, f32::NAN, f32::NAN, position);
876 assert!(
877 delta.is_nan() || delta == 0.0,
878 "{position:?} gave {delta} for a NaN container"
879 );
880 }
881 }
882
883 #[test]
884 fn axis_delta_infinite_target_start_saturates_to_infinity() {
885 let delta =
886 calculate_axis_delta(f32::INFINITY, 10.0, 0.0, 100.0, ScrollLogicalPosition::Start);
887 assert!(delta.is_infinite() && delta.is_sign_positive());
888
889 let delta = calculate_axis_delta(
890 f32::NEG_INFINITY,
891 10.0,
892 0.0,
893 100.0,
894 ScrollLogicalPosition::Start,
895 );
896 assert!(delta.is_infinite() && delta.is_sign_negative());
897 }
898
899 #[test]
900 fn axis_delta_infinity_minus_infinity_is_nan_not_a_panic() {
901 let delta = calculate_axis_delta(
902 f32::INFINITY,
903 10.0,
904 f32::INFINITY,
905 100.0,
906 ScrollLogicalPosition::Start,
907 );
908 assert!(delta.is_nan());
909 }
910
911 #[test]
912 fn axis_delta_infinite_sizes_do_not_panic() {
913 for position in [
914 ScrollLogicalPosition::Start,
915 ScrollLogicalPosition::End,
916 ScrollLogicalPosition::Center,
917 ScrollLogicalPosition::Nearest,
918 ] {
919 let delta = calculate_axis_delta(0.0, f32::INFINITY, 0.0, f32::INFINITY, position);
920 assert!(
921 delta.is_nan() || delta.is_infinite() || delta.is_finite(),
922 "{position:?} produced a non-f32 value"
923 );
924 }
925 }
926
927 #[test]
928 fn axis_delta_f32_max_overflow_saturates_instead_of_panicking() {
929 let delta =
931 calculate_axis_delta(f32::MAX, f32::MAX, 0.0, 100.0, ScrollLogicalPosition::End);
932 assert!(delta.is_infinite() && delta.is_sign_positive());
933
934 let delta =
937 calculate_axis_delta(f32::MAX, f32::MAX, 0.0, 100.0, ScrollLogicalPosition::Nearest);
938 assert_eq!(delta, f32::MAX);
939 }
940
941 #[test]
942 fn axis_delta_f32_min_does_not_panic() {
943 for position in [
944 ScrollLogicalPosition::Start,
945 ScrollLogicalPosition::End,
946 ScrollLogicalPosition::Center,
947 ScrollLogicalPosition::Nearest,
948 ] {
949 let delta = calculate_axis_delta(f32::MIN, 1.0, f32::MAX, 1.0, position);
950 assert!(!delta.is_nan(), "{position:?} produced NaN from finite input");
951 }
952 }
953
954 #[test]
959 fn scroll_delta_zero_rects_are_zero() {
960 let delta = calculate_scroll_delta(
961 LogicalRect::zero(),
962 LogicalRect::zero(),
963 ScrollLogicalPosition::Nearest,
964 ScrollLogicalPosition::Nearest,
965 true,
966 true,
967 );
968 assert_eq!((delta.x, delta.y), (0.0, 0.0));
969 }
970
971 #[test]
972 fn scroll_delta_disabled_axes_are_exactly_zero_even_for_nan_and_infinite_rects() {
973 let poison = LogicalRect::new(
974 pos(f32::NAN, f32::INFINITY),
975 size(f32::NAN, f32::NEG_INFINITY),
976 );
977 let delta = calculate_scroll_delta(
978 poison,
979 poison,
980 ScrollLogicalPosition::Start,
981 ScrollLogicalPosition::Start,
982 false,
983 false,
984 );
985 assert_eq!((delta.x, delta.y), (0.0, 0.0));
988 assert!(delta.x.is_finite() && delta.y.is_finite());
989 }
990
991 #[test]
992 fn scroll_delta_does_not_swap_the_axes() {
993 let delta = calculate_scroll_delta(
995 rect(10.0, 200.0, 5.0, 5.0),
996 rect(0.0, 0.0, 100.0, 50.0),
997 ScrollLogicalPosition::Start, ScrollLogicalPosition::End, true,
1000 true,
1001 );
1002 assert!(close(delta.x, -85.0), "x used the wrong axis/position: {}", delta.x);
1003 assert!(close(delta.y, 200.0), "y used the wrong axis/position: {}", delta.y);
1004 }
1005
1006 #[test]
1007 fn scroll_delta_only_enabled_axis_moves() {
1008 let target = rect(500.0, 500.0, 10.0, 10.0);
1009 let container = rect(0.0, 0.0, 100.0, 100.0);
1010
1011 let x_only = calculate_scroll_delta(
1012 target,
1013 container,
1014 ScrollLogicalPosition::Start,
1015 ScrollLogicalPosition::Start,
1016 true,
1017 false,
1018 );
1019 assert!(close(x_only.x, 500.0));
1020 assert_eq!(x_only.y, 0.0);
1021
1022 let y_only = calculate_scroll_delta(
1023 target,
1024 container,
1025 ScrollLogicalPosition::Start,
1026 ScrollLogicalPosition::Start,
1027 false,
1028 true,
1029 );
1030 assert_eq!(y_only.x, 0.0);
1031 assert!(close(y_only.y, 500.0));
1032 }
1033
1034 #[test]
1039 fn resolve_behavior_maps_auto_to_instant_and_passes_the_rest_through() {
1040 let empty: BTreeMap<DomId, DomLayoutResult> = BTreeMap::new();
1041 assert_eq!(
1042 resolve_scroll_behavior(
1043 ScrollIntoViewBehavior::Auto,
1044 dom_id(0),
1045 nid(TARGET),
1046 &empty
1047 ),
1048 ScrollIntoViewBehavior::Instant
1049 );
1050 assert_eq!(
1051 resolve_scroll_behavior(
1052 ScrollIntoViewBehavior::Instant,
1053 dom_id(0),
1054 nid(TARGET),
1055 &empty
1056 ),
1057 ScrollIntoViewBehavior::Instant
1058 );
1059 assert_eq!(
1060 resolve_scroll_behavior(
1061 ScrollIntoViewBehavior::Smooth,
1062 dom_id(OUT_OF_RANGE),
1063 nid(OUT_OF_RANGE),
1064 &empty
1065 ),
1066 ScrollIntoViewBehavior::Smooth
1067 );
1068 }
1069
1070 #[test]
1071 fn resolve_behavior_is_idempotent() {
1072 let empty: BTreeMap<DomId, DomLayoutResult> = BTreeMap::new();
1073 for behavior in [
1074 ScrollIntoViewBehavior::Auto,
1075 ScrollIntoViewBehavior::Instant,
1076 ScrollIntoViewBehavior::Smooth,
1077 ] {
1078 let once = resolve_scroll_behavior(behavior, dom_id(0), nid(0), &empty);
1079 let twice = resolve_scroll_behavior(once, dom_id(0), nid(0), &empty);
1080 assert_eq!(once, twice, "resolving {behavior:?} twice changed the result");
1081 }
1082 }
1083
1084 fn registered_manager() -> ScrollManager {
1090 let mut sm = ScrollManager::new();
1091 register(
1092 &mut sm,
1093 INNER,
1094 rect(0.0, 0.0, 100.0, 100.0),
1095 size(500.0, 500.0),
1096 );
1097 sm
1098 }
1099
1100 #[test]
1101 fn apply_zero_delta_leaves_the_offset_at_zero() {
1102 let mut sm = registered_manager();
1103 apply_scroll_adjustment(
1104 &mut sm,
1105 dom_id(0),
1106 nid(INNER),
1107 pos(0.0, 0.0),
1108 ScrollIntoViewBehavior::Instant,
1109 now(),
1110 );
1111 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1112 assert_eq!((offset.x, offset.y), (0.0, 0.0));
1113 }
1114
1115 #[test]
1116 fn apply_on_an_unregistered_node_creates_a_zero_bounded_state() {
1117 let mut sm = ScrollManager::new();
1120 apply_scroll_adjustment(
1121 &mut sm,
1122 dom_id(0),
1123 nid(OUT_OF_RANGE),
1124 pos(1234.0, 5678.0),
1125 ScrollIntoViewBehavior::Instant,
1126 now(),
1127 );
1128 let offset = sm.get_current_offset(dom_id(0), nid(OUT_OF_RANGE)).unwrap();
1129 assert_eq!((offset.x, offset.y), (0.0, 0.0));
1130 }
1131
1132 #[test]
1133 fn apply_instant_delta_moves_the_offset() {
1134 let mut sm = registered_manager();
1135 apply_scroll_adjustment(
1136 &mut sm,
1137 dom_id(0),
1138 nid(INNER),
1139 pos(50.0, 60.0),
1140 ScrollIntoViewBehavior::Instant,
1141 now(),
1142 );
1143 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1144 assert!(close(offset.x, 50.0) && close(offset.y, 60.0));
1145 assert!(!sm.has_active_animations());
1146 }
1147
1148 #[test]
1149 fn apply_negative_delta_clamps_to_zero() {
1150 let mut sm = registered_manager();
1151 apply_scroll_adjustment(
1152 &mut sm,
1153 dom_id(0),
1154 nid(INNER),
1155 pos(-1000.0, -1000.0),
1156 ScrollIntoViewBehavior::Instant,
1157 now(),
1158 );
1159 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1160 assert_eq!((offset.x, offset.y), (0.0, 0.0));
1161 }
1162
1163 #[test]
1164 fn apply_f32_max_delta_clamps_to_max_scroll() {
1165 let mut sm = registered_manager();
1166 apply_scroll_adjustment(
1167 &mut sm,
1168 dom_id(0),
1169 nid(INNER),
1170 pos(f32::MAX, f32::MAX),
1171 ScrollIntoViewBehavior::Instant,
1172 now(),
1173 );
1174 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1175 assert!(close(offset.x, 400.0) && close(offset.y, 400.0), "{offset:?}");
1176 }
1177
1178 #[test]
1179 fn apply_infinite_delta_clamps_to_the_scroll_bounds() {
1180 let mut sm = registered_manager();
1181 apply_scroll_adjustment(
1182 &mut sm,
1183 dom_id(0),
1184 nid(INNER),
1185 pos(f32::INFINITY, f32::NEG_INFINITY),
1186 ScrollIntoViewBehavior::Instant,
1187 now(),
1188 );
1189 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1190 assert!(offset.x.is_finite() && offset.y.is_finite(), "{offset:?}");
1191 assert!(close(offset.x, 400.0) && close(offset.y, 0.0), "{offset:?}");
1192 }
1193
1194 #[test]
1195 fn apply_nan_delta_cannot_poison_the_scroll_offset() {
1196 let mut sm = registered_manager();
1197 apply_scroll_adjustment(
1198 &mut sm,
1199 dom_id(0),
1200 nid(INNER),
1201 pos(f32::NAN, f32::NAN),
1202 ScrollIntoViewBehavior::Instant,
1203 now(),
1204 );
1205 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1206 assert!(offset.x.is_finite() && offset.y.is_finite(), "{offset:?}");
1208 assert_eq!((offset.x, offset.y), (0.0, 0.0));
1209 }
1210
1211 #[test]
1212 fn apply_auto_behaves_like_instant() {
1213 let mut sm = registered_manager();
1214 apply_scroll_adjustment(
1215 &mut sm,
1216 dom_id(0),
1217 nid(INNER),
1218 pos(25.0, 25.0),
1219 ScrollIntoViewBehavior::Auto,
1220 now(),
1221 );
1222 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1223 assert!(close(offset.x, 25.0) && close(offset.y, 25.0));
1224 assert!(!sm.has_active_animations());
1225 }
1226
1227 #[test]
1228 fn apply_smooth_animates_instead_of_jumping() {
1229 let mut sm = registered_manager();
1230 apply_scroll_adjustment(
1231 &mut sm,
1232 dom_id(0),
1233 nid(INNER),
1234 pos(50.0, 50.0),
1235 ScrollIntoViewBehavior::Smooth,
1236 now(),
1237 );
1238 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1239 assert_eq!((offset.x, offset.y), (0.0, 0.0), "smooth must not jump");
1240 assert!(sm.has_active_animations(), "smooth must arm an animation");
1241 }
1242
1243 #[test]
1244 fn apply_deltas_accumulate_onto_the_current_offset() {
1245 let mut sm = registered_manager();
1246 for _ in 0..3 {
1247 apply_scroll_adjustment(
1248 &mut sm,
1249 dom_id(0),
1250 nid(INNER),
1251 pos(100.0, 100.0),
1252 ScrollIntoViewBehavior::Instant,
1253 now(),
1254 );
1255 }
1256 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1257 assert!(close(offset.x, 300.0) && close(offset.y, 300.0), "{offset:?}");
1258 }
1259
1260 #[test]
1265 fn get_node_rect_missing_dom_is_none() {
1266 let empty: BTreeMap<DomId, DomLayoutResult> = BTreeMap::new();
1267 assert!(get_node_rect(dnid(0, TARGET), &empty).is_none());
1268 }
1269
1270 #[test]
1271 fn get_node_rect_wrong_dom_id_is_none() {
1272 let lrs = window(layout_result_with_boxes(
1273 chain_dom(SCROLL_CSS),
1274 &[(TARGET, pos(10.0, 20.0), Some(size(30.0, 40.0)))],
1275 ));
1276 assert!(get_node_rect(dnid(7, TARGET), &lrs).is_none());
1277 }
1278
1279 #[test]
1280 fn get_node_rect_null_node_id_is_none() {
1281 let lrs = window(layout_result_with_boxes(
1282 chain_dom(SCROLL_CSS),
1283 &[(TARGET, pos(10.0, 20.0), Some(size(30.0, 40.0)))],
1284 ));
1285 assert!(get_node_rect(null_dnid(0), &lrs).is_none());
1286 }
1287
1288 #[test]
1289 fn get_node_rect_unmapped_node_is_none() {
1290 let lrs = window(layout_result_with_boxes(
1291 chain_dom(SCROLL_CSS),
1292 &[(TARGET, pos(10.0, 20.0), Some(size(30.0, 40.0)))],
1293 ));
1294 assert!(get_node_rect(dnid(0, OUTER), &lrs).is_none());
1295 assert!(get_node_rect(dnid(0, OUT_OF_RANGE), &lrs).is_none());
1296 }
1297
1298 #[test]
1299 fn get_node_rect_empty_layout_index_list_is_none() {
1300 let mut lr = layout_result_with_boxes(
1301 chain_dom(SCROLL_CSS),
1302 &[(TARGET, pos(10.0, 20.0), Some(size(30.0, 40.0)))],
1303 );
1304 lr.layout_tree.dom_to_layout.insert(nid(TARGET), Vec::new());
1306 assert!(get_node_rect(dnid(0, TARGET), &window(lr)).is_none());
1307 }
1308
1309 #[test]
1310 fn get_node_rect_dangling_layout_index_is_none_not_a_panic() {
1311 let mut lr = layout_result_with_boxes(
1312 chain_dom(SCROLL_CSS),
1313 &[(TARGET, pos(10.0, 20.0), Some(size(30.0, 40.0)))],
1314 );
1315 lr.layout_tree.dom_to_layout.insert(nid(TARGET), vec![7]);
1318 assert!(get_node_rect(dnid(0, TARGET), &window(lr)).is_none());
1319 }
1320
1321 #[test]
1322 fn get_node_rect_unsized_node_is_none() {
1323 let lrs = window(layout_result_with_boxes(
1324 chain_dom(SCROLL_CSS),
1325 &[(TARGET, pos(10.0, 20.0), None)],
1326 ));
1327 assert!(get_node_rect(dnid(0, TARGET), &lrs).is_none());
1328 }
1329
1330 #[test]
1331 fn get_node_rect_returns_position_and_used_size() {
1332 let lrs = window(layout_result_with_boxes(
1333 chain_dom(SCROLL_CSS),
1334 &[(TARGET, pos(10.0, 20.0), Some(size(30.0, 40.0)))],
1335 ));
1336 let r = get_node_rect(dnid(0, TARGET), &lrs).expect("target has a layout box");
1337 assert!(close(r.origin.x, 10.0) && close(r.origin.y, 20.0));
1338 assert!(close(r.size.width, 30.0) && close(r.size.height, 40.0));
1339 }
1340
1341 #[test]
1346 fn check_if_scrollable_out_of_range_node_is_none() {
1347 let lr = layout_result(chain_dom(SCROLL_CSS));
1348 let sm = ScrollManager::new();
1349 assert!(check_if_scrollable(dom_id(0), nid(OUT_OF_RANGE), &lr, &sm).is_none());
1350 }
1351
1352 #[test]
1353 fn check_if_scrollable_without_overflow_css_is_none() {
1354 let lr = layout_result(chain_dom(NO_CSS));
1356 let mut sm = ScrollManager::new();
1357 register(
1358 &mut sm,
1359 INNER,
1360 rect(0.0, 0.0, 100.0, 100.0),
1361 size(500.0, 500.0),
1362 );
1363 assert!(check_if_scrollable(dom_id(0), nid(INNER), &lr, &sm).is_none());
1364 }
1365
1366 #[test]
1367 fn check_if_scrollable_without_scroll_state_is_none() {
1368 let lr = layout_result(chain_dom(SCROLL_CSS));
1370 let sm = ScrollManager::new();
1371 assert!(check_if_scrollable(dom_id(0), nid(INNER), &lr, &sm).is_none());
1372 }
1373
1374 #[test]
1375 fn check_if_scrollable_with_content_that_fits_is_none() {
1376 let lr = layout_result(chain_dom(SCROLL_CSS));
1377 let mut sm = ScrollManager::new();
1378 register(
1379 &mut sm,
1380 INNER,
1381 rect(0.0, 0.0, 100.0, 100.0),
1382 size(100.0, 100.0),
1383 );
1384 assert!(check_if_scrollable(dom_id(0), nid(INNER), &lr, &sm).is_none());
1385 }
1386
1387 #[test]
1388 fn check_if_scrollable_reports_the_overflowing_axes_and_visible_rect() {
1389 let lr = layout_result(chain_dom(SCROLL_CSS));
1390 let mut sm = ScrollManager::new();
1391 register(
1393 &mut sm,
1394 INNER,
1395 rect(5.0, 7.0, 100.0, 100.0),
1396 size(100.0, 1000.0),
1397 );
1398 sm.set_scroll_position(dom_id(0), nid(INNER), pos(0.0, 300.0), now());
1399
1400 let ancestor = check_if_scrollable(dom_id(0), nid(INNER), &lr, &sm)
1401 .expect("an overflowing scroll container");
1402 assert_eq!(ancestor.node_id, nid(INNER));
1403 assert_eq!(ancestor.dom_id, dom_id(0));
1404 assert!(!ancestor.scroll_x, "x does not overflow, so it is not scrollable");
1405 assert!(ancestor.scroll_y);
1406 assert!(close(ancestor.visible_rect.origin.x, 5.0));
1408 assert!(close(ancestor.visible_rect.origin.y, 307.0));
1409 assert!(close(ancestor.visible_rect.size.width, 100.0));
1410 assert!(close(ancestor.visible_rect.size.height, 100.0));
1411 }
1412
1413 #[test]
1414 fn check_if_scrollable_uses_virtual_scroll_size_over_content_rect() {
1415 let lr = layout_result(chain_dom(SCROLL_CSS));
1416 let mut sm = ScrollManager::new();
1417 register(
1419 &mut sm,
1420 INNER,
1421 rect(0.0, 0.0, 100.0, 100.0),
1422 size(100.0, 100.0),
1423 );
1424 assert!(check_if_scrollable(dom_id(0), nid(INNER), &lr, &sm).is_none());
1425
1426 sm.update_virtual_scroll_bounds(dom_id(0), nid(INNER), size(100.0, 10_000.0), None);
1428 let ancestor = check_if_scrollable(dom_id(0), nid(INNER), &lr, &sm)
1429 .expect("virtual_scroll_size must drive the overflow check");
1430 assert!(ancestor.scroll_y);
1431 assert!(!ancestor.scroll_x);
1432 }
1433
1434 #[test]
1435 fn check_if_scrollable_zero_sized_container_with_content_overflows() {
1436 let lr = layout_result(chain_dom(SCROLL_CSS));
1437 let mut sm = ScrollManager::new();
1438 register(&mut sm, INNER, LogicalRect::zero(), size(1.0, 1.0));
1439 let ancestor =
1440 check_if_scrollable(dom_id(0), nid(INNER), &lr, &sm).expect("1px > 0px overflows");
1441 assert!(ancestor.scroll_x && ancestor.scroll_y);
1442 }
1443
1444 #[test]
1445 fn check_if_scrollable_x_only_css_does_not_enable_the_y_axis() {
1446 let lr = layout_result(chain_dom(X_ONLY_CSS));
1452 let mut sm = ScrollManager::new();
1453 register(
1454 &mut sm,
1455 INNER,
1456 rect(0.0, 0.0, 100.0, 100.0),
1457 size(500.0, 500.0),
1458 );
1459 let ancestor = check_if_scrollable(dom_id(0), nid(INNER), &lr, &sm)
1460 .expect("overflow-x: scroll + overflowing content");
1461 assert!(ancestor.scroll_x);
1462 assert!(!ancestor.scroll_y);
1463 }
1464
1465 #[test]
1470 fn find_ancestors_missing_dom_is_empty() {
1471 let empty: BTreeMap<DomId, DomLayoutResult> = BTreeMap::new();
1472 let sm = ScrollManager::new();
1473 assert!(find_scrollable_ancestors(dom_id(0), nid(TARGET), &empty, &sm).is_empty());
1474 }
1475
1476 #[test]
1477 fn find_ancestors_out_of_range_node_is_empty() {
1478 let (lrs, sm) = inner_only();
1479 assert!(find_scrollable_ancestors(dom_id(0), nid(OUT_OF_RANGE), &lrs, &sm).is_empty());
1480 }
1481
1482 #[test]
1483 fn find_ancestors_of_the_root_is_empty() {
1484 let (lrs, sm) = inner_only();
1486 assert!(find_scrollable_ancestors(dom_id(0), nid(0), &lrs, &sm).is_empty());
1487 }
1488
1489 #[test]
1490 fn find_ancestors_excludes_the_target_itself() {
1491 let (lrs, sm) = inner_only();
1494 let ancestors = find_scrollable_ancestors(dom_id(0), nid(INNER), &lrs, &sm);
1495 assert!(ancestors.iter().all(|a| a.node_id != nid(INNER)));
1496 }
1497
1498 #[test]
1499 fn find_ancestors_skips_styled_but_non_overflowing_containers() {
1500 let (lrs, sm) = inner_only();
1502 let ancestors = find_scrollable_ancestors(dom_id(0), nid(TARGET), &lrs, &sm);
1503 assert_eq!(ancestors.len(), 1);
1504 assert_eq!(ancestors[0].node_id, nid(INNER));
1505 }
1506
1507 #[test]
1508 fn find_ancestors_orders_innermost_first() {
1509 let lrs = window(layout_result(chain_dom(SCROLL_CSS)));
1510 let mut sm = ScrollManager::new();
1511 register(
1512 &mut sm,
1513 INNER,
1514 rect(0.0, 400.0, 100.0, 100.0),
1515 size(100.0, 1000.0),
1516 );
1517 register(
1518 &mut sm,
1519 OUTER,
1520 rect(0.0, 0.0, 200.0, 200.0),
1521 size(200.0, 2000.0),
1522 );
1523
1524 let ancestors = find_scrollable_ancestors(dom_id(0), nid(TARGET), &lrs, &sm);
1525 assert_eq!(ancestors.len(), 2);
1526 assert_eq!(ancestors[0].node_id, nid(INNER), "innermost must come first");
1527 assert_eq!(ancestors[1].node_id, nid(OUTER));
1528 }
1529
1530 #[test]
1535 fn rect_into_view_without_scroll_containers_is_a_no_op() {
1536 let lrs = window(layout_result(chain_dom(SCROLL_CSS)));
1537 let mut sm = ScrollManager::new();
1538 let adjustments = scroll_rect_into_view(
1539 rect(0.0, 5000.0, 10.0, 10.0),
1540 dom_id(0),
1541 nid(TARGET),
1542 &lrs,
1543 &mut sm,
1544 opts(
1545 ScrollLogicalPosition::Start,
1546 ScrollLogicalPosition::Start,
1547 ScrollIntoViewBehavior::Instant,
1548 ),
1549 now(),
1550 );
1551 assert!(adjustments.is_empty());
1552 assert!(sm.get_current_offset(dom_id(0), nid(INNER)).is_none());
1553 }
1554
1555 #[test]
1556 fn rect_into_view_missing_dom_is_a_no_op() {
1557 let empty: BTreeMap<DomId, DomLayoutResult> = BTreeMap::new();
1558 let mut sm = ScrollManager::new();
1559 let adjustments = scroll_rect_into_view(
1560 rect(0.0, 5000.0, 10.0, 10.0),
1561 dom_id(0),
1562 nid(TARGET),
1563 &empty,
1564 &mut sm,
1565 ScrollIntoViewOptions::nearest(),
1566 now(),
1567 );
1568 assert!(adjustments.is_empty());
1569 }
1570
1571 #[test]
1572 fn rect_into_view_already_visible_target_does_not_scroll() {
1573 let (lrs, mut sm) = inner_only();
1574 let adjustments = scroll_rect_into_view(
1575 rect(0.0, 10.0, 50.0, 20.0),
1576 dom_id(0),
1577 nid(TARGET),
1578 &lrs,
1579 &mut sm,
1580 ScrollIntoViewOptions::nearest(),
1581 now(),
1582 );
1583 assert!(adjustments.is_empty());
1584 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1585 assert_eq!((offset.x, offset.y), (0.0, 0.0));
1586 }
1587
1588 #[test]
1589 fn rect_into_view_scrolls_and_reports_the_delta() {
1590 let (lrs, mut sm) = inner_only();
1591 let adjustments = scroll_rect_into_view(
1592 rect(0.0, 500.0, 50.0, 20.0),
1593 dom_id(0),
1594 nid(TARGET),
1595 &lrs,
1596 &mut sm,
1597 opts(
1598 ScrollLogicalPosition::Start,
1599 ScrollLogicalPosition::Start,
1600 ScrollIntoViewBehavior::Instant,
1601 ),
1602 now(),
1603 );
1604 assert_eq!(adjustments.len(), 1);
1605 assert_eq!(adjustments[0].scroll_container_node_id, nid(INNER));
1606 assert!(close(adjustments[0].delta.y, 500.0));
1607 assert_eq!(adjustments[0].delta.x, 0.0, "x does not overflow");
1608 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1609 assert!(close(offset.y, 500.0), "{offset:?}");
1610 }
1611
1612 #[test]
1613 fn rect_into_view_ignores_a_delta_at_exactly_the_threshold() {
1614 assert!(close(SCROLL_DELTA_THRESHOLD, 0.5));
1617
1618 let (lrs, mut sm) = inner_only();
1619 let at_threshold = scroll_rect_into_view(
1620 rect(0.0, 0.5, 50.0, 20.0),
1621 dom_id(0),
1622 nid(TARGET),
1623 &lrs,
1624 &mut sm,
1625 opts(
1626 ScrollLogicalPosition::Start,
1627 ScrollLogicalPosition::Start,
1628 ScrollIntoViewBehavior::Instant,
1629 ),
1630 now(),
1631 );
1632 assert!(at_threshold.is_empty(), "0.5px must be below the threshold");
1633 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1634 assert_eq!(offset.y, 0.0);
1635
1636 let above_threshold = scroll_rect_into_view(
1637 rect(0.0, 0.75, 50.0, 20.0),
1638 dom_id(0),
1639 nid(TARGET),
1640 &lrs,
1641 &mut sm,
1642 opts(
1643 ScrollLogicalPosition::Start,
1644 ScrollLogicalPosition::Start,
1645 ScrollIntoViewBehavior::Instant,
1646 ),
1647 now(),
1648 );
1649 assert_eq!(above_threshold.len(), 1, "0.75px must scroll");
1650 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1651 assert!(close(offset.y, 0.75), "{offset:?}");
1652 }
1653
1654 #[test]
1655 fn rect_into_view_f32_max_rect_clamps_to_max_scroll() {
1656 let (lrs, mut sm) = inner_only();
1657 let adjustments = scroll_rect_into_view(
1658 rect(f32::MAX, f32::MAX, f32::MAX, f32::MAX),
1659 dom_id(0),
1660 nid(TARGET),
1661 &lrs,
1662 &mut sm,
1663 opts(
1664 ScrollLogicalPosition::Start,
1665 ScrollLogicalPosition::Start,
1666 ScrollIntoViewBehavior::Instant,
1667 ),
1668 now(),
1669 );
1670 assert_eq!(adjustments.len(), 1);
1671 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1672 assert!(offset.x.is_finite() && offset.y.is_finite(), "{offset:?}");
1673 assert!(close(offset.y, 900.0), "{offset:?}");
1675 }
1676
1677 #[test]
1678 fn rect_into_view_nan_rect_does_not_scroll_or_poison_the_offset() {
1679 let (lrs, mut sm) = inner_only();
1680 let adjustments = scroll_rect_into_view(
1681 LogicalRect::new(pos(f32::NAN, f32::NAN), size(f32::NAN, f32::NAN)),
1682 dom_id(0),
1683 nid(TARGET),
1684 &lrs,
1685 &mut sm,
1686 opts(
1687 ScrollLogicalPosition::Start,
1688 ScrollLogicalPosition::Start,
1689 ScrollIntoViewBehavior::Instant,
1690 ),
1691 now(),
1692 );
1693 assert!(adjustments.is_empty());
1695 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1696 assert!(offset.x.is_finite() && offset.y.is_finite(), "{offset:?}");
1697 assert_eq!((offset.x, offset.y), (0.0, 0.0));
1698 }
1699
1700 #[test]
1701 fn rect_into_view_negative_rect_scrolls_back_and_clamps_at_zero() {
1702 let (lrs, mut sm) = inner_only();
1703 sm.set_scroll_position(dom_id(0), nid(INNER), pos(0.0, 300.0), now());
1704
1705 let adjustments = scroll_rect_into_view(
1706 rect(0.0, -500.0, 10.0, 10.0),
1707 dom_id(0),
1708 nid(TARGET),
1709 &lrs,
1710 &mut sm,
1711 opts(
1712 ScrollLogicalPosition::Start,
1713 ScrollLogicalPosition::Start,
1714 ScrollIntoViewBehavior::Instant,
1715 ),
1716 now(),
1717 );
1718 assert_eq!(adjustments.len(), 1);
1719 assert!(close(adjustments[0].delta.y, -800.0), "{:?}", adjustments[0]);
1721 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1723 assert_eq!(offset.y, 0.0);
1724 }
1725
1726 #[test]
1727 fn rect_into_view_auto_behavior_is_resolved_to_instant() {
1728 let (lrs, mut sm) = inner_only();
1729 let adjustments = scroll_rect_into_view(
1730 rect(0.0, 500.0, 50.0, 20.0),
1731 dom_id(0),
1732 nid(TARGET),
1733 &lrs,
1734 &mut sm,
1735 opts(
1736 ScrollLogicalPosition::Start,
1737 ScrollLogicalPosition::Start,
1738 ScrollIntoViewBehavior::Auto,
1739 ),
1740 now(),
1741 );
1742 assert_eq!(adjustments.len(), 1);
1743 assert_eq!(adjustments[0].behavior, ScrollIntoViewBehavior::Instant);
1744 assert!(!sm.has_active_animations());
1745 }
1746
1747 #[test]
1748 fn rect_into_view_smooth_behavior_animates() {
1749 let (lrs, mut sm) = inner_only();
1750 let adjustments = scroll_rect_into_view(
1751 rect(0.0, 500.0, 50.0, 20.0),
1752 dom_id(0),
1753 nid(TARGET),
1754 &lrs,
1755 &mut sm,
1756 opts(
1757 ScrollLogicalPosition::Start,
1758 ScrollLogicalPosition::Start,
1759 ScrollIntoViewBehavior::Smooth,
1760 ),
1761 now(),
1762 );
1763 assert_eq!(adjustments.len(), 1);
1764 assert_eq!(adjustments[0].behavior, ScrollIntoViewBehavior::Smooth);
1765 assert!(sm.has_active_animations());
1766 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1767 assert_eq!(offset.y, 0.0, "a smooth scroll must not jump");
1768 }
1769
1770 #[test]
1771 fn rect_into_view_walks_the_whole_scroll_chain_innermost_first() {
1772 let lrs = window(layout_result(chain_dom(SCROLL_CSS)));
1773 let mut sm = ScrollManager::new();
1774 register(
1777 &mut sm,
1778 INNER,
1779 rect(0.0, 400.0, 100.0, 100.0),
1780 size(100.0, 1000.0),
1781 );
1782 register(
1783 &mut sm,
1784 OUTER,
1785 rect(0.0, 0.0, 200.0, 200.0),
1786 size(200.0, 2000.0),
1787 );
1788
1789 let adjustments = scroll_rect_into_view(
1790 rect(0.0, 900.0, 50.0, 20.0),
1791 dom_id(0),
1792 nid(TARGET),
1793 &lrs,
1794 &mut sm,
1795 opts(
1796 ScrollLogicalPosition::Start,
1797 ScrollLogicalPosition::Start,
1798 ScrollIntoViewBehavior::Instant,
1799 ),
1800 now(),
1801 );
1802
1803 assert_eq!(adjustments.len(), 2);
1804 assert_eq!(adjustments[0].scroll_container_node_id, nid(INNER));
1805 assert_eq!(adjustments[1].scroll_container_node_id, nid(OUTER));
1806 assert!(close(adjustments[0].delta.y, 500.0), "{:?}", adjustments[0]);
1808 assert!(close(adjustments[1].delta.y, 400.0), "{:?}", adjustments[1]);
1811
1812 let inner_offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1813 let outer_offset = sm.get_current_offset(dom_id(0), nid(OUTER)).unwrap();
1814 assert!(close(inner_offset.y, 500.0), "{inner_offset:?}");
1815 assert!(close(outer_offset.y, 400.0), "{outer_offset:?}");
1816 }
1817
1818 #[test]
1823 fn node_into_view_missing_layout_results_is_empty() {
1824 let empty: BTreeMap<DomId, DomLayoutResult> = BTreeMap::new();
1825 let mut sm = ScrollManager::new();
1826 assert!(scroll_node_into_view(
1827 dnid(0, TARGET),
1828 &empty,
1829 &mut sm,
1830 ScrollIntoViewOptions::nearest(),
1831 now(),
1832 )
1833 .is_empty());
1834 }
1835
1836 #[test]
1837 fn node_into_view_null_node_is_empty() {
1838 let (lrs, mut sm) = inner_only();
1839 assert!(scroll_node_into_view(
1840 null_dnid(0),
1841 &lrs,
1842 &mut sm,
1843 ScrollIntoViewOptions::nearest(),
1844 now(),
1845 )
1846 .is_empty());
1847 }
1848
1849 #[test]
1850 fn node_into_view_without_a_layout_box_is_empty() {
1851 let (lrs, mut sm) = inner_only();
1853 assert!(scroll_node_into_view(
1854 dnid(0, TARGET),
1855 &lrs,
1856 &mut sm,
1857 ScrollIntoViewOptions::center(),
1858 now(),
1859 )
1860 .is_empty());
1861 }
1862
1863 #[test]
1864 fn node_into_view_scrolls_the_nodes_bounding_rect() {
1865 let lrs = window(layout_result_with_boxes(
1866 chain_dom(SCROLL_CSS),
1867 &[(TARGET, pos(0.0, 500.0), Some(size(50.0, 20.0)))],
1868 ));
1869 let mut sm = ScrollManager::new();
1870 register(
1871 &mut sm,
1872 INNER,
1873 rect(0.0, 0.0, 100.0, 100.0),
1874 size(100.0, 1000.0),
1875 );
1876
1877 let adjustments = scroll_node_into_view(
1878 dnid(0, TARGET),
1879 &lrs,
1880 &mut sm,
1881 opts(
1882 ScrollLogicalPosition::Start,
1883 ScrollLogicalPosition::Start,
1884 ScrollIntoViewBehavior::Instant,
1885 ),
1886 now(),
1887 );
1888 assert_eq!(adjustments.len(), 1);
1889 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1890 assert!(close(offset.y, 500.0), "{offset:?}");
1891 }
1892
1893 #[test]
1894 fn node_into_view_center_alignment_centers_the_node() {
1895 let lrs = window(layout_result_with_boxes(
1896 chain_dom(SCROLL_CSS),
1897 &[(TARGET, pos(0.0, 500.0), Some(size(50.0, 20.0)))],
1898 ));
1899 let mut sm = ScrollManager::new();
1900 register(
1901 &mut sm,
1902 INNER,
1903 rect(0.0, 0.0, 100.0, 100.0),
1904 size(100.0, 1000.0),
1905 );
1906
1907 let adjustments = scroll_node_into_view(
1908 dnid(0, TARGET),
1909 &lrs,
1910 &mut sm,
1911 opts(
1912 ScrollLogicalPosition::Center,
1913 ScrollLogicalPosition::Center,
1914 ScrollIntoViewBehavior::Instant,
1915 ),
1916 now(),
1917 );
1918 assert_eq!(adjustments.len(), 1);
1919 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1921 assert!(close(offset.y, 460.0), "{offset:?}");
1922 }
1923
1924 fn cursor_fixture(content: LogicalSize) -> (BTreeMap<DomId, DomLayoutResult>, ScrollManager) {
1930 let lrs = window(layout_result_with_boxes(
1931 chain_dom(SCROLL_CSS),
1932 &[(TARGET, pos(0.0, 0.0), Some(size(100.0, 1000.0)))],
1933 ));
1934 let mut sm = ScrollManager::new();
1935 register(&mut sm, INNER, rect(0.0, 0.0, 100.0, 100.0), content);
1936 (lrs, sm)
1937 }
1938
1939 #[test]
1940 fn cursor_into_view_missing_node_is_empty() {
1941 let empty: BTreeMap<DomId, DomLayoutResult> = BTreeMap::new();
1942 let mut sm = ScrollManager::new();
1943 assert!(scroll_cursor_into_view(
1944 rect(0.0, 800.0, 2.0, 16.0),
1945 dnid(0, TARGET),
1946 &empty,
1947 &mut sm,
1948 ScrollIntoViewOptions::nearest(),
1949 now(),
1950 )
1951 .is_empty());
1952 }
1953
1954 #[test]
1955 fn cursor_into_view_null_node_is_empty() {
1956 let (lrs, mut sm) = cursor_fixture(size(100.0, 1000.0));
1957 assert!(scroll_cursor_into_view(
1958 rect(0.0, 800.0, 2.0, 16.0),
1959 null_dnid(0),
1960 &lrs,
1961 &mut sm,
1962 ScrollIntoViewOptions::nearest(),
1963 now(),
1964 )
1965 .is_empty());
1966 }
1967
1968 #[test]
1969 fn cursor_into_view_zero_rect_maps_to_the_node_origin() {
1970 let (lrs, mut sm) = cursor_fixture(size(100.0, 1000.0));
1973 let adjustments = scroll_cursor_into_view(
1974 LogicalRect::zero(),
1975 dnid(0, TARGET),
1976 &lrs,
1977 &mut sm,
1978 ScrollIntoViewOptions::nearest(),
1979 now(),
1980 );
1981 assert!(adjustments.is_empty());
1982 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
1983 assert_eq!((offset.x, offset.y), (0.0, 0.0));
1984 }
1985
1986 #[test]
1987 fn cursor_into_view_transforms_local_coordinates_to_absolute() {
1988 let (lrs, mut sm) = cursor_fixture(size(100.0, 1000.0));
1989 let adjustments = scroll_cursor_into_view(
1991 rect(0.0, 800.0, 2.0, 16.0),
1992 dnid(0, TARGET),
1993 &lrs,
1994 &mut sm,
1995 opts(
1996 ScrollLogicalPosition::Start,
1997 ScrollLogicalPosition::Start,
1998 ScrollIntoViewBehavior::Instant,
1999 ),
2000 now(),
2001 );
2002 assert_eq!(adjustments.len(), 1);
2003 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
2004 assert!(close(offset.y, 800.0), "{offset:?}");
2005 }
2006
2007 #[test]
2008 fn cursor_into_view_scrolls_back_up_to_a_cursor_above_the_viewport() {
2009 let (lrs, mut sm) = cursor_fixture(size(100.0, 1000.0));
2010 sm.set_scroll_position(dom_id(0), nid(INNER), pos(0.0, 300.0), now());
2011
2012 let adjustments = scroll_cursor_into_view(
2013 rect(0.0, 50.0, 2.0, 16.0),
2014 dnid(0, TARGET),
2015 &lrs,
2016 &mut sm,
2017 opts(
2018 ScrollLogicalPosition::Start,
2019 ScrollLogicalPosition::Start,
2020 ScrollIntoViewBehavior::Instant,
2021 ),
2022 now(),
2023 );
2024 assert_eq!(adjustments.len(), 1);
2025 assert!(close(adjustments[0].delta.y, -250.0), "{:?}", adjustments[0]);
2027 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
2028 assert!(close(offset.y, 50.0), "{offset:?}");
2029 }
2030
2031 #[test]
2032 fn cursor_into_view_nan_cursor_rect_does_not_scroll_or_panic() {
2033 let (lrs, mut sm) = cursor_fixture(size(100.0, 1000.0));
2034 let adjustments = scroll_cursor_into_view(
2035 LogicalRect::new(pos(f32::NAN, f32::NAN), size(2.0, 16.0)),
2036 dnid(0, TARGET),
2037 &lrs,
2038 &mut sm,
2039 opts(
2040 ScrollLogicalPosition::Start,
2041 ScrollLogicalPosition::Start,
2042 ScrollIntoViewBehavior::Instant,
2043 ),
2044 now(),
2045 );
2046 assert!(adjustments.is_empty());
2047 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
2048 assert!(offset.x.is_finite() && offset.y.is_finite(), "{offset:?}");
2049 assert_eq!((offset.x, offset.y), (0.0, 0.0));
2050 }
2051
2052 #[test]
2053 fn cursor_into_view_f32_max_cursor_clamps_to_max_scroll_on_both_axes() {
2054 let (lrs, mut sm) = cursor_fixture(size(500.0, 1000.0));
2056 let adjustments = scroll_cursor_into_view(
2057 LogicalRect::new(pos(f32::MAX, f32::MAX), size(2.0, 16.0)),
2058 dnid(0, TARGET),
2059 &lrs,
2060 &mut sm,
2061 opts(
2062 ScrollLogicalPosition::Start,
2063 ScrollLogicalPosition::Start,
2064 ScrollIntoViewBehavior::Instant,
2065 ),
2066 now(),
2067 );
2068 assert_eq!(adjustments.len(), 1);
2069 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
2070 assert!(offset.x.is_finite() && offset.y.is_finite(), "{offset:?}");
2071 assert!(close(offset.x, 400.0) && close(offset.y, 900.0), "{offset:?}");
2072 }
2073
2074 #[test]
2075 fn cursor_into_view_infinite_cursor_rect_does_not_panic() {
2076 let (lrs, mut sm) = cursor_fixture(size(500.0, 1000.0));
2077 let adjustments = scroll_cursor_into_view(
2078 LogicalRect::new(
2079 pos(f32::NEG_INFINITY, f32::INFINITY),
2080 size(f32::INFINITY, f32::INFINITY),
2081 ),
2082 dnid(0, TARGET),
2083 &lrs,
2084 &mut sm,
2085 opts(
2086 ScrollLogicalPosition::Nearest,
2087 ScrollLogicalPosition::Nearest,
2088 ScrollIntoViewBehavior::Instant,
2089 ),
2090 now(),
2091 );
2092 let _ = adjustments;
2094 let offset = sm.get_current_offset(dom_id(0), nid(INNER)).unwrap();
2095 assert!(offset.x.is_finite() && offset.y.is_finite(), "{offset:?}");
2096 assert!(offset.x >= 0.0 && offset.x <= 400.0, "{offset:?}");
2097 assert!(offset.y >= 0.0 && offset.y <= 900.0, "{offset:?}");
2098 }
2099}