1use std::{
12 cell::{Cell, OnceCell, 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 in_progress: MutableState<bool>,
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 in_progress: MutableState::with_runtime(false, runtime.clone()),
298 inner: MutableState::with_runtime(
299 Rc::new(ScrollStateInner {
300 max_value: RefCell::new(0.0),
301 viewport_extent: RefCell::new(0.0),
302 invalidate_callbacks: InvalidationCallbacks::new(),
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 is_scroll_in_progress(&self) -> bool {
329 self.in_progress.is_alive() && self.in_progress.get()
330 }
331
332 pub(crate) fn set_scroll_in_progress(&self, active: bool) {
333 if self.in_progress.is_alive() && self.in_progress.get_non_reactive() != active {
334 self.in_progress.set(active);
335 }
336 }
337
338 pub fn id(&self) -> u64 {
340 let mut hasher = cranpose_core::hash::default::new();
341 self.inner.runtime_state_id().hash(&mut hasher);
342 hasher.finish()
343 }
344
345 pub fn value(&self) -> f32 {
350 self.value.value()
351 }
352
353 pub fn value_non_reactive(&self) -> f32 {
358 self.value.get_non_reactive()
359 }
360
361 pub fn try_value(&self) -> Option<f32> {
364 self.value.try_value()
365 }
366
367 pub fn max_value(&self) -> f32 {
369 *self.inner().max_value.borrow()
370 }
371
372 pub fn dispatch_raw_delta(&self, delta: f32) -> f32 {
375 let current = self.value_non_reactive();
376 let max = self.max_value();
377 let new_value = (current + delta).clamp(0.0, max);
378 let actual_delta = new_value - current;
379
380 if actual_delta.abs() > 0.001 {
381 self.value.set(new_value);
382
383 self.invalidate();
384 }
385
386 actual_delta
387 }
388
389 pub fn viewport_extent(&self) -> f32 {
392 *self.inner().viewport_extent.borrow()
393 }
394
395 pub fn content_extent(&self) -> f32 {
397 self.viewport_extent() + self.max_value()
398 }
399
400 pub fn metrics(&self) -> ScrollMetrics {
406 let inner = self.inner();
407 let max_offset = *inner.max_value.borrow();
408 let viewport_extent = *inner.viewport_extent.borrow();
409 ScrollMetrics {
410 offset: self.value_non_reactive(),
411 max_offset,
412 viewport_extent,
413 }
414 }
415
416 pub(crate) fn set_max_value(&self, max: f32) {
417 *self.inner().max_value.borrow_mut() = max;
418 }
419
420 pub(crate) fn set_viewport_extent(&self, extent: f32) {
421 *self.inner().viewport_extent.borrow_mut() = extent;
422 }
423
424 pub fn scroll_to(&self, position: f32) {
426 let max = self.max_value();
427 let clamped = position.clamp(0.0, max);
428
429 self.value.set(clamped);
430
431 self.invalidate();
432 }
433
434 pub(crate) fn add_invalidate_callback(&self, callback: Box<dyn Fn()>) -> u64 {
435 let inner = self.inner();
436 let callback = app_owned_invalidation_callback(callback);
437 let id = inner.invalidate_callbacks.add(Rc::clone(&callback));
438 if inner.pending_invalidation.replace(false) {
439 callback();
440 }
441 id
442 }
443
444 pub(crate) fn remove_invalidate_callback(&self, id: u64) {
445 self.inner().invalidate_callbacks.remove(id);
446 }
447
448 fn invalidate(&self) {
449 let inner = self.inner();
450 let callbacks = inner.invalidate_callbacks.snapshot();
451 if callbacks.is_empty() {
452 inner.pending_invalidation.set(true);
453 return;
454 }
455 for callback in callbacks.values() {
456 callback();
457 }
458 }
459}
460
461fn app_owned_invalidation_callback(callback: Box<dyn Fn()>) -> Rc<dyn Fn()> {
462 let Some(owner) = crate::render_state::current_app_context() else {
463 return Rc::from(callback);
464 };
465 let owner = Rc::downgrade(&owner);
466 Rc::new(move || {
467 if let Some(owner) = owner.upgrade() {
468 owner.enter(&callback);
469 }
470 })
471}
472
473#[derive(Clone)]
474pub(crate) struct ScrollMotionContext {
475 inner: Rc<ScrollMotionContextInner>,
476}
477
478#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
479pub(crate) enum ScrollMotionContextKey {
480 ScrollState {
481 state_id: u64,
482 is_vertical: bool,
483 reverse_scrolling: bool,
484 },
485 LazyList {
486 state_identity: usize,
487 is_vertical: bool,
488 reverse_scrolling: bool,
489 },
490 Draggable {
491 state_identity: usize,
492 is_vertical: bool,
493 },
494}
495
496struct ScrollMotionContextInner {
497 active: Cell<bool>,
498 activity_observer: OnceCell<Box<dyn Fn(bool)>>,
501 transient_active: Cell<bool>,
502 generation: Cell<u64>,
503 invalidate_callbacks: InvalidationCallbacks,
504 pending_invalidation: Cell<bool>,
505 overscroll: OverscrollEffect,
506}
507
508pub(crate) struct ScrollMotionContextStore {
509 contexts: RefCell<HashMap<ScrollMotionContextKey, Weak<ScrollMotionContextInner>>>,
510}
511
512impl ScrollMotionContextStore {
513 pub(crate) fn new() -> Self {
514 Self {
515 contexts: RefCell::new(HashMap::default()),
516 }
517 }
518
519 fn context_for_key(&self, key: ScrollMotionContextKey) -> ScrollMotionContext {
520 let mut contexts = self.contexts.borrow_mut();
521 if let Some(inner) = contexts.get(&key).and_then(Weak::upgrade) {
522 return ScrollMotionContext { inner };
523 }
524
525 let context = ScrollMotionContext::new();
526 contexts.insert(key, Rc::downgrade(&context.inner));
527 contexts.retain(|_, weak| weak.strong_count() > 0);
528 context
529 }
530
531 pub(crate) fn clear_transient_after_frame(&self) {
532 let contexts = {
533 let mut contexts = self.contexts.borrow_mut();
534 let live = contexts
535 .values()
536 .filter_map(Weak::upgrade)
537 .collect::<Vec<_>>();
538 contexts.retain(|_, weak| weak.strong_count() > 0);
539 live
540 };
541 for inner in contexts {
542 ScrollMotionContext { inner }.clear_transient_after_frame();
543 }
544 }
545}
546
547pub(crate) fn scroll_motion_context_for_key(key: ScrollMotionContextKey) -> ScrollMotionContext {
548 crate::render_state::with_scroll_motion_context_store(|store| store.context_for_key(key))
549}
550
551impl ScrollMotionContext {
552 pub(crate) fn new() -> Self {
553 Self {
554 inner: Rc::new(ScrollMotionContextInner {
555 active: Cell::new(false),
556 activity_observer: OnceCell::new(),
557 transient_active: Cell::new(false),
558 generation: Cell::new(0),
559 invalidate_callbacks: InvalidationCallbacks::new(),
560 pending_invalidation: Cell::new(false),
561 overscroll: OverscrollEffect::new(),
562 }),
563 }
564 }
565
566 pub(crate) fn is_active(&self) -> bool {
567 self.inner.active.get() || self.inner.transient_active.get()
568 }
569
570 pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
571 Rc::ptr_eq(&self.inner, &other.inner)
572 }
573
574 pub(crate) fn stable_key(&self) -> usize {
575 Rc::as_ptr(&self.inner) as usize
576 }
577
578 pub(crate) fn overscroll(&self) -> OverscrollEffect {
579 self.inner.overscroll.clone()
580 }
581
582 pub(crate) fn observe_activity(&self, make: impl FnOnce() -> Box<dyn Fn(bool)>) {
585 self.inner.activity_observer.get_or_init(make);
586 }
587
588 pub(crate) fn set_active(&self, active: bool) {
589 let was_active = self.is_active();
590 if self.inner.active.replace(active) != active
591 && let Some(observer) = self.inner.activity_observer.get()
592 {
593 observer(active);
594 }
595 if !active {
596 self.inner.transient_active.set(false);
597 }
598 if was_active != self.is_active() {
599 self.bump_generation();
600 self.invalidate();
601 }
602 }
603
604 pub(crate) fn activate_for_current_frame(&self) {
605 let was_active = self.is_active();
606 self.inner.transient_active.set(true);
607 self.bump_generation();
608 if !was_active {
609 self.invalidate();
610 }
611 }
612
613 pub(crate) fn add_invalidate_callback(&self, callback: Box<dyn Fn()>) -> u64 {
614 let callback = app_owned_invalidation_callback(callback);
615 let id = self.inner.invalidate_callbacks.add(Rc::clone(&callback));
616 if self.inner.pending_invalidation.replace(false) {
617 callback();
618 }
619 id
620 }
621
622 pub(crate) fn remove_invalidate_callback(&self, id: u64) {
623 self.inner.invalidate_callbacks.remove(id);
624 }
625
626 fn bump_generation(&self) -> u64 {
627 let next = self.inner.generation.get().wrapping_add(1);
628 self.inner.generation.set(next);
629 next
630 }
631
632 fn clear_transient_after_frame(&self) {
633 let was_active = self.is_active();
634 if self.inner.transient_active.replace(false) {
635 self.bump_generation();
636 if was_active != self.is_active() {
637 self.invalidate();
638 }
639 }
640 }
641
642 fn invalidate(&self) {
643 let callbacks = self.inner.invalidate_callbacks.snapshot();
644 if callbacks.is_empty() {
645 self.inner.pending_invalidation.set(true);
646 return;
647 }
648 for callback in callbacks.values() {
649 callback();
650 }
651 }
652}
653
654#[derive(Clone)]
656pub struct ScrollElement {
657 state: ScrollState,
658 overscroll: OverscrollEffect,
659 is_vertical: bool,
660 reverse_scrolling: bool,
661}
662
663impl ScrollElement {
664 pub(crate) fn new(
665 state: ScrollState,
666 overscroll: OverscrollEffect,
667 is_vertical: bool,
668 reverse_scrolling: bool,
669 ) -> Self {
670 Self {
671 state,
672 overscroll,
673 is_vertical,
674 reverse_scrolling,
675 }
676 }
677}
678
679impl std::fmt::Debug for ScrollElement {
680 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
681 f.debug_struct("ScrollElement")
682 .field("is_vertical", &self.is_vertical)
683 .field("reverse_scrolling", &self.reverse_scrolling)
684 .finish()
685 }
686}
687
688impl PartialEq for ScrollElement {
689 fn eq(&self, other: &Self) -> bool {
690 self.state == other.state
691 && self.is_vertical == other.is_vertical
692 && self.reverse_scrolling == other.reverse_scrolling
693 }
694}
695
696impl Eq for ScrollElement {}
697
698impl Hash for ScrollElement {
699 fn hash<H: Hasher>(&self, state: &mut H) {
700 self.state.inner.runtime_state_id().hash(state);
701 self.is_vertical.hash(state);
702 self.reverse_scrolling.hash(state);
703 }
704}
705
706impl ModifierNodeElement for ScrollElement {
707 type Node = ScrollNode;
708
709 fn create(&self) -> Self::Node {
710 ScrollNode::new(
711 self.state,
712 self.overscroll.clone(),
713 self.is_vertical,
714 self.reverse_scrolling,
715 )
716 }
717
718 fn key(&self) -> Option<u64> {
719 let mut hasher = cranpose_core::hash::default::new();
720 self.state.id().hash(&mut hasher);
721 self.reverse_scrolling.hash(&mut hasher);
722 self.is_vertical.hash(&mut hasher);
723 Some(hasher.finish())
724 }
725
726 fn update(&self, node: &mut Self::Node) {
727 let needs_invalidation = node.state != self.state
728 || node.is_vertical != self.is_vertical
729 || node.reverse_scrolling != self.reverse_scrolling
730 || !node.overscroll.ptr_eq(&self.overscroll);
731
732 if needs_invalidation {
733 node.state = self.state;
734 node.is_vertical = self.is_vertical;
735 node.reverse_scrolling = self.reverse_scrolling;
736 node.overscroll = self.overscroll.clone();
737 }
738 }
739
740 fn capabilities(&self) -> NodeCapabilities {
741 NodeCapabilities::LAYOUT
742 }
743}
744
745pub struct ScrollNode {
748 state: ScrollState,
749 overscroll: OverscrollEffect,
750 is_vertical: bool,
751 reverse_scrolling: bool,
752 node_state: NodeState,
753 invalidation_callback_id: Option<u64>,
754 overscroll_callback_id: Option<u64>,
755 node_id: Option<NodeId>,
756}
757
758impl ScrollNode {
759 pub(crate) fn new(
760 state: ScrollState,
761 overscroll: OverscrollEffect,
762 is_vertical: bool,
763 reverse_scrolling: bool,
764 ) -> Self {
765 Self {
766 state,
767 overscroll,
768 is_vertical,
769 reverse_scrolling,
770 node_state: NodeState::default(),
771 invalidation_callback_id: None,
772 overscroll_callback_id: None,
773 node_id: None,
774 }
775 }
776
777 pub fn state(&self) -> &ScrollState {
779 &self.state
780 }
781}
782
783impl DelegatableNode for ScrollNode {
784 fn node_state(&self) -> &NodeState {
785 &self.node_state
786 }
787}
788
789impl ModifierNode for ScrollNode {
790 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
791 let node_id = context.node_id();
792 self.node_id = node_id;
793
794 if let Some(node_id) = node_id {
795 let callback_id = self.state.add_invalidate_callback(Box::new(move || {
796 crate::schedule_layout_repass(node_id);
797 }));
798 self.invalidation_callback_id = Some(callback_id);
799 let callback_id = self.overscroll.add_invalidate_callback(Box::new(move || {
800 crate::schedule_layout_repass(node_id);
801 }));
802 self.overscroll_callback_id = Some(callback_id);
803 } else {
804 log::debug!(
805 "ScrollNode attached without a NodeId; deferring invalidation registration."
806 );
807 }
808
809 context.invalidate(cranpose_foundation::InvalidationKind::Layout);
810 }
811
812 fn on_detach(&mut self) {
813 if let Some(id) = self.invalidation_callback_id.take() {
814 self.state.remove_invalidate_callback(id);
815 }
816 if let Some(id) = self.overscroll_callback_id.take() {
817 self.overscroll.remove_invalidate_callback(id);
818 }
819 }
820
821 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
822 Some(self)
823 }
824
825 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
826 Some(self)
827 }
828}
829
830impl LayoutModifierNode for ScrollNode {
831 fn measure(
832 &self,
833 _context: &mut dyn ModifierNodeContext,
834 measurable: &dyn Measurable,
835 constraints: Constraints,
836 ) -> LayoutModifierMeasureResult {
837 let scroll_constraints = if self.is_vertical {
842 Constraints {
843 max_height: f32::INFINITY,
844 ..constraints
845 }
846 } else {
847 Constraints {
848 max_width: f32::INFINITY,
849 ..constraints
850 }
851 };
852
853 let placeable = measurable.measure(scroll_constraints);
854
855 let width = placeable.width().min(constraints.max_width);
856 let height = placeable.height().min(constraints.max_height);
857
858 let max_scroll = if self.is_vertical {
859 (placeable.height() - height).max(0.0)
860 } else {
861 (placeable.width() - width).max(0.0)
862 };
863
864 if (self.is_vertical && constraints.max_height.is_finite())
865 || (!self.is_vertical && constraints.max_width.is_finite())
866 {
867 self.state.set_max_value(max_scroll);
868 self.state
869 .set_viewport_extent(if self.is_vertical { height } else { width });
870 self.overscroll
871 .set_dimension(if self.is_vertical { height } else { width });
872 }
873
874 let scroll = self.state.value_non_reactive().clamp(0.0, max_scroll);
875
876 let abs_scroll = if self.reverse_scrolling {
877 scroll - max_scroll
878 } else {
879 -scroll
880 };
881 let abs_scroll = abs_scroll + self.overscroll.offset();
882
883 let (x_offset, y_offset) = if self.is_vertical {
884 (0.0, abs_scroll)
885 } else {
886 (abs_scroll, 0.0)
887 };
888
889 LayoutModifierMeasureResult::new(Size { width, height }, x_offset, y_offset)
890 }
891
892 fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32, _density: f32) -> f32 {
893 measurable.min_intrinsic_width(height)
894 }
895
896 fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32, _density: f32) -> f32 {
897 measurable.max_intrinsic_width(height)
898 }
899
900 fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32, _density: f32) -> f32 {
901 measurable.min_intrinsic_height(width)
902 }
903
904 fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32, _density: f32) -> f32 {
905 measurable.max_intrinsic_height(width)
906 }
907}
908
909#[macro_export]
913macro_rules! rememberScrollState {
914 ($initial:expr) => {
915 cranpose_core::remember(|| $crate::scroll::ScrollState::new($initial))
916 .with(|state| state.clone())
917 };
918 () => {
919 rememberScrollState!(0.0)
920 };
921}
922
923#[cfg(test)]
924#[path = "tests/scroll_tests.rs"]
925mod tests;