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