use std::rc::Rc;
use cranpose_foundation::lazy::{
LazyItems, LazyListIntervalContent, LazyListScope, LazyListState, LazySettlePolicy,
rememberLazyListStateWithPosition,
};
use cranpose_ui_layout::LinearArrangement;
use crate::{
AnimateScroll, composable,
modifier::Modifier,
widgets::{
Box, BoxSpec, BoxWithConstraints, BoxWithConstraintsScope, LazyColumn, LazyColumnSpec,
LazyRow, LazyRowSpec,
},
};
const PAGE_FLING_VELOCITY: f32 = 400.0;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PageSize {
Fill,
Fixed(f32),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PagerSpec {
pub page_size: PageSize,
pub page_spacing: f32,
}
impl Default for PagerSpec {
fn default() -> Self {
Self {
page_size: PageSize::Fill,
page_spacing: 0.0,
}
}
}
#[derive(Clone, Copy, PartialEq)]
pub struct PagerState {
list: LazyListState,
}
#[composable]
pub fn rememberPagerState(initial_page: usize) -> PagerState {
PagerState {
list: rememberLazyListStateWithPosition(initial_page, 0.0),
}
}
impl PagerState {
pub fn current_page(&self) -> usize {
let first = self.list.first_visible_item_index();
let offset = self.list.first_visible_item_scroll_offset();
if offset > self.page_extent() / 2.0 {
first + 1
} else {
first
}
}
pub fn current_page_offset_fraction(&self) -> f32 {
let page = self.page_extent();
if page <= 0.0 {
return 0.0;
}
let fraction = self.list.first_visible_item_scroll_offset() / page;
if fraction > 0.5 {
fraction - 1.0
} else {
fraction
}
}
pub fn scroll_to_page(&self, page: usize) {
self.list.scroll_to_item(page, 0.0);
}
pub fn animate_scroll_to_page(&self, page: usize) {
self.list.animate_scroll_to_item(page, 0.0);
}
pub fn lazy_list_state(&self) -> LazyListState {
self.list
}
fn page_extent(&self) -> f32 {
self.list
.with_layout_info(|info| {
info.visible_items_info
.first()
.map_or(info.viewport_size, |page| {
page.size + info.main_axis_item_spacing
})
})
.unwrap_or_default()
}
}
fn page_settle(state: PagerState) -> LazySettlePolicy {
Rc::new(move |proposed, velocity| {
let page = state.page_extent();
if page <= 0.0 {
return proposed;
}
let forward = if velocity.abs() >= PAGE_FLING_VELOCITY {
velocity > 0.0
} else {
proposed > page / 2.0
};
if forward { page } else { 0.0 }
})
}
#[composable(no_skip)]
pub fn HorizontalPager<F>(
modifier: Modifier,
state: PagerState,
spec: PagerSpec,
page_count: usize,
page_content: F,
) where
F: Fn(usize) + 'static,
{
Pager(
modifier,
state,
spec,
page_count,
true,
Rc::new(page_content),
);
}
#[composable(no_skip)]
pub fn VerticalPager<F>(
modifier: Modifier,
state: PagerState,
spec: PagerSpec,
page_count: usize,
page_content: F,
) where
F: Fn(usize) + 'static,
{
Pager(
modifier,
state,
spec,
page_count,
false,
Rc::new(page_content),
);
}
#[composable(no_skip)]
fn Pager(
modifier: Modifier,
state: PagerState,
spec: PagerSpec,
page_count: usize,
horizontal: bool,
page_content: Rc<dyn Fn(usize)>,
) {
cranpose_core::DisposableEffect(state, move |scope| {
state.list.set_settle_policy(Some(page_settle(state)));
scope.on_dispose(move || state.list.set_settle_policy(None))
});
BoxWithConstraints(modifier, move |scope| {
let constraints = scope.constraints();
let (width, height) = match (spec.page_size, horizontal) {
(PageSize::Fill, _) => (constraints.max_width, constraints.max_height),
(PageSize::Fixed(length), true) => (length, constraints.max_height),
(PageSize::Fixed(length), false) => (constraints.max_width, length),
};
let content = Rc::clone(&page_content);
let pages = move |pages: &mut LazyListIntervalContent| {
pages.items(
LazyItems::new(page_count).key(|page| page as u64),
move |page| {
let content = Rc::clone(&content);
Box(
Modifier::empty().size_points(width, height),
BoxSpec::default(),
move || content(page),
);
},
);
};
let fill = Modifier::empty().fill_max_size();
let spacing = LinearArrangement::SpacedBy(spec.page_spacing);
if horizontal {
let row = LazyRowSpec {
horizontal_arrangement: spacing,
..LazyRowSpec::default()
};
LazyRow(fill, state.list, row, pages);
} else {
let column = LazyColumnSpec {
vertical_arrangement: spacing,
..LazyColumnSpec::default()
};
LazyColumn(fill, state.list, column, pages);
}
});
}