1use alloc::collections::BTreeMap;
10
11use azul_core::{
12 dom::{DomId, NodeId, ScrollbarOrientation},
13 events::{
14 EasingFunction, EventData, EventProvider, EventSource, EventType, ScrollDeltaMode,
15 ScrollEventData, SyntheticEvent,
16 },
17 geom::{LogicalPosition, LogicalRect, LogicalSize},
18 hit_test::{ExternalScrollId, ScrollPosition},
19 styled_dom::NodeHierarchyItemId,
20 task::{Duration, Instant},
21};
22
23use crate::managers::hover::InputPointId;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum ScrollbarComponent {
30 Track,
32 Thumb,
34 TopButton,
36 BottomButton,
38}
39
40#[derive(Debug, Clone)]
42pub struct ScrollbarState {
43 pub visible: bool,
45 pub orientation: ScrollbarOrientation,
47 pub base_size: f32,
49 pub scale: LogicalPosition, pub thumb_position_ratio: f32,
53 pub thumb_size_ratio: f32,
55 pub track_rect: LogicalRect,
57}
58
59impl ScrollbarState {
60 pub fn hit_test_component(&self, local_pos: LogicalPosition) -> ScrollbarComponent {
63 match self.orientation {
64 ScrollbarOrientation::Vertical => {
65 let button_height = self.base_size;
66
67 if local_pos.y < button_height {
69 return ScrollbarComponent::TopButton;
70 }
71
72 let track_height = self.track_rect.size.height;
74 if local_pos.y > track_height - button_height {
75 return ScrollbarComponent::BottomButton;
76 }
77
78 let track_height_usable = track_height - 2.0 * button_height;
80 let thumb_height = track_height_usable * self.thumb_size_ratio;
81 let thumb_y_start = button_height
82 + (track_height_usable - thumb_height) * self.thumb_position_ratio;
83 let thumb_y_end = thumb_y_start + thumb_height;
84
85 if local_pos.y >= thumb_y_start && local_pos.y <= thumb_y_end {
87 ScrollbarComponent::Thumb
88 } else {
89 ScrollbarComponent::Track
90 }
91 }
92 ScrollbarOrientation::Horizontal => {
93 let button_width = self.base_size;
94
95 if local_pos.x < button_width {
97 return ScrollbarComponent::TopButton;
98 }
99
100 let track_width = self.track_rect.size.width;
102 if local_pos.x > track_width - button_width {
103 return ScrollbarComponent::BottomButton;
104 }
105
106 let track_width_usable = track_width - 2.0 * button_width;
108 let thumb_width = track_width_usable * self.thumb_size_ratio;
109 let thumb_x_start =
110 button_width + (track_width_usable - thumb_width) * self.thumb_position_ratio;
111 let thumb_x_end = thumb_x_start + thumb_width;
112
113 if local_pos.x >= thumb_x_start && local_pos.x <= thumb_x_end {
115 ScrollbarComponent::Thumb
116 } else {
117 ScrollbarComponent::Track
118 }
119 }
120 }
121 }
122}
123
124#[derive(Debug, Clone, Copy)]
129pub struct ScrollbarHit {
130 pub dom_id: DomId,
132 pub node_id: NodeId,
134 pub orientation: ScrollbarOrientation,
136 pub component: ScrollbarComponent,
138 pub local_position: LogicalPosition,
140 pub global_position: LogicalPosition,
142}
143
144#[derive(Debug, Clone, Default)]
148pub struct ScrollManager {
149 states: BTreeMap<(DomId, NodeId), AnimatedScrollState>,
151 external_scroll_ids: BTreeMap<(DomId, NodeId), ExternalScrollId>,
153 next_external_scroll_id: u64,
155 scrollbar_states: BTreeMap<(DomId, NodeId, ScrollbarOrientation), ScrollbarState>,
157 had_scroll_activity: bool,
159 had_programmatic_scroll: bool,
161 had_new_doms: bool,
163}
164
165#[derive(Debug, Clone)]
167pub struct AnimatedScrollState {
168 pub current_offset: LogicalPosition,
170 pub previous_offset: LogicalPosition,
172 pub animation: Option<ScrollAnimation>,
174 pub last_activity: Instant,
176 pub container_rect: LogicalRect,
178 pub content_rect: LogicalRect,
180}
181
182#[derive(Debug, Clone)]
184struct ScrollAnimation {
185 start_time: Instant,
187 duration: Duration,
189 start_offset: LogicalPosition,
191 target_offset: LogicalPosition,
193 easing: EasingFunction,
195}
196
197#[derive(Debug, Clone, Copy, Default)]
199pub struct FrameScrollInfo {
200 pub had_scroll_activity: bool,
202 pub had_programmatic_scroll: bool,
204 pub had_new_doms: bool,
206}
207
208#[derive(Debug, Clone)]
210pub struct ScrollEvent {
211 pub dom_id: DomId,
213 pub node_id: NodeId,
215 pub delta: LogicalPosition,
217 pub source: EventSource,
219 pub duration: Option<Duration>,
221 pub easing: EasingFunction,
223}
224
225#[derive(Debug, Default)]
227pub struct ScrollTickResult {
228 pub needs_repaint: bool,
230 pub updated_nodes: Vec<(DomId, NodeId)>,
232}
233
234impl ScrollManager {
237 pub fn new() -> Self {
239 Self::default()
240 }
241
242 pub fn begin_frame(&mut self) {
244 self.had_scroll_activity = false;
245 self.had_programmatic_scroll = false;
246 self.had_new_doms = false;
247
248 for state in self.states.values_mut() {
250 state.previous_offset = state.current_offset;
251 }
252 }
253
254 pub fn end_frame(&self) -> FrameScrollInfo {
256 FrameScrollInfo {
257 had_scroll_activity: self.had_scroll_activity,
258 had_programmatic_scroll: self.had_programmatic_scroll,
259 had_new_doms: self.had_new_doms,
260 }
261 }
262
263 pub fn tick(&mut self, now: Instant) -> ScrollTickResult {
265 let mut result = ScrollTickResult::default();
266 for ((dom_id, node_id), state) in self.states.iter_mut() {
267 if let Some(anim) = &state.animation {
268 let elapsed = now.duration_since(&anim.start_time);
269 let t = elapsed.div(&anim.duration).min(1.0);
270 let eased_t = apply_easing(t, anim.easing);
271
272 state.current_offset = LogicalPosition {
273 x: anim.start_offset.x + (anim.target_offset.x - anim.start_offset.x) * eased_t,
274 y: anim.start_offset.y + (anim.target_offset.y - anim.start_offset.y) * eased_t,
275 };
276 result.needs_repaint = true;
277 result.updated_nodes.push((*dom_id, *node_id));
278
279 if t >= 1.0 {
280 state.animation = None;
281 }
282 }
283 }
284 result
285 }
286
287 pub fn process_scroll_event(&mut self, event: ScrollEvent, now: Instant) -> bool {
289 self.had_scroll_activity = true;
290 if event.source == EventSource::Programmatic || event.source == EventSource::User {
291 self.had_programmatic_scroll = true;
292 }
293
294 if let Some(duration) = event.duration {
295 self.scroll_by(
296 event.dom_id,
297 event.node_id,
298 event.delta,
299 duration,
300 event.easing,
301 now,
302 );
303 } else {
304 let current = self
305 .get_current_offset(event.dom_id, event.node_id)
306 .unwrap_or_default();
307 let new_position = LogicalPosition {
308 x: current.x + event.delta.x,
309 y: current.y + event.delta.y,
310 };
311 self.set_scroll_position(event.dom_id, event.node_id, new_position, now);
312 }
313 true
314 }
315
316 pub fn record_sample(
321 &mut self,
322 delta_x: f32,
323 delta_y: f32,
324 hover_manager: &crate::managers::hover::HoverManager,
325 input_point_id: &InputPointId,
326 now: Instant,
327 ) -> Option<(DomId, NodeId)> {
328 let hit_test = hover_manager.get_current(input_point_id)?;
329
330 for (dom_id, hit_node) in &hit_test.hovered_nodes {
332 for (node_id, _scroll_item) in &hit_node.scroll_hit_test_nodes {
333 let is_scrollable = self.is_node_scrollable(*dom_id, *node_id);
334 if is_scrollable {
335 let delta = LogicalPosition {
336 x: delta_x,
337 y: delta_y,
338 };
339
340 let current = self
341 .get_current_offset(*dom_id, *node_id)
342 .unwrap_or_default();
343 let new_position = LogicalPosition {
344 x: current.x + delta.x,
345 y: current.y + delta.y,
346 };
347
348 self.set_scroll_position(*dom_id, *node_id, new_position, now);
349 self.had_scroll_activity = true;
350
351 return Some((*dom_id, *node_id));
352 }
353 }
354 }
355
356 None
357 }
358
359 fn is_node_scrollable(&self, dom_id: DomId, node_id: NodeId) -> bool {
361 self.states.get(&(dom_id, node_id)).map_or(false, |state| {
362 let has_horizontal = state.content_rect.size.width > state.container_rect.size.width;
363 let has_vertical = state.content_rect.size.height > state.container_rect.size.height;
364 has_horizontal || has_vertical
365 })
366 }
367
368 pub fn get_scroll_delta(&self, dom_id: DomId, node_id: NodeId) -> Option<LogicalPosition> {
370 let state = self.states.get(&(dom_id, node_id))?;
371 let delta = LogicalPosition {
372 x: state.current_offset.x - state.previous_offset.x,
373 y: state.current_offset.y - state.previous_offset.y,
374 };
375 (delta.x.abs() > 0.001 || delta.y.abs() > 0.001).then_some(delta)
376 }
377
378 pub fn had_scroll_activity_for_node(&self, dom_id: DomId, node_id: NodeId) -> bool {
380 self.get_scroll_delta(dom_id, node_id).is_some()
381 }
382
383 pub fn set_scroll_position(
385 &mut self,
386 dom_id: DomId,
387 node_id: NodeId,
388 position: LogicalPosition,
389 now: Instant,
390 ) {
391 let state = self
392 .states
393 .entry((dom_id, node_id))
394 .or_insert_with(|| AnimatedScrollState::new(now.clone()));
395 state.current_offset = state.clamp(position);
396 state.animation = None;
397 state.last_activity = now;
398 }
399
400 pub fn scroll_by(
402 &mut self,
403 dom_id: DomId,
404 node_id: NodeId,
405 delta: LogicalPosition,
406 duration: Duration,
407 easing: EasingFunction,
408 now: Instant,
409 ) {
410 let current = self.get_current_offset(dom_id, node_id).unwrap_or_default();
411 let target = LogicalPosition {
412 x: current.x + delta.x,
413 y: current.y + delta.y,
414 };
415 self.scroll_to(dom_id, node_id, target, duration, easing, now);
416 }
417
418 pub fn scroll_to(
422 &mut self,
423 dom_id: DomId,
424 node_id: NodeId,
425 target: LogicalPosition,
426 duration: Duration,
427 easing: EasingFunction,
428 now: Instant,
429 ) {
430 let is_zero = match &duration {
432 Duration::System(s) => s.secs == 0 && s.nanos == 0,
433 Duration::Tick(t) => t.tick_diff == 0,
434 };
435
436 if is_zero {
437 self.set_scroll_position(dom_id, node_id, target, now);
438 return;
439 }
440
441 let state = self
442 .states
443 .entry((dom_id, node_id))
444 .or_insert_with(|| AnimatedScrollState::new(now.clone()));
445 let clamped_target = state.clamp(target);
446 state.animation = Some(ScrollAnimation {
447 start_time: now.clone(),
448 duration,
449 start_offset: state.current_offset,
450 target_offset: clamped_target,
451 easing,
452 });
453 state.last_activity = now;
454 }
455
456 pub fn update_node_bounds(
458 &mut self,
459 dom_id: DomId,
460 node_id: NodeId,
461 container_rect: LogicalRect,
462 content_rect: LogicalRect,
463 now: Instant,
464 ) {
465 if !self.states.contains_key(&(dom_id, node_id)) {
466 self.had_new_doms = true;
467 }
468 let state = self
469 .states
470 .entry((dom_id, node_id))
471 .or_insert_with(|| AnimatedScrollState::new(now));
472 state.container_rect = container_rect;
473 state.content_rect = content_rect;
474 state.current_offset = state.clamp(state.current_offset);
475 }
476
477 pub fn get_current_offset(&self, dom_id: DomId, node_id: NodeId) -> Option<LogicalPosition> {
479 self.states
480 .get(&(dom_id, node_id))
481 .map(|s| s.current_offset)
482 }
483
484 pub fn get_last_activity_time(&self, dom_id: DomId, node_id: NodeId) -> Option<Instant> {
486 self.states
487 .get(&(dom_id, node_id))
488 .map(|s| s.last_activity.clone())
489 }
490
491 pub fn get_scroll_state(&self, dom_id: DomId, node_id: NodeId) -> Option<&AnimatedScrollState> {
493 self.states.get(&(dom_id, node_id))
494 }
495
496 pub fn get_scroll_states_for_dom(&self, dom_id: DomId) -> BTreeMap<NodeId, ScrollPosition> {
498 self.states
499 .iter()
500 .filter(|((d, _), _)| *d == dom_id)
501 .map(|((_, node_id), state)| {
502 (
503 *node_id,
504 ScrollPosition {
505 parent_rect: state.container_rect,
506 children_rect: LogicalRect::new(
507 state.current_offset,
508 state.content_rect.size,
509 ),
510 },
511 )
512 })
513 .collect()
514 }
515
516 pub fn register_or_update_scroll_node(
523 &mut self,
524 dom_id: DomId,
525 node_id: NodeId,
526 container_rect: LogicalRect,
527 content_size: LogicalSize,
528 now: Instant,
529 ) {
530 let key = (dom_id, node_id);
531
532 let content_rect = LogicalRect {
533 origin: LogicalPosition::zero(),
534 size: content_size,
535 };
536
537 if let Some(existing) = self.states.get_mut(&key) {
538 existing.container_rect = container_rect;
540 existing.content_rect = content_rect;
541 existing.current_offset = existing.clamp(existing.current_offset);
543 } else {
544 self.states.insert(
546 key,
547 AnimatedScrollState {
548 current_offset: LogicalPosition::zero(),
549 previous_offset: LogicalPosition::zero(),
550 animation: None,
551 last_activity: now,
552 container_rect,
553 content_rect,
554 },
555 );
556 }
557 }
558
559 pub fn register_scroll_node(&mut self, dom_id: DomId, node_id: NodeId) -> ExternalScrollId {
564 use azul_core::hit_test::PipelineId;
565
566 let key = (dom_id, node_id);
567 if let Some(&existing_id) = self.external_scroll_ids.get(&key) {
568 return existing_id;
569 }
570
571 let pipeline_id = PipelineId(
575 dom_id.inner as u32, node_id.index() as u32,
577 );
578 let new_id = ExternalScrollId(self.next_external_scroll_id, pipeline_id);
579 self.next_external_scroll_id += 1;
580 self.external_scroll_ids.insert(key, new_id);
581 new_id
582 }
583
584 pub fn get_external_scroll_id(
586 &self,
587 dom_id: DomId,
588 node_id: NodeId,
589 ) -> Option<ExternalScrollId> {
590 self.external_scroll_ids.get(&(dom_id, node_id)).copied()
591 }
592
593 pub fn iter_external_scroll_ids(
595 &self,
596 ) -> impl Iterator<Item = ((DomId, NodeId), ExternalScrollId)> + '_ {
597 self.external_scroll_ids.iter().map(|(k, v)| (*k, *v))
598 }
599
600 pub fn calculate_scrollbar_states(&mut self) {
605 self.scrollbar_states.clear();
606
607 let vertical_states: Vec<_> = self
609 .states
610 .iter()
611 .filter(|(_, s)| s.content_rect.size.height > s.container_rect.size.height)
612 .map(|((dom_id, node_id), scroll_state)| {
613 let v_state = Self::calculate_vertical_scrollbar_static(scroll_state);
614 ((*dom_id, *node_id, ScrollbarOrientation::Vertical), v_state)
615 })
616 .collect();
617
618 let horizontal_states: Vec<_> = self
620 .states
621 .iter()
622 .filter(|(_, s)| s.content_rect.size.width > s.container_rect.size.width)
623 .map(|((dom_id, node_id), scroll_state)| {
624 let h_state = Self::calculate_horizontal_scrollbar_static(scroll_state);
625 (
626 (*dom_id, *node_id, ScrollbarOrientation::Horizontal),
627 h_state,
628 )
629 })
630 .collect();
631
632 self.scrollbar_states.extend(vertical_states);
634 self.scrollbar_states.extend(horizontal_states);
635 }
636
637 fn calculate_vertical_scrollbar_static(scroll_state: &AnimatedScrollState) -> ScrollbarState {
639 const SCROLLBAR_WIDTH: f32 = 12.0; let container_height = scroll_state.container_rect.size.height;
642 let content_height = scroll_state.content_rect.size.height;
643
644 let thumb_size_ratio = (container_height / content_height).min(1.0);
646
647 let max_scroll = (content_height - container_height).max(0.0);
649 let thumb_position_ratio = if max_scroll > 0.0 {
650 (scroll_state.current_offset.y / max_scroll).clamp(0.0, 1.0)
651 } else {
652 0.0
653 };
654
655 let scale = LogicalPosition::new(1.0, container_height / SCROLLBAR_WIDTH);
657
658 let track_x = scroll_state.container_rect.origin.x + scroll_state.container_rect.size.width
660 - SCROLLBAR_WIDTH;
661 let track_y = scroll_state.container_rect.origin.y;
662 let track_rect = LogicalRect::new(
663 LogicalPosition::new(track_x, track_y),
664 LogicalSize::new(SCROLLBAR_WIDTH, container_height),
665 );
666
667 ScrollbarState {
668 visible: true,
669 orientation: ScrollbarOrientation::Vertical,
670 base_size: SCROLLBAR_WIDTH,
671 scale,
672 thumb_position_ratio,
673 thumb_size_ratio,
674 track_rect,
675 }
676 }
677
678 fn calculate_horizontal_scrollbar_static(scroll_state: &AnimatedScrollState) -> ScrollbarState {
680 const SCROLLBAR_HEIGHT: f32 = 12.0; let container_width = scroll_state.container_rect.size.width;
683 let content_width = scroll_state.content_rect.size.width;
684
685 let thumb_size_ratio = (container_width / content_width).min(1.0);
686
687 let max_scroll = (content_width - container_width).max(0.0);
688 let thumb_position_ratio = if max_scroll > 0.0 {
689 (scroll_state.current_offset.x / max_scroll).clamp(0.0, 1.0)
690 } else {
691 0.0
692 };
693
694 let scale = LogicalPosition::new(container_width / SCROLLBAR_HEIGHT, 1.0);
695
696 let track_x = scroll_state.container_rect.origin.x;
697 let track_y = scroll_state.container_rect.origin.y
698 + scroll_state.container_rect.size.height
699 - SCROLLBAR_HEIGHT;
700 let track_rect = LogicalRect::new(
701 LogicalPosition::new(track_x, track_y),
702 LogicalSize::new(container_width, SCROLLBAR_HEIGHT),
703 );
704
705 ScrollbarState {
706 visible: true,
707 orientation: ScrollbarOrientation::Horizontal,
708 base_size: SCROLLBAR_HEIGHT,
709 scale,
710 thumb_position_ratio,
711 thumb_size_ratio,
712 track_rect,
713 }
714 }
715
716 pub fn get_scrollbar_state(
718 &self,
719 dom_id: DomId,
720 node_id: NodeId,
721 orientation: ScrollbarOrientation,
722 ) -> Option<&ScrollbarState> {
723 self.scrollbar_states.get(&(dom_id, node_id, orientation))
724 }
725
726 pub fn iter_scrollbar_states(
728 &self,
729 ) -> impl Iterator<Item = ((DomId, NodeId, ScrollbarOrientation), &ScrollbarState)> + '_ {
730 self.scrollbar_states.iter().map(|(k, v)| (*k, v))
731 }
732
733 pub fn hit_test_scrollbar(
738 &self,
739 dom_id: DomId,
740 node_id: NodeId,
741 global_pos: LogicalPosition,
742 ) -> Option<ScrollbarHit> {
743 for orientation in [
745 ScrollbarOrientation::Vertical,
746 ScrollbarOrientation::Horizontal,
747 ] {
748 let scrollbar_state = self.scrollbar_states.get(&(dom_id, node_id, orientation))?;
749
750 if !scrollbar_state.visible {
751 continue;
752 }
753
754 if !scrollbar_state.track_rect.contains(global_pos) {
756 continue;
757 }
758
759 let local_pos = LogicalPosition::new(
761 global_pos.x - scrollbar_state.track_rect.origin.x,
762 global_pos.y - scrollbar_state.track_rect.origin.y,
763 );
764
765 let component = scrollbar_state.hit_test_component(local_pos);
767
768 return Some(ScrollbarHit {
769 dom_id,
770 node_id,
771 orientation,
772 component,
773 local_position: local_pos,
774 global_position: global_pos,
775 });
776 }
777
778 None
779 }
780
781 pub fn hit_test_scrollbars(&self, global_pos: LogicalPosition) -> Option<ScrollbarHit> {
789 for ((dom_id, node_id, orientation), scrollbar_state) in self.scrollbar_states.iter().rev()
791 {
792 if !scrollbar_state.visible {
793 continue;
794 }
795
796 if !scrollbar_state.track_rect.contains(global_pos) {
798 continue;
799 }
800
801 let local_pos = LogicalPosition::new(
803 global_pos.x - scrollbar_state.track_rect.origin.x,
804 global_pos.y - scrollbar_state.track_rect.origin.y,
805 );
806
807 let component = scrollbar_state.hit_test_component(local_pos);
809
810 return Some(ScrollbarHit {
811 dom_id: *dom_id,
812 node_id: *node_id,
813 orientation: *orientation,
814 component,
815 local_position: local_pos,
816 global_position: global_pos,
817 });
818 }
819
820 None
821 }
822}
823
824impl AnimatedScrollState {
827 pub fn new(now: Instant) -> Self {
829 Self {
830 current_offset: LogicalPosition::zero(),
831 previous_offset: LogicalPosition::zero(),
832 animation: None,
833 last_activity: now,
834 container_rect: LogicalRect::zero(),
835 content_rect: LogicalRect::zero(),
836 }
837 }
838
839 pub fn clamp(&self, position: LogicalPosition) -> LogicalPosition {
841 let max_x = (self.content_rect.size.width - self.container_rect.size.width).max(0.0);
842 let max_y = (self.content_rect.size.height - self.container_rect.size.height).max(0.0);
843 LogicalPosition {
844 x: position.x.max(0.0).min(max_x),
845 y: position.y.max(0.0).min(max_y),
846 }
847 }
848}
849
850pub fn apply_easing(t: f32, easing: EasingFunction) -> f32 {
855 match easing {
856 EasingFunction::Linear => t,
857 EasingFunction::EaseOut => 1.0 - (1.0 - t).powi(3),
858 EasingFunction::EaseInOut => {
859 if t < 0.5 {
860 4.0 * t * t * t
861 } else {
862 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
863 }
864 }
865 }
866}
867
868pub type ScrollStates = ScrollManager;
870
871impl EventProvider for ScrollManager {
874 fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
879 let mut events = Vec::new();
880
881 for ((dom_id, node_id), state) in &self.states {
883 let delta = LogicalPosition {
885 x: state.current_offset.x - state.previous_offset.x,
886 y: state.current_offset.y - state.previous_offset.y,
887 };
888
889 if delta.x.abs() > 0.001 || delta.y.abs() > 0.001 {
890 let target = azul_core::dom::DomNodeId {
891 dom: *dom_id,
892 node: NodeHierarchyItemId::from_crate_internal(Some(*node_id)),
893 };
894
895 let event_source = if self.had_programmatic_scroll {
897 EventSource::Programmatic
898 } else {
899 EventSource::User
900 };
901
902 events.push(SyntheticEvent::new(
904 EventType::Scroll,
905 event_source,
906 target,
907 timestamp.clone(),
908 EventData::Scroll(ScrollEventData {
909 delta,
910 delta_mode: ScrollDeltaMode::Pixel,
911 }),
912 ));
913
914 }
917 }
918
919 events
920 }
921}
922
923impl ScrollManager {
924 pub fn remap_node_ids(
929 &mut self,
930 dom_id: DomId,
931 node_id_map: &std::collections::BTreeMap<NodeId, NodeId>,
932 ) {
933 let states_to_update: Vec<_> = self.states.keys()
935 .filter(|(d, _)| *d == dom_id)
936 .cloned()
937 .collect();
938
939 for (d, old_node_id) in states_to_update {
940 if let Some(&new_node_id) = node_id_map.get(&old_node_id) {
941 if let Some(state) = self.states.remove(&(d, old_node_id)) {
942 self.states.insert((d, new_node_id), state);
943 }
944 } else {
945 self.states.remove(&(d, old_node_id));
947 }
948 }
949
950 let scroll_ids_to_update: Vec<_> = self.external_scroll_ids.keys()
952 .filter(|(d, _)| *d == dom_id)
953 .cloned()
954 .collect();
955
956 for (d, old_node_id) in scroll_ids_to_update {
957 if let Some(&new_node_id) = node_id_map.get(&old_node_id) {
958 if let Some(scroll_id) = self.external_scroll_ids.remove(&(d, old_node_id)) {
959 self.external_scroll_ids.insert((d, new_node_id), scroll_id);
960 }
961 } else {
962 self.external_scroll_ids.remove(&(d, old_node_id));
963 }
964 }
965
966 let scrollbar_states_to_update: Vec<_> = self.scrollbar_states.keys()
968 .filter(|(d, _, _)| *d == dom_id)
969 .cloned()
970 .collect();
971
972 for (d, old_node_id, orientation) in scrollbar_states_to_update {
973 if let Some(&new_node_id) = node_id_map.get(&old_node_id) {
974 if let Some(state) = self.scrollbar_states.remove(&(d, old_node_id, orientation)) {
975 self.scrollbar_states.insert((d, new_node_id, orientation), state);
976 }
977 } else {
978 self.scrollbar_states.remove(&(d, old_node_id, orientation));
979 }
980 }
981 }
982}