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