use std::cell::RefCell;
use std::rc::Rc;
use cranpose_core::MutableState;
use cranpose_macros::composable;
use super::nearest_range::NearestRangeState;
use super::prefetch::{PrefetchScheduler, PrefetchStrategy};
#[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 {
last_known_first_item_key: Option<u64>,
nearest_range_state: NearestRangeState,
}
impl LazyListScrollPosition {
pub fn index(&self) -> usize {
self.index.get()
}
pub fn scroll_offset(&self) -> f32 {
self.scroll_offset.get()
}
pub(crate) fn update_from_measure_result(
&self,
first_visible_index: usize,
first_visible_scroll_offset: f32,
first_visible_item_key: Option<u64>,
) {
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
inner.last_known_first_item_key = first_visible_item_key;
inner.nearest_range_state.update(first_visible_index);
});
let old_index = self.index.get();
if old_index != first_visible_index {
self.index.set(first_visible_index);
}
let old_offset = self.scroll_offset.get();
if (old_offset - 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.index.get() != index {
self.index.set(index);
}
if (self.scroll_offset.get() - scroll_offset).abs() > 0.001 {
self.scroll_offset.set(scroll_offset);
}
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
inner.last_known_first_item_key = None;
inner.nearest_range_state.update(index);
});
}
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>,
{
let current_index = self.index.get();
let last_key = self.inner.with(|rc| rc.borrow().last_known_first_item_key);
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.index.set(new_index);
self.inner.with(|rc| {
rc.borrow_mut().nearest_range_state.update(new_index);
});
}
new_index
}
pub fn nearest_range(&self) -> std::ops::Range<usize> {
self.inner
.with(|rc| rc.borrow().nearest_range_state.range())
}
}
#[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 {
std::ptr::eq(self.inner_ptr(), other.inner_ptr())
}
}
struct LazyListStateInner {
scroll_to_be_consumed: f32,
pending_scroll_to_index: Option<(usize, f32)>,
layout_info: LazyListLayoutInfo,
invalidate_callbacks: Vec<(u64, Rc<dyn Fn()>)>,
next_callback_id: u64,
has_layout_invalidation_callback: bool,
total_composed: usize,
reuse_count: usize,
item_size_cache: std::collections::HashMap<usize, f32>,
item_size_lru: std::collections::VecDeque<usize>,
average_item_size: f32,
total_measured_items: usize,
prefetch_scheduler: PrefetchScheduler,
prefetch_strategy: PrefetchStrategy,
last_scroll_direction: f32,
}
#[composable]
pub fn remember_lazy_list_state() -> LazyListState {
remember_lazy_list_state_with_position(0, 0.0)
}
#[composable]
pub fn remember_lazy_list_state_with_position(
initial_first_visible_item_index: usize,
initial_first_visible_item_scroll_offset: f32,
) -> LazyListState {
let scroll_position = LazyListScrollPosition {
index: cranpose_core::useState(|| initial_first_visible_item_index),
scroll_offset: cranpose_core::useState(|| initial_first_visible_item_scroll_offset),
inner: cranpose_core::useState(|| {
Rc::new(RefCell::new(ScrollPositionInner {
last_known_first_item_key: None,
nearest_range_state: NearestRangeState::new(initial_first_visible_item_index),
}))
}),
};
let inner = cranpose_core::useState(|| {
Rc::new(RefCell::new(LazyListStateInner {
scroll_to_be_consumed: 0.0,
pending_scroll_to_index: None,
layout_info: LazyListLayoutInfo::default(),
invalidate_callbacks: Vec::new(),
next_callback_id: 1,
has_layout_invalidation_callback: false,
total_composed: 0,
reuse_count: 0,
item_size_cache: std::collections::HashMap::new(),
item_size_lru: std::collections::VecDeque::new(),
average_item_size: super::DEFAULT_ITEM_SIZE_ESTIMATE,
total_measured_items: 0,
prefetch_scheduler: PrefetchScheduler::new(),
prefetch_strategy: PrefetchStrategy::default(),
last_scroll_direction: 0.0,
}))
});
let can_scroll_forward_state = cranpose_core::useState(|| false);
let can_scroll_backward_state = cranpose_core::useState(|| false);
let stats_state = cranpose_core::useState(LazyLayoutStats::default);
LazyListState {
scroll_position,
can_scroll_forward_state,
can_scroll_backward_state,
stats_state,
inner,
}
}
impl LazyListState {
pub fn inner_ptr(&self) -> *const () {
self.inner.with(|rc| Rc::as_ptr(rc) as *const ())
}
pub fn first_visible_item_index(&self) -> usize {
self.scroll_position.index()
}
pub fn first_visible_item_scroll_offset(&self) -> f32 {
self.scroll_position.scroll_offset()
}
pub fn layout_info(&self) -> LazyListLayoutInfo {
self.inner.with(|rc| rc.borrow().layout_info.clone())
}
pub fn stats(&self) -> LazyLayoutStats {
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) {
let current = self.stats_state.get();
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) {
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 {
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,
) {
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.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
})
}
pub fn scroll_to_item(&self, index: usize, scroll_offset: f32) {
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 {
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
inner.scroll_to_be_consumed += delta;
});
self.invalidate();
delta }
pub(crate) fn consume_scroll_delta(&self) -> f32 {
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
let delta = inner.scroll_to_be_consumed;
inner.scroll_to_be_consumed = 0.0;
delta
})
}
pub fn peek_scroll_delta(&self) -> f32 {
self.inner.with(|rc| rc.borrow().scroll_to_be_consumed)
}
pub(crate) fn consume_scroll_to_index(&self) -> Option<(usize, f32)> {
self.inner
.with(|rc| rc.borrow_mut().pending_scroll_to_index.take())
}
pub fn cache_item_size(&self, index: usize, size: f32) {
use std::collections::hash_map::Entry;
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
const MAX_CACHE_SIZE: usize = 100;
if let Entry::Occupied(mut entry) = inner.item_size_cache.entry(index) {
entry.insert(size);
if let Some(pos) = inner.item_size_lru.iter().position(|&k| k == index) {
inner.item_size_lru.remove(pos);
}
inner.item_size_lru.push_back(index);
return;
}
while inner.item_size_cache.len() >= MAX_CACHE_SIZE {
if let Some(oldest) = inner.item_size_lru.pop_front() {
if inner.item_size_cache.remove(&oldest).is_some() {
break; }
} else {
break; }
}
inner.item_size_cache.insert(index, size);
inner.item_size_lru.push_back(index);
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;
});
}
pub fn get_cached_size(&self, index: usize) -> Option<f32> {
self.inner
.with(|rc| rc.borrow().item_size_cache.get(&index).copied())
}
pub fn average_item_size(&self) -> f32 {
self.inner.with(|rc| rc.borrow().average_item_size)
}
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, info: LazyListLayoutInfo) {
self.inner.with(|rc| rc.borrow_mut().layout_info = info);
}
pub fn can_scroll_forward(&self) -> bool {
self.can_scroll_forward_state.get()
}
pub fn can_scroll_backward(&self) -> bool {
self.can_scroll_backward_state.get()
}
pub(crate) fn update_scroll_bounds(&self) {
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.index() > 0 || self.scroll_position.scroll_offset() > 0.0;
if self.can_scroll_forward_state.get() != can_forward {
self.can_scroll_forward_state.set(can_forward);
}
if self.can_scroll_backward_state.get() != can_backward {
self.can_scroll_backward_state.set(can_backward);
}
}
pub fn add_invalidate_callback(&self, callback: Rc<dyn Fn()>) -> u64 {
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, callback: Rc<dyn Fn()>) -> bool {
self.inner.with(|rc| {
let mut inner = rc.borrow_mut();
if inner.has_layout_invalidation_callback {
return false;
}
inner.has_layout_invalidation_callback = true;
let id = inner.next_callback_id;
inner.next_callback_id += 1;
inner.invalidate_callbacks.push((id, callback));
true
})
}
pub fn remove_invalidate_callback(&self, id: u64) {
self.inner.with(|rc| {
rc.borrow_mut()
.invalidate_callbacks
.retain(|(cb_id, _)| *cb_id != id);
});
}
fn invalidate(&self) {
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 viewport_size: f32,
pub viewport_start_offset: f32,
pub viewport_end_offset: f32,
pub before_content_padding: f32,
pub after_content_padding: f32,
}
#[derive(Clone, Debug)]
pub struct LazyListItemInfo {
pub index: usize,
pub key: u64,
pub offset: f32,
pub size: f32,
}
#[cfg(test)]
pub mod test_helpers {
use super::*;
use cranpose_core::{DefaultScheduler, Runtime};
use std::sync::Arc;
pub fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
let _runtime = Runtime::new(Arc::new(DefaultScheduler));
f()
}
pub fn new_lazy_list_state() -> LazyListState {
new_lazy_list_state_with_position(0, 0.0)
}
pub fn new_lazy_list_state_with_position(
initial_first_visible_item_index: usize,
initial_first_visible_item_scroll_offset: f32,
) -> LazyListState {
let 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::mutableStateOf(Rc::new(RefCell::new(ScrollPositionInner {
last_known_first_item_key: None,
nearest_range_state: NearestRangeState::new(initial_first_visible_item_index),
}))),
};
let inner = cranpose_core::mutableStateOf(Rc::new(RefCell::new(LazyListStateInner {
scroll_to_be_consumed: 0.0,
pending_scroll_to_index: None,
layout_info: LazyListLayoutInfo::default(),
invalidate_callbacks: Vec::new(),
next_callback_id: 1,
has_layout_invalidation_callback: false,
total_composed: 0,
reuse_count: 0,
item_size_cache: std::collections::HashMap::new(),
item_size_lru: std::collections::VecDeque::new(),
average_item_size: super::super::DEFAULT_ITEM_SIZE_ESTIMATE,
total_measured_items: 0,
prefetch_scheduler: PrefetchScheduler::new(),
prefetch_strategy: PrefetchStrategy::default(),
last_scroll_direction: 0.0,
})));
let can_scroll_forward_state = cranpose_core::mutableStateOf(false);
let can_scroll_backward_state = cranpose_core::mutableStateOf(false);
let stats_state = cranpose_core::mutableStateOf(LazyLayoutStats::default());
LazyListState {
scroll_position,
can_scroll_forward_state,
can_scroll_backward_state,
stats_state,
inner,
}
}
}