1use alloc::collections::BTreeMap;
47#[cfg(feature = "std")]
48use alloc::vec::Vec;
49
50use azul_core::{
51 dom::{DomId, NodeId, ScrollbarOrientation},
52 events::EasingFunction,
53 geom::{LogicalPosition, LogicalRect, LogicalSize},
54 hit_test::{ExternalScrollId, ScrollPosition},
55 styled_dom::NodeHierarchyItemId,
56 task::{Duration, Instant},
57};
58
59#[cfg(feature = "std")]
60use std::sync::{Arc, Mutex};
61
62use crate::managers::hover::InputPointId;
63
64#[derive(Debug, Clone, Copy, PartialEq)]
75pub enum ScrollInputSource {
76 TrackpadContinuous,
79 WheelDiscrete,
82 Programmatic,
85}
86
87#[derive(Debug, Clone)]
93pub struct ScrollInput {
94 pub dom_id: DomId,
96 pub node_id: NodeId,
98 pub delta: LogicalPosition,
100 pub timestamp: Instant,
102 pub source: ScrollInputSource,
104}
105
106#[cfg(feature = "std")]
112#[derive(Debug, Clone, Default)]
113pub struct ScrollInputQueue {
114 inner: Arc<Mutex<Vec<ScrollInput>>>,
115}
116
117#[cfg(feature = "std")]
118impl ScrollInputQueue {
119 pub fn new() -> Self {
120 Self {
121 inner: Arc::new(Mutex::new(Vec::new())),
122 }
123 }
124
125 pub fn push(&self, input: ScrollInput) {
127 if let Ok(mut queue) = self.inner.lock() {
128 queue.push(input);
129 }
130 }
131
132 pub fn take_all(&self) -> Vec<ScrollInput> {
134 if let Ok(mut queue) = self.inner.lock() {
135 core::mem::take(&mut *queue)
136 } else {
137 Vec::new()
138 }
139 }
140
141 pub fn take_recent(&self, max_events: usize) -> Vec<ScrollInput> {
145 if let Ok(mut queue) = self.inner.lock() {
146 let mut events = core::mem::take(&mut *queue);
147 if events.len() > max_events {
148 events.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
150 events.drain(..events.len() - max_events);
151 }
152 events
153 } else {
154 Vec::new()
155 }
156 }
157
158 pub fn has_pending(&self) -> bool {
160 self.inner
161 .lock()
162 .map(|q| !q.is_empty())
163 .unwrap_or(false)
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
171pub enum ScrollbarComponent {
172 Track,
174 Thumb,
176 TopButton,
178 BottomButton,
180}
181
182#[derive(Debug, Clone)]
184pub struct ScrollbarState {
185 pub visible: bool,
187 pub orientation: ScrollbarOrientation,
189 pub base_size: f32,
191 pub scale: LogicalPosition, pub thumb_position_ratio: f32,
195 pub thumb_size_ratio: f32,
197 pub track_rect: LogicalRect,
199}
200
201impl ScrollbarState {
202 pub fn hit_test_component(&self, local_pos: LogicalPosition) -> ScrollbarComponent {
205 match self.orientation {
206 ScrollbarOrientation::Vertical => {
207 let button_height = self.base_size;
208
209 if local_pos.y < button_height {
211 return ScrollbarComponent::TopButton;
212 }
213
214 let track_height = self.track_rect.size.height;
216 if local_pos.y > track_height - button_height {
217 return ScrollbarComponent::BottomButton;
218 }
219
220 let track_height_usable = track_height - 2.0 * button_height;
222 let thumb_height = track_height_usable * self.thumb_size_ratio;
223 let thumb_y_start = button_height
224 + (track_height_usable - thumb_height) * self.thumb_position_ratio;
225 let thumb_y_end = thumb_y_start + thumb_height;
226
227 if local_pos.y >= thumb_y_start && local_pos.y <= thumb_y_end {
229 ScrollbarComponent::Thumb
230 } else {
231 ScrollbarComponent::Track
232 }
233 }
234 ScrollbarOrientation::Horizontal => {
235 let button_width = self.base_size;
236
237 if local_pos.x < button_width {
239 return ScrollbarComponent::TopButton;
240 }
241
242 let track_width = self.track_rect.size.width;
244 if local_pos.x > track_width - button_width {
245 return ScrollbarComponent::BottomButton;
246 }
247
248 let track_width_usable = track_width - 2.0 * button_width;
250 let thumb_width = track_width_usable * self.thumb_size_ratio;
251 let thumb_x_start =
252 button_width + (track_width_usable - thumb_width) * self.thumb_position_ratio;
253 let thumb_x_end = thumb_x_start + thumb_width;
254
255 if local_pos.x >= thumb_x_start && local_pos.x <= thumb_x_end {
257 ScrollbarComponent::Thumb
258 } else {
259 ScrollbarComponent::Track
260 }
261 }
262 }
263 }
264}
265
266#[derive(Debug, Clone, Copy)]
271pub struct ScrollbarHit {
272 pub dom_id: DomId,
274 pub node_id: NodeId,
276 pub orientation: ScrollbarOrientation,
278 pub component: ScrollbarComponent,
280 pub local_position: LogicalPosition,
282 pub global_position: LogicalPosition,
284}
285
286#[derive(Debug, Clone, Default)]
290pub struct ScrollManager {
291 states: BTreeMap<(DomId, NodeId), AnimatedScrollState>,
293 external_scroll_ids: BTreeMap<(DomId, NodeId), ExternalScrollId>,
295 next_external_scroll_id: u64,
297 scrollbar_states: BTreeMap<(DomId, NodeId, ScrollbarOrientation), ScrollbarState>,
299 #[cfg(feature = "std")]
301 pub scroll_input_queue: ScrollInputQueue,
302}
303
304#[derive(Debug, Clone)]
306pub struct AnimatedScrollState {
307 pub current_offset: LogicalPosition,
309 pub animation: Option<ScrollAnimation>,
311 pub last_activity: Instant,
313 pub container_rect: LogicalRect,
315 pub content_rect: LogicalRect,
317 pub virtual_scroll_size: Option<LogicalSize>,
320 pub virtual_scroll_offset: Option<LogicalPosition>,
322 pub overscroll_behavior_x: azul_css::props::style::scrollbar::OverscrollBehavior,
324 pub overscroll_behavior_y: azul_css::props::style::scrollbar::OverscrollBehavior,
326 pub overflow_scrolling: azul_css::props::style::scrollbar::OverflowScrolling,
328}
329
330#[derive(Debug, Clone)]
332struct ScrollAnimation {
333 start_time: Instant,
335 duration: Duration,
337 start_offset: LogicalPosition,
339 target_offset: LogicalPosition,
341 easing: EasingFunction,
343}
344
345#[derive(Debug, Clone)]
350pub struct ScrollNodeInfo {
351 pub current_offset: LogicalPosition,
353 pub container_rect: LogicalRect,
355 pub content_rect: LogicalRect,
357 pub max_scroll_x: f32,
359 pub max_scroll_y: f32,
361 pub overscroll_behavior_x: azul_css::props::style::scrollbar::OverscrollBehavior,
363 pub overscroll_behavior_y: azul_css::props::style::scrollbar::OverscrollBehavior,
365 pub overflow_scrolling: azul_css::props::style::scrollbar::OverflowScrolling,
367}
368
369#[derive(Debug, Default)]
371pub struct ScrollTickResult {
372 pub needs_repaint: bool,
374 pub updated_nodes: Vec<(DomId, NodeId)>,
376}
377
378impl ScrollManager {
381 pub fn new() -> Self {
383 Self::default()
384 }
385
386 #[cfg(feature = "std")]
399 pub fn record_scroll_input(&mut self, input: ScrollInput) -> bool {
400 let was_empty = !self.scroll_input_queue.has_pending();
401 self.scroll_input_queue.push(input);
402 was_empty }
404
405 #[cfg(feature = "std")]
411 pub fn record_scroll_from_hit_test(
412 &mut self,
413 delta_x: f32,
414 delta_y: f32,
415 source: ScrollInputSource,
416 hover_manager: &crate::managers::hover::HoverManager,
417 input_point_id: &InputPointId,
418 now: Instant,
419 ) -> Option<(DomId, NodeId, bool)> {
420 let hit_test = hover_manager.get_current(input_point_id)?;
421
422 for (dom_id, hit_node) in &hit_test.hovered_nodes {
423 for (node_id, _scroll_item) in &hit_node.scroll_hit_test_nodes {
424 if !self.is_node_scrollable(*dom_id, *node_id) {
425 continue;
426 }
427 let input = ScrollInput {
428 dom_id: *dom_id,
429 node_id: *node_id,
430 delta: LogicalPosition { x: delta_x, y: delta_y },
431 timestamp: now,
432 source,
433 };
434 let should_start_timer = self.record_scroll_input(input);
435 return Some((*dom_id, *node_id, should_start_timer));
436 }
437 }
438
439 None
440 }
441
442 #[cfg(feature = "std")]
447 pub fn get_input_queue(&self) -> ScrollInputQueue {
448 self.scroll_input_queue.clone()
449 }
450
451 pub fn tick(&mut self, now: Instant) -> ScrollTickResult {
453 let mut result = ScrollTickResult::default();
454 for ((dom_id, node_id), state) in self.states.iter_mut() {
455 if let Some(anim) = &state.animation {
456 let elapsed = now.duration_since(&anim.start_time);
457 let t = elapsed.div(&anim.duration).min(1.0);
458 let eased_t = apply_easing(t, anim.easing);
459
460 state.current_offset = LogicalPosition {
461 x: anim.start_offset.x + (anim.target_offset.x - anim.start_offset.x) * eased_t,
462 y: anim.start_offset.y + (anim.target_offset.y - anim.start_offset.y) * eased_t,
463 };
464 result.needs_repaint = true;
465 result.updated_nodes.push((*dom_id, *node_id));
466
467 if t >= 1.0 {
468 state.animation = None;
469 }
470 }
471 }
472 result
473 }
474
475 pub fn find_scroll_parent(
481 &self,
482 dom_id: DomId,
483 node_id: NodeId,
484 node_hierarchy: &[azul_core::styled_dom::NodeHierarchyItem],
485 ) -> Option<NodeId> {
486 let mut current = Some(node_id);
487 while let Some(nid) = current {
488 if self.states.contains_key(&(dom_id, nid)) && nid != node_id {
489 return Some(nid);
490 }
491 current = node_hierarchy
492 .get(nid.index())
493 .and_then(|item| item.parent_id());
494 }
495 None
496 }
497
498 fn is_node_scrollable(&self, dom_id: DomId, node_id: NodeId) -> bool {
504 self.states.get(&(dom_id, node_id)).map_or(false, |state| {
505 let effective_width = state.virtual_scroll_size
506 .map(|s| s.width)
507 .unwrap_or(state.content_rect.size.width);
508 let effective_height = state.virtual_scroll_size
509 .map(|s| s.height)
510 .unwrap_or(state.content_rect.size.height);
511 let has_horizontal = effective_width > state.container_rect.size.width;
512 let has_vertical = effective_height > state.container_rect.size.height;
513 has_horizontal || has_vertical
514 })
515 }
516
517 pub fn set_scroll_position(
519 &mut self,
520 dom_id: DomId,
521 node_id: NodeId,
522 position: LogicalPosition,
523 now: Instant,
524 ) {
525 let state = self
526 .states
527 .entry((dom_id, node_id))
528 .or_insert_with(|| AnimatedScrollState::new(now.clone()));
529 state.current_offset = state.clamp(position);
530 state.animation = None;
531 state.last_activity = now;
532 }
533
534 pub fn scroll_by(
536 &mut self,
537 dom_id: DomId,
538 node_id: NodeId,
539 delta: LogicalPosition,
540 duration: Duration,
541 easing: EasingFunction,
542 now: Instant,
543 ) {
544 let current = self.get_current_offset(dom_id, node_id).unwrap_or_default();
545 let target = LogicalPosition {
546 x: current.x + delta.x,
547 y: current.y + delta.y,
548 };
549 self.scroll_to(dom_id, node_id, target, duration, easing, now);
550 }
551
552 pub fn scroll_to(
556 &mut self,
557 dom_id: DomId,
558 node_id: NodeId,
559 target: LogicalPosition,
560 duration: Duration,
561 easing: EasingFunction,
562 now: Instant,
563 ) {
564 let is_zero = match &duration {
566 Duration::System(s) => s.secs == 0 && s.nanos == 0,
567 Duration::Tick(t) => t.tick_diff == 0,
568 };
569
570 if is_zero {
571 self.set_scroll_position(dom_id, node_id, target, now);
572 return;
573 }
574
575 let state = self
576 .states
577 .entry((dom_id, node_id))
578 .or_insert_with(|| AnimatedScrollState::new(now.clone()));
579 let clamped_target = state.clamp(target);
580 state.animation = Some(ScrollAnimation {
581 start_time: now.clone(),
582 duration,
583 start_offset: state.current_offset,
584 target_offset: clamped_target,
585 easing,
586 });
587 state.last_activity = now;
588 }
589
590 pub fn update_node_bounds(
592 &mut self,
593 dom_id: DomId,
594 node_id: NodeId,
595 container_rect: LogicalRect,
596 content_rect: LogicalRect,
597 now: Instant,
598 ) {
599 let state = self
600 .states
601 .entry((dom_id, node_id))
602 .or_insert_with(|| AnimatedScrollState::new(now));
603 state.container_rect = container_rect;
604 state.content_rect = content_rect;
605 state.current_offset = state.clamp(state.current_offset);
606 }
607
608 pub fn update_virtual_scroll_bounds(
614 &mut self,
615 dom_id: DomId,
616 node_id: NodeId,
617 virtual_scroll_size: LogicalSize,
618 virtual_scroll_offset: Option<LogicalPosition>,
619 ) {
620 if let Some(state) = self.states.get_mut(&(dom_id, node_id)) {
621 state.virtual_scroll_size = Some(virtual_scroll_size);
622 state.virtual_scroll_offset = virtual_scroll_offset;
623 state.current_offset = state.clamp(state.current_offset);
625 }
626 }
627
628 pub fn get_current_offset(&self, dom_id: DomId, node_id: NodeId) -> Option<LogicalPosition> {
630 self.states
631 .get(&(dom_id, node_id))
632 .map(|s| s.current_offset)
633 }
634
635 pub fn get_last_activity_time(&self, dom_id: DomId, node_id: NodeId) -> Option<Instant> {
637 self.states
638 .get(&(dom_id, node_id))
639 .map(|s| s.last_activity.clone())
640 }
641
642 pub fn get_scroll_state(&self, dom_id: DomId, node_id: NodeId) -> Option<&AnimatedScrollState> {
644 self.states.get(&(dom_id, node_id))
645 }
646
647 pub fn get_scroll_node_info(
655 &self,
656 dom_id: DomId,
657 node_id: NodeId,
658 ) -> Option<ScrollNodeInfo> {
659 let state = self.states.get(&(dom_id, node_id))?;
660 let effective_content_width = state.virtual_scroll_size
661 .map(|s| s.width)
662 .unwrap_or(state.content_rect.size.width);
663 let effective_content_height = state.virtual_scroll_size
664 .map(|s| s.height)
665 .unwrap_or(state.content_rect.size.height);
666 let max_x = (effective_content_width - state.container_rect.size.width).max(0.0);
667 let max_y = (effective_content_height - state.container_rect.size.height).max(0.0);
668 Some(ScrollNodeInfo {
669 current_offset: state.current_offset,
670 container_rect: state.container_rect,
671 content_rect: state.content_rect,
672 max_scroll_x: max_x,
673 max_scroll_y: max_y,
674 overscroll_behavior_x: state.overscroll_behavior_x,
675 overscroll_behavior_y: state.overscroll_behavior_y,
676 overflow_scrolling: state.overflow_scrolling,
677 })
678 }
679
680 pub fn get_scroll_states_for_dom(&self, dom_id: DomId) -> BTreeMap<NodeId, ScrollPosition> {
682 self.states
683 .iter()
684 .filter(|((d, _), _)| *d == dom_id)
685 .map(|((_, node_id), state)| {
686 (
687 *node_id,
688 ScrollPosition {
689 parent_rect: state.container_rect,
690 children_rect: LogicalRect::new(
691 state.current_offset,
692 state.content_rect.size,
693 ),
694 },
695 )
696 })
697 .collect()
698 }
699
700 pub fn register_or_update_scroll_node(
707 &mut self,
708 dom_id: DomId,
709 node_id: NodeId,
710 container_rect: LogicalRect,
711 content_size: LogicalSize,
712 now: Instant,
713 ) {
714 let key = (dom_id, node_id);
715
716 let content_rect = LogicalRect {
717 origin: LogicalPosition::zero(),
718 size: content_size,
719 };
720
721 if let Some(existing) = self.states.get_mut(&key) {
722 existing.container_rect = container_rect;
724 existing.content_rect = content_rect;
725 existing.current_offset = existing.clamp(existing.current_offset);
727 } else {
728 self.states.insert(
730 key,
731 AnimatedScrollState {
732 current_offset: LogicalPosition::zero(),
733 animation: None,
734 last_activity: now,
735 container_rect,
736 content_rect,
737 virtual_scroll_size: None,
738 virtual_scroll_offset: None,
739 overscroll_behavior_x: azul_css::props::style::scrollbar::OverscrollBehavior::Auto,
740 overscroll_behavior_y: azul_css::props::style::scrollbar::OverscrollBehavior::Auto,
741 overflow_scrolling: azul_css::props::style::scrollbar::OverflowScrolling::Auto,
742 },
743 );
744 }
745 }
746
747 pub fn register_scroll_node(&mut self, dom_id: DomId, node_id: NodeId) -> ExternalScrollId {
752 use azul_core::hit_test::PipelineId;
753
754 let key = (dom_id, node_id);
755 if let Some(&existing_id) = self.external_scroll_ids.get(&key) {
756 return existing_id;
757 }
758
759 let pipeline_id = PipelineId(
763 dom_id.inner as u32, node_id.index() as u32,
765 );
766 let new_id = ExternalScrollId(self.next_external_scroll_id, pipeline_id);
767 self.next_external_scroll_id += 1;
768 self.external_scroll_ids.insert(key, new_id);
769 new_id
770 }
771
772 pub fn get_external_scroll_id(
774 &self,
775 dom_id: DomId,
776 node_id: NodeId,
777 ) -> Option<ExternalScrollId> {
778 self.external_scroll_ids.get(&(dom_id, node_id)).copied()
779 }
780
781 pub fn iter_external_scroll_ids(
783 &self,
784 ) -> impl Iterator<Item = ((DomId, NodeId), ExternalScrollId)> + '_ {
785 self.external_scroll_ids.iter().map(|(k, v)| (*k, *v))
786 }
787
788 pub fn calculate_scrollbar_states(&mut self) {
793 self.scrollbar_states.clear();
794
795 let vertical_states: Vec<_> = self
799 .states
800 .iter()
801 .filter(|(_, s)| {
802 let effective_height = s.virtual_scroll_size
803 .map(|vs| vs.height)
804 .unwrap_or(s.content_rect.size.height);
805 effective_height > s.container_rect.size.height
806 })
807 .map(|((dom_id, node_id), scroll_state)| {
808 let v_state = Self::calculate_vertical_scrollbar_static(scroll_state);
809 ((*dom_id, *node_id, ScrollbarOrientation::Vertical), v_state)
810 })
811 .collect();
812
813 let horizontal_states: Vec<_> = self
815 .states
816 .iter()
817 .filter(|(_, s)| {
818 let effective_width = s.virtual_scroll_size
819 .map(|vs| vs.width)
820 .unwrap_or(s.content_rect.size.width);
821 effective_width > s.container_rect.size.width
822 })
823 .map(|((dom_id, node_id), scroll_state)| {
824 let h_state = Self::calculate_horizontal_scrollbar_static(scroll_state);
825 (
826 (*dom_id, *node_id, ScrollbarOrientation::Horizontal),
827 h_state,
828 )
829 })
830 .collect();
831
832 self.scrollbar_states.extend(vertical_states);
834 self.scrollbar_states.extend(horizontal_states);
835 }
836
837 fn calculate_vertical_scrollbar_static(scroll_state: &AnimatedScrollState) -> ScrollbarState {
839 const SCROLLBAR_WIDTH: f32 = 16.0;
842
843 let container_height = scroll_state.container_rect.size.height;
844 let content_height = scroll_state.virtual_scroll_size
845 .map(|s| s.height)
846 .unwrap_or(scroll_state.content_rect.size.height);
847
848 let thumb_size_ratio = (container_height / content_height).min(1.0);
850
851 let max_scroll = (content_height - container_height).max(0.0);
853 let thumb_position_ratio = if max_scroll > 0.0 {
854 (scroll_state.current_offset.y / max_scroll).clamp(0.0, 1.0)
855 } else {
856 0.0
857 };
858
859 let scale = LogicalPosition::new(1.0, container_height / SCROLLBAR_WIDTH);
861
862 let track_x = scroll_state.container_rect.origin.x + scroll_state.container_rect.size.width
864 - SCROLLBAR_WIDTH;
865 let track_y = scroll_state.container_rect.origin.y;
866 let track_rect = LogicalRect::new(
867 LogicalPosition::new(track_x, track_y),
868 LogicalSize::new(SCROLLBAR_WIDTH, container_height),
869 );
870
871 ScrollbarState {
872 visible: true,
873 orientation: ScrollbarOrientation::Vertical,
874 base_size: SCROLLBAR_WIDTH,
875 scale,
876 thumb_position_ratio,
877 thumb_size_ratio,
878 track_rect,
879 }
880 }
881
882 fn calculate_horizontal_scrollbar_static(scroll_state: &AnimatedScrollState) -> ScrollbarState {
884 const SCROLLBAR_HEIGHT: f32 = 16.0;
887
888 let container_width = scroll_state.container_rect.size.width;
889 let content_width = scroll_state.virtual_scroll_size
890 .map(|s| s.width)
891 .unwrap_or(scroll_state.content_rect.size.width);
892
893 let thumb_size_ratio = (container_width / content_width).min(1.0);
894
895 let max_scroll = (content_width - container_width).max(0.0);
896 let thumb_position_ratio = if max_scroll > 0.0 {
897 (scroll_state.current_offset.x / max_scroll).clamp(0.0, 1.0)
898 } else {
899 0.0
900 };
901
902 let scale = LogicalPosition::new(container_width / SCROLLBAR_HEIGHT, 1.0);
903
904 let track_x = scroll_state.container_rect.origin.x;
905 let track_y = scroll_state.container_rect.origin.y
906 + scroll_state.container_rect.size.height
907 - SCROLLBAR_HEIGHT;
908 let track_rect = LogicalRect::new(
909 LogicalPosition::new(track_x, track_y),
910 LogicalSize::new(container_width, SCROLLBAR_HEIGHT),
911 );
912
913 ScrollbarState {
914 visible: true,
915 orientation: ScrollbarOrientation::Horizontal,
916 base_size: SCROLLBAR_HEIGHT,
917 scale,
918 thumb_position_ratio,
919 thumb_size_ratio,
920 track_rect,
921 }
922 }
923
924 pub fn get_scrollbar_state(
926 &self,
927 dom_id: DomId,
928 node_id: NodeId,
929 orientation: ScrollbarOrientation,
930 ) -> Option<&ScrollbarState> {
931 self.scrollbar_states.get(&(dom_id, node_id, orientation))
932 }
933
934 pub fn iter_scrollbar_states(
936 &self,
937 ) -> impl Iterator<Item = ((DomId, NodeId, ScrollbarOrientation), &ScrollbarState)> + '_ {
938 self.scrollbar_states.iter().map(|(k, v)| (*k, v))
939 }
940
941 pub fn hit_test_scrollbar(
946 &self,
947 dom_id: DomId,
948 node_id: NodeId,
949 global_pos: LogicalPosition,
950 ) -> Option<ScrollbarHit> {
951 for orientation in [
953 ScrollbarOrientation::Vertical,
954 ScrollbarOrientation::Horizontal,
955 ] {
956 let scrollbar_state = self.scrollbar_states.get(&(dom_id, node_id, orientation))?;
957
958 if !scrollbar_state.visible {
959 continue;
960 }
961
962 if !scrollbar_state.track_rect.contains(global_pos) {
964 continue;
965 }
966
967 let local_pos = LogicalPosition::new(
969 global_pos.x - scrollbar_state.track_rect.origin.x,
970 global_pos.y - scrollbar_state.track_rect.origin.y,
971 );
972
973 let component = scrollbar_state.hit_test_component(local_pos);
975
976 return Some(ScrollbarHit {
977 dom_id,
978 node_id,
979 orientation,
980 component,
981 local_position: local_pos,
982 global_position: global_pos,
983 });
984 }
985
986 None
987 }
988
989 pub fn hit_test_scrollbars(&self, global_pos: LogicalPosition) -> Option<ScrollbarHit> {
997 for ((dom_id, node_id, orientation), scrollbar_state) in self.scrollbar_states.iter().rev()
999 {
1000 if !scrollbar_state.visible {
1001 continue;
1002 }
1003
1004 if !scrollbar_state.track_rect.contains(global_pos) {
1006 continue;
1007 }
1008
1009 let local_pos = LogicalPosition::new(
1011 global_pos.x - scrollbar_state.track_rect.origin.x,
1012 global_pos.y - scrollbar_state.track_rect.origin.y,
1013 );
1014
1015 let component = scrollbar_state.hit_test_component(local_pos);
1017
1018 return Some(ScrollbarHit {
1019 dom_id: *dom_id,
1020 node_id: *node_id,
1021 orientation: *orientation,
1022 component,
1023 local_position: local_pos,
1024 global_position: global_pos,
1025 });
1026 }
1027
1028 None
1029 }
1030}
1031
1032impl AnimatedScrollState {
1035 pub fn new(now: Instant) -> Self {
1037 Self {
1038 current_offset: LogicalPosition::zero(),
1039 animation: None,
1040 last_activity: now,
1041 container_rect: LogicalRect::zero(),
1042 content_rect: LogicalRect::zero(),
1043 virtual_scroll_size: None,
1044 virtual_scroll_offset: None,
1045 overscroll_behavior_x: azul_css::props::style::scrollbar::OverscrollBehavior::Auto,
1046 overscroll_behavior_y: azul_css::props::style::scrollbar::OverscrollBehavior::Auto,
1047 overflow_scrolling: azul_css::props::style::scrollbar::OverflowScrolling::Auto,
1048 }
1049 }
1050
1051 pub fn clamp(&self, position: LogicalPosition) -> LogicalPosition {
1056 let effective_width = self.virtual_scroll_size
1057 .map(|s| s.width)
1058 .unwrap_or(self.content_rect.size.width);
1059 let effective_height = self.virtual_scroll_size
1060 .map(|s| s.height)
1061 .unwrap_or(self.content_rect.size.height);
1062 let max_x = (effective_width - self.container_rect.size.width).max(0.0);
1063 let max_y = (effective_height - self.container_rect.size.height).max(0.0);
1064 LogicalPosition {
1065 x: position.x.max(0.0).min(max_x),
1066 y: position.y.max(0.0).min(max_y),
1067 }
1068 }
1069}
1070
1071pub fn apply_easing(t: f32, easing: EasingFunction) -> f32 {
1076 match easing {
1077 EasingFunction::Linear => t,
1078 EasingFunction::EaseOut => 1.0 - (1.0 - t).powi(3),
1079 EasingFunction::EaseInOut => {
1080 if t < 0.5 {
1081 4.0 * t * t * t
1082 } else {
1083 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
1084 }
1085 }
1086 }
1087}
1088
1089pub type ScrollStates = ScrollManager;
1091
1092impl ScrollManager {
1093 pub fn remap_node_ids(
1098 &mut self,
1099 dom_id: DomId,
1100 node_id_map: &std::collections::BTreeMap<NodeId, NodeId>,
1101 ) {
1102 let states_to_update: Vec<_> = self.states.keys()
1104 .filter(|(d, _)| *d == dom_id)
1105 .cloned()
1106 .collect();
1107
1108 for (d, old_node_id) in states_to_update {
1109 if let Some(&new_node_id) = node_id_map.get(&old_node_id) {
1110 if let Some(state) = self.states.remove(&(d, old_node_id)) {
1111 self.states.insert((d, new_node_id), state);
1112 }
1113 } else {
1114 self.states.remove(&(d, old_node_id));
1116 }
1117 }
1118
1119 let scroll_ids_to_update: Vec<_> = self.external_scroll_ids.keys()
1121 .filter(|(d, _)| *d == dom_id)
1122 .cloned()
1123 .collect();
1124
1125 for (d, old_node_id) in scroll_ids_to_update {
1126 if let Some(&new_node_id) = node_id_map.get(&old_node_id) {
1127 if let Some(scroll_id) = self.external_scroll_ids.remove(&(d, old_node_id)) {
1128 self.external_scroll_ids.insert((d, new_node_id), scroll_id);
1129 }
1130 } else {
1131 self.external_scroll_ids.remove(&(d, old_node_id));
1132 }
1133 }
1134
1135 let scrollbar_states_to_update: Vec<_> = self.scrollbar_states.keys()
1137 .filter(|(d, _, _)| *d == dom_id)
1138 .cloned()
1139 .collect();
1140
1141 for (d, old_node_id, orientation) in scrollbar_states_to_update {
1142 if let Some(&new_node_id) = node_id_map.get(&old_node_id) {
1143 if let Some(state) = self.scrollbar_states.remove(&(d, old_node_id, orientation)) {
1144 self.scrollbar_states.insert((d, new_node_id, orientation), state);
1145 }
1146 } else {
1147 self.scrollbar_states.remove(&(d, old_node_id, orientation));
1148 }
1149 }
1150 }
1151}