1use std::{cell::RefCell, rc::Rc};
13
14use cranpose_core::{MutableState, NodeId, RuntimeCell, StateId, collections::map::HashMap};
15use cranpose_macros::composable;
16
17use super::diagnostics;
18
19mod scroll_position;
20
21pub use scroll_position::LazyListScrollPosition;
22
23const MAX_PENDING_SCROLL_DELTA: f32 = 2000.0;
24const ITEM_SIZE_CACHE_CAPACITY: usize = 8192;
25
26#[derive(Clone, Copy, Debug, PartialEq)]
27pub(crate) struct LazyListMeasureStateSnapshot {
28 pub(crate) first_visible_item_index: usize,
29 pub(crate) first_visible_item_scroll_offset: f32,
30 pub(crate) pending_scroll_delta: f32,
31 pub(crate) window_scroll_delta: f32,
32 pub(crate) pending_scroll_to: Option<(usize, f32)>,
33 pub(crate) average_item_size: f32,
34}
35
36#[derive(Clone, Debug, Default, PartialEq)]
40pub struct LazyLayoutStats {
41 pub items_in_use: usize,
43
44 pub items_in_pool: usize,
46
47 pub total_composed: usize,
49
50 pub reuse_count: usize,
52}
53
54#[derive(Clone, Copy)]
85pub struct LazyListState {
86 scroll_position: LazyListScrollPosition,
87 can_scroll_forward_state: MutableState<bool>,
88 can_scroll_backward_state: MutableState<bool>,
89 stats_state: MutableState<LazyLayoutStats>,
90 scroll_in_progress_state: MutableState<bool>,
91 dragged_state: MutableState<bool>,
92 inner: RuntimeCell<RefCell<LazyListStateInner>>,
93}
94
95impl PartialEq for LazyListState {
96 fn eq(&self, other: &Self) -> bool {
97 self.inner == other.inner
98 }
99}
100
101#[derive(Clone, Copy)]
102struct CachedItemSize {
103 size: f32,
104 last_used: u64,
105}
106
107pub type LazySettlePolicy = Rc<dyn Fn(f32, f32) -> f32>;
112
113struct LazyListStateInner {
114 scroll_to_be_consumed: f32,
115 settle_policy: Option<LazySettlePolicy>,
116 scroll_animation: Option<(u64, Rc<dyn Fn()>)>,
118 next_scroll_animation: u64,
119 gesture_scrolling: bool,
121 scroll_start: Option<(usize, f32)>,
124
125 pending_scroll_to_index: Option<(usize, f32)>,
126
127 layout_info: LazyListLayoutInfo,
128 spare_items_info: Vec<LazyListItemInfo>,
131 current_can_scroll_forward: bool,
132 current_can_scroll_backward: bool,
133
134 invalidate_callbacks: Vec<(u64, Rc<dyn Fn()>)>,
135 next_callback_id: u64,
136
137 layout_invalidation_callback_id: Option<u64>,
138 layout_invalidation_node_id: Option<NodeId>,
139
140 total_composed: usize,
141 reuse_count: usize,
142
143 item_size_cache: HashMap<usize, CachedItemSize>,
144 item_size_clock: u64,
145
146 average_item_size: f32,
147 total_measured_items: usize,
148 next_measure_cycle_id: u64,
149 next_item_measure_pass_id: u64,
150
151 last_scroll_delta: f32,
152
153 hold_scroll_window: bool,
154}
155
156#[composable]
171#[track_caller]
172pub fn rememberLazyListState() -> LazyListState {
173 rememberLazyListStateWithPosition(0, 0.0)
174}
175
176#[composable]
180pub fn rememberLazyListStateWithPosition(
181 initial_first_visible_item_index: usize,
182 initial_first_visible_item_scroll_offset: f32,
183) -> LazyListState {
184 cranpose_core::remember(move || {
185 LazyListState::new(
186 initial_first_visible_item_index,
187 initial_first_visible_item_scroll_offset,
188 )
189 })
190 .with(|state| *state)
191}
192
193impl LazyListState {
194 pub fn new(
200 initial_first_visible_item_index: usize,
201 initial_first_visible_item_scroll_offset: f32,
202 ) -> Self {
203 Self {
204 scroll_position: LazyListScrollPosition::new(
205 initial_first_visible_item_index,
206 initial_first_visible_item_scroll_offset,
207 ),
208 can_scroll_forward_state: cranpose_core::mutableStateOf(false),
209 can_scroll_backward_state: cranpose_core::mutableStateOf(false),
210 stats_state: cranpose_core::mutableStateOf(LazyLayoutStats::default()),
211 scroll_in_progress_state: cranpose_core::mutableStateOf(false),
212 dragged_state: cranpose_core::mutableStateOf(false),
213 inner: RuntimeCell::new(RefCell::new(LazyListStateInner {
214 scroll_to_be_consumed: 0.0,
215 settle_policy: None,
216 scroll_animation: None,
217 next_scroll_animation: 1,
218 gesture_scrolling: false,
219 scroll_start: None,
220 pending_scroll_to_index: None,
221 layout_info: LazyListLayoutInfo::default(),
222 spare_items_info: Vec::new(),
223 current_can_scroll_forward: false,
224 current_can_scroll_backward: false,
225 invalidate_callbacks: Vec::new(),
226 next_callback_id: 1,
227 layout_invalidation_callback_id: None,
228 layout_invalidation_node_id: None,
229 total_composed: 0,
230 reuse_count: 0,
231 item_size_cache: HashMap::default(),
232 item_size_clock: 0,
233 average_item_size: super::DEFAULT_ITEM_SIZE_ESTIMATE,
234 total_measured_items: 0,
235 next_measure_cycle_id: 1,
236 next_item_measure_pass_id: 1,
237 last_scroll_delta: 0.0,
238 hold_scroll_window: false,
239 })),
240 }
241 }
242
243 pub fn try_position(&self) -> Option<(usize, f32)> {
247 self.scroll_position.current()
248 }
249
250 pub fn inner_ptr(&self) -> *const () {
256 self.inner
257 .try_with(|inner| std::ptr::from_ref(inner).cast::<()>())
258 .unwrap_or(std::ptr::null())
259 }
260
261 pub fn first_visible_item_index(&self) -> usize {
266 self.scroll_position.index()
267 }
268
269 pub fn total_items_count(&self) -> usize {
271 self.inner
272 .with(|cell| cell.borrow().layout_info.total_items_count)
273 }
274
275 pub fn first_visible_item_index_non_reactive(&self) -> usize {
280 self.scroll_position.current_index()
281 }
282
283 pub fn first_visible_item_scroll_offset(&self) -> f32 {
289 self.scroll_position.scroll_offset()
290 }
291
292 pub fn first_visible_item_scroll_offset_non_reactive(&self) -> f32 {
297 self.scroll_position.current_scroll_offset()
298 }
299
300 #[doc(hidden)]
301 pub fn reactive_state_ids(&self) -> [StateId; 5] {
302 [
303 self.scroll_position.index.runtime_state_id(),
304 self.scroll_position.scroll_offset.runtime_state_id(),
305 self.can_scroll_forward_state.runtime_state_id(),
306 self.can_scroll_backward_state.runtime_state_id(),
307 self.stats_state.runtime_state_id(),
308 ]
309 }
310
311 pub fn layout_info(&self) -> LazyListLayoutInfo {
313 self.with_layout_info(LazyListLayoutInfo::clone)
314 .unwrap_or_default()
315 }
316
317 pub fn with_layout_info<R>(&self, read: impl FnOnce(&LazyListLayoutInfo) -> R) -> Option<R> {
321 self.inner.try_with(|cell| read(&cell.borrow().layout_info))
322 }
323
324 pub fn stats(&self) -> LazyLayoutStats {
330 if !self.stats_state.is_alive() || !self.inner.is_alive() {
331 return LazyLayoutStats::default();
332 }
333 let reactive = self.stats_state.get();
334 let (total_composed, reuse_count) = self.inner.with(|cell| {
335 let inner = cell.borrow();
336 (inner.total_composed, inner.reuse_count)
337 });
338 LazyLayoutStats {
339 items_in_use: reactive.items_in_use,
340 items_in_pool: reactive.items_in_pool,
341 total_composed,
342 reuse_count,
343 }
344 }
345
346 pub fn update_stats(&self, items_in_use: usize, items_in_pool: usize) {
351 if !self.stats_state.is_alive() || !self.inner.is_alive() {
352 return;
353 }
354
355 let current = self.stats_state.get_non_reactive();
356
357 let should_update_reactive = if items_in_use > current.items_in_use {
358 true
359 } else if items_in_use < current.items_in_use {
360 current.items_in_use - items_in_use > 1
361 } else {
362 false
363 };
364
365 if should_update_reactive {
366 self.stats_state.set(LazyLayoutStats {
367 items_in_use,
368 items_in_pool,
369 ..current
370 });
371 }
372 }
373
374 pub fn record_composition(&self, was_reused: bool) {
379 self.inner.try_with(|cell| {
380 let mut inner = cell.borrow_mut();
381 inner.total_composed += 1;
382 if was_reused {
383 inner.reuse_count += 1;
384 }
385 });
386 }
387
388 pub fn hold_scroll_window(&self) {
392 self.inner
393 .try_with(|cell| cell.borrow_mut().hold_scroll_window = true);
394 }
395
396 pub fn set_settle_policy(&self, policy: Option<LazySettlePolicy>) {
399 self.inner
400 .try_with(|cell| cell.borrow_mut().settle_policy = policy);
401 }
402
403 pub fn settle_policy(&self) -> Option<LazySettlePolicy> {
405 self.inner
406 .try_with(|cell| cell.borrow().settle_policy.clone())
407 .flatten()
408 }
409
410 pub fn start_scroll_animation(&self, stop: Rc<dyn Fn()>) -> u64 {
414 let Some((id, previous)) = self.inner.try_with(|cell| {
415 let mut inner = cell.borrow_mut();
416 let id = inner.next_scroll_animation;
417 inner.next_scroll_animation += 1;
418 (id, inner.scroll_animation.replace((id, stop)))
419 }) else {
420 return 0;
421 };
422 if let Some((_, stop)) = previous {
423 stop();
424 }
425 self.refresh_scroll_in_progress();
426 id
427 }
428
429 pub fn end_scroll_animation(&self, id: u64) {
431 let _ = self.inner.try_with(|cell| {
432 let mut inner = cell.borrow_mut();
433 if inner
434 .scroll_animation
435 .as_ref()
436 .is_some_and(|(running, _)| *running == id)
437 {
438 inner.scroll_animation = None;
439 }
440 });
441 self.refresh_scroll_in_progress();
442 }
443
444 pub fn stop_scroll_animation(&self) -> bool {
446 let running = self
447 .inner
448 .try_with(|cell| cell.borrow_mut().scroll_animation.take())
449 .flatten();
450 let stopped = running.map(|(_, stop)| stop()).is_some();
451 self.refresh_scroll_in_progress();
452 stopped
453 }
454
455 pub fn is_scroll_in_progress(&self) -> bool {
459 self.scroll_in_progress_state.is_alive() && self.scroll_in_progress_state.get()
460 }
461
462 pub fn is_dragged(&self) -> bool {
466 self.dragged_state.is_alive() && self.dragged_state.get()
467 }
468
469 pub fn set_dragged(&self, dragged: bool) {
472 if self.dragged_state.is_alive() && self.dragged_state.get_non_reactive() != dragged {
473 self.dragged_state.set(dragged);
474 }
475 }
476
477 pub fn set_gesture_scroll_active(&self, active: bool) {
481 let changed = self.inner.try_with(|cell| {
482 std::mem::replace(&mut cell.borrow_mut().gesture_scrolling, active) != active
483 });
484 if changed == Some(true) {
485 self.refresh_scroll_in_progress();
486 }
487 }
488
489 pub fn position_before_scroll(&self) -> Option<(usize, f32)> {
493 self.inner
494 .try_with(|cell| cell.borrow().scroll_start)
495 .flatten()
496 }
497
498 fn refresh_scroll_in_progress(&self) {
499 if !self.scroll_in_progress_state.is_alive() {
500 return;
501 }
502 let Some(moving) = self.inner.try_with(|cell| {
503 let inner = cell.borrow();
504 inner.gesture_scrolling || inner.scroll_animation.is_some()
505 }) else {
506 return;
507 };
508 if self.scroll_in_progress_state.get_non_reactive() == moving {
509 return;
510 }
511 let start = if moving { self.try_position() } else { None };
512 let _ = self
513 .inner
514 .try_with(|cell| cell.borrow_mut().scroll_start = start);
515 self.scroll_in_progress_state.set(moving);
516 }
517
518 pub fn scroll_to_item(&self, index: usize, scroll_offset: f32) {
524 if !self.inner.is_alive() {
525 return;
526 }
527 if diagnostics::telemetry_enabled() {
528 log::warn!(
529 "[lazy-measure-telemetry] scroll_to_item request index={index} offset={scroll_offset:.2}"
530 );
531 }
532 self.inner.with(|cell| {
533 let mut inner = cell.borrow_mut();
534 inner.pending_scroll_to_index = Some((index, scroll_offset));
535 inner.scroll_to_be_consumed = 0.0;
537 });
538
539 self.scroll_position
540 .request_position_and_forget_last_known_key(index, scroll_offset);
541
542 self.invalidate();
543 }
544
545 pub fn dispatch_scroll_delta(&self, delta: f32) -> f32 {
556 let Some(cell) = self.inner.try_get() else {
557 return 0.0;
558 };
559 let has_scroll_bounds = cell.borrow().layout_info.total_items_count > 0;
560 let pushing_forward = delta < -0.001;
561 let pushing_backward = delta > 0.001;
562 let can_scroll_forward =
563 self.can_scroll_forward_state.is_alive() && self.can_scroll_forward_non_reactive();
564 let can_scroll_backward =
565 self.can_scroll_backward_state.is_alive() && self.can_scroll_backward_non_reactive();
566 let blocked_by_bounds = has_scroll_bounds
567 && ((pushing_forward && !can_scroll_forward)
568 || (pushing_backward && !can_scroll_backward));
569
570 if blocked_by_bounds {
571 let should_invalidate = {
572 let mut inner = cell.borrow_mut();
573 let pending_before = inner.scroll_to_be_consumed;
574 if pending_before.abs() > 0.001 && pending_before.signum() == delta.signum() {
575 inner.scroll_to_be_consumed = 0.0;
576 }
577 if diagnostics::telemetry_enabled() {
578 log::warn!(
579 "[lazy-measure-telemetry] dispatch_scroll_delta blocked_by_bounds delta={:.2} pending_before={:.2} pending_after={:.2}",
580 delta,
581 pending_before,
582 inner.scroll_to_be_consumed
583 );
584 }
585 (inner.scroll_to_be_consumed - pending_before).abs() > 0.001
586 };
587 if should_invalidate {
588 Self::run_invalidate_callbacks(&cell);
589 }
590 return 0.0;
591 }
592
593 let should_invalidate = {
594 let mut inner = cell.borrow_mut();
595 let pending_before = inner.scroll_to_be_consumed;
596 let pending = inner.scroll_to_be_consumed;
597 let reverse_input =
598 pending.abs() > 0.001 && delta.abs() > 0.001 && pending.signum() != delta.signum();
599 if reverse_input {
600 if diagnostics::telemetry_enabled() {
601 log::warn!(
602 "[lazy-measure-telemetry] dispatch_scroll_delta direction_change pending={pending:.2} new_delta={delta:.2}"
603 );
604 }
605 inner.scroll_to_be_consumed = delta;
606 } else {
607 inner.scroll_to_be_consumed += delta;
608 }
609 inner.scroll_to_be_consumed = inner
610 .scroll_to_be_consumed
611 .clamp(-MAX_PENDING_SCROLL_DELTA, MAX_PENDING_SCROLL_DELTA);
612 if diagnostics::telemetry_enabled() {
613 log::warn!(
614 "[lazy-measure-telemetry] dispatch_scroll_delta delta={:.2} pending={:.2}",
615 delta,
616 inner.scroll_to_be_consumed
617 );
618 }
619 (inner.scroll_to_be_consumed - pending_before).abs() > 0.001
620 };
621 if should_invalidate {
622 Self::run_invalidate_callbacks(&cell);
623 }
624 delta
625 }
626
627 pub fn peek_scroll_delta(&self) -> f32 {
634 self.inner
635 .try_with(|cell| cell.borrow().scroll_to_be_consumed)
636 .unwrap_or(0.0)
637 }
638
639 pub(crate) fn begin_measure_pass(&self) -> LazyListMeasureStateSnapshot {
640 let snapshot = self.measure_pass_snapshot();
641 self.consume_measure_pass();
642 snapshot
643 }
644
645 pub(crate) fn measure_pass_snapshot(&self) -> LazyListMeasureStateSnapshot {
648 let (pending_scroll_delta, window_scroll_delta, pending_scroll_to, average_item_size) =
649 self.inner
650 .try_with(|cell| {
651 let inner = cell.borrow();
652 let pending_scroll_delta = inner.scroll_to_be_consumed;
653 let window_scroll_delta = if pending_scroll_delta.abs() > 0.001 {
654 pending_scroll_delta
655 } else if inner.hold_scroll_window {
656 inner.last_scroll_delta
657 } else {
658 0.0
659 };
660 (
661 pending_scroll_delta,
662 window_scroll_delta,
663 inner.pending_scroll_to_index,
664 inner.average_item_size,
665 )
666 })
667 .unwrap_or((0.0, 0.0, None, super::DEFAULT_ITEM_SIZE_ESTIMATE));
668
669 let (first_visible_item_index, first_visible_item_scroll_offset) =
670 self.try_position().unwrap_or((0, 0.0));
671 LazyListMeasureStateSnapshot {
672 first_visible_item_index,
673 first_visible_item_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 consume_measure_pass(&self) {
684 self.inner.try_with(|cell| {
685 let mut inner = cell.borrow_mut();
686 inner.pending_scroll_to_index = None;
687 inner.hold_scroll_window = false;
688 let pending_scroll_delta = std::mem::take(&mut inner.scroll_to_be_consumed);
689 if pending_scroll_delta.abs() > 0.001 {
690 inner.last_scroll_delta = pending_scroll_delta;
691 }
692 });
693 }
694
695 pub(crate) fn next_measure_cycle_id(&self) -> u64 {
696 self.inner
697 .try_with(|cell| {
698 let mut inner = cell.borrow_mut();
699 let id = inner.next_measure_cycle_id;
700 inner.next_measure_cycle_id = inner.next_measure_cycle_id.saturating_add(1);
701 id
702 })
703 .unwrap_or(0)
704 }
705
706 pub(crate) fn next_item_measure_pass_id(&self) -> u64 {
707 self.inner
708 .try_with(|cell| {
709 let mut inner = cell.borrow_mut();
710 let id = inner.next_item_measure_pass_id;
711 inner.next_item_measure_pass_id = inner.next_item_measure_pass_id.saturating_add(1);
712 id
713 })
714 .unwrap_or(0)
715 }
716
717 fn record_item_size_sample(inner: &mut LazyListStateInner, size: f32) {
718 inner.total_measured_items += 1;
719 let n = inner.total_measured_items as f32;
720 inner.average_item_size = inner.average_item_size * ((n - 1.0) / n) + size / n;
721 }
722
723 fn next_item_size_cache_tick(inner: &mut LazyListStateInner) -> u64 {
724 inner.item_size_clock = inner.item_size_clock.saturating_add(1);
725 inner.item_size_clock
726 }
727
728 fn insert_item_size(inner: &mut LazyListStateInner, index: usize, size: f32) -> bool {
729 let tick = Self::next_item_size_cache_tick(inner);
730 let item = CachedItemSize {
731 size,
732 last_used: tick,
733 };
734 if let Some(cached) = inner.item_size_cache.get_mut(&index) {
735 *cached = item;
736 return false;
737 }
738 if inner.item_size_cache.len() >= ITEM_SIZE_CACHE_CAPACITY {
739 Self::evict_stale_item_sizes(inner, tick);
740 }
741 inner.item_size_cache.insert(index, item);
742 true
743 }
744
745 fn evict_stale_item_sizes(inner: &mut LazyListStateInner, tick: u64) {
750 let oldest_kept = tick.saturating_sub((ITEM_SIZE_CACHE_CAPACITY / 2) as u64);
751 inner
752 .item_size_cache
753 .retain(|_, item| item.last_used > oldest_kept);
754 }
755
756 pub fn cache_item_size(&self, index: usize, size: f32) {
758 self.inner.try_with(|cell| {
759 let mut inner = cell.borrow_mut();
760 if Self::insert_item_size(&mut inner, index, size) {
761 Self::record_item_size_sample(&mut inner, size);
762 }
763 });
764 }
765
766 pub fn cache_item_sizes<I>(&self, sizes: I) -> f32
768 where
769 I: IntoIterator<Item = (usize, f32)>,
770 {
771 self.inner
772 .try_with(|cell| {
773 let mut inner = cell.borrow_mut();
774 for (index, size) in sizes {
775 if Self::insert_item_size(&mut inner, index, size) {
776 Self::record_item_size_sample(&mut inner, size);
777 }
778 }
779 inner.average_item_size
780 })
781 .unwrap_or(super::DEFAULT_ITEM_SIZE_ESTIMATE)
782 }
783
784 pub fn get_cached_size(&self, index: usize) -> Option<f32> {
786 self.inner
787 .try_with(|cell| {
788 let mut inner = cell.borrow_mut();
789 let tick = Self::next_item_size_cache_tick(&mut inner);
790 let item = inner.item_size_cache.get_mut(&index)?;
791 item.last_used = tick;
792 Some(item.size)
793 })
794 .flatten()
795 }
796
797 pub fn average_item_size(&self) -> f32 {
799 self.inner
800 .try_with(|cell| cell.borrow().average_item_size)
801 .unwrap_or(super::DEFAULT_ITEM_SIZE_ESTIMATE)
802 }
803
804 pub fn nearest_range(&self) -> std::ops::Range<usize> {
806 self.scroll_position.nearest_range()
807 }
808
809 pub(crate) fn update_scroll_position(
810 &self,
811 first_visible_item_index: usize,
812 first_visible_item_scroll_offset: f32,
813 ) {
814 self.scroll_position.update_from_measure_result(
815 first_visible_item_index,
816 first_visible_item_scroll_offset,
817 None,
818 );
819 }
820
821 pub(crate) fn update_scroll_position_with_key(
822 &self,
823 first_visible_item_index: usize,
824 first_visible_item_scroll_offset: f32,
825 first_visible_item_key: u64,
826 ) {
827 self.scroll_position.update_from_measure_result(
828 first_visible_item_index,
829 first_visible_item_scroll_offset,
830 Some(first_visible_item_key),
831 );
832 }
833
834 pub fn update_scroll_position_if_item_moved<F>(
842 &self,
843 new_item_count: usize,
844 get_index_by_key: F,
845 ) -> usize
846 where
847 F: Fn(u64) -> Option<usize>,
848 {
849 self.scroll_position
850 .update_if_first_item_moved(new_item_count, get_index_by_key)
851 }
852
853 pub fn first_visible_item_moved<F>(&self, new_item_count: usize, get_index_by_key: F) -> bool
856 where
857 F: Fn(u64) -> Option<usize>,
858 {
859 self.scroll_position
860 .first_item_moved(new_item_count, get_index_by_key)
861 }
862
863 pub(crate) fn update_layout_info(&self, mut info: LazyListLayoutInfo) {
864 self.inner.try_with(|cell| {
865 let mut inner = cell.borrow_mut();
866 info.snap_anchor_offset = continuous_snap_anchor_offset(&inner.layout_info, &info);
867 let mut spare = std::mem::replace(&mut inner.layout_info, info).visible_items_info;
868 spare.clear();
869 inner.spare_items_info = spare;
870 });
871 }
872
873 pub(crate) fn spare_items_info(&self) -> Vec<LazyListItemInfo> {
876 self.inner
877 .try_with(|cell| std::mem::take(&mut cell.borrow_mut().spare_items_info))
878 .unwrap_or_default()
879 }
880
881 pub fn can_scroll_forward(&self) -> bool {
886 if !self.can_scroll_forward_state.is_alive() {
887 return false;
888 }
889 self.can_scroll_forward_state.subscribe_current_scope_only();
890 self.can_scroll_forward_non_reactive()
891 }
892
893 pub fn can_scroll_forward_non_reactive(&self) -> bool {
895 if !self.can_scroll_forward_state.is_alive() {
896 return false;
897 }
898 self.inner
899 .try_with(|cell| cell.borrow().current_can_scroll_forward)
900 .unwrap_or(false)
901 }
902
903 pub fn can_scroll_backward(&self) -> bool {
908 if !self.can_scroll_backward_state.is_alive() {
909 return false;
910 }
911 self.can_scroll_backward_state
912 .subscribe_current_scope_only();
913 self.can_scroll_backward_non_reactive()
914 }
915
916 pub fn can_scroll_backward_non_reactive(&self) -> bool {
918 if !self.can_scroll_backward_state.is_alive() {
919 return false;
920 }
921 self.inner
922 .try_with(|cell| cell.borrow().current_can_scroll_backward)
923 .unwrap_or(false)
924 }
925
926 pub(crate) fn update_scroll_bounds(&self) {
927 if !self.can_scroll_forward_state.is_alive() || !self.can_scroll_backward_state.is_alive() {
928 return;
929 }
930 let Some(cell) = self.inner.try_get() else {
931 return;
932 };
933 let can_forward = {
934 let inner = cell.borrow();
935 let info = &inner.layout_info;
936 let viewport_end = info.viewport_end_offset - info.after_content_padding;
937 if let Some(last_visible) = info.visible_items_info.last() {
940 last_visible.index < info.total_items_count.saturating_sub(1)
941 || info
942 .visible_items_info
943 .iter()
944 .rev()
945 .any(|item| item.offset + item.size > viewport_end)
946 } else {
947 false
948 }
949 };
950
951 let (first_index, first_offset) = self.try_position().unwrap_or((0, 0.0));
952 let can_backward = first_index > 0 || first_offset > 0.0;
953
954 {
955 let mut inner = cell.borrow_mut();
956 inner.current_can_scroll_forward = can_forward;
957 inner.current_can_scroll_backward = can_backward;
958 }
959
960 if self.can_scroll_forward_state.get_non_reactive() != can_forward {
961 self.can_scroll_forward_state.set(can_forward);
962 }
963 if self.can_scroll_backward_state.get_non_reactive() != can_backward {
964 self.can_scroll_backward_state.set(can_backward);
965 }
966 }
967
968 pub fn add_invalidate_callback(&self, callback: Rc<dyn Fn()>) -> u64 {
970 self.inner
971 .try_with(|cell| {
972 let mut inner = cell.borrow_mut();
973 let id = inner.next_callback_id;
974 inner.next_callback_id += 1;
975 inner.invalidate_callbacks.push((id, callback));
976 id
977 })
978 .unwrap_or(0)
979 }
980
981 pub fn try_register_layout_callback(
989 &self,
990 node_id: NodeId,
991 callback: Rc<dyn Fn()>,
992 ) -> Option<u64> {
993 if !self.inner.is_alive() {
994 return None;
995 }
996 self.inner.with(|cell| {
997 let mut inner = cell.borrow_mut();
998 if let Some(existing_id) = inner.layout_invalidation_callback_id {
999 inner
1000 .invalidate_callbacks
1001 .retain(|(cb_id, _)| *cb_id != existing_id);
1002 }
1003 let id = inner.next_callback_id;
1004 inner.next_callback_id += 1;
1005 inner.invalidate_callbacks.push((id, callback));
1006 inner.layout_invalidation_callback_id = Some(id);
1007 inner.layout_invalidation_node_id = Some(node_id);
1008 Some(id)
1009 })
1010 }
1011
1012 pub fn remove_invalidate_callback(&self, id: u64) {
1014 self.inner.try_with(|cell| {
1015 let mut inner = cell.borrow_mut();
1016 inner.invalidate_callbacks.retain(|(cb_id, _)| *cb_id != id);
1017 if inner.layout_invalidation_callback_id == Some(id) {
1018 inner.layout_invalidation_callback_id = None;
1019 inner.layout_invalidation_node_id = None;
1020 }
1021 });
1022 }
1023
1024 fn invalidate(&self) {
1025 if let Some(cell) = self.inner.try_get() {
1026 Self::run_invalidate_callbacks(&cell);
1027 }
1028 }
1029
1030 fn run_invalidate_callbacks(cell: &RefCell<LazyListStateInner>) {
1033 let callbacks: Vec<_> = cell
1034 .borrow()
1035 .invalidate_callbacks
1036 .iter()
1037 .map(|(_, cb)| Rc::clone(cb))
1038 .collect();
1039
1040 for callback in callbacks {
1041 callback();
1042 }
1043 }
1044}
1045
1046#[derive(Clone, Default, Debug)]
1048pub struct LazyListLayoutInfo {
1049 pub visible_items_info: Vec<LazyListItemInfo>,
1051
1052 pub total_items_count: usize,
1054
1055 pub raw_viewport_size: f32,
1057
1058 pub is_infinite_viewport: bool,
1060
1061 pub viewport_size: f32,
1063
1064 pub viewport_start_offset: f32,
1066
1067 pub viewport_end_offset: f32,
1069
1070 pub before_content_padding: f32,
1072
1073 pub after_content_padding: f32,
1075
1076 pub main_axis_item_spacing: f32,
1078
1079 pub snap_anchor_offset: f32,
1081
1082 pub reverse_layout: bool,
1084}
1085
1086#[derive(Clone, Debug)]
1088pub struct LazyListItemInfo {
1089 pub index: usize,
1091
1092 pub key: u64,
1094
1095 pub offset: f32,
1097
1098 pub size: f32,
1100}
1101
1102fn continuous_snap_anchor_offset(
1103 previous: &LazyListLayoutInfo,
1104 current: &LazyListLayoutInfo,
1105) -> f32 {
1106 let Some(first_current) = current.visible_items_info.first() else {
1107 return 0.0;
1108 };
1109
1110 for current_item in ¤t.visible_items_info {
1111 if let Some(previous_item) = previous
1112 .visible_items_info
1113 .iter()
1114 .find(|item| item.key == current_item.key)
1115 {
1116 let previous_offset = snap_anchor_item_offset(previous, previous_item);
1117 let current_offset = snap_anchor_item_offset(current, current_item);
1118 return previous.snap_anchor_offset + current_offset - previous_offset;
1119 }
1120 }
1121
1122 snap_anchor_item_offset(current, first_current)
1123}
1124
1125fn snap_anchor_item_offset(info: &LazyListLayoutInfo, item: &LazyListItemInfo) -> f32 {
1126 if info.reverse_layout {
1127 info.viewport_size + info.viewport_start_offset - item.offset - item.size
1128 } else {
1129 item.offset - info.viewport_start_offset
1130 }
1131}
1132
1133#[cfg(test)]
1134#[path = "tests/lazy_list_state_test_helpers.rs"]
1135pub(crate) mod test_helpers;
1136
1137#[cfg(test)]
1138#[path = "tests/lazy_list_state_tests.rs"]
1139mod tests;