1use std::rc::Rc;
7
8use cranpose_foundation::lazy::{
9 LazyItems, LazyListIntervalContent, LazyListScope, LazyListState, LazySettlePolicy,
10 rememberLazyListStateWithPosition,
11};
12use cranpose_ui_layout::LinearArrangement;
13
14use crate::{
15 AnimateScroll, composable,
16 modifier::Modifier,
17 widgets::{
18 Box, BoxSpec, BoxWithConstraints, BoxWithConstraintsScope, LazyColumn, LazyColumnSpec,
19 LazyRow, LazyRowSpec,
20 },
21};
22
23const PAGE_FLING_VELOCITY: f32 = 400.0;
26
27#[derive(Clone, Copy, Debug, PartialEq)]
29pub enum PageSize {
30 Fill,
32 Fixed(f32),
35}
36
37#[derive(Clone, Copy, Debug, PartialEq)]
40pub struct PagerSpec {
41 pub page_size: PageSize,
42 pub page_spacing: f32,
44}
45
46impl Default for PagerSpec {
47 fn default() -> Self {
48 Self {
49 page_size: PageSize::Fill,
50 page_spacing: 0.0,
51 }
52 }
53}
54
55#[derive(Clone, Copy, PartialEq)]
58pub struct PagerState {
59 list: LazyListState,
60}
61
62#[composable]
64pub fn rememberPagerState(initial_page: usize) -> PagerState {
65 PagerState {
66 list: rememberLazyListStateWithPosition(initial_page, 0.0),
67 }
68}
69
70impl PagerState {
71 pub fn current_page(&self) -> usize {
74 let first = self.list.first_visible_item_index();
75 let offset = self.list.first_visible_item_scroll_offset();
76 if offset > self.page_extent() / 2.0 {
77 first + 1
78 } else {
79 first
80 }
81 }
82
83 pub fn current_page_offset_fraction(&self) -> f32 {
86 let page = self.page_extent();
87 if page <= 0.0 {
88 return 0.0;
89 }
90 let fraction = self.list.first_visible_item_scroll_offset() / page;
91 if fraction > 0.5 {
92 fraction - 1.0
93 } else {
94 fraction
95 }
96 }
97
98 pub fn scroll_to_page(&self, page: usize) {
100 self.list.scroll_to_item(page, 0.0);
101 }
102
103 pub fn animate_scroll_to_page(&self, page: usize) {
106 self.list.animate_scroll_to_item(page, 0.0);
107 }
108
109 pub fn lazy_list_state(&self) -> LazyListState {
112 self.list
113 }
114
115 fn page_extent(&self) -> f32 {
117 self.list
118 .with_layout_info(|info| {
119 info.visible_items_info
120 .first()
121 .map_or(info.viewport_size, |page| {
122 page.size + info.main_axis_item_spacing
123 })
124 })
125 .unwrap_or_default()
126 }
127}
128
129fn page_settle(state: PagerState) -> LazySettlePolicy {
133 Rc::new(move |proposed, velocity| {
134 let page = state.page_extent();
135 if page <= 0.0 {
136 return proposed;
137 }
138 let forward = if velocity.abs() >= PAGE_FLING_VELOCITY {
139 velocity > 0.0
140 } else {
141 proposed > page / 2.0
142 };
143 if forward { page } else { 0.0 }
144 })
145}
146
147#[composable(no_skip)]
151pub fn HorizontalPager<F>(
152 modifier: Modifier,
153 state: PagerState,
154 spec: PagerSpec,
155 page_count: usize,
156 page_content: F,
157) where
158 F: Fn(usize) + 'static,
159{
160 Pager(
161 modifier,
162 state,
163 spec,
164 page_count,
165 true,
166 Rc::new(page_content),
167 );
168}
169
170#[composable(no_skip)]
172pub fn VerticalPager<F>(
173 modifier: Modifier,
174 state: PagerState,
175 spec: PagerSpec,
176 page_count: usize,
177 page_content: F,
178) where
179 F: Fn(usize) + 'static,
180{
181 Pager(
182 modifier,
183 state,
184 spec,
185 page_count,
186 false,
187 Rc::new(page_content),
188 );
189}
190
191#[composable(no_skip)]
192fn Pager(
193 modifier: Modifier,
194 state: PagerState,
195 spec: PagerSpec,
196 page_count: usize,
197 horizontal: bool,
198 page_content: Rc<dyn Fn(usize)>,
199) {
200 cranpose_core::DisposableEffect(state, move |scope| {
201 state.list.set_settle_policy(Some(page_settle(state)));
202 scope.on_dispose(move || state.list.set_settle_policy(None))
203 });
204 BoxWithConstraints(modifier, move |scope| {
205 let constraints = scope.constraints();
206 let (width, height) = match (spec.page_size, horizontal) {
207 (PageSize::Fill, _) => (constraints.max_width, constraints.max_height),
208 (PageSize::Fixed(length), true) => (length, constraints.max_height),
209 (PageSize::Fixed(length), false) => (constraints.max_width, length),
210 };
211 let content = Rc::clone(&page_content);
212 let pages = move |pages: &mut LazyListIntervalContent| {
213 pages.items(
214 LazyItems::new(page_count).key(|page| page as u64),
215 move |page| {
216 let content = Rc::clone(&content);
217 Box(
218 Modifier::empty().size_points(width, height),
219 BoxSpec::default(),
220 move || content(page),
221 );
222 },
223 );
224 };
225 let fill = Modifier::empty().fill_max_size();
226 let spacing = LinearArrangement::SpacedBy(spec.page_spacing);
227 if horizontal {
228 let row = LazyRowSpec {
229 horizontal_arrangement: spacing,
230 ..LazyRowSpec::default()
231 };
232 LazyRow(fill, state.list, row, pages);
233 } else {
234 let column = LazyColumnSpec {
235 vertical_arrangement: spacing,
236 ..LazyColumnSpec::default()
237 };
238 LazyColumn(fill, state.list, column, pages);
239 }
240 });
241}