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