1use cranpose_core::{mutableStateOf, 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::hash::{DefaultHasher, Hash, Hasher};
20use std::rc::Rc;
21use std::sync::atomic::{AtomicU64, Ordering};
22
23static NEXT_SCROLL_STATE_ID: AtomicU64 = AtomicU64::new(1);
24
25#[derive(Clone)]
33pub struct ScrollState {
34 inner: Rc<ScrollStateInner>,
35}
36
37pub(crate) struct ScrollStateInner {
38 id: u64,
40 value: MutableState<f32>,
44 max_value: RefCell<f32>,
47 invalidate_callbacks: RefCell<std::collections::HashMap<u64, Box<dyn Fn()>>>,
50 pending_invalidation: Cell<bool>,
52}
53
54impl ScrollState {
55 pub fn new(initial: f32) -> Self {
57 let id = NEXT_SCROLL_STATE_ID.fetch_add(1, Ordering::Relaxed);
58
59 Self {
60 inner: Rc::new(ScrollStateInner {
61 id,
62 value: mutableStateOf(initial),
63 max_value: RefCell::new(0.0),
64 invalidate_callbacks: RefCell::new(std::collections::HashMap::new()),
65 pending_invalidation: Cell::new(false),
66 }),
67 }
68 }
69
70 pub fn id(&self) -> u64 {
72 self.inner.id
73 }
74
75 pub fn value(&self) -> f32 {
80 self.inner.value.with(|v| *v)
81 }
82
83 pub fn value_non_reactive(&self) -> f32 {
88 self.inner.value.get_non_reactive()
89 }
90
91 pub fn max_value(&self) -> f32 {
93 *self.inner.max_value.borrow()
94 }
95
96 pub fn dispatch_raw_delta(&self, delta: f32) -> f32 {
99 let current = self.value();
100 let max = self.max_value();
101 let new_value = (current + delta).clamp(0.0, max);
102 let actual_delta = new_value - current;
103
104 if actual_delta.abs() > 0.001 {
105 self.inner.value.set(new_value);
107
108 let callbacks = self.inner.invalidate_callbacks.borrow();
110 if callbacks.is_empty() {
111 self.inner.pending_invalidation.set(true);
113 } else {
114 for callback in callbacks.values() {
115 callback();
116 }
117 }
118 }
119
120 actual_delta
121 }
122
123 pub(crate) fn set_max_value(&self, max: f32) {
125 *self.inner.max_value.borrow_mut() = max;
126 }
127
128 pub fn scroll_to(&self, position: f32) {
130 let max = self.max_value();
131 let clamped = position.clamp(0.0, max);
132
133 self.inner.value.set(clamped);
134
135 let callbacks = self.inner.invalidate_callbacks.borrow();
137 if callbacks.is_empty() {
138 self.inner.pending_invalidation.set(true);
139 } else {
140 for callback in callbacks.values() {
141 callback();
142 }
143 }
144 }
145
146 pub(crate) fn add_invalidate_callback(&self, callback: Box<dyn Fn()>) -> u64 {
148 static NEXT_CALLBACK_ID: std::sync::atomic::AtomicU64 =
149 std::sync::atomic::AtomicU64::new(1);
150 let id = NEXT_CALLBACK_ID.fetch_add(1, Ordering::Relaxed);
151 self.inner
152 .invalidate_callbacks
153 .borrow_mut()
154 .insert(id, callback);
155 if self.inner.pending_invalidation.replace(false) {
156 if let Some(callback) = self.inner.invalidate_callbacks.borrow().get(&id) {
157 callback();
158 }
159 }
160 id
161 }
162
163 pub(crate) fn remove_invalidate_callback(&self, id: u64) {
165 self.inner.invalidate_callbacks.borrow_mut().remove(&id);
166 }
167}
168
169#[derive(Clone)]
171pub struct ScrollElement {
172 state: ScrollState,
173 is_vertical: bool,
174 reverse_scrolling: bool,
175}
176
177impl ScrollElement {
178 pub fn new(state: ScrollState, is_vertical: bool, reverse_scrolling: bool) -> Self {
179 Self {
180 state,
181 is_vertical,
182 reverse_scrolling,
183 }
184 }
185}
186
187impl std::fmt::Debug for ScrollElement {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 f.debug_struct("ScrollElement")
190 .field("is_vertical", &self.is_vertical)
191 .field("reverse_scrolling", &self.reverse_scrolling)
192 .finish()
193 }
194}
195
196impl PartialEq for ScrollElement {
197 fn eq(&self, other: &Self) -> bool {
198 Rc::ptr_eq(&self.state.inner, &other.state.inner)
200 && self.is_vertical == other.is_vertical
201 && self.reverse_scrolling == other.reverse_scrolling
202 }
203}
204
205impl Eq for ScrollElement {}
206
207impl Hash for ScrollElement {
208 fn hash<H: Hasher>(&self, state: &mut H) {
209 (Rc::as_ptr(&self.state.inner) as usize).hash(state);
210 self.is_vertical.hash(state);
211 self.reverse_scrolling.hash(state);
212 }
213}
214
215impl ModifierNodeElement for ScrollElement {
216 type Node = ScrollNode;
217
218 fn create(&self) -> Self::Node {
219 ScrollNode::new(self.state.clone(), self.is_vertical, self.reverse_scrolling)
221 }
222
223 fn key(&self) -> Option<u64> {
224 let mut hasher = DefaultHasher::new();
225 self.state.id().hash(&mut hasher);
226 self.reverse_scrolling.hash(&mut hasher);
227 self.is_vertical.hash(&mut hasher);
228 Some(hasher.finish())
229 }
230
231 fn update(&self, node: &mut Self::Node) {
232 let needs_invalidation = !Rc::ptr_eq(&node.state.inner, &self.state.inner)
233 || node.is_vertical != self.is_vertical
234 || node.reverse_scrolling != self.reverse_scrolling;
235
236 if needs_invalidation {
237 node.state = self.state.clone();
238 node.is_vertical = self.is_vertical;
239 node.reverse_scrolling = self.reverse_scrolling;
240 }
241 }
242
243 fn capabilities(&self) -> NodeCapabilities {
244 NodeCapabilities::LAYOUT
245 }
246}
247
248pub struct ScrollNode {
251 state: ScrollState,
252 is_vertical: bool,
253 reverse_scrolling: bool,
254 node_state: NodeState,
255 invalidation_callback_id: Option<u64>,
257 node_id: Option<NodeId>,
259}
260
261impl ScrollNode {
262 pub fn new(state: ScrollState, is_vertical: bool, reverse_scrolling: bool) -> Self {
263 Self {
264 state,
265 is_vertical,
266 reverse_scrolling,
267 node_state: NodeState::default(),
268 invalidation_callback_id: None,
269 node_id: None,
270 }
271 }
272
273 pub fn state(&self) -> &ScrollState {
275 &self.state
276 }
277}
278
279impl DelegatableNode for ScrollNode {
280 fn node_state(&self) -> &NodeState {
281 &self.node_state
282 }
283}
284
285impl ModifierNode for ScrollNode {
286 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
287 let node_id = context.node_id();
291 self.node_id = node_id;
292
293 if let Some(node_id) = node_id {
294 let callback_id = self.state.add_invalidate_callback(Box::new(move || {
295 crate::schedule_layout_repass(node_id);
297 }));
298 self.invalidation_callback_id = Some(callback_id);
299 } else {
300 log::debug!(
301 "ScrollNode attached without a NodeId; deferring invalidation registration."
302 );
303 }
304
305 context.invalidate(cranpose_foundation::InvalidationKind::Layout);
307 }
308
309 fn on_detach(&mut self) {
310 if let Some(id) = self.invalidation_callback_id.take() {
312 self.state.remove_invalidate_callback(id);
313 }
314 }
315
316 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
317 Some(self)
318 }
319
320 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
321 Some(self)
322 }
323}
324
325impl LayoutModifierNode for ScrollNode {
326 fn measure(
327 &self,
328 _context: &mut dyn ModifierNodeContext,
329 measurable: &dyn Measurable,
330 constraints: Constraints,
331 ) -> LayoutModifierMeasureResult {
332 let scroll_constraints = if self.is_vertical {
334 Constraints {
335 min_height: 0.0,
336 max_height: f32::INFINITY,
337 ..constraints
338 }
339 } else {
340 Constraints {
341 min_width: 0.0,
342 max_width: f32::INFINITY,
343 ..constraints
344 }
345 };
346
347 let placeable = measurable.measure(scroll_constraints);
349
350 let width = placeable.width().min(constraints.max_width);
352 let height = placeable.height().min(constraints.max_height);
353
354 let max_scroll = if self.is_vertical {
356 (placeable.height() - height).max(0.0)
357 } else {
358 (placeable.width() - width).max(0.0)
359 };
360
361 if (self.is_vertical && constraints.max_height.is_finite())
364 || (!self.is_vertical && constraints.max_width.is_finite())
365 {
366 self.state.set_max_value(max_scroll);
367 }
368
369 let scroll = self.state.value_non_reactive().clamp(0.0, max_scroll);
372
373 let abs_scroll = if self.reverse_scrolling {
374 scroll - max_scroll
375 } else {
376 -scroll
377 };
378
379 let (x_offset, y_offset) = if self.is_vertical {
380 (0.0, abs_scroll)
381 } else {
382 (abs_scroll, 0.0)
383 };
384
385 LayoutModifierMeasureResult::new(Size { width, height }, x_offset, y_offset)
389 }
390
391 fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
392 measurable.min_intrinsic_width(height)
393 }
394
395 fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
396 measurable.max_intrinsic_width(height)
397 }
398
399 fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
400 measurable.min_intrinsic_height(width)
401 }
402
403 fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
404 measurable.max_intrinsic_height(width)
405 }
406
407 fn create_measurement_proxy(&self) -> Option<Box<dyn cranpose_foundation::MeasurementProxy>> {
408 None
409 }
410}
411
412#[macro_export]
416macro_rules! rememberScrollState {
417 ($initial:expr) => {
418 cranpose_core::remember(|| $crate::scroll::ScrollState::new($initial))
419 .with(|state| state.clone())
420 };
421 () => {
422 rememberScrollState!(0.0)
423 };
424}