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