1use cranpose_core::{MutableState, NodeId};
12use cranpose_foundation::{
13 Constraints, DelegatableNode, LayoutModifierNode, Measurable, ModifierNode,
14 ModifierNodeContext, ModifierNodeElement, NodeCapabilities, NodeState,
15};
16use cranpose_ui_graphics::Size;
17use cranpose_ui_layout::LayoutModifierMeasureResult;
18use std::cell::{Cell, RefCell};
19use std::collections::HashMap;
20use std::hash::{DefaultHasher, Hash, Hasher};
21use std::rc::{Rc, Weak};
22
23#[derive(Clone, Copy)]
31pub struct ScrollState {
32 value: MutableState<f32>,
33 inner: MutableState<Rc<ScrollStateInner>>,
34}
35
36pub(crate) struct ScrollStateInner {
37 max_value: RefCell<f32>,
38 viewport_extent: RefCell<f32>,
42 invalidate_callbacks: RefCell<HashMap<u64, Rc<dyn Fn()>>>,
43 next_invalidate_callback_id: Cell<u64>,
44 pending_invalidation: Cell<bool>,
45 settle_policy: RefCell<Option<ScrollSettlePolicy>>,
46}
47
48#[derive(Clone, Copy, Debug, PartialEq)]
50pub struct ScrollMetrics {
51 pub offset: f32,
53 pub max_offset: f32,
55 pub viewport_extent: f32,
57}
58
59impl ScrollMetrics {
60 pub fn content_extent(self) -> f32 {
62 self.viewport_extent + self.max_offset
63 }
64
65 pub fn progress(self) -> f32 {
67 if self.max_offset <= 0.0 {
68 0.0
69 } else {
70 (self.offset / self.max_offset).clamp(0.0, 1.0)
71 }
72 }
73
74 pub fn thumb(
77 self,
78 bounds: crate::scrollbar::ThumbBounds,
79 ) -> Option<crate::scrollbar::ThumbGeometry> {
80 crate::scrollbar::thumb_geometry(
81 self.content_extent(),
82 self.viewport_extent,
83 self.offset,
84 bounds,
85 )
86 }
87}
88
89pub type ScrollSettlePolicy = Rc<dyn Fn(f32, f32) -> f32>;
97
98#[derive(Clone)]
99pub(crate) struct OverscrollEffect {
100 inner: Rc<OverscrollEffectInner>,
101}
102
103struct OverscrollEffectInner {
104 offset: Cell<f32>,
105 limit: Cell<f32>,
106 invalidate_callbacks: RefCell<HashMap<u64, Rc<dyn Fn()>>>,
107 next_callback_id: Cell<u64>,
108}
109
110impl OverscrollEffect {
111 fn new() -> Self {
112 Self {
113 inner: Rc::new(OverscrollEffectInner {
114 offset: Cell::new(0.0),
115 limit: Cell::new(160.0),
116 invalidate_callbacks: RefCell::new(HashMap::new()),
117 next_callback_id: Cell::new(1),
118 }),
119 }
120 }
121
122 pub(crate) fn offset(&self) -> f32 {
123 self.inner.offset.get()
124 }
125
126 pub(crate) fn set_limit(&self, limit: f32) {
127 if !limit.is_finite() || limit <= 0.0 {
128 return;
129 }
130 self.inner.limit.set(limit);
131 self.set_offset(self.offset().clamp(-limit, limit));
132 }
133
134 pub(crate) fn apply_drag_delta(&self, delta: f32) -> f32 {
135 if !delta.is_finite() || delta.abs() <= f32::EPSILON {
136 return 0.0;
137 }
138 let limit = self.inner.limit.get();
139 let offset = self.offset();
140 let resistance = (1.0 - offset.abs() / limit).clamp(0.12, 1.0) * 0.5;
141 let next = (offset + delta * resistance).clamp(-limit, limit);
142 let applied = next - offset;
143 self.set_offset(next);
144 applied
145 }
146
147 pub(crate) fn apply_settle_delta(&self, delta: f32) -> f32 {
148 let offset = self.offset();
149 if offset.abs() <= f32::EPSILON {
150 return 0.0;
151 }
152 let proposed = offset + delta;
153 let crosses_edge = offset.abs() > f32::EPSILON && proposed.signum() != offset.signum();
154 let next = if crosses_edge {
155 0.0
156 } else {
157 proposed.clamp(-self.inner.limit.get(), self.inner.limit.get())
158 };
159 let applied = next - offset;
160 self.set_offset(next);
161 applied
162 }
163
164 pub(crate) fn apply_to_scroll<F>(&self, delta: f32, perform_scroll: F) -> f32
165 where
166 F: FnOnce(f32) -> f32,
167 {
168 if !delta.is_finite() || delta.abs() <= f32::EPSILON {
169 return 0.0;
170 }
171 let mut remaining = delta;
172 let mut consumed = 0.0;
173 let offset = self.offset();
174 if offset.abs() > f32::EPSILON && delta.signum() != offset.signum() {
175 let release = delta.abs().min(offset.abs()) * delta.signum();
176 let released = self.apply_settle_delta(release);
177 consumed += released;
178 remaining -= released;
179 }
180 if remaining.abs() > f32::EPSILON {
181 let target_consumed = perform_scroll(remaining);
182 consumed += target_consumed;
183 let unconsumed = remaining - target_consumed;
184 if unconsumed.abs() > f32::EPSILON {
185 consumed += self.apply_drag_delta(unconsumed);
186 }
187 }
188 consumed
189 }
190
191 pub(crate) fn apply_to_fling<F>(&self, delta: f32, perform_scroll: F) -> f32
192 where
193 F: FnOnce(f32) -> f32,
194 {
195 let consumed = perform_scroll(delta);
196 let unconsumed = delta - consumed;
197 if unconsumed.abs() > f32::EPSILON {
198 self.apply_drag_delta(-unconsumed);
199 }
200 consumed
201 }
202
203 pub(crate) fn add_invalidate_callback(&self, callback: Box<dyn Fn()>) -> u64 {
204 let id = self.inner.next_callback_id.get();
205 self.inner.next_callback_id.set(id.saturating_add(1));
206 self.inner
207 .invalidate_callbacks
208 .borrow_mut()
209 .insert(id, Rc::from(callback));
210 id
211 }
212
213 pub(crate) fn remove_invalidate_callback(&self, id: u64) {
214 self.inner.invalidate_callbacks.borrow_mut().remove(&id);
215 }
216
217 pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
218 Rc::ptr_eq(&self.inner, &other.inner)
219 }
220
221 fn set_offset(&self, offset: f32) {
222 if (offset - self.offset()).abs() <= f32::EPSILON {
223 return;
224 }
225 self.inner.offset.set(offset);
226 let callbacks = self
227 .inner
228 .invalidate_callbacks
229 .borrow()
230 .values()
231 .cloned()
232 .collect::<Vec<_>>();
233 for callback in callbacks {
234 callback();
235 }
236 }
237}
238
239impl PartialEq for ScrollState {
240 fn eq(&self, other: &Self) -> bool {
243 self.inner == other.inner
244 }
245}
246
247impl ScrollState {
248 pub fn new(initial: f32) -> Self {
250 let runtime = cranpose_core::current_runtime_handle()
251 .expect("ScrollState::new requires an active runtime");
252 Self {
253 value: MutableState::with_runtime(initial, runtime.clone()),
254 inner: MutableState::with_runtime(
255 Rc::new(ScrollStateInner {
256 max_value: RefCell::new(0.0),
257 viewport_extent: RefCell::new(0.0),
258 invalidate_callbacks: RefCell::new(HashMap::new()),
259 next_invalidate_callback_id: Cell::new(1),
260 pending_invalidation: Cell::new(false),
261 settle_policy: RefCell::new(None),
262 }),
263 runtime,
264 ),
265 }
266 }
267
268 fn inner(&self) -> Rc<ScrollStateInner> {
269 self.inner.get_non_reactive()
270 }
271
272 pub fn set_settle_policy(&self, policy: Option<ScrollSettlePolicy>) {
274 *self.inner().settle_policy.borrow_mut() = policy;
275 }
276
277 pub fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
279 self.inner().settle_policy.borrow().clone()
280 }
281
282 pub fn id(&self) -> u64 {
284 let mut hasher = DefaultHasher::new();
285 self.inner.runtime_state_id().hash(&mut hasher);
286 hasher.finish()
287 }
288
289 pub fn value(&self) -> f32 {
294 self.value.value()
295 }
296
297 pub fn value_non_reactive(&self) -> f32 {
302 self.value.get_non_reactive()
303 }
304
305 pub fn max_value(&self) -> f32 {
307 *self.inner().max_value.borrow()
308 }
309
310 pub fn dispatch_raw_delta(&self, delta: f32) -> f32 {
313 let current = self.value_non_reactive();
314 let max = self.max_value();
315 let new_value = (current + delta).clamp(0.0, max);
316 let actual_delta = new_value - current;
317
318 if actual_delta.abs() > 0.001 {
319 self.value.set(new_value);
321
322 self.invalidate();
323 }
324
325 actual_delta
326 }
327
328 pub fn viewport_extent(&self) -> f32 {
331 *self.inner().viewport_extent.borrow()
332 }
333
334 pub fn content_extent(&self) -> f32 {
336 self.viewport_extent() + self.max_value()
337 }
338
339 pub fn metrics(&self) -> ScrollMetrics {
345 let inner = self.inner();
346 let max_offset = *inner.max_value.borrow();
347 let viewport_extent = *inner.viewport_extent.borrow();
348 ScrollMetrics {
349 offset: self.value_non_reactive(),
350 max_offset,
351 viewport_extent,
352 }
353 }
354
355 pub(crate) fn set_max_value(&self, max: f32) {
357 *self.inner().max_value.borrow_mut() = max;
358 }
359
360 pub(crate) fn set_viewport_extent(&self, extent: f32) {
362 *self.inner().viewport_extent.borrow_mut() = extent;
363 }
364
365 pub fn scroll_to(&self, position: f32) {
367 let max = self.max_value();
368 let clamped = position.clamp(0.0, max);
369
370 self.value.set(clamped);
371
372 self.invalidate();
373 }
374
375 pub(crate) fn add_invalidate_callback(&self, callback: Box<dyn Fn()>) -> u64 {
377 let inner = self.inner();
378 let id = inner.next_invalidate_callback_id.get();
379 inner.next_invalidate_callback_id.set(id.saturating_add(1));
380 let callback: Rc<dyn Fn()> = Rc::from(callback);
381 inner
382 .invalidate_callbacks
383 .borrow_mut()
384 .insert(id, Rc::clone(&callback));
385 if inner.pending_invalidation.replace(false) {
386 callback();
387 }
388 id
389 }
390
391 pub(crate) fn remove_invalidate_callback(&self, id: u64) {
393 self.inner().invalidate_callbacks.borrow_mut().remove(&id);
394 }
395
396 fn invalidate(&self) {
397 let inner = self.inner();
398 let callbacks: Vec<Rc<dyn Fn()>> = {
399 let callbacks = inner.invalidate_callbacks.borrow();
400 if callbacks.is_empty() {
401 inner.pending_invalidation.set(true);
402 return;
403 }
404 callbacks.values().cloned().collect()
405 };
406 for callback in callbacks {
407 callback();
408 }
409 }
410}
411
412#[derive(Clone)]
413pub(crate) struct ScrollMotionContext {
414 inner: Rc<ScrollMotionContextInner>,
415}
416
417#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
418pub(crate) enum ScrollMotionContextKey {
419 ScrollState {
420 state_id: u64,
421 is_vertical: bool,
422 reverse_scrolling: bool,
423 },
424 LazyList {
425 state_identity: usize,
426 is_vertical: bool,
427 reverse_scrolling: bool,
428 },
429 Draggable {
433 state_identity: usize,
434 is_vertical: bool,
435 },
436}
437
438struct ScrollMotionContextInner {
439 active: Cell<bool>,
440 transient_active: Cell<bool>,
441 generation: Cell<u64>,
442 invalidate_callbacks: RefCell<HashMap<u64, Rc<dyn Fn()>>>,
443 next_invalidate_callback_id: Cell<u64>,
444 pending_invalidation: Cell<bool>,
445 overscroll: OverscrollEffect,
446}
447
448pub(crate) struct ScrollMotionContextStore {
449 contexts: RefCell<HashMap<ScrollMotionContextKey, Weak<ScrollMotionContextInner>>>,
450}
451
452impl ScrollMotionContextStore {
453 pub(crate) fn new() -> Self {
454 Self {
455 contexts: RefCell::new(HashMap::new()),
456 }
457 }
458
459 fn context_for_key(&self, key: ScrollMotionContextKey) -> ScrollMotionContext {
460 let mut contexts = self.contexts.borrow_mut();
461 if let Some(inner) = contexts.get(&key).and_then(Weak::upgrade) {
462 return ScrollMotionContext { inner };
463 }
464
465 let context = ScrollMotionContext::new();
466 contexts.insert(key, Rc::downgrade(&context.inner));
467 contexts.retain(|_, weak| weak.strong_count() > 0);
468 context
469 }
470
471 pub(crate) fn clear_transient_after_frame(&self) {
472 let contexts = {
473 let mut contexts = self.contexts.borrow_mut();
474 let live = contexts
475 .values()
476 .filter_map(Weak::upgrade)
477 .collect::<Vec<_>>();
478 contexts.retain(|_, weak| weak.strong_count() > 0);
479 live
480 };
481 for inner in contexts {
482 ScrollMotionContext { inner }.clear_transient_after_frame();
483 }
484 }
485}
486
487pub(crate) fn scroll_motion_context_for_key(key: ScrollMotionContextKey) -> ScrollMotionContext {
488 crate::render_state::with_scroll_motion_context_store(|store| store.context_for_key(key))
489}
490
491impl ScrollMotionContext {
492 pub(crate) fn new() -> Self {
493 Self {
494 inner: Rc::new(ScrollMotionContextInner {
495 active: Cell::new(false),
496 transient_active: Cell::new(false),
497 generation: Cell::new(0),
498 invalidate_callbacks: RefCell::new(HashMap::new()),
499 next_invalidate_callback_id: Cell::new(1),
500 pending_invalidation: Cell::new(false),
501 overscroll: OverscrollEffect::new(),
502 }),
503 }
504 }
505
506 pub(crate) fn is_active(&self) -> bool {
507 self.inner.active.get() || self.inner.transient_active.get()
508 }
509
510 pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
511 Rc::ptr_eq(&self.inner, &other.inner)
512 }
513
514 pub(crate) fn stable_key(&self) -> usize {
515 Rc::as_ptr(&self.inner) as usize
516 }
517
518 pub(crate) fn overscroll(&self) -> OverscrollEffect {
519 self.inner.overscroll.clone()
520 }
521
522 pub(crate) fn set_active(&self, active: bool) {
523 let was_active = self.is_active();
524 self.inner.active.set(active);
525 if !active {
526 self.inner.transient_active.set(false);
527 }
528 if was_active != self.is_active() {
529 self.bump_generation();
530 self.invalidate();
531 }
532 }
533
534 pub(crate) fn activate_for_current_frame(&self) {
535 let was_active = self.is_active();
536 self.inner.transient_active.set(true);
537 self.bump_generation();
538 if !was_active {
539 self.invalidate();
540 }
541 }
542
543 pub(crate) fn add_invalidate_callback(&self, callback: Box<dyn Fn()>) -> u64 {
544 let id = self.inner.next_invalidate_callback_id.get();
545 self.inner
546 .next_invalidate_callback_id
547 .set(id.saturating_add(1));
548 let callback: Rc<dyn Fn()> = Rc::from(callback);
549 self.inner
550 .invalidate_callbacks
551 .borrow_mut()
552 .insert(id, Rc::clone(&callback));
553 if self.inner.pending_invalidation.replace(false) {
554 callback();
555 }
556 id
557 }
558
559 pub(crate) fn remove_invalidate_callback(&self, id: u64) {
560 self.inner.invalidate_callbacks.borrow_mut().remove(&id);
561 }
562
563 fn bump_generation(&self) -> u64 {
564 let next = self.inner.generation.get().wrapping_add(1);
565 self.inner.generation.set(next);
566 next
567 }
568
569 fn clear_transient_after_frame(&self) {
570 let was_active = self.is_active();
571 if self.inner.transient_active.replace(false) {
572 self.bump_generation();
573 if was_active != self.is_active() {
574 self.invalidate();
575 }
576 }
577 }
578
579 fn invalidate(&self) {
580 let callbacks: Vec<Rc<dyn Fn()>> = {
581 let callbacks = self.inner.invalidate_callbacks.borrow();
582 if callbacks.is_empty() {
583 self.inner.pending_invalidation.set(true);
584 return;
585 }
586 callbacks.values().cloned().collect()
587 };
588 for callback in callbacks {
589 callback();
590 }
591 }
592}
593
594#[derive(Clone)]
596pub struct ScrollElement {
597 state: ScrollState,
598 overscroll: OverscrollEffect,
599 is_vertical: bool,
600 reverse_scrolling: bool,
601}
602
603impl ScrollElement {
604 pub(crate) fn new(
605 state: ScrollState,
606 overscroll: OverscrollEffect,
607 is_vertical: bool,
608 reverse_scrolling: bool,
609 ) -> Self {
610 Self {
611 state,
612 overscroll,
613 is_vertical,
614 reverse_scrolling,
615 }
616 }
617}
618
619impl std::fmt::Debug for ScrollElement {
620 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
621 f.debug_struct("ScrollElement")
622 .field("is_vertical", &self.is_vertical)
623 .field("reverse_scrolling", &self.reverse_scrolling)
624 .finish()
625 }
626}
627
628impl PartialEq for ScrollElement {
629 fn eq(&self, other: &Self) -> bool {
630 self.state == other.state
632 && self.is_vertical == other.is_vertical
633 && self.reverse_scrolling == other.reverse_scrolling
634 }
635}
636
637impl Eq for ScrollElement {}
638
639impl Hash for ScrollElement {
640 fn hash<H: Hasher>(&self, state: &mut H) {
641 self.state.inner.runtime_state_id().hash(state);
642 self.is_vertical.hash(state);
643 self.reverse_scrolling.hash(state);
644 }
645}
646
647impl ModifierNodeElement for ScrollElement {
648 type Node = ScrollNode;
649
650 fn create(&self) -> Self::Node {
651 ScrollNode::new(
653 self.state,
654 self.overscroll.clone(),
655 self.is_vertical,
656 self.reverse_scrolling,
657 )
658 }
659
660 fn key(&self) -> Option<u64> {
661 let mut hasher = DefaultHasher::new();
662 self.state.id().hash(&mut hasher);
663 self.reverse_scrolling.hash(&mut hasher);
664 self.is_vertical.hash(&mut hasher);
665 Some(hasher.finish())
666 }
667
668 fn update(&self, node: &mut Self::Node) {
669 let needs_invalidation = node.state != self.state
670 || node.is_vertical != self.is_vertical
671 || node.reverse_scrolling != self.reverse_scrolling
672 || !node.overscroll.ptr_eq(&self.overscroll);
673
674 if needs_invalidation {
675 node.state = self.state;
676 node.is_vertical = self.is_vertical;
677 node.reverse_scrolling = self.reverse_scrolling;
678 node.overscroll = self.overscroll.clone();
679 }
680 }
681
682 fn capabilities(&self) -> NodeCapabilities {
683 NodeCapabilities::LAYOUT
684 }
685}
686
687pub struct ScrollNode {
690 state: ScrollState,
691 overscroll: OverscrollEffect,
692 is_vertical: bool,
693 reverse_scrolling: bool,
694 node_state: NodeState,
695 invalidation_callback_id: Option<u64>,
697 overscroll_callback_id: Option<u64>,
698 node_id: Option<NodeId>,
700}
701
702impl ScrollNode {
703 pub(crate) fn new(
704 state: ScrollState,
705 overscroll: OverscrollEffect,
706 is_vertical: bool,
707 reverse_scrolling: bool,
708 ) -> Self {
709 Self {
710 state,
711 overscroll,
712 is_vertical,
713 reverse_scrolling,
714 node_state: NodeState::default(),
715 invalidation_callback_id: None,
716 overscroll_callback_id: None,
717 node_id: None,
718 }
719 }
720
721 pub fn state(&self) -> &ScrollState {
723 &self.state
724 }
725}
726
727impl DelegatableNode for ScrollNode {
728 fn node_state(&self) -> &NodeState {
729 &self.node_state
730 }
731}
732
733impl ModifierNode for ScrollNode {
734 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
735 let node_id = context.node_id();
739 self.node_id = node_id;
740
741 if let Some(node_id) = node_id {
742 let callback_id = self.state.add_invalidate_callback(Box::new(move || {
743 crate::schedule_layout_repass(node_id);
745 }));
746 self.invalidation_callback_id = Some(callback_id);
747 let callback_id = self.overscroll.add_invalidate_callback(Box::new(move || {
748 crate::schedule_layout_repass(node_id);
749 }));
750 self.overscroll_callback_id = Some(callback_id);
751 } else {
752 log::debug!(
753 "ScrollNode attached without a NodeId; deferring invalidation registration."
754 );
755 }
756
757 context.invalidate(cranpose_foundation::InvalidationKind::Layout);
759 }
760
761 fn on_detach(&mut self) {
762 if let Some(id) = self.invalidation_callback_id.take() {
764 self.state.remove_invalidate_callback(id);
765 }
766 if let Some(id) = self.overscroll_callback_id.take() {
767 self.overscroll.remove_invalidate_callback(id);
768 }
769 }
770
771 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
772 Some(self)
773 }
774
775 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
776 Some(self)
777 }
778}
779
780impl LayoutModifierNode for ScrollNode {
781 fn measure(
782 &self,
783 _context: &mut dyn ModifierNodeContext,
784 measurable: &dyn Measurable,
785 constraints: Constraints,
786 ) -> LayoutModifierMeasureResult {
787 let scroll_constraints = if self.is_vertical {
789 Constraints {
790 min_height: 0.0,
791 max_height: f32::INFINITY,
792 ..constraints
793 }
794 } else {
795 Constraints {
796 min_width: 0.0,
797 max_width: f32::INFINITY,
798 ..constraints
799 }
800 };
801
802 let placeable = measurable.measure(scroll_constraints);
804
805 let width = placeable.width().min(constraints.max_width);
807 let height = placeable.height().min(constraints.max_height);
808
809 let max_scroll = if self.is_vertical {
811 (placeable.height() - height).max(0.0)
812 } else {
813 (placeable.width() - width).max(0.0)
814 };
815
816 if (self.is_vertical && constraints.max_height.is_finite())
819 || (!self.is_vertical && constraints.max_width.is_finite())
820 {
821 self.state.set_max_value(max_scroll);
822 self.state
823 .set_viewport_extent(if self.is_vertical { height } else { width });
824 self.overscroll
825 .set_limit((if self.is_vertical { height } else { width }) * 0.5);
826 }
827
828 let scroll = self.state.value_non_reactive().clamp(0.0, max_scroll);
831
832 let abs_scroll = if self.reverse_scrolling {
833 scroll - max_scroll
834 } else {
835 -scroll
836 };
837 let abs_scroll = abs_scroll + self.overscroll.offset();
838
839 let (x_offset, y_offset) = if self.is_vertical {
840 (0.0, abs_scroll)
841 } else {
842 (abs_scroll, 0.0)
843 };
844
845 LayoutModifierMeasureResult::new(Size { width, height }, x_offset, y_offset)
849 }
850
851 fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
852 measurable.min_intrinsic_width(height)
853 }
854
855 fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
856 measurable.max_intrinsic_width(height)
857 }
858
859 fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
860 measurable.min_intrinsic_height(width)
861 }
862
863 fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
864 measurable.max_intrinsic_height(width)
865 }
866}
867
868#[macro_export]
872macro_rules! rememberScrollState {
873 ($initial:expr) => {
874 cranpose_core::remember(|| $crate::scroll::ScrollState::new($initial))
875 .with(|state| state.clone())
876 };
877 () => {
878 rememberScrollState!(0.0)
879 };
880}
881
882#[cfg(test)]
883#[path = "tests/scroll_tests.rs"]
884mod tests;