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, Rc::from(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: Rc<dyn Fn()> = Rc::from(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
424#[derive(Clone)]
425pub(crate) struct ScrollMotionContext {
426 inner: Rc<ScrollMotionContextInner>,
427}
428
429#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
430pub(crate) enum ScrollMotionContextKey {
431 ScrollState {
432 state_id: u64,
433 is_vertical: bool,
434 reverse_scrolling: bool,
435 },
436 LazyList {
437 state_identity: usize,
438 is_vertical: bool,
439 reverse_scrolling: bool,
440 },
441 Draggable {
442 state_identity: usize,
443 is_vertical: bool,
444 },
445}
446
447struct ScrollMotionContextInner {
448 active: Cell<bool>,
449 transient_active: Cell<bool>,
450 generation: Cell<u64>,
451 invalidate_callbacks: RefCell<HashMap<u64, Rc<dyn Fn()>>>,
452 next_invalidate_callback_id: Cell<u64>,
453 pending_invalidation: Cell<bool>,
454 overscroll: OverscrollEffect,
455}
456
457pub(crate) struct ScrollMotionContextStore {
458 contexts: RefCell<HashMap<ScrollMotionContextKey, Weak<ScrollMotionContextInner>>>,
459}
460
461impl ScrollMotionContextStore {
462 pub(crate) fn new() -> Self {
463 Self {
464 contexts: RefCell::new(HashMap::new()),
465 }
466 }
467
468 fn context_for_key(&self, key: ScrollMotionContextKey) -> ScrollMotionContext {
469 let mut contexts = self.contexts.borrow_mut();
470 if let Some(inner) = contexts.get(&key).and_then(Weak::upgrade) {
471 return ScrollMotionContext { inner };
472 }
473
474 let context = ScrollMotionContext::new();
475 contexts.insert(key, Rc::downgrade(&context.inner));
476 contexts.retain(|_, weak| weak.strong_count() > 0);
477 context
478 }
479
480 pub(crate) fn clear_transient_after_frame(&self) {
481 let contexts = {
482 let mut contexts = self.contexts.borrow_mut();
483 let live = contexts
484 .values()
485 .filter_map(Weak::upgrade)
486 .collect::<Vec<_>>();
487 contexts.retain(|_, weak| weak.strong_count() > 0);
488 live
489 };
490 for inner in contexts {
491 ScrollMotionContext { inner }.clear_transient_after_frame();
492 }
493 }
494}
495
496pub(crate) fn scroll_motion_context_for_key(key: ScrollMotionContextKey) -> ScrollMotionContext {
497 crate::render_state::with_scroll_motion_context_store(|store| store.context_for_key(key))
498}
499
500impl ScrollMotionContext {
501 pub(crate) fn new() -> Self {
502 Self {
503 inner: Rc::new(ScrollMotionContextInner {
504 active: Cell::new(false),
505 transient_active: Cell::new(false),
506 generation: Cell::new(0),
507 invalidate_callbacks: RefCell::new(HashMap::new()),
508 next_invalidate_callback_id: Cell::new(1),
509 pending_invalidation: Cell::new(false),
510 overscroll: OverscrollEffect::new(),
511 }),
512 }
513 }
514
515 pub(crate) fn is_active(&self) -> bool {
516 self.inner.active.get() || self.inner.transient_active.get()
517 }
518
519 pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
520 Rc::ptr_eq(&self.inner, &other.inner)
521 }
522
523 pub(crate) fn stable_key(&self) -> usize {
524 Rc::as_ptr(&self.inner) as usize
525 }
526
527 pub(crate) fn overscroll(&self) -> OverscrollEffect {
528 self.inner.overscroll.clone()
529 }
530
531 pub(crate) fn set_active(&self, active: bool) {
532 let was_active = self.is_active();
533 self.inner.active.set(active);
534 if !active {
535 self.inner.transient_active.set(false);
536 }
537 if was_active != self.is_active() {
538 self.bump_generation();
539 self.invalidate();
540 }
541 }
542
543 pub(crate) fn activate_for_current_frame(&self) {
544 let was_active = self.is_active();
545 self.inner.transient_active.set(true);
546 self.bump_generation();
547 if !was_active {
548 self.invalidate();
549 }
550 }
551
552 pub(crate) fn add_invalidate_callback(&self, callback: Box<dyn Fn()>) -> u64 {
553 let id = self.inner.next_invalidate_callback_id.get();
554 self.inner
555 .next_invalidate_callback_id
556 .set(id.saturating_add(1));
557 let callback: Rc<dyn Fn()> = Rc::from(callback);
558 self.inner
559 .invalidate_callbacks
560 .borrow_mut()
561 .insert(id, Rc::clone(&callback));
562 if self.inner.pending_invalidation.replace(false) {
563 callback();
564 }
565 id
566 }
567
568 pub(crate) fn remove_invalidate_callback(&self, id: u64) {
569 self.inner.invalidate_callbacks.borrow_mut().remove(&id);
570 }
571
572 fn bump_generation(&self) -> u64 {
573 let next = self.inner.generation.get().wrapping_add(1);
574 self.inner.generation.set(next);
575 next
576 }
577
578 fn clear_transient_after_frame(&self) {
579 let was_active = self.is_active();
580 if self.inner.transient_active.replace(false) {
581 self.bump_generation();
582 if was_active != self.is_active() {
583 self.invalidate();
584 }
585 }
586 }
587
588 fn invalidate(&self) {
589 let callbacks: Vec<Rc<dyn Fn()>> = {
590 let callbacks = self.inner.invalidate_callbacks.borrow();
591 if callbacks.is_empty() {
592 self.inner.pending_invalidation.set(true);
593 return;
594 }
595 callbacks.values().cloned().collect()
596 };
597 for callback in callbacks {
598 callback();
599 }
600 }
601}
602
603#[derive(Clone)]
605pub struct ScrollElement {
606 state: ScrollState,
607 overscroll: OverscrollEffect,
608 is_vertical: bool,
609 reverse_scrolling: bool,
610}
611
612impl ScrollElement {
613 pub(crate) fn new(
614 state: ScrollState,
615 overscroll: OverscrollEffect,
616 is_vertical: bool,
617 reverse_scrolling: bool,
618 ) -> Self {
619 Self {
620 state,
621 overscroll,
622 is_vertical,
623 reverse_scrolling,
624 }
625 }
626}
627
628impl std::fmt::Debug for ScrollElement {
629 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
630 f.debug_struct("ScrollElement")
631 .field("is_vertical", &self.is_vertical)
632 .field("reverse_scrolling", &self.reverse_scrolling)
633 .finish()
634 }
635}
636
637impl PartialEq for ScrollElement {
638 fn eq(&self, other: &Self) -> bool {
639 self.state == other.state
640 && self.is_vertical == other.is_vertical
641 && self.reverse_scrolling == other.reverse_scrolling
642 }
643}
644
645impl Eq for ScrollElement {}
646
647impl Hash for ScrollElement {
648 fn hash<H: Hasher>(&self, state: &mut H) {
649 self.state.inner.runtime_state_id().hash(state);
650 self.is_vertical.hash(state);
651 self.reverse_scrolling.hash(state);
652 }
653}
654
655impl ModifierNodeElement for ScrollElement {
656 type Node = ScrollNode;
657
658 fn create(&self) -> Self::Node {
659 ScrollNode::new(
660 self.state,
661 self.overscroll.clone(),
662 self.is_vertical,
663 self.reverse_scrolling,
664 )
665 }
666
667 fn key(&self) -> Option<u64> {
668 let mut hasher = DefaultHasher::new();
669 self.state.id().hash(&mut hasher);
670 self.reverse_scrolling.hash(&mut hasher);
671 self.is_vertical.hash(&mut hasher);
672 Some(hasher.finish())
673 }
674
675 fn update(&self, node: &mut Self::Node) {
676 let needs_invalidation = node.state != self.state
677 || node.is_vertical != self.is_vertical
678 || node.reverse_scrolling != self.reverse_scrolling
679 || !node.overscroll.ptr_eq(&self.overscroll);
680
681 if needs_invalidation {
682 node.state = self.state;
683 node.is_vertical = self.is_vertical;
684 node.reverse_scrolling = self.reverse_scrolling;
685 node.overscroll = self.overscroll.clone();
686 }
687 }
688
689 fn capabilities(&self) -> NodeCapabilities {
690 NodeCapabilities::LAYOUT
691 }
692}
693
694pub struct ScrollNode {
697 state: ScrollState,
698 overscroll: OverscrollEffect,
699 is_vertical: bool,
700 reverse_scrolling: bool,
701 node_state: NodeState,
702 invalidation_callback_id: Option<u64>,
703 overscroll_callback_id: Option<u64>,
704 node_id: Option<NodeId>,
705}
706
707impl ScrollNode {
708 pub(crate) fn new(
709 state: ScrollState,
710 overscroll: OverscrollEffect,
711 is_vertical: bool,
712 reverse_scrolling: bool,
713 ) -> Self {
714 Self {
715 state,
716 overscroll,
717 is_vertical,
718 reverse_scrolling,
719 node_state: NodeState::default(),
720 invalidation_callback_id: None,
721 overscroll_callback_id: None,
722 node_id: None,
723 }
724 }
725
726 pub fn state(&self) -> &ScrollState {
728 &self.state
729 }
730}
731
732impl DelegatableNode for ScrollNode {
733 fn node_state(&self) -> &NodeState {
734 &self.node_state
735 }
736}
737
738impl ModifierNode for ScrollNode {
739 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
740 let node_id = context.node_id();
741 self.node_id = node_id;
742
743 if let Some(node_id) = node_id {
744 let callback_id = self.state.add_invalidate_callback(Box::new(move || {
745 crate::schedule_layout_repass(node_id);
746 }));
747 self.invalidation_callback_id = Some(callback_id);
748 let callback_id = self.overscroll.add_invalidate_callback(Box::new(move || {
749 crate::schedule_layout_repass(node_id);
750 }));
751 self.overscroll_callback_id = Some(callback_id);
752 } else {
753 log::debug!(
754 "ScrollNode attached without a NodeId; deferring invalidation registration."
755 );
756 }
757
758 context.invalidate(cranpose_foundation::InvalidationKind::Layout);
759 }
760
761 fn on_detach(&mut self) {
762 if let Some(id) = self.invalidation_callback_id.take() {
763 self.state.remove_invalidate_callback(id);
764 }
765 if let Some(id) = self.overscroll_callback_id.take() {
766 self.overscroll.remove_invalidate_callback(id);
767 }
768 }
769
770 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
771 Some(self)
772 }
773
774 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
775 Some(self)
776 }
777}
778
779impl LayoutModifierNode for ScrollNode {
780 fn measure(
781 &self,
782 _context: &mut dyn ModifierNodeContext,
783 measurable: &dyn Measurable,
784 constraints: Constraints,
785 ) -> LayoutModifierMeasureResult {
786 let scroll_constraints = if self.is_vertical {
787 Constraints {
788 min_height: 0.0,
789 max_height: f32::INFINITY,
790 ..constraints
791 }
792 } else {
793 Constraints {
794 min_width: 0.0,
795 max_width: f32::INFINITY,
796 ..constraints
797 }
798 };
799
800 let placeable = measurable.measure(scroll_constraints);
801
802 let width = placeable.width().min(constraints.max_width);
803 let height = placeable.height().min(constraints.max_height);
804
805 let max_scroll = if self.is_vertical {
806 (placeable.height() - height).max(0.0)
807 } else {
808 (placeable.width() - width).max(0.0)
809 };
810
811 if (self.is_vertical && constraints.max_height.is_finite())
812 || (!self.is_vertical && constraints.max_width.is_finite())
813 {
814 self.state.set_max_value(max_scroll);
815 self.state
816 .set_viewport_extent(if self.is_vertical { height } else { width });
817 self.overscroll
818 .set_dimension(if self.is_vertical { height } else { width });
819 }
820
821 let scroll = self.state.value_non_reactive().clamp(0.0, max_scroll);
822
823 let abs_scroll = if self.reverse_scrolling {
824 scroll - max_scroll
825 } else {
826 -scroll
827 };
828 let abs_scroll = abs_scroll + self.overscroll.offset();
829
830 let (x_offset, y_offset) = if self.is_vertical {
831 (0.0, abs_scroll)
832 } else {
833 (abs_scroll, 0.0)
834 };
835
836 LayoutModifierMeasureResult::new(Size { width, height }, x_offset, y_offset)
837 }
838
839 fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
840 measurable.min_intrinsic_width(height)
841 }
842
843 fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
844 measurable.max_intrinsic_width(height)
845 }
846
847 fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
848 measurable.min_intrinsic_height(width)
849 }
850
851 fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
852 measurable.max_intrinsic_height(width)
853 }
854}
855
856#[macro_export]
860macro_rules! rememberScrollState {
861 ($initial:expr) => {
862 cranpose_core::remember(|| $crate::scroll::ScrollState::new($initial))
863 .with(|state| state.clone())
864 };
865 () => {
866 rememberScrollState!(0.0)
867 };
868}
869
870#[cfg(test)]
871#[path = "tests/scroll_tests.rs"]
872mod tests;