use std::{cell::RefCell, cmp::Reverse, collections::BinaryHeap, rc::Rc};
use cranpose_core::{MutableState, NodeId, StateId};
use cranpose_macros::composable;
use super::{
diagnostics,
nearest_range::NearestRangeState,
prefetch::{PrefetchScheduler, PrefetchStrategy},
};
const MAX_PENDING_SCROLL_DELTA: f32 = 2000.0;
const ITEM_SIZE_CACHE_CAPACITY: usize = 8192;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct LazyListMeasureStateSnapshot {
pub(crate) first_visible_item_index: usize,
pub(crate) first_visible_item_scroll_offset: f32,
pub(crate) pending_scroll_delta: f32,
pub(crate) pending_scroll_to: Option<(usize, f32)>,
pub(crate) average_item_size: f32,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct LazyLayoutStats {
pub items_in_use: usize,
pub items_in_pool: usize,
pub total_composed: usize,
pub reuse_count: usize,
}
#[derive(Clone, Copy)]
pub struct LazyListScrollPosition {
index: MutableState<usize>,
scroll_offset: MutableState<f32>,
inner: MutableState<Rc<RefCell<ScrollPositionInner>>>,
}
struct ScrollPositionInner {
current_index: usize,
current_scroll_offset: f32,
last_known_first_item_key: Option<u64>,
nearest_range_state: NearestRangeState,
}
impl LazyListScrollPosition {
fn is_alive(&self) -> bool {
self.index.is_alive() && self.scroll_offset.is_alive() && self.inner.is_alive()
}
fn current_index(&self) -> usize {
self.inner
.try_with(|rc| rc.borrow().current_index)
.unwrap_or(0)
}
fn current_scroll_offset(&self) -> f32 {
self.inner
.try_with(|rc| rc.borrow().current_scroll_offset)
.unwrap_or(0.0)
}
pub fn index(&self) -> usize {
if !self.index.is_alive() || !self.inner.is_alive() {
return 0;
}
self.index.subscribe_current_scope_only();
self.current_index()
}
pub fn scroll_offset(&self) -> f32 {
if !self.scroll_offset.is_alive() || !self.inner.is_alive() {
return 0.0;
}
self.scroll_offset.subscribe_current_scope_only();
self.current_scroll_offset()
}
pub(crate) fn update_from_measure_result(
&self,
first_visible_index: usize,
first_visible_scroll_offset: f32,
first_visible_item_key: Option<u64>,
) {
if !self.is_alive() {
return;
}
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
inner.current_index = first_visible_index;
inner.current_scroll_offset = first_visible_scroll_offset;
inner.last_known_first_item_key = first_visible_item_key;
inner.nearest_range_state.update(first_visible_index);
});
if self.index.get_non_reactive() != first_visible_index {
self.index.set(first_visible_index);
}
if (self.scroll_offset.get_non_reactive() - first_visible_scroll_offset).abs() > 0.001 {
self.scroll_offset.set(first_visible_scroll_offset);
}
}
pub(crate) fn request_position_and_forget_last_known_key(
&self,
index: usize,
scroll_offset: f32,
) {
if !self.is_alive() {
return;
}
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
inner.current_index = index;
inner.current_scroll_offset = scroll_offset;
inner.last_known_first_item_key = None;
inner.nearest_range_state.update(index);
});
if self.index.get_non_reactive() != index {
self.index.set(index);
}
if (self.scroll_offset.get_non_reactive() - scroll_offset).abs() > 0.001 {
self.scroll_offset.set(scroll_offset);
}
}
pub(crate) fn update_if_first_item_moved<F>(
&self,
new_item_count: usize,
find_by_key: F,
) -> usize
where
F: Fn(u64) -> Option<usize>,
{
if !self.index.is_alive() || !self.inner.is_alive() {
return 0;
}
let current_index = self.current_index();
let last_key = self
.inner
.try_with(|rc| rc.borrow().last_known_first_item_key)
.flatten();
let new_index = match last_key {
None => current_index.min(new_item_count.saturating_sub(1)),
Some(key) => find_by_key(key)
.unwrap_or_else(|| current_index.min(new_item_count.saturating_sub(1))),
};
if current_index != new_index {
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
inner.current_index = new_index;
inner.nearest_range_state.update(new_index);
});
self.index.set(new_index);
}
new_index
}
pub fn nearest_range(&self) -> std::ops::Range<usize> {
self.inner
.try_with(|rc| rc.borrow().nearest_range_state.range())
.unwrap_or(0..0)
}
}
#[derive(Clone, Copy)]
pub struct LazyListState {
scroll_position: LazyListScrollPosition,
can_scroll_forward_state: MutableState<bool>,
can_scroll_backward_state: MutableState<bool>,
stats_state: MutableState<LazyLayoutStats>,
inner: MutableState<Rc<RefCell<LazyListStateInner>>>,
}
impl PartialEq for LazyListState {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
#[derive(Clone, Copy)]
struct CachedItemSize {
size: f32,
last_used: u64,
}
struct LazyListStateInner {
scroll_to_be_consumed: f32,
pending_scroll_to_index: Option<(usize, f32)>,
layout_info: LazyListLayoutInfo,
current_can_scroll_forward: bool,
current_can_scroll_backward: bool,
invalidate_callbacks: Vec<(u64, Rc<dyn Fn()>)>,
next_callback_id: u64,
layout_invalidation_callback_id: Option<u64>,
layout_invalidation_node_id: Option<NodeId>,
total_composed: usize,
reuse_count: usize,
item_size_cache: std::collections::HashMap<usize, CachedItemSize>,
item_size_eviction_queue: BinaryHeap<Reverse<(u64, usize)>>,
item_size_clock: u64,
average_item_size: f32,
total_measured_items: usize,
next_measure_cycle_id: u64,
next_item_measure_pass_id: u64,
prefetch_scheduler: PrefetchScheduler,
prefetch_strategy: PrefetchStrategy,
last_scroll_direction: f32,
}
#[composable]
#[track_caller]
pub fn rememberLazyListState() -> LazyListState {
rememberLazyListStateWithPosition(0, 0.0)
}
#[composable]
pub fn rememberLazyListStateWithPosition(
initial_first_visible_item_index: usize,
initial_first_visible_item_scroll_offset: f32,
) -> LazyListState {
cranpose_core::remember(move || {
LazyListState::new(
initial_first_visible_item_index,
initial_first_visible_item_scroll_offset,
)
})
.with(|state| *state)
}
impl LazyListState {
pub fn new(
initial_first_visible_item_index: usize,
initial_first_visible_item_scroll_offset: f32,
) -> Self {
LazyListState {
scroll_position: LazyListScrollPosition {
index: cranpose_core::mutableStateOf(initial_first_visible_item_index),
scroll_offset: cranpose_core::mutableStateOf(
initial_first_visible_item_scroll_offset,
),
inner: cranpose_core::mutableStateOfNeverEqual(Rc::new(RefCell::new(
ScrollPositionInner {
current_index: initial_first_visible_item_index,
current_scroll_offset: initial_first_visible_item_scroll_offset,
last_known_first_item_key: None,
nearest_range_state: NearestRangeState::new(
initial_first_visible_item_index,
),
},
))),
},
can_scroll_forward_state: cranpose_core::mutableStateOf(false),
can_scroll_backward_state: cranpose_core::mutableStateOf(false),
stats_state: cranpose_core::mutableStateOf(LazyLayoutStats::default()),
inner: cranpose_core::mutableStateOfNeverEqual(Rc::new(RefCell::new(
LazyListStateInner {
scroll_to_be_consumed: 0.0,
pending_scroll_to_index: None,
layout_info: LazyListLayoutInfo::default(),
current_can_scroll_forward: false,
current_can_scroll_backward: false,
invalidate_callbacks: Vec::new(),
next_callback_id: 1,
layout_invalidation_callback_id: None,
layout_invalidation_node_id: None,
total_composed: 0,
reuse_count: 0,
item_size_cache: std::collections::HashMap::new(),
item_size_eviction_queue: BinaryHeap::new(),
item_size_clock: 0,
average_item_size: super::DEFAULT_ITEM_SIZE_ESTIMATE,
total_measured_items: 0,
next_measure_cycle_id: 1,
next_item_measure_pass_id: 1,
prefetch_scheduler: PrefetchScheduler::new(),
prefetch_strategy: PrefetchStrategy::default(),
last_scroll_direction: 0.0,
},
))),
}
}
pub fn inner_ptr(&self) -> *const () {
self.inner
.try_with(|rc| Rc::as_ptr(rc) as *const ())
.unwrap_or(std::ptr::null())
}
pub fn first_visible_item_index(&self) -> usize {
self.scroll_position.index()
}
pub fn total_items_count(&self) -> usize {
self.inner
.with(|rc| rc.borrow().layout_info.total_items_count)
}
pub fn first_visible_item_index_non_reactive(&self) -> usize {
self.scroll_position.current_index()
}
pub fn first_visible_item_scroll_offset(&self) -> f32 {
self.scroll_position.scroll_offset()
}
pub fn first_visible_item_scroll_offset_non_reactive(&self) -> f32 {
self.scroll_position.current_scroll_offset()
}
#[doc(hidden)]
pub fn reactive_state_ids(&self) -> [StateId; 5] {
[
self.scroll_position.index.runtime_state_id(),
self.scroll_position.scroll_offset.runtime_state_id(),
self.can_scroll_forward_state.runtime_state_id(),
self.can_scroll_backward_state.runtime_state_id(),
self.stats_state.runtime_state_id(),
]
}
pub fn layout_info(&self) -> LazyListLayoutInfo {
self.inner
.try_with(|rc| rc.borrow().layout_info.clone())
.unwrap_or_default()
}
pub fn stats(&self) -> LazyLayoutStats {
if !self.stats_state.is_alive() || !self.inner.is_alive() {
return LazyLayoutStats::default();
}
let reactive = self.stats_state.get();
let (total_composed, reuse_count) = self.inner.with(|rc| {
let inner = rc.borrow();
(inner.total_composed, inner.reuse_count)
});
LazyLayoutStats {
items_in_use: reactive.items_in_use,
items_in_pool: reactive.items_in_pool,
total_composed,
reuse_count,
}
}
pub fn update_stats(&self, items_in_use: usize, items_in_pool: usize) {
if !self.stats_state.is_alive() || !self.inner.is_alive() {
return;
}
let current = self.stats_state.get_non_reactive();
let should_update_reactive = if items_in_use > current.items_in_use {
true
} else if items_in_use < current.items_in_use {
current.items_in_use - items_in_use > 1
} else {
false
};
if should_update_reactive {
self.stats_state.set(LazyLayoutStats {
items_in_use,
items_in_pool,
..current
});
}
}
pub fn record_composition(&self, was_reused: bool) {
if !self.inner.is_alive() {
return;
}
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
inner.total_composed += 1;
if was_reused {
inner.reuse_count += 1;
}
});
}
pub fn record_scroll_direction(&self, delta: f32) {
if delta.abs() > 0.001 {
if !self.inner.is_alive() {
return;
}
self.inner.with(|rc| {
rc.borrow_mut().last_scroll_direction = -delta.signum();
});
}
}
pub fn update_prefetch_queue(
&self,
first_visible_index: usize,
last_visible_index: usize,
total_items: usize,
) {
if !self.inner.is_alive() {
return;
}
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
let direction = inner.last_scroll_direction;
let strategy = inner.prefetch_strategy.clone();
inner.prefetch_scheduler.update(
first_visible_index,
last_visible_index,
total_items,
direction,
&strategy,
);
});
}
pub fn take_prefetch_indices(&self) -> Vec<usize> {
self.inner
.try_with(|rc| {
let mut inner = rc.borrow_mut();
let mut indices = Vec::new();
while let Some(idx) = inner.prefetch_scheduler.next_prefetch() {
indices.push(idx);
}
indices
})
.unwrap_or_default()
}
pub fn scroll_to_item(&self, index: usize, scroll_offset: f32) {
if !self.inner.is_alive() {
return;
}
if diagnostics::telemetry_enabled() {
log::warn!(
"[lazy-measure-telemetry] scroll_to_item request index={index} offset={scroll_offset:.2}"
);
}
self.inner.with(|rc| {
rc.borrow_mut().pending_scroll_to_index = Some((index, scroll_offset));
});
self.scroll_position
.request_position_and_forget_last_known_key(index, scroll_offset);
self.invalidate();
}
pub fn dispatch_scroll_delta(&self, delta: f32) -> f32 {
if !self.inner.is_alive() {
return 0.0;
}
let has_scroll_bounds = self
.inner
.with(|rc| rc.borrow().layout_info.total_items_count > 0);
let pushing_forward = delta < -0.001;
let pushing_backward = delta > 0.001;
let can_scroll_forward =
self.can_scroll_forward_state.is_alive() && self.can_scroll_forward_non_reactive();
let can_scroll_backward =
self.can_scroll_backward_state.is_alive() && self.can_scroll_backward_non_reactive();
let blocked_by_bounds = has_scroll_bounds
&& ((pushing_forward && !can_scroll_forward)
|| (pushing_backward && !can_scroll_backward));
if blocked_by_bounds {
let should_invalidate = self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
let pending_before = inner.scroll_to_be_consumed;
if pending_before.abs() > 0.001 && pending_before.signum() == delta.signum() {
inner.scroll_to_be_consumed = 0.0;
}
if diagnostics::telemetry_enabled() {
log::warn!(
"[lazy-measure-telemetry] dispatch_scroll_delta blocked_by_bounds delta={:.2} pending_before={:.2} pending_after={:.2}",
delta,
pending_before,
inner.scroll_to_be_consumed
);
}
(inner.scroll_to_be_consumed - pending_before).abs() > 0.001
});
if should_invalidate {
self.invalidate();
}
return 0.0;
}
let mut accepted_delta = 0.0f32;
let should_invalidate = self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
accepted_delta = delta;
let pending_before = inner.scroll_to_be_consumed;
let pending = inner.scroll_to_be_consumed;
let reverse_input = pending.abs() > 0.001
&& delta.abs() > 0.001
&& pending.signum() != delta.signum();
if reverse_input {
if diagnostics::telemetry_enabled() {
log::warn!(
"[lazy-measure-telemetry] dispatch_scroll_delta direction_change pending={pending:.2} new_delta={delta:.2}"
);
}
inner.scroll_to_be_consumed = delta;
} else {
inner.scroll_to_be_consumed += delta;
}
inner.scroll_to_be_consumed = inner
.scroll_to_be_consumed
.clamp(-MAX_PENDING_SCROLL_DELTA, MAX_PENDING_SCROLL_DELTA);
if diagnostics::telemetry_enabled() {
log::warn!(
"[lazy-measure-telemetry] dispatch_scroll_delta delta={:.2} pending={:.2}",
delta,
inner.scroll_to_be_consumed
);
}
(inner.scroll_to_be_consumed - pending_before).abs() > 0.001
});
if should_invalidate {
self.invalidate();
}
accepted_delta
}
pub fn peek_scroll_delta(&self) -> f32 {
self.inner
.try_with(|rc| rc.borrow().scroll_to_be_consumed)
.unwrap_or(0.0)
}
pub(crate) fn begin_measure_pass(&self) -> LazyListMeasureStateSnapshot {
let (pending_scroll_delta, pending_scroll_to, average_item_size) = self
.inner
.try_with(|rc| {
let mut inner = rc.borrow_mut();
let pending_scroll_to = inner.pending_scroll_to_index.take();
let pending_scroll_delta = inner.scroll_to_be_consumed;
inner.scroll_to_be_consumed = 0.0;
(
pending_scroll_delta,
pending_scroll_to,
inner.average_item_size,
)
})
.unwrap_or((0.0, None, super::DEFAULT_ITEM_SIZE_ESTIMATE));
LazyListMeasureStateSnapshot {
first_visible_item_index: self.scroll_position.current_index(),
first_visible_item_scroll_offset: self.scroll_position.current_scroll_offset(),
pending_scroll_delta,
pending_scroll_to,
average_item_size,
}
}
pub(crate) fn next_measure_cycle_id(&self) -> u64 {
self.inner
.try_with(|rc| {
let mut inner = rc.borrow_mut();
let id = inner.next_measure_cycle_id;
inner.next_measure_cycle_id = inner.next_measure_cycle_id.saturating_add(1);
id
})
.unwrap_or(0)
}
pub(crate) fn next_item_measure_pass_id(&self) -> u64 {
self.inner
.try_with(|rc| {
let mut inner = rc.borrow_mut();
let id = inner.next_item_measure_pass_id;
inner.next_item_measure_pass_id = inner.next_item_measure_pass_id.saturating_add(1);
id
})
.unwrap_or(0)
}
fn record_item_size_sample(inner: &mut LazyListStateInner, size: f32) {
inner.total_measured_items += 1;
let n = inner.total_measured_items as f32;
inner.average_item_size = inner.average_item_size * ((n - 1.0) / n) + size / n;
}
fn next_item_size_cache_tick(inner: &mut LazyListStateInner) -> u64 {
inner.item_size_clock = inner.item_size_clock.saturating_add(1);
inner.item_size_clock
}
fn insert_item_size(inner: &mut LazyListStateInner, index: usize, size: f32) -> bool {
use std::collections::hash_map::Entry;
let tick = Self::next_item_size_cache_tick(inner);
if let Entry::Occupied(mut entry) = inner.item_size_cache.entry(index) {
entry.insert(CachedItemSize {
size,
last_used: tick,
});
Self::push_item_size_cache_ticket(inner, tick, index);
return false;
}
if inner.item_size_cache.len() >= ITEM_SIZE_CACHE_CAPACITY {
Self::evict_one_item_size(inner);
}
inner.item_size_cache.insert(
index,
CachedItemSize {
size,
last_used: tick,
},
);
Self::push_item_size_cache_ticket(inner, tick, index);
true
}
fn push_item_size_cache_ticket(inner: &mut LazyListStateInner, last_used: u64, index: usize) {
inner
.item_size_eviction_queue
.push(Reverse((last_used, index)));
let compact_limit = inner
.item_size_cache
.len()
.saturating_mul(4)
.max(ITEM_SIZE_CACHE_CAPACITY);
if inner.item_size_eviction_queue.len() > compact_limit {
Self::rebuild_item_size_eviction_queue(inner);
}
}
fn rebuild_item_size_eviction_queue(inner: &mut LazyListStateInner) {
inner.item_size_eviction_queue = inner
.item_size_cache
.iter()
.map(|(index, item)| Reverse((item.last_used, *index)))
.collect();
}
fn evict_one_item_size(inner: &mut LazyListStateInner) {
while let Some(Reverse((last_used, index))) = inner.item_size_eviction_queue.pop() {
let Some(current) = inner.item_size_cache.get(&index) else {
continue;
};
if current.last_used != last_used {
continue;
}
inner.item_size_cache.remove(&index);
return;
}
}
pub fn cache_item_size(&self, index: usize, size: f32) {
if !self.inner.is_alive() {
return;
}
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
if Self::insert_item_size(&mut inner, index, size) {
Self::record_item_size_sample(&mut inner, size);
}
});
}
pub fn cache_item_sizes<I>(&self, sizes: I) -> f32
where
I: IntoIterator<Item = (usize, f32)>,
{
if !self.inner.is_alive() {
return super::DEFAULT_ITEM_SIZE_ESTIMATE;
}
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
for (index, size) in sizes {
if Self::insert_item_size(&mut inner, index, size) {
Self::record_item_size_sample(&mut inner, size);
}
}
inner.average_item_size
})
}
pub fn get_cached_size(&self, index: usize) -> Option<f32> {
self.inner
.try_with(|rc| {
let mut inner = rc.borrow_mut();
let tick = Self::next_item_size_cache_tick(&mut inner);
let item = inner.item_size_cache.get_mut(&index)?;
item.last_used = tick;
let size = item.size;
Self::push_item_size_cache_ticket(&mut inner, tick, index);
Some(size)
})
.flatten()
}
pub fn average_item_size(&self) -> f32 {
self.inner
.try_with(|rc| rc.borrow().average_item_size)
.unwrap_or(super::DEFAULT_ITEM_SIZE_ESTIMATE)
}
pub fn nearest_range(&self) -> std::ops::Range<usize> {
self.scroll_position.nearest_range()
}
pub(crate) fn update_scroll_position(
&self,
first_visible_item_index: usize,
first_visible_item_scroll_offset: f32,
) {
self.scroll_position.update_from_measure_result(
first_visible_item_index,
first_visible_item_scroll_offset,
None,
);
}
pub(crate) fn update_scroll_position_with_key(
&self,
first_visible_item_index: usize,
first_visible_item_scroll_offset: f32,
first_visible_item_key: u64,
) {
self.scroll_position.update_from_measure_result(
first_visible_item_index,
first_visible_item_scroll_offset,
Some(first_visible_item_key),
);
}
pub fn update_scroll_position_if_item_moved<F>(
&self,
new_item_count: usize,
get_index_by_key: F,
) -> usize
where
F: Fn(u64) -> Option<usize>,
{
self.scroll_position
.update_if_first_item_moved(new_item_count, get_index_by_key)
}
pub(crate) fn update_layout_info(&self, mut info: LazyListLayoutInfo) {
if !self.inner.is_alive() {
return;
}
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
info.snap_anchor_offset = continuous_snap_anchor_offset(&inner.layout_info, &info);
inner.layout_info = info;
});
}
pub fn can_scroll_forward(&self) -> bool {
if !self.can_scroll_forward_state.is_alive() {
return false;
}
self.can_scroll_forward_state.subscribe_current_scope_only();
self.can_scroll_forward_non_reactive()
}
pub fn can_scroll_forward_non_reactive(&self) -> bool {
if !self.can_scroll_forward_state.is_alive() {
return false;
}
self.inner
.try_with(|rc| rc.borrow().current_can_scroll_forward)
.unwrap_or(false)
}
pub fn can_scroll_backward(&self) -> bool {
if !self.can_scroll_backward_state.is_alive() {
return false;
}
self.can_scroll_backward_state
.subscribe_current_scope_only();
self.can_scroll_backward_non_reactive()
}
pub fn can_scroll_backward_non_reactive(&self) -> bool {
if !self.can_scroll_backward_state.is_alive() {
return false;
}
self.inner
.try_with(|rc| rc.borrow().current_can_scroll_backward)
.unwrap_or(false)
}
pub(crate) fn update_scroll_bounds(&self) {
if !self.inner.is_alive()
|| !self.can_scroll_forward_state.is_alive()
|| !self.can_scroll_backward_state.is_alive()
{
return;
}
let can_forward = self.inner.with(|rc| {
let inner = rc.borrow();
let info = &inner.layout_info;
let viewport_end = info.viewport_size - info.after_content_padding;
if let Some(last_visible) = info.visible_items_info.last() {
last_visible.index < info.total_items_count.saturating_sub(1)
|| (last_visible.offset + last_visible.size) > viewport_end
} else {
false
}
});
let can_backward = self.scroll_position.current_index() > 0
|| self.scroll_position.current_scroll_offset() > 0.0;
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
inner.current_can_scroll_forward = can_forward;
inner.current_can_scroll_backward = can_backward;
});
if self.can_scroll_forward_state.get_non_reactive() != can_forward {
self.can_scroll_forward_state.set(can_forward);
}
if self.can_scroll_backward_state.get_non_reactive() != can_backward {
self.can_scroll_backward_state.set(can_backward);
}
}
pub fn add_invalidate_callback(&self, callback: Rc<dyn Fn()>) -> u64 {
if !self.inner.is_alive() {
return 0;
}
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
let id = inner.next_callback_id;
inner.next_callback_id += 1;
inner.invalidate_callbacks.push((id, callback));
id
})
}
pub fn try_register_layout_callback(
&self,
node_id: NodeId,
callback: Rc<dyn Fn()>,
) -> Option<u64> {
if !self.inner.is_alive() {
return None;
}
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
if let Some(existing_id) = inner.layout_invalidation_callback_id {
inner
.invalidate_callbacks
.retain(|(cb_id, _)| *cb_id != existing_id);
}
let id = inner.next_callback_id;
inner.next_callback_id += 1;
inner.invalidate_callbacks.push((id, callback));
inner.layout_invalidation_callback_id = Some(id);
inner.layout_invalidation_node_id = Some(node_id);
Some(id)
})
}
pub fn remove_invalidate_callback(&self, id: u64) {
if !self.inner.is_alive() {
return;
}
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
inner.invalidate_callbacks.retain(|(cb_id, _)| *cb_id != id);
if inner.layout_invalidation_callback_id == Some(id) {
inner.layout_invalidation_callback_id = None;
inner.layout_invalidation_node_id = None;
}
});
}
fn invalidate(&self) {
if !self.inner.is_alive() {
return;
}
let callbacks: Vec<_> = self.inner.with(|rc| {
rc.borrow()
.invalidate_callbacks
.iter()
.map(|(_, cb)| Rc::clone(cb))
.collect()
});
for callback in callbacks {
callback();
}
}
}
#[derive(Clone, Default, Debug)]
pub struct LazyListLayoutInfo {
pub visible_items_info: Vec<LazyListItemInfo>,
pub total_items_count: usize,
pub raw_viewport_size: f32,
pub is_infinite_viewport: bool,
pub viewport_size: f32,
pub viewport_start_offset: f32,
pub viewport_end_offset: f32,
pub before_content_padding: f32,
pub after_content_padding: f32,
pub snap_anchor_offset: f32,
pub reverse_layout: bool,
}
#[derive(Clone, Debug)]
pub struct LazyListItemInfo {
pub index: usize,
pub key: u64,
pub offset: f32,
pub size: f32,
}
fn continuous_snap_anchor_offset(
previous: &LazyListLayoutInfo,
current: &LazyListLayoutInfo,
) -> f32 {
let Some(first_current) = current.visible_items_info.first() else {
return 0.0;
};
for current_item in ¤t.visible_items_info {
if let Some(previous_item) = previous
.visible_items_info
.iter()
.find(|item| item.key == current_item.key)
{
let previous_offset = snap_anchor_item_offset(previous, previous_item);
let current_offset = snap_anchor_item_offset(current, current_item);
return previous.snap_anchor_offset + current_offset - previous_offset;
}
}
snap_anchor_item_offset(current, first_current)
}
fn snap_anchor_item_offset(info: &LazyListLayoutInfo, item: &LazyListItemInfo) -> f32 {
if info.reverse_layout {
info.viewport_size - item.offset - item.size
} else {
item.offset
}
}
#[cfg(test)]
#[path = "tests/lazy_list_state_test_helpers.rs"]
pub mod test_helpers;
#[cfg(test)]
#[path = "tests/lazy_list_state_tests.rs"]
mod tests;