1use std::{cell::RefCell, cmp::Reverse, collections::BinaryHeap, rc::Rc};
13
14use cranpose_core::{MutableState, NodeId, StateId};
15use cranpose_macros::composable;
16
17use super::{diagnostics, nearest_range::NearestRangeState};
18
19const MAX_PENDING_SCROLL_DELTA: f32 = 2000.0;
20const ITEM_SIZE_CACHE_CAPACITY: usize = 8192;
21
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub(crate) struct LazyListMeasureStateSnapshot {
24 pub(crate) first_visible_item_index: usize,
25 pub(crate) first_visible_item_scroll_offset: f32,
26 pub(crate) pending_scroll_delta: f32,
27 pub(crate) window_scroll_delta: f32,
28 pub(crate) pending_scroll_to: Option<(usize, f32)>,
29 pub(crate) average_item_size: f32,
30}
31
32#[derive(Clone, Debug, Default, PartialEq)]
36pub struct LazyLayoutStats {
37 pub items_in_use: usize,
39
40 pub items_in_pool: usize,
42
43 pub total_composed: usize,
45
46 pub reuse_count: usize,
48}
49
50#[derive(Clone, Copy)]
58pub struct LazyListScrollPosition {
59 index: MutableState<usize>,
60 scroll_offset: MutableState<f32>,
61 inner: MutableState<Rc<RefCell<ScrollPositionInner>>>,
62}
63
64struct ScrollPositionInner {
66 current_index: usize,
67 current_scroll_offset: f32,
68 last_known_first_item_key: Option<u64>,
69 nearest_range_state: NearestRangeState,
70}
71
72impl LazyListScrollPosition {
73 fn is_alive(&self) -> bool {
74 self.index.is_alive() && self.scroll_offset.is_alive() && self.inner.is_alive()
75 }
76
77 fn current_index(&self) -> usize {
78 self.inner
79 .try_with(|rc| rc.borrow().current_index)
80 .unwrap_or(0)
81 }
82
83 fn current_scroll_offset(&self) -> f32 {
84 self.inner
85 .try_with(|rc| rc.borrow().current_scroll_offset)
86 .unwrap_or(0.0)
87 }
88
89 pub fn index(&self) -> usize {
91 if !self.index.is_alive() || !self.inner.is_alive() {
92 return 0;
93 }
94 self.index.subscribe_current_scope_only();
95 self.current_index()
96 }
97
98 pub fn scroll_offset(&self) -> f32 {
100 if !self.scroll_offset.is_alive() || !self.inner.is_alive() {
101 return 0.0;
102 }
103 self.scroll_offset.subscribe_current_scope_only();
104 self.current_scroll_offset()
105 }
106
107 pub(crate) fn update_from_measure_result(
108 &self,
109 first_visible_index: usize,
110 first_visible_scroll_offset: f32,
111 first_visible_item_key: Option<u64>,
112 ) {
113 if !self.is_alive() {
114 return;
115 }
116 self.inner.with(|rc| {
117 let mut inner = rc.borrow_mut();
118 inner.current_index = first_visible_index;
119 inner.current_scroll_offset = first_visible_scroll_offset;
120 inner.last_known_first_item_key = first_visible_item_key;
121 inner.nearest_range_state.update(first_visible_index);
122 });
123
124 if self.index.get_non_reactive() != first_visible_index {
125 self.index.set(first_visible_index);
126 }
127 if (self.scroll_offset.get_non_reactive() - first_visible_scroll_offset).abs() > 0.001 {
128 self.scroll_offset.set(first_visible_scroll_offset);
129 }
130 }
131
132 pub(crate) fn request_position_and_forget_last_known_key(
133 &self,
134 index: usize,
135 scroll_offset: f32,
136 ) {
137 if !self.is_alive() {
138 return;
139 }
140 self.inner.with(|rc| {
141 let mut inner = rc.borrow_mut();
142 inner.current_index = index;
143 inner.current_scroll_offset = scroll_offset;
144 inner.last_known_first_item_key = None;
145 inner.nearest_range_state.update(index);
146 });
147
148 if self.index.get_non_reactive() != index {
149 self.index.set(index);
150 }
151 if (self.scroll_offset.get_non_reactive() - scroll_offset).abs() > 0.001 {
152 self.scroll_offset.set(scroll_offset);
153 }
154 }
155
156 pub(crate) fn update_if_first_item_moved<F>(
157 &self,
158 new_item_count: usize,
159 find_by_key: F,
160 ) -> usize
161 where
162 F: Fn(u64) -> Option<usize>,
163 {
164 if !self.index.is_alive() || !self.inner.is_alive() {
165 return 0;
166 }
167
168 let current_index = self.current_index();
169 let last_key = self
170 .inner
171 .try_with(|rc| rc.borrow().last_known_first_item_key)
172 .flatten();
173
174 let new_index = match last_key {
175 None => current_index.min(new_item_count.saturating_sub(1)),
176 Some(key) => find_by_key(key)
177 .unwrap_or_else(|| current_index.min(new_item_count.saturating_sub(1))),
178 };
179
180 if current_index != new_index {
181 self.inner.with(|rc| {
182 let mut inner = rc.borrow_mut();
183 inner.current_index = new_index;
184 inner.nearest_range_state.update(new_index);
185 });
186 self.index.set(new_index);
187 }
188 new_index
189 }
190
191 pub fn nearest_range(&self) -> std::ops::Range<usize> {
193 self.inner
194 .try_with(|rc| rc.borrow().nearest_range_state.range())
195 .unwrap_or(0..0)
196 }
197}
198
199#[derive(Clone, Copy)]
230pub struct LazyListState {
231 scroll_position: LazyListScrollPosition,
232 can_scroll_forward_state: MutableState<bool>,
233 can_scroll_backward_state: MutableState<bool>,
234 stats_state: MutableState<LazyLayoutStats>,
235 inner: MutableState<Rc<RefCell<LazyListStateInner>>>,
236}
237
238impl PartialEq for LazyListState {
239 fn eq(&self, other: &Self) -> bool {
240 self.inner == other.inner
241 }
242}
243
244#[derive(Clone, Copy)]
245struct CachedItemSize {
246 size: f32,
247 last_used: u64,
248}
249
250struct LazyListStateInner {
252 scroll_to_be_consumed: f32,
253
254 pending_scroll_to_index: Option<(usize, f32)>,
255
256 layout_info: LazyListLayoutInfo,
257 current_can_scroll_forward: bool,
258 current_can_scroll_backward: bool,
259
260 invalidate_callbacks: Vec<(u64, Rc<dyn Fn()>)>,
261 next_callback_id: u64,
262
263 layout_invalidation_callback_id: Option<u64>,
264 layout_invalidation_node_id: Option<NodeId>,
265
266 total_composed: usize,
267 reuse_count: usize,
268
269 item_size_cache: std::collections::HashMap<usize, CachedItemSize>,
270 item_size_eviction_queue: BinaryHeap<Reverse<(u64, usize)>>,
271 item_size_clock: u64,
272
273 average_item_size: f32,
274 total_measured_items: usize,
275 next_measure_cycle_id: u64,
276 next_item_measure_pass_id: u64,
277
278 last_scroll_delta: f32,
279
280 hold_scroll_window: bool,
281}
282
283#[composable]
298#[track_caller]
299pub fn rememberLazyListState() -> LazyListState {
300 rememberLazyListStateWithPosition(0, 0.0)
301}
302
303#[composable]
307pub fn rememberLazyListStateWithPosition(
308 initial_first_visible_item_index: usize,
309 initial_first_visible_item_scroll_offset: f32,
310) -> LazyListState {
311 cranpose_core::remember(move || {
312 LazyListState::new(
313 initial_first_visible_item_index,
314 initial_first_visible_item_scroll_offset,
315 )
316 })
317 .with(|state| *state)
318}
319
320impl LazyListState {
321 pub fn new(
327 initial_first_visible_item_index: usize,
328 initial_first_visible_item_scroll_offset: f32,
329 ) -> Self {
330 LazyListState {
331 scroll_position: LazyListScrollPosition {
332 index: cranpose_core::mutableStateOf(initial_first_visible_item_index),
333 scroll_offset: cranpose_core::mutableStateOf(
334 initial_first_visible_item_scroll_offset,
335 ),
336 inner: cranpose_core::mutableStateOfNeverEqual(Rc::new(RefCell::new(
337 ScrollPositionInner {
338 current_index: initial_first_visible_item_index,
339 current_scroll_offset: initial_first_visible_item_scroll_offset,
340 last_known_first_item_key: None,
341 nearest_range_state: NearestRangeState::new(
342 initial_first_visible_item_index,
343 ),
344 },
345 ))),
346 },
347 can_scroll_forward_state: cranpose_core::mutableStateOf(false),
348 can_scroll_backward_state: cranpose_core::mutableStateOf(false),
349 stats_state: cranpose_core::mutableStateOf(LazyLayoutStats::default()),
350 inner: cranpose_core::mutableStateOfNeverEqual(Rc::new(RefCell::new(
351 LazyListStateInner {
352 scroll_to_be_consumed: 0.0,
353 pending_scroll_to_index: None,
354 layout_info: LazyListLayoutInfo::default(),
355 current_can_scroll_forward: false,
356 current_can_scroll_backward: false,
357 invalidate_callbacks: Vec::new(),
358 next_callback_id: 1,
359 layout_invalidation_callback_id: None,
360 layout_invalidation_node_id: None,
361 total_composed: 0,
362 reuse_count: 0,
363 item_size_cache: std::collections::HashMap::new(),
364 item_size_eviction_queue: BinaryHeap::new(),
365 item_size_clock: 0,
366 average_item_size: super::DEFAULT_ITEM_SIZE_ESTIMATE,
367 total_measured_items: 0,
368 next_measure_cycle_id: 1,
369 next_item_measure_pass_id: 1,
370 last_scroll_delta: 0.0,
371 hold_scroll_window: false,
372 },
373 ))),
374 }
375 }
376
377 pub fn inner_ptr(&self) -> *const () {
382 self.inner
383 .try_with(|rc| Rc::as_ptr(rc) as *const ())
384 .unwrap_or(std::ptr::null())
385 }
386
387 pub fn first_visible_item_index(&self) -> usize {
392 self.scroll_position.index()
393 }
394
395 pub fn total_items_count(&self) -> usize {
397 self.inner
398 .with(|rc| rc.borrow().layout_info.total_items_count)
399 }
400
401 pub fn first_visible_item_index_non_reactive(&self) -> usize {
406 self.scroll_position.current_index()
407 }
408
409 pub fn first_visible_item_scroll_offset(&self) -> f32 {
415 self.scroll_position.scroll_offset()
416 }
417
418 pub fn first_visible_item_scroll_offset_non_reactive(&self) -> f32 {
423 self.scroll_position.current_scroll_offset()
424 }
425
426 #[doc(hidden)]
427 pub fn reactive_state_ids(&self) -> [StateId; 5] {
428 [
429 self.scroll_position.index.runtime_state_id(),
430 self.scroll_position.scroll_offset.runtime_state_id(),
431 self.can_scroll_forward_state.runtime_state_id(),
432 self.can_scroll_backward_state.runtime_state_id(),
433 self.stats_state.runtime_state_id(),
434 ]
435 }
436
437 pub fn layout_info(&self) -> LazyListLayoutInfo {
439 self.inner
440 .try_with(|rc| rc.borrow().layout_info.clone())
441 .unwrap_or_default()
442 }
443
444 pub fn stats(&self) -> LazyLayoutStats {
450 if !self.stats_state.is_alive() || !self.inner.is_alive() {
451 return LazyLayoutStats::default();
452 }
453 let reactive = self.stats_state.get();
454 let (total_composed, reuse_count) = self.inner.with(|rc| {
455 let inner = rc.borrow();
456 (inner.total_composed, inner.reuse_count)
457 });
458 LazyLayoutStats {
459 items_in_use: reactive.items_in_use,
460 items_in_pool: reactive.items_in_pool,
461 total_composed,
462 reuse_count,
463 }
464 }
465
466 pub fn update_stats(&self, items_in_use: usize, items_in_pool: usize) {
471 if !self.stats_state.is_alive() || !self.inner.is_alive() {
472 return;
473 }
474
475 let current = self.stats_state.get_non_reactive();
476
477 let should_update_reactive = if items_in_use > current.items_in_use {
478 true
479 } else if items_in_use < current.items_in_use {
480 current.items_in_use - items_in_use > 1
481 } else {
482 false
483 };
484
485 if should_update_reactive {
486 self.stats_state.set(LazyLayoutStats {
487 items_in_use,
488 items_in_pool,
489 ..current
490 });
491 }
492 }
493
494 pub fn record_composition(&self, was_reused: bool) {
499 if !self.inner.is_alive() {
500 return;
501 }
502 self.inner.with(|rc| {
503 let mut inner = rc.borrow_mut();
504 inner.total_composed += 1;
505 if was_reused {
506 inner.reuse_count += 1;
507 }
508 });
509 }
510
511 pub fn hold_scroll_window(&self) {
515 if !self.inner.is_alive() {
516 return;
517 }
518 self.inner
519 .with(|rc| rc.borrow_mut().hold_scroll_window = true);
520 }
521
522 pub fn scroll_to_item(&self, index: usize, scroll_offset: f32) {
528 if !self.inner.is_alive() {
529 return;
530 }
531 if diagnostics::telemetry_enabled() {
532 log::warn!(
533 "[lazy-measure-telemetry] scroll_to_item request index={index} offset={scroll_offset:.2}"
534 );
535 }
536 self.inner.with(|rc| {
537 rc.borrow_mut().pending_scroll_to_index = Some((index, scroll_offset));
538 });
539
540 self.scroll_position
541 .request_position_and_forget_last_known_key(index, scroll_offset);
542
543 self.invalidate();
544 }
545
546 pub fn dispatch_scroll_delta(&self, delta: f32) -> f32 {
557 if !self.inner.is_alive() {
558 return 0.0;
559 }
560 let has_scroll_bounds = self
561 .inner
562 .with(|rc| rc.borrow().layout_info.total_items_count > 0);
563 let pushing_forward = delta < -0.001;
564 let pushing_backward = delta > 0.001;
565 let can_scroll_forward =
566 self.can_scroll_forward_state.is_alive() && self.can_scroll_forward_non_reactive();
567 let can_scroll_backward =
568 self.can_scroll_backward_state.is_alive() && self.can_scroll_backward_non_reactive();
569 let blocked_by_bounds = has_scroll_bounds
570 && ((pushing_forward && !can_scroll_forward)
571 || (pushing_backward && !can_scroll_backward));
572
573 if blocked_by_bounds {
574 let should_invalidate = self.inner.with(|rc| {
575 let mut inner = rc.borrow_mut();
576 let pending_before = inner.scroll_to_be_consumed;
577 if pending_before.abs() > 0.001 && pending_before.signum() == delta.signum() {
578 inner.scroll_to_be_consumed = 0.0;
579 }
580 if diagnostics::telemetry_enabled() {
581 log::warn!(
582 "[lazy-measure-telemetry] dispatch_scroll_delta blocked_by_bounds delta={:.2} pending_before={:.2} pending_after={:.2}",
583 delta,
584 pending_before,
585 inner.scroll_to_be_consumed
586 );
587 }
588 (inner.scroll_to_be_consumed - pending_before).abs() > 0.001
589 });
590 if should_invalidate {
591 self.invalidate();
592 }
593 return 0.0;
594 }
595
596 let mut accepted_delta = 0.0f32;
597 let should_invalidate = self.inner.with(|rc| {
598 let mut inner = rc.borrow_mut();
599 accepted_delta = delta;
600 let pending_before = inner.scroll_to_be_consumed;
601 let pending = inner.scroll_to_be_consumed;
602 let reverse_input = pending.abs() > 0.001
603 && delta.abs() > 0.001
604 && pending.signum() != delta.signum();
605 if reverse_input {
606 if diagnostics::telemetry_enabled() {
607 log::warn!(
608 "[lazy-measure-telemetry] dispatch_scroll_delta direction_change pending={pending:.2} new_delta={delta:.2}"
609 );
610 }
611 inner.scroll_to_be_consumed = delta;
612 } else {
613 inner.scroll_to_be_consumed += delta;
614 }
615 inner.scroll_to_be_consumed = inner
616 .scroll_to_be_consumed
617 .clamp(-MAX_PENDING_SCROLL_DELTA, MAX_PENDING_SCROLL_DELTA);
618 if diagnostics::telemetry_enabled() {
619 log::warn!(
620 "[lazy-measure-telemetry] dispatch_scroll_delta delta={:.2} pending={:.2}",
621 delta,
622 inner.scroll_to_be_consumed
623 );
624 }
625 (inner.scroll_to_be_consumed - pending_before).abs() > 0.001
626 });
627 if should_invalidate {
628 self.invalidate();
629 }
630 accepted_delta
631 }
632
633 pub fn peek_scroll_delta(&self) -> f32 {
640 self.inner
641 .try_with(|rc| rc.borrow().scroll_to_be_consumed)
642 .unwrap_or(0.0)
643 }
644
645 pub(crate) fn begin_measure_pass(&self) -> LazyListMeasureStateSnapshot {
646 let (pending_scroll_delta, window_scroll_delta, pending_scroll_to, average_item_size) =
647 self.inner
648 .try_with(|rc| {
649 let mut inner = rc.borrow_mut();
650 let pending_scroll_to = inner.pending_scroll_to_index.take();
651 let pending_scroll_delta = inner.scroll_to_be_consumed;
652 inner.scroll_to_be_consumed = 0.0;
653 let held = std::mem::take(&mut inner.hold_scroll_window);
654 let window_scroll_delta = if pending_scroll_delta.abs() > 0.001 {
655 inner.last_scroll_delta = pending_scroll_delta;
656 pending_scroll_delta
657 } else if held {
658 inner.last_scroll_delta
659 } else {
660 0.0
661 };
662 (
663 pending_scroll_delta,
664 window_scroll_delta,
665 pending_scroll_to,
666 inner.average_item_size,
667 )
668 })
669 .unwrap_or((0.0, 0.0, None, super::DEFAULT_ITEM_SIZE_ESTIMATE));
670
671 LazyListMeasureStateSnapshot {
672 first_visible_item_index: self.scroll_position.current_index(),
673 first_visible_item_scroll_offset: self.scroll_position.current_scroll_offset(),
674 pending_scroll_delta,
675 window_scroll_delta,
676 pending_scroll_to,
677 average_item_size,
678 }
679 }
680
681 pub(crate) fn next_measure_cycle_id(&self) -> u64 {
682 self.inner
683 .try_with(|rc| {
684 let mut inner = rc.borrow_mut();
685 let id = inner.next_measure_cycle_id;
686 inner.next_measure_cycle_id = inner.next_measure_cycle_id.saturating_add(1);
687 id
688 })
689 .unwrap_or(0)
690 }
691
692 pub(crate) fn next_item_measure_pass_id(&self) -> u64 {
693 self.inner
694 .try_with(|rc| {
695 let mut inner = rc.borrow_mut();
696 let id = inner.next_item_measure_pass_id;
697 inner.next_item_measure_pass_id = inner.next_item_measure_pass_id.saturating_add(1);
698 id
699 })
700 .unwrap_or(0)
701 }
702
703 fn record_item_size_sample(inner: &mut LazyListStateInner, size: f32) {
704 inner.total_measured_items += 1;
705 let n = inner.total_measured_items as f32;
706 inner.average_item_size = inner.average_item_size * ((n - 1.0) / n) + size / n;
707 }
708
709 fn next_item_size_cache_tick(inner: &mut LazyListStateInner) -> u64 {
710 inner.item_size_clock = inner.item_size_clock.saturating_add(1);
711 inner.item_size_clock
712 }
713
714 fn insert_item_size(inner: &mut LazyListStateInner, index: usize, size: f32) -> bool {
715 use std::collections::hash_map::Entry;
716
717 let tick = Self::next_item_size_cache_tick(inner);
718 if let Entry::Occupied(mut entry) = inner.item_size_cache.entry(index) {
719 entry.insert(CachedItemSize {
720 size,
721 last_used: tick,
722 });
723 Self::push_item_size_cache_ticket(inner, tick, index);
724 return false;
725 }
726
727 if inner.item_size_cache.len() >= ITEM_SIZE_CACHE_CAPACITY {
728 Self::evict_one_item_size(inner);
729 }
730
731 inner.item_size_cache.insert(
732 index,
733 CachedItemSize {
734 size,
735 last_used: tick,
736 },
737 );
738 Self::push_item_size_cache_ticket(inner, tick, index);
739 true
740 }
741
742 fn push_item_size_cache_ticket(inner: &mut LazyListStateInner, last_used: u64, index: usize) {
743 inner
744 .item_size_eviction_queue
745 .push(Reverse((last_used, index)));
746 let compact_limit = inner
747 .item_size_cache
748 .len()
749 .saturating_mul(4)
750 .max(ITEM_SIZE_CACHE_CAPACITY);
751 if inner.item_size_eviction_queue.len() > compact_limit {
752 Self::rebuild_item_size_eviction_queue(inner);
753 }
754 }
755
756 fn rebuild_item_size_eviction_queue(inner: &mut LazyListStateInner) {
757 inner.item_size_eviction_queue = inner
758 .item_size_cache
759 .iter()
760 .map(|(index, item)| Reverse((item.last_used, *index)))
761 .collect();
762 }
763
764 fn evict_one_item_size(inner: &mut LazyListStateInner) {
765 while let Some(Reverse((last_used, index))) = inner.item_size_eviction_queue.pop() {
766 let Some(current) = inner.item_size_cache.get(&index) else {
767 continue;
768 };
769 if current.last_used != last_used {
770 continue;
771 }
772 inner.item_size_cache.remove(&index);
773 return;
774 }
775 }
776
777 pub fn cache_item_size(&self, index: usize, size: f32) {
779 if !self.inner.is_alive() {
780 return;
781 }
782 self.inner.with(|rc| {
783 let mut inner = rc.borrow_mut();
784 if Self::insert_item_size(&mut inner, index, size) {
785 Self::record_item_size_sample(&mut inner, size);
786 }
787 });
788 }
789
790 pub fn cache_item_sizes<I>(&self, sizes: I) -> f32
792 where
793 I: IntoIterator<Item = (usize, f32)>,
794 {
795 if !self.inner.is_alive() {
796 return super::DEFAULT_ITEM_SIZE_ESTIMATE;
797 }
798
799 self.inner.with(|rc| {
800 let mut inner = rc.borrow_mut();
801 for (index, size) in sizes {
802 if Self::insert_item_size(&mut inner, index, size) {
803 Self::record_item_size_sample(&mut inner, size);
804 }
805 }
806 inner.average_item_size
807 })
808 }
809
810 pub fn get_cached_size(&self, index: usize) -> Option<f32> {
812 self.inner
813 .try_with(|rc| {
814 let mut inner = rc.borrow_mut();
815 let tick = Self::next_item_size_cache_tick(&mut inner);
816 let item = inner.item_size_cache.get_mut(&index)?;
817 item.last_used = tick;
818 let size = item.size;
819 Self::push_item_size_cache_ticket(&mut inner, tick, index);
820 Some(size)
821 })
822 .flatten()
823 }
824
825 pub fn average_item_size(&self) -> f32 {
827 self.inner
828 .try_with(|rc| rc.borrow().average_item_size)
829 .unwrap_or(super::DEFAULT_ITEM_SIZE_ESTIMATE)
830 }
831
832 pub fn nearest_range(&self) -> std::ops::Range<usize> {
834 self.scroll_position.nearest_range()
835 }
836
837 pub(crate) fn update_scroll_position(
838 &self,
839 first_visible_item_index: usize,
840 first_visible_item_scroll_offset: f32,
841 ) {
842 self.scroll_position.update_from_measure_result(
843 first_visible_item_index,
844 first_visible_item_scroll_offset,
845 None,
846 );
847 }
848
849 pub(crate) fn update_scroll_position_with_key(
850 &self,
851 first_visible_item_index: usize,
852 first_visible_item_scroll_offset: f32,
853 first_visible_item_key: u64,
854 ) {
855 self.scroll_position.update_from_measure_result(
856 first_visible_item_index,
857 first_visible_item_scroll_offset,
858 Some(first_visible_item_key),
859 );
860 }
861
862 pub fn update_scroll_position_if_item_moved<F>(
870 &self,
871 new_item_count: usize,
872 get_index_by_key: F,
873 ) -> usize
874 where
875 F: Fn(u64) -> Option<usize>,
876 {
877 self.scroll_position
878 .update_if_first_item_moved(new_item_count, get_index_by_key)
879 }
880
881 pub(crate) fn update_layout_info(&self, mut info: LazyListLayoutInfo) {
882 if !self.inner.is_alive() {
883 return;
884 }
885 self.inner.with(|rc| {
886 let mut inner = rc.borrow_mut();
887 info.snap_anchor_offset = continuous_snap_anchor_offset(&inner.layout_info, &info);
888 inner.layout_info = info;
889 });
890 }
891
892 pub fn can_scroll_forward(&self) -> bool {
897 if !self.can_scroll_forward_state.is_alive() {
898 return false;
899 }
900 self.can_scroll_forward_state.subscribe_current_scope_only();
901 self.can_scroll_forward_non_reactive()
902 }
903
904 pub fn can_scroll_forward_non_reactive(&self) -> bool {
906 if !self.can_scroll_forward_state.is_alive() {
907 return false;
908 }
909 self.inner
910 .try_with(|rc| rc.borrow().current_can_scroll_forward)
911 .unwrap_or(false)
912 }
913
914 pub fn can_scroll_backward(&self) -> bool {
919 if !self.can_scroll_backward_state.is_alive() {
920 return false;
921 }
922 self.can_scroll_backward_state
923 .subscribe_current_scope_only();
924 self.can_scroll_backward_non_reactive()
925 }
926
927 pub fn can_scroll_backward_non_reactive(&self) -> bool {
929 if !self.can_scroll_backward_state.is_alive() {
930 return false;
931 }
932 self.inner
933 .try_with(|rc| rc.borrow().current_can_scroll_backward)
934 .unwrap_or(false)
935 }
936
937 pub(crate) fn update_scroll_bounds(&self) {
938 if !self.inner.is_alive()
939 || !self.can_scroll_forward_state.is_alive()
940 || !self.can_scroll_backward_state.is_alive()
941 {
942 return;
943 }
944 let can_forward = self.inner.with(|rc| {
945 let inner = rc.borrow();
946 let info = &inner.layout_info;
947 let viewport_end = info.viewport_size - info.after_content_padding;
948 if let Some(last_visible) = info.visible_items_info.last() {
949 last_visible.index < info.total_items_count.saturating_sub(1)
950 || (last_visible.offset + last_visible.size) > viewport_end
951 } else {
952 false
953 }
954 });
955
956 let can_backward = self.scroll_position.current_index() > 0
957 || self.scroll_position.current_scroll_offset() > 0.0;
958
959 self.inner.with(|rc| {
960 let mut inner = rc.borrow_mut();
961 inner.current_can_scroll_forward = can_forward;
962 inner.current_can_scroll_backward = can_backward;
963 });
964
965 if self.can_scroll_forward_state.get_non_reactive() != can_forward {
966 self.can_scroll_forward_state.set(can_forward);
967 }
968 if self.can_scroll_backward_state.get_non_reactive() != can_backward {
969 self.can_scroll_backward_state.set(can_backward);
970 }
971 }
972
973 pub fn add_invalidate_callback(&self, callback: Rc<dyn Fn()>) -> u64 {
975 if !self.inner.is_alive() {
976 return 0;
977 }
978 self.inner.with(|rc| {
979 let mut inner = rc.borrow_mut();
980 let id = inner.next_callback_id;
981 inner.next_callback_id += 1;
982 inner.invalidate_callbacks.push((id, callback));
983 id
984 })
985 }
986
987 pub fn try_register_layout_callback(
995 &self,
996 node_id: NodeId,
997 callback: Rc<dyn Fn()>,
998 ) -> Option<u64> {
999 if !self.inner.is_alive() {
1000 return None;
1001 }
1002 self.inner.with(|rc| {
1003 let mut inner = rc.borrow_mut();
1004 if let Some(existing_id) = inner.layout_invalidation_callback_id {
1005 inner
1006 .invalidate_callbacks
1007 .retain(|(cb_id, _)| *cb_id != existing_id);
1008 }
1009 let id = inner.next_callback_id;
1010 inner.next_callback_id += 1;
1011 inner.invalidate_callbacks.push((id, callback));
1012 inner.layout_invalidation_callback_id = Some(id);
1013 inner.layout_invalidation_node_id = Some(node_id);
1014 Some(id)
1015 })
1016 }
1017
1018 pub fn remove_invalidate_callback(&self, id: u64) {
1020 if !self.inner.is_alive() {
1021 return;
1022 }
1023 self.inner.with(|rc| {
1024 let mut inner = rc.borrow_mut();
1025 inner.invalidate_callbacks.retain(|(cb_id, _)| *cb_id != id);
1026 if inner.layout_invalidation_callback_id == Some(id) {
1027 inner.layout_invalidation_callback_id = None;
1028 inner.layout_invalidation_node_id = None;
1029 }
1030 });
1031 }
1032
1033 fn invalidate(&self) {
1034 if !self.inner.is_alive() {
1035 return;
1036 }
1037 let callbacks: Vec<_> = self.inner.with(|rc| {
1038 rc.borrow()
1039 .invalidate_callbacks
1040 .iter()
1041 .map(|(_, cb)| Rc::clone(cb))
1042 .collect()
1043 });
1044
1045 for callback in callbacks {
1046 callback();
1047 }
1048 }
1049}
1050
1051#[derive(Clone, Default, Debug)]
1053pub struct LazyListLayoutInfo {
1054 pub visible_items_info: Vec<LazyListItemInfo>,
1056
1057 pub total_items_count: usize,
1059
1060 pub raw_viewport_size: f32,
1062
1063 pub is_infinite_viewport: bool,
1065
1066 pub viewport_size: f32,
1068
1069 pub viewport_start_offset: f32,
1071
1072 pub viewport_end_offset: f32,
1074
1075 pub before_content_padding: f32,
1077
1078 pub after_content_padding: f32,
1080
1081 pub snap_anchor_offset: f32,
1083
1084 pub reverse_layout: bool,
1086}
1087
1088#[derive(Clone, Debug)]
1090pub struct LazyListItemInfo {
1091 pub index: usize,
1093
1094 pub key: u64,
1096
1097 pub offset: f32,
1099
1100 pub size: f32,
1102}
1103
1104fn continuous_snap_anchor_offset(
1105 previous: &LazyListLayoutInfo,
1106 current: &LazyListLayoutInfo,
1107) -> f32 {
1108 let Some(first_current) = current.visible_items_info.first() else {
1109 return 0.0;
1110 };
1111
1112 for current_item in ¤t.visible_items_info {
1113 if let Some(previous_item) = previous
1114 .visible_items_info
1115 .iter()
1116 .find(|item| item.key == current_item.key)
1117 {
1118 let previous_offset = snap_anchor_item_offset(previous, previous_item);
1119 let current_offset = snap_anchor_item_offset(current, current_item);
1120 return previous.snap_anchor_offset + current_offset - previous_offset;
1121 }
1122 }
1123
1124 snap_anchor_item_offset(current, first_current)
1125}
1126
1127fn snap_anchor_item_offset(info: &LazyListLayoutInfo, item: &LazyListItemInfo) -> f32 {
1128 if info.reverse_layout {
1129 info.viewport_size - item.offset - item.size
1130 } else {
1131 item.offset
1132 }
1133}
1134
1135#[cfg(test)]
1136#[path = "tests/lazy_list_state_test_helpers.rs"]
1137pub mod test_helpers;
1138
1139#[cfg(test)]
1140#[path = "tests/lazy_list_state_tests.rs"]
1141mod tests;