1use repose_core::*;
2use std::cell::RefCell;
3use std::rc::Rc;
4
5use crate::anim_ext::{AnimatedContent, AnimatedContentConfig, EnterTransition, ExitTransition};
6
7#[derive(Clone)]
9pub struct PagerConfig {
10 pub modifier: Modifier,
11 pub page_spacing: Dp,
12 pub user_scroll_enabled: bool,
13 pub content_padding: PaddingValues,
14}
15
16impl Default for PagerConfig {
17 fn default() -> Self {
18 Self {
19 modifier: Modifier::new(),
20 page_spacing: Dp::ZERO,
21 user_scroll_enabled: true,
22 content_padding: PaddingValues::default(),
23 }
24 }
25}
26
27struct PagerDrag {
30 start: Option<(f32, f32)>,
31}
32
33fn pager_threshold_px() -> f32 {
34 Dp(24.0).to_px().0.max(8.0)
35}
36
37fn horizontal_handlers(
39 key: &str,
40 state: &Rc<PagerState>,
41) -> (
42 impl Fn(PointerEvent) + Clone + 'static,
43 impl Fn(PointerEvent) + Clone + 'static,
44 impl Fn(PointerEvent) + Clone + 'static,
45) {
46 let drag = Rc::new(remember_with_key(format!("pager_drag:{key}"), || {
47 RefCell::new(PagerDrag { start: None })
48 }));
49 let st = state.clone();
50 let on_down = {
51 let drag = drag.clone();
52 move |e: PointerEvent| {
53 drag.borrow_mut().start = Some((e.position.x, e.position.y));
54 }
55 };
56 let on_up = {
57 let drag = drag.clone();
58 move |e: PointerEvent| {
59 if let Some((sx, sy)) = drag.borrow().start {
60 let dx = e.position.x - sx;
61 let dy = e.position.y - sy;
62 let threshold = pager_threshold_px();
63 if dx.abs() > threshold && dx.abs() > dy.abs() * 1.5 {
64 if dx < 0.0 {
65 st.set_page(st.current_page.get().saturating_add(1));
66 } else {
67 st.set_page(st.current_page.get().saturating_sub(1));
68 }
69 }
70 }
71 drag.borrow_mut().start = None;
72 }
73 };
74 let on_cancel = {
75 let drag = drag.clone();
76 move |_: PointerEvent| {
77 drag.borrow_mut().start = None;
78 }
79 };
80 (on_down, on_up, on_cancel)
81}
82
83fn vertical_handlers(
84 key: &str,
85 state: &Rc<PagerState>,
86) -> (
87 impl Fn(PointerEvent) + Clone + 'static,
88 impl Fn(PointerEvent) + Clone + 'static,
89 impl Fn(PointerEvent) + Clone + 'static,
90) {
91 let drag = Rc::new(remember_with_key(format!("vpager_drag:{key}"), || {
92 RefCell::new(PagerDrag { start: None })
93 }));
94 let st = state.clone();
95 let on_down = {
96 let drag = drag.clone();
97 move |e: PointerEvent| {
98 drag.borrow_mut().start = Some((e.position.x, e.position.y));
99 }
100 };
101 let on_up = {
102 let drag = drag.clone();
103 move |e: PointerEvent| {
104 if let Some((sx, sy)) = drag.borrow().start {
105 let dx = e.position.x - sx;
106 let dy = e.position.y - sy;
107 let threshold = pager_threshold_px();
108 if dy.abs() > threshold && dy.abs() > dx.abs() * 1.5 {
109 if dy < 0.0 {
110 st.set_page(st.current_page.get().saturating_add(1));
111 } else {
112 st.set_page(st.current_page.get().saturating_sub(1));
113 }
114 }
115 }
116 drag.borrow_mut().start = None;
117 }
118 };
119 let on_cancel = {
120 let drag = drag.clone();
121 move |_: PointerEvent| {
122 drag.borrow_mut().start = None;
123 }
124 };
125 (on_down, on_up, on_cancel)
126}
127
128pub struct PagerState {
130 current_page: Signal<usize>,
131 previous_page: Signal<usize>,
132 page_count: Signal<usize>,
133}
134
135impl PagerState {
136 pub fn new(page_count: usize) -> Self {
137 Self {
138 current_page: signal(0),
139 previous_page: signal(0),
140 page_count: signal(page_count.max(1)),
141 }
142 }
143
144 pub fn current_page(&self) -> usize {
145 self.current_page.get()
146 }
147
148 pub fn set_page(&self, page: usize) {
150 let max_page = self.page_count.get().saturating_sub(1);
151 let prev = self.current_page.get();
152 let next = page.min(max_page);
153 if next == prev {
154 return;
155 }
156 batch(|| {
159 self.previous_page.set(prev);
160 self.current_page.set(next);
161 });
162 }
163
164 fn direction(&self) -> i32 {
168 let cur = self.current_page.get() as i64;
169 let prev = self.previous_page.get() as i64;
170 (cur - prev).signum() as i32
171 }
172
173 pub fn page_count(&self) -> usize {
174 self.page_count.get()
175 }
176
177 pub fn set_page_count(&self, count: usize) {
180 let count = count.max(1);
181 let cur = self.current_page.get();
182 let max_page = count - 1;
183 batch(|| {
184 self.page_count.set(count);
185 if cur > max_page {
186 self.previous_page.set(cur);
187 self.current_page.set(max_page);
188 }
189 });
190 }
191}
192
193#[allow(non_snake_case)]
212pub fn HorizontalPager(
213 key: impl Into<String>,
214 state: Rc<PagerState>,
215 page_builder: impl Fn(usize) -> View + 'static,
216 config: PagerConfig,
217) -> View {
218 let key = key.into();
219 let page = state.current_page.get();
220 let page_spacing = config.page_spacing;
221 let dir = if state.direction() < 0 { -1.0 } else { 1.0 };
222 let slide_offset = (Dp(800.0) + page_spacing) * dir;
223
224 let (on_down, on_up, on_cancel) = horizontal_handlers(&key, &state);
225
226 let content = AnimatedContent(
227 page,
228 page_builder,
229 AnimatedContentConfig {
230 key: format!("page_content:{key}"),
231 spec: AnimationSpec::spring_gentle(),
232 enter: EnterTransition::Composite(vec![
233 EnterTransition::FadeIn,
234 EnterTransition::SlideIn {
235 offset_x: slide_offset,
236 offset_y: Dp::ZERO,
237 },
238 ]),
239 exit: ExitTransition::Composite(vec![
240 ExitTransition::FadeOut,
241 ExitTransition::SlideOut {
242 offset_x: -(slide_offset),
243 offset_y: Dp::ZERO,
244 },
245 ]),
246 },
247 );
248
249 let pager_mod = Modifier::new().fill_max_size().then(config.modifier);
250 if config.user_scroll_enabled {
251 crate::Box(
252 pager_mod
253 .on_pointer_down(on_down)
254 .on_pointer_up(on_up)
255 .on_pointer_cancel(on_cancel),
256 )
257 .with_children(vec![
258 crate::Box(Modifier::new().fill_max_size()).with_children(vec![content]),
259 ])
260 } else {
261 crate::Box(pager_mod).with_children(vec![
262 crate::Box(Modifier::new().fill_max_size()).with_children(vec![content]),
263 ])
264 }
265}
266
267#[allow(non_snake_case)]
271pub fn VerticalPager(
272 key: impl Into<String>,
273 state: Rc<PagerState>,
274 page_builder: impl Fn(usize) -> View + 'static,
275 config: PagerConfig,
276) -> View {
277 let key = key.into();
278 let page = state.current_page.get();
279 let page_spacing = config.page_spacing;
280 let dir = if state.direction() < 0 { -1.0 } else { 1.0 };
281 let slide_offset = (Dp(600.0) + page_spacing) * dir;
282
283 let (on_down, on_up, on_cancel) = vertical_handlers(&key, &state);
284
285 let content = AnimatedContent(
286 page,
287 page_builder,
288 AnimatedContentConfig {
289 key: format!("vpage_content:{key}"),
290 spec: AnimationSpec::spring_gentle(),
291 enter: EnterTransition::Composite(vec![
292 EnterTransition::FadeIn,
293 EnterTransition::SlideIn {
294 offset_x: Dp::ZERO,
295 offset_y: slide_offset,
296 },
297 ]),
298 exit: ExitTransition::Composite(vec![
299 ExitTransition::FadeOut,
300 ExitTransition::SlideOut {
301 offset_x: Dp::ZERO,
302 offset_y: -(slide_offset),
303 },
304 ]),
305 },
306 );
307
308 let pager_mod = Modifier::new().fill_max_size().then(config.modifier);
309 if config.user_scroll_enabled {
310 crate::Box(
311 pager_mod
312 .on_pointer_down(on_down)
313 .on_pointer_up(on_up)
314 .on_pointer_cancel(on_cancel),
315 )
316 .with_children(vec![
317 crate::Box(Modifier::new().fill_max_size()).with_children(vec![content]),
318 ])
319 } else {
320 crate::Box(pager_mod).with_children(vec![
321 crate::Box(Modifier::new().fill_max_size()).with_children(vec![content]),
322 ])
323 }
324}