1use cranpose_core::{ownedMutableStateOf, NodeId, OwnedMutableState};
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)]
31pub struct ScrollState {
32 inner: Rc<ScrollStateInner>,
33}
34
35pub(crate) struct ScrollStateInner {
36 value: OwnedMutableState<f32>,
40 max_value: RefCell<f32>,
43 invalidate_callbacks: RefCell<HashMap<u64, Rc<dyn Fn()>>>,
46 next_invalidate_callback_id: Cell<u64>,
47 pending_invalidation: Cell<bool>,
49 settle_policy: RefCell<Option<ScrollSettlePolicy>>,
52}
53
54pub type ScrollSettlePolicy = Rc<dyn Fn(f32, f32) -> f32>;
62
63impl PartialEq for ScrollState {
64 fn eq(&self, other: &Self) -> bool {
67 Rc::ptr_eq(&self.inner, &other.inner)
68 }
69}
70
71impl ScrollState {
72 pub fn new(initial: f32) -> Self {
74 Self {
75 inner: Rc::new(ScrollStateInner {
76 value: ownedMutableStateOf(initial),
77 max_value: RefCell::new(0.0),
78 invalidate_callbacks: RefCell::new(HashMap::new()),
79 next_invalidate_callback_id: Cell::new(1),
80 pending_invalidation: Cell::new(false),
81 settle_policy: RefCell::new(None),
82 }),
83 }
84 }
85
86 pub fn set_settle_policy(&self, policy: Option<ScrollSettlePolicy>) {
88 *self.inner.settle_policy.borrow_mut() = policy;
89 }
90
91 pub fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
93 self.inner.settle_policy.borrow().clone()
94 }
95
96 pub fn id(&self) -> u64 {
98 Rc::as_ptr(&self.inner) as usize as u64
99 }
100
101 pub fn value(&self) -> f32 {
106 self.inner.value.with(|v| *v)
107 }
108
109 pub fn value_non_reactive(&self) -> f32 {
114 self.inner.value.get_non_reactive()
115 }
116
117 pub fn max_value(&self) -> f32 {
119 *self.inner.max_value.borrow()
120 }
121
122 pub fn dispatch_raw_delta(&self, delta: f32) -> f32 {
125 let current = self.value();
126 let max = self.max_value();
127 let new_value = (current + delta).clamp(0.0, max);
128 let actual_delta = new_value - current;
129
130 if actual_delta.abs() > 0.001 {
131 self.inner.value.set(new_value);
133
134 self.invalidate();
135 }
136
137 actual_delta
138 }
139
140 pub(crate) fn set_max_value(&self, max: f32) {
142 *self.inner.max_value.borrow_mut() = max;
143 }
144
145 pub fn scroll_to(&self, position: f32) {
147 let max = self.max_value();
148 let clamped = position.clamp(0.0, max);
149
150 self.inner.value.set(clamped);
151
152 self.invalidate();
153 }
154
155 pub(crate) fn add_invalidate_callback(&self, callback: Box<dyn Fn()>) -> u64 {
157 let id = self.inner.next_invalidate_callback_id.get();
158 self.inner
159 .next_invalidate_callback_id
160 .set(id.saturating_add(1));
161 let callback: Rc<dyn Fn()> = Rc::from(callback);
162 self.inner
163 .invalidate_callbacks
164 .borrow_mut()
165 .insert(id, Rc::clone(&callback));
166 if self.inner.pending_invalidation.replace(false) {
167 callback();
168 }
169 id
170 }
171
172 pub(crate) fn remove_invalidate_callback(&self, id: u64) {
174 self.inner.invalidate_callbacks.borrow_mut().remove(&id);
175 }
176
177 fn invalidate(&self) {
178 let callbacks: Vec<Rc<dyn Fn()>> = {
179 let callbacks = self.inner.invalidate_callbacks.borrow();
180 if callbacks.is_empty() {
181 self.inner.pending_invalidation.set(true);
182 return;
183 }
184 callbacks.values().cloned().collect()
185 };
186 for callback in callbacks {
187 callback();
188 }
189 }
190}
191
192#[derive(Clone)]
193pub(crate) struct ScrollMotionContext {
194 inner: Rc<ScrollMotionContextInner>,
195}
196
197#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
198pub(crate) enum ScrollMotionContextKey {
199 ScrollState {
200 state_id: u64,
201 is_vertical: bool,
202 reverse_scrolling: bool,
203 },
204 LazyList {
205 state_identity: usize,
206 is_vertical: bool,
207 reverse_scrolling: bool,
208 },
209}
210
211struct ScrollMotionContextInner {
212 active: Cell<bool>,
213 transient_active: Cell<bool>,
214 generation: Cell<u64>,
215 invalidate_callbacks: RefCell<HashMap<u64, Rc<dyn Fn()>>>,
216 next_invalidate_callback_id: Cell<u64>,
217 pending_invalidation: Cell<bool>,
218}
219
220pub(crate) struct ScrollMotionContextStore {
221 contexts: RefCell<HashMap<ScrollMotionContextKey, Weak<ScrollMotionContextInner>>>,
222}
223
224impl ScrollMotionContextStore {
225 pub(crate) fn new() -> Self {
226 Self {
227 contexts: RefCell::new(HashMap::new()),
228 }
229 }
230
231 fn context_for_key(&self, key: ScrollMotionContextKey) -> ScrollMotionContext {
232 let mut contexts = self.contexts.borrow_mut();
233 if let Some(inner) = contexts.get(&key).and_then(Weak::upgrade) {
234 return ScrollMotionContext { inner };
235 }
236
237 let context = ScrollMotionContext::new();
238 contexts.insert(key, Rc::downgrade(&context.inner));
239 contexts.retain(|_, weak| weak.strong_count() > 0);
240 context
241 }
242
243 pub(crate) fn clear_transient_after_frame(&self) {
244 let contexts = {
245 let mut contexts = self.contexts.borrow_mut();
246 let live = contexts
247 .values()
248 .filter_map(Weak::upgrade)
249 .collect::<Vec<_>>();
250 contexts.retain(|_, weak| weak.strong_count() > 0);
251 live
252 };
253 for inner in contexts {
254 ScrollMotionContext { inner }.clear_transient_after_frame();
255 }
256 }
257}
258
259pub(crate) fn scroll_motion_context_for_key(key: ScrollMotionContextKey) -> ScrollMotionContext {
260 crate::render_state::with_scroll_motion_context_store(|store| store.context_for_key(key))
261}
262
263impl ScrollMotionContext {
264 pub(crate) fn new() -> Self {
265 Self {
266 inner: Rc::new(ScrollMotionContextInner {
267 active: Cell::new(false),
268 transient_active: Cell::new(false),
269 generation: Cell::new(0),
270 invalidate_callbacks: RefCell::new(HashMap::new()),
271 next_invalidate_callback_id: Cell::new(1),
272 pending_invalidation: Cell::new(false),
273 }),
274 }
275 }
276
277 pub(crate) fn is_active(&self) -> bool {
278 self.inner.active.get() || self.inner.transient_active.get()
279 }
280
281 pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
282 Rc::ptr_eq(&self.inner, &other.inner)
283 }
284
285 pub(crate) fn stable_key(&self) -> usize {
286 Rc::as_ptr(&self.inner) as usize
287 }
288
289 pub(crate) fn set_active(&self, active: bool) {
290 let was_active = self.is_active();
291 self.inner.active.set(active);
292 if !active {
293 self.inner.transient_active.set(false);
294 }
295 if was_active != self.is_active() {
296 self.bump_generation();
297 self.invalidate();
298 }
299 }
300
301 pub(crate) fn activate_for_current_frame(&self) {
302 let was_active = self.is_active();
303 self.inner.transient_active.set(true);
304 self.bump_generation();
305 if !was_active {
306 self.invalidate();
307 }
308 }
309
310 pub(crate) fn add_invalidate_callback(&self, callback: Box<dyn Fn()>) -> u64 {
311 let id = self.inner.next_invalidate_callback_id.get();
312 self.inner
313 .next_invalidate_callback_id
314 .set(id.saturating_add(1));
315 let callback: Rc<dyn Fn()> = Rc::from(callback);
316 self.inner
317 .invalidate_callbacks
318 .borrow_mut()
319 .insert(id, Rc::clone(&callback));
320 if self.inner.pending_invalidation.replace(false) {
321 callback();
322 }
323 id
324 }
325
326 pub(crate) fn remove_invalidate_callback(&self, id: u64) {
327 self.inner.invalidate_callbacks.borrow_mut().remove(&id);
328 }
329
330 fn bump_generation(&self) -> u64 {
331 let next = self.inner.generation.get().wrapping_add(1);
332 self.inner.generation.set(next);
333 next
334 }
335
336 fn clear_transient_after_frame(&self) {
337 let was_active = self.is_active();
338 if self.inner.transient_active.replace(false) {
339 self.bump_generation();
340 if was_active != self.is_active() {
341 self.invalidate();
342 }
343 }
344 }
345
346 fn invalidate(&self) {
347 let callbacks: Vec<Rc<dyn Fn()>> = {
348 let callbacks = self.inner.invalidate_callbacks.borrow();
349 if callbacks.is_empty() {
350 self.inner.pending_invalidation.set(true);
351 return;
352 }
353 callbacks.values().cloned().collect()
354 };
355 for callback in callbacks {
356 callback();
357 }
358 }
359}
360
361#[derive(Clone)]
363pub struct ScrollElement {
364 state: ScrollState,
365 is_vertical: bool,
366 reverse_scrolling: bool,
367}
368
369impl ScrollElement {
370 pub fn new(state: ScrollState, is_vertical: bool, reverse_scrolling: bool) -> Self {
371 Self {
372 state,
373 is_vertical,
374 reverse_scrolling,
375 }
376 }
377}
378
379impl std::fmt::Debug for ScrollElement {
380 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381 f.debug_struct("ScrollElement")
382 .field("is_vertical", &self.is_vertical)
383 .field("reverse_scrolling", &self.reverse_scrolling)
384 .finish()
385 }
386}
387
388impl PartialEq for ScrollElement {
389 fn eq(&self, other: &Self) -> bool {
390 Rc::ptr_eq(&self.state.inner, &other.state.inner)
392 && self.is_vertical == other.is_vertical
393 && self.reverse_scrolling == other.reverse_scrolling
394 }
395}
396
397impl Eq for ScrollElement {}
398
399impl Hash for ScrollElement {
400 fn hash<H: Hasher>(&self, state: &mut H) {
401 (Rc::as_ptr(&self.state.inner) as usize).hash(state);
402 self.is_vertical.hash(state);
403 self.reverse_scrolling.hash(state);
404 }
405}
406
407impl ModifierNodeElement for ScrollElement {
408 type Node = ScrollNode;
409
410 fn create(&self) -> Self::Node {
411 ScrollNode::new(self.state.clone(), self.is_vertical, self.reverse_scrolling)
413 }
414
415 fn key(&self) -> Option<u64> {
416 let mut hasher = DefaultHasher::new();
417 self.state.id().hash(&mut hasher);
418 self.reverse_scrolling.hash(&mut hasher);
419 self.is_vertical.hash(&mut hasher);
420 Some(hasher.finish())
421 }
422
423 fn update(&self, node: &mut Self::Node) {
424 let needs_invalidation = !Rc::ptr_eq(&node.state.inner, &self.state.inner)
425 || node.is_vertical != self.is_vertical
426 || node.reverse_scrolling != self.reverse_scrolling;
427
428 if needs_invalidation {
429 node.state = self.state.clone();
430 node.is_vertical = self.is_vertical;
431 node.reverse_scrolling = self.reverse_scrolling;
432 }
433 }
434
435 fn capabilities(&self) -> NodeCapabilities {
436 NodeCapabilities::LAYOUT
437 }
438}
439
440pub struct ScrollNode {
443 state: ScrollState,
444 is_vertical: bool,
445 reverse_scrolling: bool,
446 node_state: NodeState,
447 invalidation_callback_id: Option<u64>,
449 node_id: Option<NodeId>,
451}
452
453impl ScrollNode {
454 pub fn new(state: ScrollState, is_vertical: bool, reverse_scrolling: bool) -> Self {
455 Self {
456 state,
457 is_vertical,
458 reverse_scrolling,
459 node_state: NodeState::default(),
460 invalidation_callback_id: None,
461 node_id: None,
462 }
463 }
464
465 pub fn state(&self) -> &ScrollState {
467 &self.state
468 }
469}
470
471impl DelegatableNode for ScrollNode {
472 fn node_state(&self) -> &NodeState {
473 &self.node_state
474 }
475}
476
477impl ModifierNode for ScrollNode {
478 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
479 let node_id = context.node_id();
483 self.node_id = node_id;
484
485 if let Some(node_id) = node_id {
486 let callback_id = self.state.add_invalidate_callback(Box::new(move || {
487 crate::schedule_layout_repass(node_id);
489 }));
490 self.invalidation_callback_id = Some(callback_id);
491 } else {
492 log::debug!(
493 "ScrollNode attached without a NodeId; deferring invalidation registration."
494 );
495 }
496
497 context.invalidate(cranpose_foundation::InvalidationKind::Layout);
499 }
500
501 fn on_detach(&mut self) {
502 if let Some(id) = self.invalidation_callback_id.take() {
504 self.state.remove_invalidate_callback(id);
505 }
506 }
507
508 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
509 Some(self)
510 }
511
512 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
513 Some(self)
514 }
515}
516
517impl LayoutModifierNode for ScrollNode {
518 fn measure(
519 &self,
520 _context: &mut dyn ModifierNodeContext,
521 measurable: &dyn Measurable,
522 constraints: Constraints,
523 ) -> LayoutModifierMeasureResult {
524 let scroll_constraints = if self.is_vertical {
526 Constraints {
527 min_height: 0.0,
528 max_height: f32::INFINITY,
529 ..constraints
530 }
531 } else {
532 Constraints {
533 min_width: 0.0,
534 max_width: f32::INFINITY,
535 ..constraints
536 }
537 };
538
539 let placeable = measurable.measure(scroll_constraints);
541
542 let width = placeable.width().min(constraints.max_width);
544 let height = placeable.height().min(constraints.max_height);
545
546 let max_scroll = if self.is_vertical {
548 (placeable.height() - height).max(0.0)
549 } else {
550 (placeable.width() - width).max(0.0)
551 };
552
553 if (self.is_vertical && constraints.max_height.is_finite())
556 || (!self.is_vertical && constraints.max_width.is_finite())
557 {
558 self.state.set_max_value(max_scroll);
559 }
560
561 let scroll = self.state.value_non_reactive().clamp(0.0, max_scroll);
564
565 let abs_scroll = if self.reverse_scrolling {
566 scroll - max_scroll
567 } else {
568 -scroll
569 };
570
571 let (x_offset, y_offset) = if self.is_vertical {
572 (0.0, abs_scroll)
573 } else {
574 (abs_scroll, 0.0)
575 };
576
577 LayoutModifierMeasureResult::new(Size { width, height }, x_offset, y_offset)
581 }
582
583 fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
584 measurable.min_intrinsic_width(height)
585 }
586
587 fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
588 measurable.max_intrinsic_width(height)
589 }
590
591 fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
592 measurable.min_intrinsic_height(width)
593 }
594
595 fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
596 measurable.max_intrinsic_height(width)
597 }
598}
599
600#[macro_export]
604macro_rules! rememberScrollState {
605 ($initial:expr) => {
606 cranpose_core::remember(|| $crate::scroll::ScrollState::new($initial))
607 .with(|state| state.clone())
608 };
609 () => {
610 rememberScrollState!(0.0)
611 };
612}
613
614#[cfg(test)]
615#[path = "tests/scroll_tests.rs"]
616mod tests;