1use alloc::collections::BTreeMap;
9
10#[cfg(feature = "std")]
11use std::collections::HashMap;
12#[cfg(not(feature = "std"))]
13use alloc::collections::BTreeMap as HashMap;
14
15use azul_core::{
16 dom::{DomId, NodeId},
17 dom::ScrollbarOrientation,
18 geom::{LogicalPosition, LogicalRect, LogicalSize},
19 gpu::{GpuEventChanges, GpuTransformKeyEvent, GpuValueCache},
20 resources::TransformKey,
21 task::{Duration, SystemTimeDiff},
22 transform::ComputedTransform3D,
23};
24
25use crate::{
26 managers::scroll_state::ScrollManager,
27 solver3::{
28 fc::DEFAULT_SCROLLBAR_WIDTH_PX,
29 layout_tree::LayoutTree,
30 scrollbar::compute_scrollbar_geometry_with_button_size,
31 },
32};
33
34pub const DEFAULT_FADE_DELAY_MS: u64 = 500;
36pub const DEFAULT_FADE_DURATION_MS: u64 = 200;
38
39#[derive(Debug, Clone)]
48pub struct GpuStateManager {
49 pub caches: BTreeMap<DomId, GpuValueCache>,
51 pub fade_delay: Duration,
53 pub fade_duration: Duration,
55 pub scrollbar_fade_active: bool,
60 pub pending_changes: GpuEventChanges,
65}
66
67impl Default for GpuStateManager {
68 fn default() -> Self {
69 Self::new(
70 Duration::System(SystemTimeDiff::from_millis(DEFAULT_FADE_DELAY_MS)),
71 Duration::System(SystemTimeDiff::from_millis(DEFAULT_FADE_DURATION_MS)),
72 )
73 }
74}
75
76impl GpuStateManager {
77 #[must_use] pub fn new(fade_delay: Duration, fade_duration: Duration) -> Self {
79 Self {
80 caches: BTreeMap::new(),
81 fade_delay,
82 fade_duration,
83 scrollbar_fade_active: false,
84 pending_changes: GpuEventChanges::empty(),
85 }
86 }
87
88 pub fn take_pending_changes(&mut self) -> GpuEventChanges {
91 core::mem::take(&mut self.pending_changes)
92 }
93
94 #[must_use] pub fn get_cache(&self, dom_id: DomId) -> Option<&GpuValueCache> {
104 self.caches.get(&dom_id)
105 }
106
107 pub fn get_or_create_cache(&mut self, dom_id: DomId) -> &mut GpuValueCache {
108 self.caches.entry(dom_id).or_default()
109 }
110
111 pub fn update_scrollbar_transforms(
117 &mut self,
118 dom_id: DomId,
119 scroll_manager: &ScrollManager,
120 layout_tree: &LayoutTree,
121 ) -> GpuEventChanges {
122 let mut changes = GpuEventChanges::empty();
123 let gpu_cache = self.get_or_create_cache(dom_id);
124
125 for (node_idx, node) in layout_tree.nodes.iter().enumerate() {
126 let warm = layout_tree.warm(node_idx);
127 let Some(scrollbar_info) = warm.and_then(|w| w.scrollbar_info.as_ref()) else {
128 continue;
129 };
130 let Some(node_id) = node.dom_node_id else {
131 continue;
132 };
133
134 let scroll_offset = scroll_manager
135 .get_current_offset(dom_id, node_id)
136 .unwrap_or_default();
137
138 let border_box_size = node.used_size.unwrap_or_default();
140 let nbp = node.box_props.unpack();
141 let border = &nbp.border;
142 let inner_size = LogicalSize {
143 width: (border_box_size.width - border.left - border.right).max(0.0),
144 height: (border_box_size.height - border.top - border.bottom).max(0.0),
145 };
146 let inner_rect = LogicalRect {
148 origin: LogicalPosition::new(0.0, 0.0),
149 size: inner_size,
150 };
151
152 let content_size = layout_tree.get_content_size(node_idx);
154
155 if scrollbar_info.needs_vertical {
156 let is_overlay = scrollbar_info.scrollbar_height == 0.0;
161 let scrollbar_width_px = if scrollbar_info.visual_width_px > 0.0 {
162 scrollbar_info.visual_width_px
163 } else if !is_overlay {
164 scrollbar_info.scrollbar_height
165 } else {
166 DEFAULT_SCROLLBAR_WIDTH_PX
167 };
168 let button_size = if is_overlay { 0.0 } else { scrollbar_width_px };
170
171 let v_geom = compute_scrollbar_geometry_with_button_size(
172 ScrollbarOrientation::Vertical,
173 inner_rect,
174 content_size,
175 scroll_offset.y,
176 scrollbar_width_px,
177 scrollbar_info.needs_horizontal,
178 button_size,
179 );
180
181 let transform =
182 ComputedTransform3D::new_translation(0.0, v_geom.thumb_offset, 0.0);
183 update_scrollbar_transform_key(gpu_cache, &mut changes, node_id, transform, ScrollbarOrientation::Vertical);
184 }
185
186 if scrollbar_info.needs_horizontal {
187 let is_overlay = scrollbar_info.scrollbar_width == 0.0;
188 let scrollbar_width_px = if scrollbar_info.visual_width_px > 0.0 {
189 scrollbar_info.visual_width_px
190 } else if !is_overlay {
191 scrollbar_info.scrollbar_width
192 } else {
193 DEFAULT_SCROLLBAR_WIDTH_PX
194 };
195 let button_size = if is_overlay { 0.0 } else { scrollbar_width_px };
196
197 let h_geom = compute_scrollbar_geometry_with_button_size(
198 ScrollbarOrientation::Horizontal,
199 inner_rect,
200 content_size,
201 scroll_offset.x,
202 scrollbar_width_px,
203 scrollbar_info.needs_vertical,
204 button_size,
205 );
206
207 let transform =
208 ComputedTransform3D::new_translation(h_geom.thumb_offset, 0.0, 0.0);
209 update_scrollbar_transform_key(gpu_cache, &mut changes, node_id, transform, ScrollbarOrientation::Horizontal);
210 }
211 }
212
213 changes
214 }
215}
216
217fn update_scrollbar_transform_key(
219 gpu_cache: &mut GpuValueCache,
220 changes: &mut GpuEventChanges,
221 node_id: NodeId,
222 transform: ComputedTransform3D,
223 orientation: ScrollbarOrientation,
224) {
225 let (keys, values) = match orientation {
226 ScrollbarOrientation::Vertical => (
227 &mut gpu_cache.transform_keys,
228 &mut gpu_cache.current_transform_values,
229 ),
230 ScrollbarOrientation::Horizontal => (
231 &mut gpu_cache.h_transform_keys,
232 &mut gpu_cache.h_current_transform_values,
233 ),
234 };
235
236 if let Some(existing_transform) = values.get(&node_id) {
237 if *existing_transform != transform {
238 let Some(&transform_key) = keys.get(&node_id) else {
239 return;
240 };
241 changes
242 .transform_key_changes
243 .push(GpuTransformKeyEvent::Changed(
244 node_id,
245 transform_key,
246 *existing_transform,
247 transform,
248 ));
249 values.insert(node_id, transform);
250 }
251 } else {
252 let transform_key = TransformKey::unique();
253 keys.insert(node_id, transform_key);
254 values.insert(node_id, transform);
255 changes
256 .transform_key_changes
257 .push(GpuTransformKeyEvent::Added(
258 node_id,
259 transform_key,
260 transform,
261 ));
262 }
263}
264
265impl crate::managers::NodeIdRemap for GpuStateManager {
266 fn remap_node_ids(&mut self, dom: DomId, map: &crate::managers::NodeIdMap) {
275 let Some(cache) = self.caches.get_mut(&dom) else {
276 return;
277 };
278
279 remap_hashmap(&mut cache.transform_keys, map);
280 remap_hashmap(&mut cache.current_transform_values, map);
281 remap_hashmap(&mut cache.h_transform_keys, map);
282 remap_hashmap(&mut cache.h_current_transform_values, map);
283 remap_hashmap(&mut cache.css_transform_keys, map);
284 remap_hashmap(&mut cache.css_current_transform_values, map);
285 remap_hashmap(&mut cache.opacity_keys, map);
286 remap_hashmap(&mut cache.current_opacity_values, map);
287 remap_dom_hashmap(&mut cache.scrollbar_v_opacity_keys, dom, map);
288 remap_dom_hashmap(&mut cache.scrollbar_h_opacity_keys, dom, map);
289 remap_dom_hashmap(&mut cache.scrollbar_v_opacity_values, dom, map);
290 remap_dom_hashmap(&mut cache.scrollbar_h_opacity_values, dom, map);
291 }
292}
293
294fn remap_hashmap<V>(map: &mut HashMap<NodeId, V>, node_map: &crate::managers::NodeIdMap) {
296 let old = core::mem::take(map);
297 for (old_id, v) in old {
298 if let Some(new_id) = node_map.resolve(old_id) {
299 map.insert(new_id, v);
300 }
301 }
302}
303
304fn remap_dom_hashmap<V>(
306 map: &mut HashMap<(DomId, NodeId), V>,
307 dom: DomId,
308 node_map: &crate::managers::NodeIdMap,
309) {
310 let old = core::mem::take(map);
311 for ((d, old_id), v) in old {
312 if d != dom {
313 map.insert((d, old_id), v);
314 } else if let Some(new_id) = node_map.resolve(old_id) {
315 map.insert((d, new_id), v);
316 }
317 }
318}
319
320#[cfg(test)]
321mod autotest_generated {
322 use azul_core::{
323 dom::FormattingContext,
324 resources::OpacityKey,
325 task::{Instant, SystemTick, SystemTickDiff},
326 };
327
328 use super::*;
329 use crate::{
330 managers::{NodeIdMap, NodeIdRemap},
331 solver3::{
332 geometry::PackedBoxProps,
333 layout_tree::{LayoutNodeCold, LayoutNodeHot, LayoutNodeWarm},
334 scrollbar::ScrollbarRequirements,
335 },
336 };
337
338 fn dom(inner: usize) -> DomId {
343 DomId { inner }
344 }
345
346 fn t0() -> Instant {
347 Instant::Tick(SystemTick::new(0))
348 }
349
350 fn millis(ms: u64) -> Duration {
351 Duration::System(SystemTimeDiff::from_millis(ms))
352 }
353
354 fn tx(x: f32, y: f32) -> ComputedTransform3D {
357 ComputedTransform3D::new_translation(x, y, 0.0)
358 }
359
360 fn hot(dom_node_id: Option<NodeId>, used: Option<LogicalSize>) -> LayoutNodeHot {
361 LayoutNodeHot {
362 box_props: PackedBoxProps::default(),
363 dom_node_id,
364 used_size: used,
365 formatting_context: FormattingContext::Block {
366 establishes_new_context: true,
367 },
368 parent: None,
369 }
370 }
371
372 fn warm_node(
373 scrollbar_info: Option<ScrollbarRequirements>,
374 content: Option<LogicalSize>,
375 ) -> LayoutNodeWarm {
376 LayoutNodeWarm {
377 scrollbar_info,
378 overflow_content_size: content,
379 ..Default::default()
380 }
381 }
382
383 fn v_scrollbar() -> ScrollbarRequirements {
386 ScrollbarRequirements {
387 needs_horizontal: false,
388 needs_vertical: true,
389 scrollbar_width: 16.0,
390 scrollbar_height: 16.0,
391 visual_width_px: 16.0,
392 }
393 }
394
395 fn h_scrollbar() -> ScrollbarRequirements {
396 ScrollbarRequirements {
397 needs_horizontal: true,
398 needs_vertical: false,
399 scrollbar_width: 16.0,
400 scrollbar_height: 16.0,
401 visual_width_px: 16.0,
402 }
403 }
404
405 fn tree(nodes: Vec<LayoutNodeHot>, warm: Vec<LayoutNodeWarm>) -> LayoutTree {
406 let n = nodes.len();
407 LayoutTree {
408 nodes,
409 warm,
410 cold: vec![LayoutNodeCold::default(); n],
411 root: 0,
412 dom_to_layout: BTreeMap::new(),
413 children_arena: Vec::new(),
414 children_offsets: vec![(0, 0); n],
415 subtree_needs_intrinsic: Vec::new(),
416 }
417 }
418
419 fn one_node_tree(sb: ScrollbarRequirements, content: LogicalSize) -> LayoutTree {
421 tree(
422 vec![hot(
423 Some(NodeId::new(1)),
424 Some(LogicalSize::new(100.0, 100.0)),
425 )],
426 vec![warm_node(Some(sb), Some(content))],
427 )
428 }
429
430 fn sole_added_y(changes: &GpuEventChanges) -> f32 {
432 assert_eq!(changes.transform_key_changes.len(), 1);
433 match changes.transform_key_changes[0] {
434 GpuTransformKeyEvent::Added(_, _, t) => t.m[3][1],
435 ref other => panic!("expected Added, got {other:?}"),
436 }
437 }
438
439 fn sole_added_x(changes: &GpuEventChanges) -> f32 {
440 assert_eq!(changes.transform_key_changes.len(), 1);
441 match changes.transform_key_changes[0] {
442 GpuTransformKeyEvent::Added(_, _, t) => t.m[3][0],
443 ref other => panic!("expected Added, got {other:?}"),
444 }
445 }
446
447 #[test]
452 fn new_stores_both_durations_and_starts_empty_and_idle() {
453 let m = GpuStateManager::new(millis(1), millis(2));
454 assert_eq!(m.fade_delay, millis(1));
455 assert_eq!(m.fade_duration, millis(2));
456 assert!(m.caches.is_empty());
457 assert!(!m.scrollbar_fade_active);
458 assert!(m.pending_changes.is_empty());
459 }
460
461 #[test]
462 fn new_survives_zero_and_u64_max_durations_without_panicking() {
463 let zero = GpuStateManager::new(millis(0), millis(0));
466 assert_eq!(zero.fade_delay, millis(0));
467 assert_eq!(zero.fade_duration, millis(0));
468
469 let huge = GpuStateManager::new(millis(u64::MAX), millis(u64::MAX));
471 assert_eq!(huge.fade_delay, millis(u64::MAX));
472 assert!(huge.caches.is_empty());
473 }
474
475 #[test]
476 fn new_accepts_tick_durations_not_just_system_durations() {
477 let tick = Duration::Tick(SystemTickDiff { tick_diff: u64::MAX });
479 let m = GpuStateManager::new(tick, tick);
480 assert_eq!(m.fade_delay, tick);
481 assert_eq!(m.fade_duration, tick);
482 }
483
484 #[test]
485 fn default_matches_the_documented_fade_constants() {
486 let m = GpuStateManager::default();
487 assert_eq!(m.fade_delay, millis(DEFAULT_FADE_DELAY_MS));
488 assert_eq!(m.fade_duration, millis(DEFAULT_FADE_DURATION_MS));
489 assert_eq!(DEFAULT_FADE_DELAY_MS, 500);
490 assert_eq!(DEFAULT_FADE_DURATION_MS, 200);
491 }
492
493 #[test]
498 fn take_pending_changes_drains_the_buffer_and_the_second_take_is_empty() {
499 let mut m = GpuStateManager::default();
500 m.pending_changes
501 .transform_key_changes
502 .push(GpuTransformKeyEvent::Added(
503 NodeId::new(1),
504 TransformKey::unique(),
505 tx(0.0, 4.0),
506 ));
507
508 let taken = m.take_pending_changes();
509 assert_eq!(taken.transform_key_changes.len(), 1);
510 assert!(m.pending_changes.is_empty());
512 assert!(m.take_pending_changes().is_empty());
513 }
514
515 #[test]
516 fn take_pending_changes_on_a_fresh_manager_is_an_empty_no_panic() {
517 let mut m = GpuStateManager::default();
518 for _ in 0..3 {
519 assert_eq!(m.take_pending_changes(), GpuEventChanges::empty());
520 }
521 }
522
523 #[test]
528 fn get_cache_on_an_unknown_dom_returns_none_and_does_not_create_it() {
529 let m = GpuStateManager::default();
530 assert!(m.get_cache(dom(0)).is_none());
531 assert!(m.get_cache(dom(usize::MAX)).is_none());
532 assert!(m.caches.is_empty(), "get_cache must not mutate");
533 }
534
535 #[test]
536 fn get_or_create_cache_is_idempotent_and_never_clobbers_existing_state() {
537 let mut m = GpuStateManager::default();
538 let node = NodeId::new(7);
539 let key = TransformKey::unique();
540
541 m.get_or_create_cache(dom(0)).transform_keys.insert(node, key);
542 assert_eq!(m.caches.len(), 1);
543
544 let again = m.get_or_create_cache(dom(0));
547 assert_eq!(again.transform_keys.get(&node), Some(&key));
548 assert_eq!(m.caches.len(), 1);
549 assert!(m.get_cache(dom(0)).is_some());
550 }
551
552 #[test]
553 fn caches_for_distinct_dom_ids_including_usize_max_do_not_alias() {
554 let mut m = GpuStateManager::default();
555 let node = NodeId::new(0);
556
557 m.get_or_create_cache(dom(0))
558 .current_opacity_values
559 .insert(node, 0.25);
560 m.get_or_create_cache(dom(usize::MAX))
561 .current_opacity_values
562 .insert(node, 0.75);
563
564 assert_eq!(m.caches.len(), 2);
565 assert_eq!(
566 m.get_cache(dom(0)).unwrap().current_opacity_values.get(&node),
567 Some(&0.25)
568 );
569 assert_eq!(
570 m.get_cache(dom(usize::MAX))
571 .unwrap()
572 .current_opacity_values
573 .get(&node),
574 Some(&0.75)
575 );
576 }
577
578 #[test]
583 fn first_update_emits_added_and_populates_both_the_key_and_value_map() {
584 let mut cache = GpuValueCache::default();
585 let mut changes = GpuEventChanges::empty();
586 let node = NodeId::new(3);
587 let t = tx(0.0, 12.0);
588
589 update_scrollbar_transform_key(
590 &mut cache,
591 &mut changes,
592 node,
593 t,
594 ScrollbarOrientation::Vertical,
595 );
596
597 let key = cache.transform_keys.get(&node).copied().expect("key stored");
598 assert_eq!(cache.current_transform_values.get(&node), Some(&t));
599 assert_eq!(
600 changes.transform_key_changes,
601 vec![GpuTransformKeyEvent::Added(node, key, t)]
602 );
603 }
604
605 #[test]
606 fn re_updating_with_an_identical_transform_emits_nothing() {
607 let mut cache = GpuValueCache::default();
610 let node = NodeId::new(3);
611 let t = tx(0.0, 12.0);
612
613 let mut first = GpuEventChanges::empty();
614 update_scrollbar_transform_key(
615 &mut cache,
616 &mut first,
617 node,
618 t,
619 ScrollbarOrientation::Vertical,
620 );
621 let key = cache.transform_keys.get(&node).copied().unwrap();
622
623 for _ in 0..10 {
624 let mut again = GpuEventChanges::empty();
625 update_scrollbar_transform_key(
626 &mut cache,
627 &mut again,
628 node,
629 t,
630 ScrollbarOrientation::Vertical,
631 );
632 assert!(again.is_empty(), "unchanged transform must not re-emit");
633 }
634 assert_eq!(cache.transform_keys.get(&node).copied(), Some(key));
636 }
637
638 #[test]
639 fn changing_the_transform_emits_changed_with_old_and_new_and_reuses_the_key() {
640 let mut cache = GpuValueCache::default();
641 let node = NodeId::new(3);
642 let old = tx(0.0, 12.0);
643 let new = tx(0.0, 40.0);
644
645 let mut changes = GpuEventChanges::empty();
646 update_scrollbar_transform_key(
647 &mut cache,
648 &mut changes,
649 node,
650 old,
651 ScrollbarOrientation::Vertical,
652 );
653 let key = cache.transform_keys.get(&node).copied().unwrap();
654
655 let mut changes = GpuEventChanges::empty();
656 update_scrollbar_transform_key(
657 &mut cache,
658 &mut changes,
659 node,
660 new,
661 ScrollbarOrientation::Vertical,
662 );
663
664 assert_eq!(
665 changes.transform_key_changes,
666 vec![GpuTransformKeyEvent::Changed(node, key, old, new)]
667 );
668 assert_eq!(cache.transform_keys.get(&node).copied(), Some(key));
670 assert_eq!(cache.current_transform_values.get(&node), Some(&new));
671 }
672
673 #[test]
674 fn vertical_and_horizontal_keys_for_the_same_node_are_fully_independent() {
675 let mut cache = GpuValueCache::default();
679 let mut changes = GpuEventChanges::empty();
680 let node = NodeId::new(5);
681 let v = tx(0.0, 10.0);
682 let h = tx(20.0, 0.0);
683
684 update_scrollbar_transform_key(
685 &mut cache,
686 &mut changes,
687 node,
688 v,
689 ScrollbarOrientation::Vertical,
690 );
691 update_scrollbar_transform_key(
692 &mut cache,
693 &mut changes,
694 node,
695 h,
696 ScrollbarOrientation::Horizontal,
697 );
698
699 assert_eq!(changes.transform_key_changes.len(), 2);
700 assert_eq!(cache.current_transform_values.get(&node), Some(&v));
701 assert_eq!(cache.h_current_transform_values.get(&node), Some(&h));
702 assert_ne!(
703 cache.transform_keys.get(&node),
704 cache.h_transform_keys.get(&node),
705 "the two orientations must not share a TransformKey"
706 );
707 }
708
709 #[test]
710 fn a_value_without_its_key_silently_drops_the_update_and_stays_stale() {
711 let mut cache = GpuValueCache::default();
716 let node = NodeId::new(3);
717 let stale = tx(0.0, 5.0);
718 cache.current_transform_values.insert(node, stale);
719
720 let mut changes = GpuEventChanges::empty();
721 update_scrollbar_transform_key(
722 &mut cache,
723 &mut changes,
724 node,
725 tx(0.0, 99.0),
726 ScrollbarOrientation::Vertical,
727 );
728
729 assert!(changes.is_empty(), "no event is emitted for a keyless value");
730 assert_eq!(
731 cache.current_transform_values.get(&node),
732 Some(&stale),
733 "the value is left stale rather than repaired"
734 );
735 assert!(
736 !cache.transform_keys.contains_key(&node),
737 "and no key is minted to recover"
738 );
739 }
740
741 #[test]
742 fn a_key_without_its_value_mints_a_fresh_key_and_orphans_the_old_one() {
743 let mut cache = GpuValueCache::default();
748 let node = NodeId::new(3);
749 let orphan = TransformKey::unique();
750 cache.transform_keys.insert(node, orphan);
751
752 let mut changes = GpuEventChanges::empty();
753 let t = tx(0.0, 7.0);
754 update_scrollbar_transform_key(
755 &mut cache,
756 &mut changes,
757 node,
758 t,
759 ScrollbarOrientation::Vertical,
760 );
761
762 let fresh = cache.transform_keys.get(&node).copied().unwrap();
763 assert_ne!(fresh, orphan, "a brand-new key replaces the orphan");
764 assert_eq!(
765 changes.transform_key_changes,
766 vec![GpuTransformKeyEvent::Added(node, fresh, t)],
767 "and it is announced as Added, never as Removed(orphan)"
768 );
769 }
770
771 #[test]
772 fn a_nan_transform_never_converges_and_re_emits_changed_every_single_call() {
773 let mut cache = GpuValueCache::default();
779 let node = NodeId::new(3);
780 let nan = tx(0.0, f32::NAN);
781
782 let mut changes = GpuEventChanges::empty();
783 update_scrollbar_transform_key(
784 &mut cache,
785 &mut changes,
786 node,
787 nan,
788 ScrollbarOrientation::Vertical,
789 );
790 assert_eq!(changes.transform_key_changes.len(), 1, "Added");
791
792 for _ in 0..5 {
794 update_scrollbar_transform_key(
795 &mut cache,
796 &mut changes,
797 node,
798 nan,
799 ScrollbarOrientation::Vertical,
800 );
801 }
802 assert_eq!(
803 changes.transform_key_changes.len(),
804 6,
805 "NaN re-emits Changed on every call instead of settling"
806 );
807 assert!(cache.current_transform_values[&node].m[3][1].is_nan());
808 }
809
810 fn no_survivors() -> NodeIdMap {
816 NodeIdMap::from_pairs(Vec::<(NodeId, NodeId)>::new())
817 }
818
819 #[test]
820 fn remap_hashmap_on_empty_inputs_is_a_no_op() {
821 let mut map: HashMap<NodeId, u32> = HashMap::new();
822 remap_hashmap(&mut map, &no_survivors());
823 assert!(map.is_empty());
824 }
825
826 #[test]
827 fn remap_hashmap_drops_entries_for_unmounted_nodes() {
828 let mut map: HashMap<NodeId, u32> = HashMap::new();
829 map.insert(NodeId::new(1), 10);
830 map.insert(NodeId::new(2), 20);
831 map.insert(NodeId::new(3), 30);
832
833 remap_hashmap(
835 &mut map,
836 &NodeIdMap::from_pairs([(NodeId::new(2), NodeId::new(9))]),
837 );
838
839 assert_eq!(map.len(), 1);
840 assert_eq!(map.get(&NodeId::new(9)), Some(&20));
841 assert!(!map.contains_key(&NodeId::new(1)));
842 assert!(!map.contains_key(&NodeId::new(2)), "old id must not linger");
843 }
844
845 #[test]
846 fn remap_hashmap_survives_a_full_id_swap_without_losing_entries() {
847 let mut map: HashMap<NodeId, u32> = HashMap::new();
850 map.insert(NodeId::new(1), 111);
851 map.insert(NodeId::new(2), 222);
852
853 remap_hashmap(
854 &mut map,
855 &NodeIdMap::from_pairs([
856 (NodeId::new(1), NodeId::new(2)),
857 (NodeId::new(2), NodeId::new(1)),
858 ]),
859 );
860
861 assert_eq!(map.len(), 2, "no entry may be lost to the swap");
862 assert_eq!(map.get(&NodeId::new(2)), Some(&111));
863 assert_eq!(map.get(&NodeId::new(1)), Some(&222));
864 }
865
866 #[test]
867 fn remap_hashmap_collapses_two_old_ids_that_alias_onto_one_new_id() {
868 let mut map: HashMap<NodeId, u32> = HashMap::new();
872 map.insert(NodeId::new(1), 111);
873 map.insert(NodeId::new(2), 222);
874
875 remap_hashmap(
876 &mut map,
877 &NodeIdMap::from_pairs([
878 (NodeId::new(1), NodeId::new(5)),
879 (NodeId::new(2), NodeId::new(5)),
880 ]),
881 );
882
883 assert_eq!(map.len(), 1, "the alias collapses both entries into one");
884 let survivor = map.get(&NodeId::new(5)).copied().unwrap();
885 assert!(survivor == 111 || survivor == 222);
886 }
887
888 #[test]
889 fn remap_hashmap_handles_node_id_max_without_overflowing() {
890 let mut map: HashMap<NodeId, u32> = HashMap::new();
891 map.insert(NodeId::new(usize::MAX), 1);
892 map.insert(NodeId::ZERO, 2);
893
894 remap_hashmap(
895 &mut map,
896 &NodeIdMap::from_pairs([
897 (NodeId::new(usize::MAX), NodeId::ZERO),
898 (NodeId::ZERO, NodeId::new(usize::MAX)),
899 ]),
900 );
901
902 assert_eq!(map.get(&NodeId::ZERO), Some(&1));
903 assert_eq!(map.get(&NodeId::new(usize::MAX)), Some(&2));
904 }
905
906 #[test]
911 fn remap_dom_hashmap_leaves_other_doms_completely_untouched() {
912 let mut map: HashMap<(DomId, NodeId), u32> = HashMap::new();
913 map.insert((dom(0), NodeId::new(1)), 1);
914 map.insert((dom(1), NodeId::new(1)), 2);
915
916 remap_dom_hashmap(
920 &mut map,
921 dom(0),
922 &NodeIdMap::from_pairs([(NodeId::new(1), NodeId::new(4))]),
923 );
924
925 assert_eq!(map.len(), 2);
926 assert_eq!(map.get(&(dom(0), NodeId::new(4))), Some(&1));
927 assert_eq!(
928 map.get(&(dom(1), NodeId::new(1))),
929 Some(&2),
930 "a foreign DOM's entry must not be dropped as 'unmounted'"
931 );
932 }
933
934 #[test]
935 fn remap_dom_hashmap_drops_only_the_target_doms_unmounted_nodes() {
936 let mut map: HashMap<(DomId, NodeId), u32> = HashMap::new();
937 map.insert((dom(0), NodeId::new(1)), 1); map.insert((dom(0), NodeId::new(2)), 2); map.insert((dom(1), NodeId::new(2)), 3); remap_dom_hashmap(
942 &mut map,
943 dom(0),
944 &NodeIdMap::from_pairs([(NodeId::new(1), NodeId::new(4))]),
945 );
946
947 assert_eq!(map.len(), 2);
948 assert_eq!(map.get(&(dom(0), NodeId::new(4))), Some(&1));
949 assert!(!map.contains_key(&(dom(0), NodeId::new(2))));
950 assert_eq!(map.get(&(dom(1), NodeId::new(2))), Some(&3));
951 }
952
953 #[test]
954 fn remap_dom_hashmap_does_not_let_a_remap_collide_across_doms() {
955 let mut map: HashMap<(DomId, NodeId), u32> = HashMap::new();
958 map.insert((dom(0), NodeId::new(1)), 11);
959 map.insert((dom(1), NodeId::new(2)), 22);
960
961 remap_dom_hashmap(
962 &mut map,
963 dom(0),
964 &NodeIdMap::from_pairs([(NodeId::new(1), NodeId::new(2))]),
965 );
966
967 assert_eq!(map.len(), 2);
968 assert_eq!(map.get(&(dom(0), NodeId::new(2))), Some(&11));
969 assert_eq!(map.get(&(dom(1), NodeId::new(2))), Some(&22));
970 }
971
972 #[test]
973 fn remap_dom_hashmap_survives_a_shift_chain_within_one_dom() {
974 let mut map: HashMap<(DomId, NodeId), u32> = HashMap::new();
977 map.insert((dom(0), NodeId::new(1)), 11);
978 map.insert((dom(0), NodeId::new(2)), 22);
979
980 remap_dom_hashmap(
981 &mut map,
982 dom(0),
983 &NodeIdMap::from_pairs([
984 (NodeId::new(1), NodeId::new(2)),
985 (NodeId::new(2), NodeId::new(3)),
986 ]),
987 );
988
989 assert_eq!(map.len(), 2);
990 assert_eq!(map.get(&(dom(0), NodeId::new(2))), Some(&11));
991 assert_eq!(map.get(&(dom(0), NodeId::new(3))), Some(&22));
992 }
993
994 #[test]
999 fn remap_node_ids_for_a_dom_with_no_cache_is_a_no_op() {
1000 let mut m = GpuStateManager::default();
1001 m.remap_node_ids(
1002 dom(3),
1003 &NodeIdMap::from_pairs([(NodeId::new(1), NodeId::new(2))]),
1004 );
1005 assert!(m.caches.is_empty());
1006 }
1007
1008 #[test]
1009 fn remap_node_ids_rewrites_every_one_of_the_twelve_cache_maps() {
1010 let mut m = GpuStateManager::default();
1013 let old = NodeId::new(1);
1014 let new = NodeId::new(8);
1015 let d = dom(0);
1016 {
1017 let c = m.get_or_create_cache(d);
1018 c.transform_keys.insert(old, TransformKey::unique());
1019 c.current_transform_values.insert(old, tx(0.0, 1.0));
1020 c.h_transform_keys.insert(old, TransformKey::unique());
1021 c.h_current_transform_values.insert(old, tx(2.0, 0.0));
1022 c.css_transform_keys.insert(old, TransformKey::unique());
1023 c.css_current_transform_values.insert(old, tx(3.0, 3.0));
1024 c.opacity_keys.insert(old, OpacityKey::unique());
1025 c.current_opacity_values.insert(old, 0.5);
1026 c.scrollbar_v_opacity_keys.insert((d, old), OpacityKey::unique());
1027 c.scrollbar_h_opacity_keys.insert((d, old), OpacityKey::unique());
1028 c.scrollbar_v_opacity_values.insert((d, old), 0.25);
1029 c.scrollbar_h_opacity_values.insert((d, old), 0.75);
1030 }
1031
1032 m.remap_node_ids(d, &NodeIdMap::from_pairs([(old, new)]));
1033
1034 let c = m.get_cache(d).unwrap();
1035 assert!(c.transform_keys.contains_key(&new));
1036 assert_eq!(c.current_transform_values.get(&new), Some(&tx(0.0, 1.0)));
1037 assert!(c.h_transform_keys.contains_key(&new));
1038 assert_eq!(c.h_current_transform_values.get(&new), Some(&tx(2.0, 0.0)));
1039 assert!(c.css_transform_keys.contains_key(&new));
1040 assert_eq!(c.css_current_transform_values.get(&new), Some(&tx(3.0, 3.0)));
1041 assert!(c.opacity_keys.contains_key(&new));
1042 assert_eq!(c.current_opacity_values.get(&new), Some(&0.5));
1043 assert!(c.scrollbar_v_opacity_keys.contains_key(&(d, new)));
1044 assert!(c.scrollbar_h_opacity_keys.contains_key(&(d, new)));
1045 assert_eq!(c.scrollbar_v_opacity_values.get(&(d, new)), Some(&0.25));
1046 assert_eq!(c.scrollbar_h_opacity_values.get(&(d, new)), Some(&0.75));
1047
1048 assert!(!c.transform_keys.contains_key(&old));
1050 assert!(!c.current_opacity_values.contains_key(&old));
1051 assert!(!c.scrollbar_v_opacity_values.contains_key(&(d, old)));
1052 }
1053
1054 #[test]
1055 fn remap_node_ids_drops_the_gpu_keys_of_an_unmounted_node() {
1056 let mut m = GpuStateManager::default();
1057 let gone = NodeId::new(1);
1058 let d = dom(0);
1059 {
1060 let c = m.get_or_create_cache(d);
1061 c.transform_keys.insert(gone, TransformKey::unique());
1062 c.current_transform_values.insert(gone, tx(0.0, 1.0));
1063 c.scrollbar_v_opacity_values.insert((d, gone), 1.0);
1064 }
1065
1066 m.remap_node_ids(d, &no_survivors());
1068
1069 let c = m.get_cache(d).unwrap();
1070 assert!(c.transform_keys.is_empty());
1071 assert!(c.current_transform_values.is_empty());
1072 assert!(c.scrollbar_v_opacity_values.is_empty());
1073 }
1074
1075 #[test]
1080 fn update_scrollbar_transforms_on_an_empty_tree_yields_no_events() {
1081 let mut m = GpuStateManager::default();
1082 let sm = ScrollManager::new();
1083 let t = tree(Vec::new(), Vec::new());
1084
1085 let changes = m.update_scrollbar_transforms(dom(0), &sm, &t);
1086 assert!(changes.is_empty());
1087 assert!(m.get_cache(dom(0)).is_some());
1089 }
1090
1091 #[test]
1092 fn nodes_without_scrollbar_info_or_without_a_dom_node_id_are_skipped() {
1093 let mut m = GpuStateManager::default();
1094 let sm = ScrollManager::new();
1095 let t = tree(
1096 vec![
1097 hot(None, Some(LogicalSize::new(100.0, 100.0))),
1099 hot(Some(NodeId::new(2)), Some(LogicalSize::new(100.0, 100.0))),
1101 ],
1102 vec![
1103 warm_node(Some(v_scrollbar()), Some(LogicalSize::new(100.0, 1000.0))),
1104 warm_node(None, None),
1105 ],
1106 );
1107
1108 let changes = m.update_scrollbar_transforms(dom(0), &sm, &t);
1109 assert!(changes.is_empty());
1110 assert!(m.get_cache(dom(0)).unwrap().transform_keys.is_empty());
1111 }
1112
1113 #[test]
1114 fn a_warm_array_shorter_than_the_node_array_does_not_index_out_of_bounds() {
1115 let mut m = GpuStateManager::default();
1118 let sm = ScrollManager::new();
1119 let t = tree(
1120 vec![
1121 hot(Some(NodeId::new(1)), Some(LogicalSize::new(100.0, 100.0))),
1122 hot(Some(NodeId::new(2)), Some(LogicalSize::new(100.0, 100.0))),
1123 ],
1124 vec![warm_node(Some(v_scrollbar()), Some(LogicalSize::new(100.0, 1000.0)))],
1126 );
1127
1128 let changes = m.update_scrollbar_transforms(dom(0), &sm, &t);
1129 assert_eq!(changes.transform_key_changes.len(), 1, "only node 0 is seen");
1130 }
1131
1132 #[test]
1133 fn a_vertical_scrollbar_at_scroll_zero_parks_the_thumb_at_offset_zero() {
1134 let mut m = GpuStateManager::default();
1135 let sm = ScrollManager::new();
1136 let t = one_node_tree(v_scrollbar(), LogicalSize::new(100.0, 1000.0));
1137
1138 let changes = m.update_scrollbar_transforms(dom(0), &sm, &t);
1139 assert_eq!(sole_added_y(&changes), 0.0);
1142 }
1143
1144 #[test]
1145 fn scrolling_to_the_bottom_drives_the_thumb_to_the_end_of_the_usable_track() {
1146 let mut m = GpuStateManager::default();
1152 let mut sm = ScrollManager::new();
1153 sm.set_scroll_position_unclamped(
1154 dom(0),
1155 NodeId::new(1),
1156 LogicalPosition::new(0.0, 900.0),
1157 t0(),
1158 );
1159 let t = one_node_tree(v_scrollbar(), LogicalSize::new(100.0, 1000.0));
1160
1161 let y = sole_added_y(&m.update_scrollbar_transforms(dom(0), &sm, &t));
1162 assert!((y - 36.0).abs() < 0.01, "expected thumb at 36.0, got {y}");
1163 }
1164
1165 #[test]
1166 fn an_overscrolled_offset_clamps_the_thumb_instead_of_running_off_the_track() {
1167 let mut m = GpuStateManager::default();
1170 let mut sm = ScrollManager::new();
1171 sm.set_scroll_position_unclamped(
1172 dom(0),
1173 NodeId::new(1),
1174 LogicalPosition::new(0.0, 1.0e9),
1175 t0(),
1176 );
1177 let t = one_node_tree(v_scrollbar(), LogicalSize::new(100.0, 1000.0));
1178
1179 let y = sole_added_y(&m.update_scrollbar_transforms(dom(0), &sm, &t));
1180 assert!((y - 36.0).abs() < 0.01, "overscroll must clamp, got {y}");
1181 }
1182
1183 #[test]
1184 fn a_negative_scroll_offset_is_treated_as_its_absolute_value() {
1185 let mut m = GpuStateManager::default();
1188 let mut sm = ScrollManager::new();
1189 sm.set_scroll_position_unclamped(
1190 dom(0),
1191 NodeId::new(1),
1192 LogicalPosition::new(0.0, -900.0),
1193 t0(),
1194 );
1195 let t = one_node_tree(v_scrollbar(), LogicalSize::new(100.0, 1000.0));
1196
1197 let y = sole_added_y(&m.update_scrollbar_transforms(dom(0), &sm, &t));
1198 assert!(y >= 0.0, "thumb offset must never go negative, got {y}");
1199 assert!((y - 36.0).abs() < 0.01);
1200 }
1201
1202 #[test]
1203 fn running_the_same_layout_twice_emits_no_second_event() {
1204 let mut m = GpuStateManager::default();
1207 let sm = ScrollManager::new();
1208 let t = one_node_tree(v_scrollbar(), LogicalSize::new(100.0, 1000.0));
1209
1210 let first = m.update_scrollbar_transforms(dom(0), &sm, &t);
1211 assert_eq!(first.transform_key_changes.len(), 1);
1212
1213 for _ in 0..5 {
1214 let again = m.update_scrollbar_transforms(dom(0), &sm, &t);
1215 assert!(again.is_empty(), "an idle scrollbar must stay silent");
1216 }
1217 }
1218
1219 #[test]
1220 fn scrolling_after_a_first_pass_emits_changed_and_reuses_the_key() {
1221 let mut m = GpuStateManager::default();
1222 let mut sm = ScrollManager::new();
1223 let t = one_node_tree(v_scrollbar(), LogicalSize::new(100.0, 1000.0));
1224
1225 m.update_scrollbar_transforms(dom(0), &sm, &t);
1226 let key = m
1227 .get_cache(dom(0))
1228 .unwrap()
1229 .transform_keys
1230 .get(&NodeId::new(1))
1231 .copied()
1232 .unwrap();
1233
1234 sm.set_scroll_position_unclamped(
1235 dom(0),
1236 NodeId::new(1),
1237 LogicalPosition::new(0.0, 900.0),
1238 t0(),
1239 );
1240 let changes = m.update_scrollbar_transforms(dom(0), &sm, &t);
1241
1242 assert_eq!(changes.transform_key_changes.len(), 1);
1243 match changes.transform_key_changes[0] {
1244 GpuTransformKeyEvent::Changed(node, k, old, new) => {
1245 assert_eq!(node, NodeId::new(1));
1246 assert_eq!(k, key, "the key must be reused across the scroll");
1247 assert_eq!(old.m[3][1], 0.0);
1248 assert!((new.m[3][1] - 36.0).abs() < 0.01);
1249 }
1250 ref other => panic!("expected Changed, got {other:?}"),
1251 }
1252 }
1253
1254 #[test]
1255 fn a_horizontal_scrollbar_translates_on_x_and_leaves_y_at_zero() {
1256 let mut m = GpuStateManager::default();
1257 let mut sm = ScrollManager::new();
1258 sm.set_scroll_position_unclamped(
1259 dom(0),
1260 NodeId::new(1),
1261 LogicalPosition::new(900.0, 0.0),
1262 t0(),
1263 );
1264 let t = one_node_tree(h_scrollbar(), LogicalSize::new(1000.0, 100.0));
1265
1266 let changes = m.update_scrollbar_transforms(dom(0), &sm, &t);
1267 let x = sole_added_x(&changes);
1268 assert!((x - 36.0).abs() < 0.01, "expected thumb at x=36.0, got {x}");
1269
1270 let c = m.get_cache(dom(0)).unwrap();
1272 assert!(c.h_transform_keys.contains_key(&NodeId::new(1)));
1273 assert!(c.transform_keys.is_empty());
1274 }
1275
1276 #[test]
1277 fn a_node_needing_both_scrollbars_gets_two_independent_keys_and_two_events() {
1278 let mut m = GpuStateManager::default();
1279 let sm = ScrollManager::new();
1280 let both = ScrollbarRequirements {
1281 needs_horizontal: true,
1282 needs_vertical: true,
1283 scrollbar_width: 16.0,
1284 scrollbar_height: 16.0,
1285 visual_width_px: 16.0,
1286 };
1287 let t = one_node_tree(both, LogicalSize::new(1000.0, 1000.0));
1288
1289 let changes = m.update_scrollbar_transforms(dom(0), &sm, &t);
1290 assert_eq!(changes.transform_key_changes.len(), 2);
1291
1292 let c = m.get_cache(dom(0)).unwrap();
1293 let node = NodeId::new(1);
1294 let v = c.transform_keys.get(&node).copied().unwrap();
1295 let h = c.h_transform_keys.get(&node).copied().unwrap();
1296 assert_ne!(v, h);
1297 }
1298
1299 #[test]
1300 fn borders_wider_than_the_border_box_clamp_the_inner_size_to_zero() {
1301 let mut m = GpuStateManager::default();
1305 let sm = ScrollManager::new();
1306 let mut node = hot(Some(NodeId::new(1)), Some(LogicalSize::new(50.0, 50.0)));
1307 node.box_props = PackedBoxProps {
1308 border: [1000, 1000, 1000, 1000], ..Default::default()
1310 };
1311 let t = tree(
1312 vec![node],
1313 vec![warm_node(
1314 Some(v_scrollbar()),
1315 Some(LogicalSize::new(100.0, 1000.0)),
1316 )],
1317 );
1318
1319 let y = sole_added_y(&m.update_scrollbar_transforms(dom(0), &sm, &t));
1320 assert!(y.is_finite(), "a degenerate inner box must not yield {y}");
1321 assert_eq!(y, 0.0);
1322 }
1323
1324 #[test]
1325 fn a_zero_sized_node_with_zero_content_does_not_divide_by_zero() {
1326 let mut m = GpuStateManager::default();
1327 let sm = ScrollManager::new();
1328 let t = tree(
1329 vec![hot(Some(NodeId::new(1)), Some(LogicalSize::zero()))],
1330 vec![warm_node(Some(v_scrollbar()), Some(LogicalSize::zero()))],
1331 );
1332
1333 let y = sole_added_y(&m.update_scrollbar_transforms(dom(0), &sm, &t));
1334 assert!(y.is_finite());
1335 assert_eq!(y, 0.0);
1336 }
1337
1338 #[test]
1339 fn a_missing_used_size_defaults_to_zero_rather_than_panicking() {
1340 let mut m = GpuStateManager::default();
1341 let sm = ScrollManager::new();
1342 let t = tree(
1343 vec![hot(Some(NodeId::new(1)), None)],
1344 vec![warm_node(Some(v_scrollbar()), None)],
1345 );
1346
1347 let y = sole_added_y(&m.update_scrollbar_transforms(dom(0), &sm, &t));
1348 assert!(y.is_finite());
1349 assert_eq!(y, 0.0);
1350 }
1351
1352 #[test]
1353 fn an_infinite_used_size_produces_a_nan_thumb_offset() {
1354 let mut m = GpuStateManager::default();
1363 let sm = ScrollManager::new();
1364 let t = tree(
1365 vec![hot(
1366 Some(NodeId::new(1)),
1367 Some(LogicalSize::new(f32::INFINITY, f32::INFINITY)),
1368 )],
1369 vec![warm_node(Some(v_scrollbar()), None)],
1370 );
1371
1372 let y = sole_added_y(&m.update_scrollbar_transforms(dom(0), &sm, &t));
1373 assert!(y.is_nan(), "expected the documented NaN, got {y}");
1374
1375 let again = m.update_scrollbar_transforms(dom(0), &sm, &t);
1377 assert_eq!(
1378 again.transform_key_changes.len(),
1379 1,
1380 "NaN keeps the cache from ever converging"
1381 );
1382 }
1383
1384 #[test]
1385 fn a_nan_used_size_is_sanitised_to_zero_by_the_max_clamp() {
1386 let mut m = GpuStateManager::default();
1389 let sm = ScrollManager::new();
1390 let t = tree(
1391 vec![hot(
1392 Some(NodeId::new(1)),
1393 Some(LogicalSize::new(f32::NAN, f32::NAN)),
1394 )],
1395 vec![warm_node(Some(v_scrollbar()), None)],
1396 );
1397
1398 let y = sole_added_y(&m.update_scrollbar_transforms(dom(0), &sm, &t));
1399 assert!(y.is_finite(), "NaN width/height must be clamped, got {y}");
1400 assert_eq!(y, 0.0);
1401 }
1402
1403 #[test]
1404 fn overlay_vertical_scrollbars_fall_back_to_the_default_width() {
1405 let mut m = GpuStateManager::default();
1409 let mut sm = ScrollManager::new();
1410 sm.set_scroll_position_unclamped(
1411 dom(0),
1412 NodeId::new(1),
1413 LogicalPosition::new(0.0, 900.0),
1414 t0(),
1415 );
1416 let overlay = ScrollbarRequirements {
1417 needs_horizontal: false,
1418 needs_vertical: true,
1419 scrollbar_width: 0.0,
1420 scrollbar_height: 0.0,
1421 visual_width_px: 0.0,
1422 };
1423 let t = one_node_tree(overlay, LogicalSize::new(100.0, 1000.0));
1424
1425 let y = sole_added_y(&m.update_scrollbar_transforms(dom(0), &sm, &t));
1426 assert!((y - 68.0).abs() < 0.01, "expected the 16px default, got {y}");
1427 assert_eq!(DEFAULT_SCROLLBAR_WIDTH_PX, 16.0);
1428 }
1429
1430 #[test]
1431 fn classic_vertical_scrollbar_overlay_check_reads_the_wrong_reserved_field() {
1432 let mut m = GpuStateManager::default();
1447 let mut sm = ScrollManager::new();
1448 sm.set_scroll_position_unclamped(
1449 dom(0),
1450 NodeId::new(1),
1451 LogicalPosition::new(0.0, 900.0),
1452 t0(),
1453 );
1454 let classic_vertical_only = ScrollbarRequirements {
1455 needs_horizontal: false,
1456 needs_vertical: true,
1457 scrollbar_width: 16.0,
1458 scrollbar_height: 0.0,
1459 visual_width_px: 0.0,
1460 };
1461 let t = one_node_tree(classic_vertical_only, LogicalSize::new(100.0, 1000.0));
1462
1463 let y = sole_added_y(&m.update_scrollbar_transforms(dom(0), &sm, &t));
1464 assert!(
1465 (y - 68.0).abs() < 0.01,
1466 "pinning the buggy value; 36.0 once the field swap is fixed, got {y}"
1467 );
1468 }
1469
1470 #[test]
1471 fn classic_horizontal_scrollbar_overlay_check_reads_the_wrong_reserved_field() {
1472 let mut m = GpuStateManager::default();
1476 let mut sm = ScrollManager::new();
1477 sm.set_scroll_position_unclamped(
1478 dom(0),
1479 NodeId::new(1),
1480 LogicalPosition::new(900.0, 0.0),
1481 t0(),
1482 );
1483 let classic_horizontal_only = ScrollbarRequirements {
1484 needs_horizontal: true,
1485 needs_vertical: false,
1486 scrollbar_width: 0.0,
1487 scrollbar_height: 16.0,
1488 visual_width_px: 0.0,
1489 };
1490 let t = one_node_tree(classic_horizontal_only, LogicalSize::new(1000.0, 100.0));
1491
1492 let x = sole_added_x(&m.update_scrollbar_transforms(dom(0), &sm, &t));
1493 assert!(
1494 (x - 68.0).abs() < 0.01,
1495 "pinning the buggy value; 36.0 once the field swap is fixed, got {x}"
1496 );
1497 }
1498
1499 #[test]
1500 fn transforms_are_recorded_per_dom_and_do_not_leak_across_caches() {
1501 let mut m = GpuStateManager::default();
1502 let sm = ScrollManager::new();
1503 let t = one_node_tree(v_scrollbar(), LogicalSize::new(100.0, 1000.0));
1504
1505 let a = m.update_scrollbar_transforms(dom(0), &sm, &t);
1506 let b = m.update_scrollbar_transforms(dom(1), &sm, &t);
1507
1508 assert_eq!(a.transform_key_changes.len(), 1);
1511 assert_eq!(b.transform_key_changes.len(), 1);
1512 assert_eq!(m.caches.len(), 2);
1513
1514 let node = NodeId::new(1);
1515 let ka = m.get_cache(dom(0)).unwrap().transform_keys[&node];
1516 let kb = m.get_cache(dom(1)).unwrap().transform_keys[&node];
1517 assert_ne!(ka, kb, "each DOM must get its own TransformKey");
1518 }
1519
1520 #[test]
1521 fn update_scrollbar_transforms_does_not_touch_pending_changes() {
1522 let mut m = GpuStateManager::default();
1525 let sm = ScrollManager::new();
1526 let t = one_node_tree(v_scrollbar(), LogicalSize::new(100.0, 1000.0));
1527
1528 let changes = m.update_scrollbar_transforms(dom(0), &sm, &t);
1529 assert!(!changes.is_empty());
1530 assert!(
1531 m.pending_changes.is_empty(),
1532 "returned events must not be double-queued"
1533 );
1534 }
1535
1536 #[test]
1537 fn a_thousand_scrollable_nodes_each_get_exactly_one_distinct_key() {
1538 let mut m = GpuStateManager::default();
1539 let sm = ScrollManager::new();
1540 let n = 1000;
1541 let t = tree(
1542 (0..n)
1543 .map(|i| hot(Some(NodeId::new(i)), Some(LogicalSize::new(100.0, 100.0))))
1544 .collect(),
1545 (0..n)
1546 .map(|_| warm_node(Some(v_scrollbar()), Some(LogicalSize::new(100.0, 1000.0))))
1547 .collect(),
1548 );
1549
1550 let changes = m.update_scrollbar_transforms(dom(0), &sm, &t);
1551 assert_eq!(changes.transform_key_changes.len(), n);
1552
1553 let cache = m.get_cache(dom(0)).unwrap();
1554 assert_eq!(cache.transform_keys.len(), n);
1555 let mut keys: Vec<_> = cache.transform_keys.values().map(|k| k.id).collect();
1556 keys.sort_unstable();
1557 keys.dedup();
1558 assert_eq!(keys.len(), n, "every node must get a unique TransformKey");
1559 }
1560
1561 #[test]
1562 fn two_layout_nodes_sharing_one_dom_node_id_fight_over_a_single_key() {
1563 let mut m = GpuStateManager::default();
1571 let mut sm = ScrollManager::new();
1572 sm.set_scroll_position_unclamped(
1573 dom(0),
1574 NodeId::new(1),
1575 LogicalPosition::new(0.0, 900.0),
1576 t0(),
1577 );
1578 let t = tree(
1579 vec![
1580 hot(Some(NodeId::new(1)), Some(LogicalSize::new(100.0, 100.0))),
1581 hot(Some(NodeId::new(1)), Some(LogicalSize::new(200.0, 200.0))),
1582 ],
1583 vec![
1584 warm_node(Some(v_scrollbar()), Some(LogicalSize::new(100.0, 1000.0))),
1585 warm_node(Some(v_scrollbar()), Some(LogicalSize::new(200.0, 2000.0))),
1586 ],
1587 );
1588
1589 let changes = m.update_scrollbar_transforms(dom(0), &sm, &t);
1590 assert_eq!(
1591 m.get_cache(dom(0)).unwrap().transform_keys.len(),
1592 1,
1593 "both layout nodes collapse onto one TransformKey"
1594 );
1595 assert!(matches!(
1596 changes.transform_key_changes.as_slice(),
1597 [
1598 GpuTransformKeyEvent::Added(..),
1599 GpuTransformKeyEvent::Changed(..)
1600 ]
1601 ));
1602 let stored = m.get_cache(dom(0)).unwrap().current_transform_values[&NodeId::new(1)];
1604 assert!((stored.m[3][1] - 68.0).abs() < 0.01);
1605 }
1606}