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: f32,
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: 0.0,
21 user_scroll_enabled: true,
22 content_padding: PaddingValues::default(),
23 }
24 }
25}
26
27pub struct PagerState {
29 current_page: Signal<usize>,
30 page_count: Signal<usize>,
31}
32
33impl PagerState {
34 pub fn new(page_count: usize) -> Self {
35 Self {
36 current_page: signal(0),
37 page_count: signal(page_count.max(1)),
38 }
39 }
40
41 pub fn current_page(&self) -> usize {
42 self.current_page.get()
43 }
44
45 pub fn set_page(&self, page: usize) {
47 let max_page = self.page_count.get().saturating_sub(1);
48 self.current_page.set(page.min(max_page));
49 }
50
51 pub fn page_count(&self) -> usize {
52 self.page_count.get()
53 }
54}
55
56#[allow(non_snake_case)]
75pub fn HorizontalPager(
76 key: impl Into<String>,
77 state: Rc<PagerState>,
78 page_builder: impl Fn(usize) -> View + 'static,
79 config: PagerConfig,
80) -> View {
81 let key = key.into();
82 let page = state.current_page.get();
83 let page_spacing = config.page_spacing;
84 let slide_offset = 800.0 + page_spacing;
85
86 let drag_start_x = Rc::new(remember_with_key(format!("pager_drag:{key}"), || {
88 RefCell::new(None::<f32>)
89 }));
90
91 let on_down = {
92 let d = drag_start_x.clone();
93 move |e: PointerEvent| {
94 *d.borrow_mut() = Some(e.position.x);
95 }
96 };
97
98 let st = state.clone();
99 let on_up = {
100 let d = drag_start_x.clone();
101 move |e: PointerEvent| {
102 if let Some(start_x) = *d.borrow() {
103 let delta = e.position.x - start_x;
104 let threshold = 50.0;
105 if delta.abs() > threshold {
106 if delta < 0.0 {
107 let next =
108 (st.current_page.get() + 1).min(st.page_count.get().saturating_sub(1));
109 st.current_page.set(next);
110 } else {
111 let prev = st.current_page.get().saturating_sub(1);
112 st.current_page.set(prev);
113 }
114 }
115 }
116 *d.borrow_mut() = None;
117 }
118 };
119
120 let content = AnimatedContent(
121 page,
122 page_builder,
123 AnimatedContentConfig {
124 key: format!("page_content:{key}"),
125 spec: AnimationSpec::spring_gentle(),
126 enter: EnterTransition::Composite(vec![
127 EnterTransition::FadeIn,
128 EnterTransition::SlideIn {
129 offset_x: slide_offset,
130 offset_y: 0.0,
131 },
132 ]),
133 exit: ExitTransition::Composite(vec![
134 ExitTransition::FadeOut,
135 ExitTransition::SlideOut {
136 offset_x: -(slide_offset),
137 offset_y: 0.0,
138 },
139 ]),
140 },
141 );
142
143 let gesture = if config.user_scroll_enabled {
144 crate::Box(
145 Modifier::new()
146 .fill_max_size()
147 .hit_passthrough()
148 .on_pointer_down(on_down)
149 .on_pointer_up(on_up),
150 )
151 } else {
152 crate::Box(Modifier::new().fill_max_size().hit_passthrough())
153 };
154
155 crate::ZStack(Modifier::new().fill_max_size().then(config.modifier)).with_children(vec![
156 crate::Box(Modifier::new().fill_max_size()).with_children(vec![content]),
157 gesture,
158 ])
159}
160
161#[allow(non_snake_case)]
165pub fn VerticalPager(
166 key: impl Into<String>,
167 state: Rc<PagerState>,
168 page_builder: impl Fn(usize) -> View + 'static,
169 config: PagerConfig,
170) -> View {
171 let key = key.into();
172 let page = state.current_page.get();
173 let page_spacing = config.page_spacing;
174 let slide_offset = 600.0 + page_spacing;
175
176 let drag_start_y = Rc::new(remember_with_key(format!("vpager_drag:{key}"), || {
177 RefCell::new(None::<f32>)
178 }));
179
180 let on_down = {
181 let d = drag_start_y.clone();
182 move |e: PointerEvent| {
183 *d.borrow_mut() = Some(e.position.y);
184 }
185 };
186
187 let st = state.clone();
188 let on_up = {
189 let d = drag_start_y.clone();
190 move |e: PointerEvent| {
191 if let Some(start_y) = *d.borrow() {
192 let delta = e.position.y - start_y;
193 let threshold = 50.0;
194 if delta.abs() > threshold {
195 if delta < 0.0 {
196 let next =
197 (st.current_page.get() + 1).min(st.page_count.get().saturating_sub(1));
198 st.current_page.set(next);
199 } else {
200 let prev = st.current_page.get().saturating_sub(1);
201 st.current_page.set(prev);
202 }
203 }
204 }
205 *d.borrow_mut() = None;
206 }
207 };
208
209 let content = AnimatedContent(
210 page,
211 page_builder,
212 AnimatedContentConfig {
213 key: format!("vpage_content:{key}"),
214 spec: AnimationSpec::spring_gentle(),
215 enter: EnterTransition::Composite(vec![
216 EnterTransition::FadeIn,
217 EnterTransition::SlideIn {
218 offset_x: 0.0,
219 offset_y: slide_offset,
220 },
221 ]),
222 exit: ExitTransition::Composite(vec![
223 ExitTransition::FadeOut,
224 ExitTransition::SlideOut {
225 offset_x: 0.0,
226 offset_y: -(slide_offset),
227 },
228 ]),
229 },
230 );
231
232 let gesture = if config.user_scroll_enabled {
233 crate::Box(
234 Modifier::new()
235 .fill_max_size()
236 .hit_passthrough()
237 .on_pointer_down(on_down)
238 .on_pointer_up(on_up),
239 )
240 } else {
241 crate::Box(Modifier::new().fill_max_size().hit_passthrough())
242 };
243
244 crate::ZStack(Modifier::new().fill_max_size().then(config.modifier)).with_children(vec![
245 crate::Box(Modifier::new().fill_max_size()).with_children(vec![content]),
246 gesture,
247 ])
248}