1use cranpose_core::{MutableState, NodeId};
12use cranpose_foundation::{
13 Constraints, DelegatableNode, LayoutModifierNode, Measurable, ModifierNode,
14 ModifierNodeContext, ModifierNodeElement, NodeCapabilities, NodeState,
15};
16use cranpose_ui_graphics::Size;
17use cranpose_ui_layout::LayoutModifierMeasureResult;
18use std::cell::{Cell, RefCell};
19use std::collections::HashMap;
20use std::hash::{DefaultHasher, Hash, Hasher};
21use std::rc::{Rc, Weak};
22
23#[derive(Clone, Copy)]
31pub struct ScrollState {
32 value: MutableState<f32>,
33 inner: MutableState<Rc<ScrollStateInner>>,
34}
35
36pub(crate) struct ScrollStateInner {
37 max_value: RefCell<f32>,
38 invalidate_callbacks: RefCell<HashMap<u64, Rc<dyn Fn()>>>,
39 next_invalidate_callback_id: Cell<u64>,
40 pending_invalidation: Cell<bool>,
41 settle_policy: RefCell<Option<ScrollSettlePolicy>>,
42}
43
44pub type ScrollSettlePolicy = Rc<dyn Fn(f32, f32) -> f32>;
52
53#[derive(Clone)]
54pub(crate) struct OverscrollEffect {
55 inner: Rc<OverscrollEffectInner>,
56}
57
58struct OverscrollEffectInner {
59 offset: Cell<f32>,
60 limit: Cell<f32>,
61 invalidate_callbacks: RefCell<HashMap<u64, Rc<dyn Fn()>>>,
62 next_callback_id: Cell<u64>,
63}
64
65impl OverscrollEffect {
66 fn new() -> Self {
67 Self {
68 inner: Rc::new(OverscrollEffectInner {
69 offset: Cell::new(0.0),
70 limit: Cell::new(160.0),
71 invalidate_callbacks: RefCell::new(HashMap::new()),
72 next_callback_id: Cell::new(1),
73 }),
74 }
75 }
76
77 pub(crate) fn offset(&self) -> f32 {
78 self.inner.offset.get()
79 }
80
81 pub(crate) fn set_limit(&self, limit: f32) {
82 if !limit.is_finite() || limit <= 0.0 {
83 return;
84 }
85 self.inner.limit.set(limit);
86 self.set_offset(self.offset().clamp(-limit, limit));
87 }
88
89 pub(crate) fn apply_drag_delta(&self, delta: f32) -> f32 {
90 if !delta.is_finite() || delta.abs() <= f32::EPSILON {
91 return 0.0;
92 }
93 let limit = self.inner.limit.get();
94 let offset = self.offset();
95 let resistance = (1.0 - offset.abs() / limit).clamp(0.12, 1.0) * 0.5;
96 let next = (offset + delta * resistance).clamp(-limit, limit);
97 let applied = next - offset;
98 self.set_offset(next);
99 applied
100 }
101
102 pub(crate) fn apply_settle_delta(&self, delta: f32) -> f32 {
103 let offset = self.offset();
104 if offset.abs() <= f32::EPSILON {
105 return 0.0;
106 }
107 let proposed = offset + delta;
108 let crosses_edge = offset.abs() > f32::EPSILON && proposed.signum() != offset.signum();
109 let next = if crosses_edge {
110 0.0
111 } else {
112 proposed.clamp(-self.inner.limit.get(), self.inner.limit.get())
113 };
114 let applied = next - offset;
115 self.set_offset(next);
116 applied
117 }
118
119 pub(crate) fn apply_to_scroll<F>(&self, delta: f32, perform_scroll: F) -> f32
120 where
121 F: FnOnce(f32) -> f32,
122 {
123 if !delta.is_finite() || delta.abs() <= f32::EPSILON {
124 return 0.0;
125 }
126 let mut remaining = delta;
127 let mut consumed = 0.0;
128 let offset = self.offset();
129 if offset.abs() > f32::EPSILON && delta.signum() != offset.signum() {
130 let release = delta.abs().min(offset.abs()) * delta.signum();
131 let released = self.apply_settle_delta(release);
132 consumed += released;
133 remaining -= released;
134 }
135 if remaining.abs() > f32::EPSILON {
136 let target_consumed = perform_scroll(remaining);
137 consumed += target_consumed;
138 let unconsumed = remaining - target_consumed;
139 if unconsumed.abs() > f32::EPSILON {
140 consumed += self.apply_drag_delta(unconsumed);
141 }
142 }
143 consumed
144 }
145
146 pub(crate) fn apply_to_fling<F>(&self, delta: f32, perform_scroll: F) -> f32
147 where
148 F: FnOnce(f32) -> f32,
149 {
150 let consumed = perform_scroll(delta);
151 let unconsumed = delta - consumed;
152 if unconsumed.abs() > f32::EPSILON {
153 self.apply_drag_delta(-unconsumed);
154 }
155 consumed
156 }
157
158 pub(crate) fn add_invalidate_callback(&self, callback: Box<dyn Fn()>) -> u64 {
159 let id = self.inner.next_callback_id.get();
160 self.inner.next_callback_id.set(id.saturating_add(1));
161 self.inner
162 .invalidate_callbacks
163 .borrow_mut()
164 .insert(id, Rc::from(callback));
165 id
166 }
167
168 pub(crate) fn remove_invalidate_callback(&self, id: u64) {
169 self.inner.invalidate_callbacks.borrow_mut().remove(&id);
170 }
171
172 pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
173 Rc::ptr_eq(&self.inner, &other.inner)
174 }
175
176 fn set_offset(&self, offset: f32) {
177 if (offset - self.offset()).abs() <= f32::EPSILON {
178 return;
179 }
180 self.inner.offset.set(offset);
181 let callbacks = self
182 .inner
183 .invalidate_callbacks
184 .borrow()
185 .values()
186 .cloned()
187 .collect::<Vec<_>>();
188 for callback in callbacks {
189 callback();
190 }
191 }
192}
193
194impl PartialEq for ScrollState {
195 fn eq(&self, other: &Self) -> bool {
198 self.inner == other.inner
199 }
200}
201
202impl ScrollState {
203 pub fn new(initial: f32) -> Self {
205 let runtime = cranpose_core::current_runtime_handle()
206 .expect("ScrollState::new requires an active runtime");
207 Self {
208 value: MutableState::with_runtime(initial, runtime.clone()),
209 inner: MutableState::with_runtime(
210 Rc::new(ScrollStateInner {
211 max_value: RefCell::new(0.0),
212 invalidate_callbacks: RefCell::new(HashMap::new()),
213 next_invalidate_callback_id: Cell::new(1),
214 pending_invalidation: Cell::new(false),
215 settle_policy: RefCell::new(None),
216 }),
217 runtime,
218 ),
219 }
220 }
221
222 fn inner(&self) -> Rc<ScrollStateInner> {
223 self.inner.get_non_reactive()
224 }
225
226 pub fn set_settle_policy(&self, policy: Option<ScrollSettlePolicy>) {
228 *self.inner().settle_policy.borrow_mut() = policy;
229 }
230
231 pub fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
233 self.inner().settle_policy.borrow().clone()
234 }
235
236 pub fn id(&self) -> u64 {
238 let mut hasher = DefaultHasher::new();
239 self.inner.runtime_state_id().hash(&mut hasher);
240 hasher.finish()
241 }
242
243 pub fn value(&self) -> f32 {
248 self.value.value()
249 }
250
251 pub fn value_non_reactive(&self) -> f32 {
256 self.value.get_non_reactive()
257 }
258
259 pub fn max_value(&self) -> f32 {
261 *self.inner().max_value.borrow()
262 }
263
264 pub fn dispatch_raw_delta(&self, delta: f32) -> f32 {
267 let current = self.value_non_reactive();
268 let max = self.max_value();
269 let new_value = (current + delta).clamp(0.0, max);
270 let actual_delta = new_value - current;
271
272 if actual_delta.abs() > 0.001 {
273 self.value.set(new_value);
275
276 self.invalidate();
277 }
278
279 actual_delta
280 }
281
282 pub(crate) fn set_max_value(&self, max: f32) {
284 *self.inner().max_value.borrow_mut() = max;
285 }
286
287 pub fn scroll_to(&self, position: f32) {
289 let max = self.max_value();
290 let clamped = position.clamp(0.0, max);
291
292 self.value.set(clamped);
293
294 self.invalidate();
295 }
296
297 pub(crate) fn add_invalidate_callback(&self, callback: Box<dyn Fn()>) -> u64 {
299 let inner = self.inner();
300 let id = inner.next_invalidate_callback_id.get();
301 inner.next_invalidate_callback_id.set(id.saturating_add(1));
302 let callback: Rc<dyn Fn()> = Rc::from(callback);
303 inner
304 .invalidate_callbacks
305 .borrow_mut()
306 .insert(id, Rc::clone(&callback));
307 if inner.pending_invalidation.replace(false) {
308 callback();
309 }
310 id
311 }
312
313 pub(crate) fn remove_invalidate_callback(&self, id: u64) {
315 self.inner().invalidate_callbacks.borrow_mut().remove(&id);
316 }
317
318 fn invalidate(&self) {
319 let inner = self.inner();
320 let callbacks: Vec<Rc<dyn Fn()>> = {
321 let callbacks = inner.invalidate_callbacks.borrow();
322 if callbacks.is_empty() {
323 inner.pending_invalidation.set(true);
324 return;
325 }
326 callbacks.values().cloned().collect()
327 };
328 for callback in callbacks {
329 callback();
330 }
331 }
332}
333
334#[derive(Clone)]
335pub(crate) struct ScrollMotionContext {
336 inner: Rc<ScrollMotionContextInner>,
337}
338
339#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
340pub(crate) enum ScrollMotionContextKey {
341 ScrollState {
342 state_id: u64,
343 is_vertical: bool,
344 reverse_scrolling: bool,
345 },
346 LazyList {
347 state_identity: usize,
348 is_vertical: bool,
349 reverse_scrolling: bool,
350 },
351}
352
353struct ScrollMotionContextInner {
354 active: Cell<bool>,
355 transient_active: Cell<bool>,
356 generation: Cell<u64>,
357 invalidate_callbacks: RefCell<HashMap<u64, Rc<dyn Fn()>>>,
358 next_invalidate_callback_id: Cell<u64>,
359 pending_invalidation: Cell<bool>,
360 overscroll: OverscrollEffect,
361}
362
363pub(crate) struct ScrollMotionContextStore {
364 contexts: RefCell<HashMap<ScrollMotionContextKey, Weak<ScrollMotionContextInner>>>,
365}
366
367impl ScrollMotionContextStore {
368 pub(crate) fn new() -> Self {
369 Self {
370 contexts: RefCell::new(HashMap::new()),
371 }
372 }
373
374 fn context_for_key(&self, key: ScrollMotionContextKey) -> ScrollMotionContext {
375 let mut contexts = self.contexts.borrow_mut();
376 if let Some(inner) = contexts.get(&key).and_then(Weak::upgrade) {
377 return ScrollMotionContext { inner };
378 }
379
380 let context = ScrollMotionContext::new();
381 contexts.insert(key, Rc::downgrade(&context.inner));
382 contexts.retain(|_, weak| weak.strong_count() > 0);
383 context
384 }
385
386 pub(crate) fn clear_transient_after_frame(&self) {
387 let contexts = {
388 let mut contexts = self.contexts.borrow_mut();
389 let live = contexts
390 .values()
391 .filter_map(Weak::upgrade)
392 .collect::<Vec<_>>();
393 contexts.retain(|_, weak| weak.strong_count() > 0);
394 live
395 };
396 for inner in contexts {
397 ScrollMotionContext { inner }.clear_transient_after_frame();
398 }
399 }
400}
401
402pub(crate) fn scroll_motion_context_for_key(key: ScrollMotionContextKey) -> ScrollMotionContext {
403 crate::render_state::with_scroll_motion_context_store(|store| store.context_for_key(key))
404}
405
406impl ScrollMotionContext {
407 pub(crate) fn new() -> Self {
408 Self {
409 inner: Rc::new(ScrollMotionContextInner {
410 active: Cell::new(false),
411 transient_active: Cell::new(false),
412 generation: Cell::new(0),
413 invalidate_callbacks: RefCell::new(HashMap::new()),
414 next_invalidate_callback_id: Cell::new(1),
415 pending_invalidation: Cell::new(false),
416 overscroll: OverscrollEffect::new(),
417 }),
418 }
419 }
420
421 pub(crate) fn is_active(&self) -> bool {
422 self.inner.active.get() || self.inner.transient_active.get()
423 }
424
425 pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
426 Rc::ptr_eq(&self.inner, &other.inner)
427 }
428
429 pub(crate) fn stable_key(&self) -> usize {
430 Rc::as_ptr(&self.inner) as usize
431 }
432
433 pub(crate) fn overscroll(&self) -> OverscrollEffect {
434 self.inner.overscroll.clone()
435 }
436
437 pub(crate) fn set_active(&self, active: bool) {
438 let was_active = self.is_active();
439 self.inner.active.set(active);
440 if !active {
441 self.inner.transient_active.set(false);
442 }
443 if was_active != self.is_active() {
444 self.bump_generation();
445 self.invalidate();
446 }
447 }
448
449 pub(crate) fn activate_for_current_frame(&self) {
450 let was_active = self.is_active();
451 self.inner.transient_active.set(true);
452 self.bump_generation();
453 if !was_active {
454 self.invalidate();
455 }
456 }
457
458 pub(crate) fn add_invalidate_callback(&self, callback: Box<dyn Fn()>) -> u64 {
459 let id = self.inner.next_invalidate_callback_id.get();
460 self.inner
461 .next_invalidate_callback_id
462 .set(id.saturating_add(1));
463 let callback: Rc<dyn Fn()> = Rc::from(callback);
464 self.inner
465 .invalidate_callbacks
466 .borrow_mut()
467 .insert(id, Rc::clone(&callback));
468 if self.inner.pending_invalidation.replace(false) {
469 callback();
470 }
471 id
472 }
473
474 pub(crate) fn remove_invalidate_callback(&self, id: u64) {
475 self.inner.invalidate_callbacks.borrow_mut().remove(&id);
476 }
477
478 fn bump_generation(&self) -> u64 {
479 let next = self.inner.generation.get().wrapping_add(1);
480 self.inner.generation.set(next);
481 next
482 }
483
484 fn clear_transient_after_frame(&self) {
485 let was_active = self.is_active();
486 if self.inner.transient_active.replace(false) {
487 self.bump_generation();
488 if was_active != self.is_active() {
489 self.invalidate();
490 }
491 }
492 }
493
494 fn invalidate(&self) {
495 let callbacks: Vec<Rc<dyn Fn()>> = {
496 let callbacks = self.inner.invalidate_callbacks.borrow();
497 if callbacks.is_empty() {
498 self.inner.pending_invalidation.set(true);
499 return;
500 }
501 callbacks.values().cloned().collect()
502 };
503 for callback in callbacks {
504 callback();
505 }
506 }
507}
508
509#[derive(Clone)]
511pub struct ScrollElement {
512 state: ScrollState,
513 overscroll: OverscrollEffect,
514 is_vertical: bool,
515 reverse_scrolling: bool,
516}
517
518impl ScrollElement {
519 pub(crate) fn new(
520 state: ScrollState,
521 overscroll: OverscrollEffect,
522 is_vertical: bool,
523 reverse_scrolling: bool,
524 ) -> Self {
525 Self {
526 state,
527 overscroll,
528 is_vertical,
529 reverse_scrolling,
530 }
531 }
532}
533
534impl std::fmt::Debug for ScrollElement {
535 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
536 f.debug_struct("ScrollElement")
537 .field("is_vertical", &self.is_vertical)
538 .field("reverse_scrolling", &self.reverse_scrolling)
539 .finish()
540 }
541}
542
543impl PartialEq for ScrollElement {
544 fn eq(&self, other: &Self) -> bool {
545 self.state == other.state
547 && self.is_vertical == other.is_vertical
548 && self.reverse_scrolling == other.reverse_scrolling
549 }
550}
551
552impl Eq for ScrollElement {}
553
554impl Hash for ScrollElement {
555 fn hash<H: Hasher>(&self, state: &mut H) {
556 self.state.inner.runtime_state_id().hash(state);
557 self.is_vertical.hash(state);
558 self.reverse_scrolling.hash(state);
559 }
560}
561
562impl ModifierNodeElement for ScrollElement {
563 type Node = ScrollNode;
564
565 fn create(&self) -> Self::Node {
566 ScrollNode::new(
568 self.state,
569 self.overscroll.clone(),
570 self.is_vertical,
571 self.reverse_scrolling,
572 )
573 }
574
575 fn key(&self) -> Option<u64> {
576 let mut hasher = DefaultHasher::new();
577 self.state.id().hash(&mut hasher);
578 self.reverse_scrolling.hash(&mut hasher);
579 self.is_vertical.hash(&mut hasher);
580 Some(hasher.finish())
581 }
582
583 fn update(&self, node: &mut Self::Node) {
584 let needs_invalidation = node.state != self.state
585 || node.is_vertical != self.is_vertical
586 || node.reverse_scrolling != self.reverse_scrolling
587 || !node.overscroll.ptr_eq(&self.overscroll);
588
589 if needs_invalidation {
590 node.state = self.state;
591 node.is_vertical = self.is_vertical;
592 node.reverse_scrolling = self.reverse_scrolling;
593 node.overscroll = self.overscroll.clone();
594 }
595 }
596
597 fn capabilities(&self) -> NodeCapabilities {
598 NodeCapabilities::LAYOUT
599 }
600}
601
602pub struct ScrollNode {
605 state: ScrollState,
606 overscroll: OverscrollEffect,
607 is_vertical: bool,
608 reverse_scrolling: bool,
609 node_state: NodeState,
610 invalidation_callback_id: Option<u64>,
612 overscroll_callback_id: Option<u64>,
613 node_id: Option<NodeId>,
615}
616
617impl ScrollNode {
618 pub(crate) fn new(
619 state: ScrollState,
620 overscroll: OverscrollEffect,
621 is_vertical: bool,
622 reverse_scrolling: bool,
623 ) -> Self {
624 Self {
625 state,
626 overscroll,
627 is_vertical,
628 reverse_scrolling,
629 node_state: NodeState::default(),
630 invalidation_callback_id: None,
631 overscroll_callback_id: None,
632 node_id: None,
633 }
634 }
635
636 pub fn state(&self) -> &ScrollState {
638 &self.state
639 }
640}
641
642impl DelegatableNode for ScrollNode {
643 fn node_state(&self) -> &NodeState {
644 &self.node_state
645 }
646}
647
648impl ModifierNode for ScrollNode {
649 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
650 let node_id = context.node_id();
654 self.node_id = node_id;
655
656 if let Some(node_id) = node_id {
657 let callback_id = self.state.add_invalidate_callback(Box::new(move || {
658 crate::schedule_layout_repass(node_id);
660 }));
661 self.invalidation_callback_id = Some(callback_id);
662 let callback_id = self.overscroll.add_invalidate_callback(Box::new(move || {
663 crate::schedule_layout_repass(node_id);
664 }));
665 self.overscroll_callback_id = Some(callback_id);
666 } else {
667 log::debug!(
668 "ScrollNode attached without a NodeId; deferring invalidation registration."
669 );
670 }
671
672 context.invalidate(cranpose_foundation::InvalidationKind::Layout);
674 }
675
676 fn on_detach(&mut self) {
677 if let Some(id) = self.invalidation_callback_id.take() {
679 self.state.remove_invalidate_callback(id);
680 }
681 if let Some(id) = self.overscroll_callback_id.take() {
682 self.overscroll.remove_invalidate_callback(id);
683 }
684 }
685
686 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
687 Some(self)
688 }
689
690 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
691 Some(self)
692 }
693}
694
695impl LayoutModifierNode for ScrollNode {
696 fn measure(
697 &self,
698 _context: &mut dyn ModifierNodeContext,
699 measurable: &dyn Measurable,
700 constraints: Constraints,
701 ) -> LayoutModifierMeasureResult {
702 let scroll_constraints = if self.is_vertical {
704 Constraints {
705 min_height: 0.0,
706 max_height: f32::INFINITY,
707 ..constraints
708 }
709 } else {
710 Constraints {
711 min_width: 0.0,
712 max_width: f32::INFINITY,
713 ..constraints
714 }
715 };
716
717 let placeable = measurable.measure(scroll_constraints);
719
720 let width = placeable.width().min(constraints.max_width);
722 let height = placeable.height().min(constraints.max_height);
723
724 let max_scroll = if self.is_vertical {
726 (placeable.height() - height).max(0.0)
727 } else {
728 (placeable.width() - width).max(0.0)
729 };
730
731 if (self.is_vertical && constraints.max_height.is_finite())
734 || (!self.is_vertical && constraints.max_width.is_finite())
735 {
736 self.state.set_max_value(max_scroll);
737 self.overscroll
738 .set_limit((if self.is_vertical { height } else { width }) * 0.5);
739 }
740
741 let scroll = self.state.value_non_reactive().clamp(0.0, max_scroll);
744
745 let abs_scroll = if self.reverse_scrolling {
746 scroll - max_scroll
747 } else {
748 -scroll
749 };
750 let abs_scroll = abs_scroll + self.overscroll.offset();
751
752 let (x_offset, y_offset) = if self.is_vertical {
753 (0.0, abs_scroll)
754 } else {
755 (abs_scroll, 0.0)
756 };
757
758 LayoutModifierMeasureResult::new(Size { width, height }, x_offset, y_offset)
762 }
763
764 fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
765 measurable.min_intrinsic_width(height)
766 }
767
768 fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
769 measurable.max_intrinsic_width(height)
770 }
771
772 fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
773 measurable.min_intrinsic_height(width)
774 }
775
776 fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
777 measurable.max_intrinsic_height(width)
778 }
779}
780
781#[macro_export]
785macro_rules! rememberScrollState {
786 ($initial:expr) => {
787 cranpose_core::remember(|| $crate::scroll::ScrollState::new($initial))
788 .with(|state| state.clone())
789 };
790 () => {
791 rememberScrollState!(0.0)
792 };
793}
794
795#[cfg(test)]
796#[path = "tests/scroll_tests.rs"]
797mod tests;