1use crate::ext::ArmasContextExt;
19use crate::{Button, ButtonVariant};
20use egui::{vec2, Sense, Ui};
21
22pub struct PaginationResponse {
24 pub response: egui::Response,
26 pub page: usize,
28 pub changed: bool,
30}
31
32const BUTTON_SIZE: f32 = 36.0; const BUTTON_GAP: f32 = 4.0; const ICON_SIZE: f32 = 16.0; const CORNER_RADIUS: f32 = 6.0; const DEFAULT_SIBLING_COUNT: usize = 1;
38
39pub struct Pagination {
55 id: Option<egui::Id>,
56 initial_page: usize,
57 total_pages: usize,
58 sibling_count: usize,
59 show_prev_next: bool,
60 button_size: f32,
61}
62
63impl Pagination {
64 #[must_use]
70 pub fn new(initial_page: usize, total_pages: usize) -> Self {
71 Self {
72 id: None,
73 initial_page: initial_page.max(1).min(total_pages.max(1)),
74 total_pages: total_pages.max(1),
75 sibling_count: DEFAULT_SIBLING_COUNT,
76 show_prev_next: true,
77 button_size: BUTTON_SIZE,
78 }
79 }
80
81 #[must_use]
83 pub fn id(mut self, id: impl Into<egui::Id>) -> Self {
84 self.id = Some(id.into());
85 self
86 }
87
88 #[must_use]
90 pub const fn sibling_count(mut self, count: usize) -> Self {
91 self.sibling_count = count;
92 self
93 }
94
95 #[must_use]
97 pub const fn show_prev_next(mut self, show: bool) -> Self {
98 self.show_prev_next = show;
99 self
100 }
101
102 #[must_use]
104 pub const fn button_size(mut self, button_size: f32) -> Self {
105 self.button_size = button_size;
106 self
107 }
108
109 pub fn show(self, ui: &mut Ui) -> PaginationResponse {
111 let theme = ui.ctx().armas_theme();
112 let total_pages = self.total_pages;
113
114 let mut current_page = self.id.map_or(self.initial_page, |id| {
116 let state_id = id.with("page");
117 ui.ctx()
118 .data_mut(|d| d.get_temp(state_id).unwrap_or(self.initial_page))
119 });
120 let page_before = current_page;
121
122 let pages = calculate_visible_pages(current_page, total_pages, self.sibling_count);
124
125 let response = ui
126 .horizontal(|ui| {
127 ui.spacing_mut().item_spacing.x = BUTTON_GAP;
128
129 if self.show_prev_next {
131 let can_go_prev = current_page > 1;
132 let prev_clicked = draw_nav_button(
133 ui,
134 &theme,
135 "Previous",
136 true,
137 can_go_prev,
138 self.button_size,
139 );
140 if prev_clicked {
141 current_page -= 1;
142 }
143 }
144
145 for page in &pages {
147 if let Some(page_num) = page {
148 let is_current = *page_num == current_page;
149 let variant = if is_current {
150 ButtonVariant::Outlined
151 } else {
152 ButtonVariant::Ghost
153 };
154
155 let btn = Button::new(page_num.to_string())
156 .variant(variant)
157 .min_width(self.button_size)
158 .show(ui);
159
160 if btn.clicked() && !is_current {
161 current_page = *page_num;
162 }
163 } else {
164 let (rect, _) = ui.allocate_exact_size(
166 vec2(self.button_size, self.button_size),
167 Sense::hover(),
168 );
169
170 if ui.is_rect_visible(rect) {
171 let dot_radius = 2.0;
173 let dot_spacing = 4.0;
174 let center = rect.center();
175 let color = theme.muted_foreground();
176
177 for i in -1..=1 {
178 let x = center.x + (i as f32 * dot_spacing);
179 ui.painter().circle_filled(
180 egui::pos2(x, center.y),
181 dot_radius,
182 color,
183 );
184 }
185 }
186 }
187 }
188
189 if self.show_prev_next {
191 let can_go_next = current_page < total_pages;
192 let next_clicked =
193 draw_nav_button(ui, &theme, "Next", false, can_go_next, self.button_size);
194 if next_clicked {
195 current_page += 1;
196 }
197 }
198 })
199 .response;
200
201 if let Some(id) = self.id {
203 let state_id = id.with("page");
204 ui.ctx().data_mut(|d| {
205 d.insert_temp(state_id, current_page);
206 });
207 }
208
209 let changed = current_page != page_before;
210 PaginationResponse {
211 response,
212 page: current_page,
213 changed,
214 }
215 }
216}
217
218fn draw_nav_button(
221 ui: &mut Ui,
222 theme: &crate::Theme,
223 label: &str,
224 is_previous: bool,
225 enabled: bool,
226 button_size: f32,
227) -> bool {
228 let font_id = egui::FontId::proportional(theme.typography.base);
229 let text_width = 8.0 * label.len() as f32;
231 let icon_width = ICON_SIZE;
232 let padding = 10.0;
233 let gap = 4.0;
234
235 let total_width = padding + icon_width + gap + text_width + padding;
236 let (rect, response) = ui.allocate_exact_size(vec2(total_width, button_size), Sense::click());
237
238 let clicked = enabled && response.clicked();
239 let hovered = enabled && response.hovered();
240
241 if ui.is_rect_visible(rect) {
242 if hovered {
244 ui.painter()
245 .rect_filled(rect, CORNER_RADIUS, theme.accent());
246 }
247
248 let text_color = if enabled {
249 if hovered {
250 theme.accent_foreground()
251 } else {
252 theme.foreground()
253 }
254 } else {
255 theme.muted_foreground()
256 };
257
258 let icon_color = text_color;
259
260 if is_previous {
261 let icon_center = egui::pos2(rect.left() + padding + icon_width / 2.0, rect.center().y);
263 draw_chevron_left(ui.painter(), icon_center, icon_color);
264
265 let text_pos = egui::pos2(rect.left() + padding + icon_width + gap, rect.center().y);
266 ui.painter().text(
267 text_pos,
268 egui::Align2::LEFT_CENTER,
269 label,
270 font_id,
271 text_color,
272 );
273 } else {
274 let text_pos = egui::pos2(rect.left() + padding, rect.center().y);
276 ui.painter().text(
277 text_pos,
278 egui::Align2::LEFT_CENTER,
279 label,
280 font_id,
281 text_color,
282 );
283
284 let icon_center =
285 egui::pos2(rect.right() - padding - icon_width / 2.0, rect.center().y);
286 draw_chevron_right(ui.painter(), icon_center, icon_color);
287 }
288 }
289
290 clicked
291}
292
293fn draw_chevron_left(painter: &egui::Painter, center: egui::Pos2, color: egui::Color32) {
295 let half = ICON_SIZE * 0.15;
296 let stroke = egui::Stroke::new(1.5, color);
297
298 painter.line_segment(
299 [
300 egui::pos2(center.x + half, center.y - half * 2.0),
301 egui::pos2(center.x - half, center.y),
302 ],
303 stroke,
304 );
305 painter.line_segment(
306 [
307 egui::pos2(center.x - half, center.y),
308 egui::pos2(center.x + half, center.y + half * 2.0),
309 ],
310 stroke,
311 );
312}
313
314fn draw_chevron_right(painter: &egui::Painter, center: egui::Pos2, color: egui::Color32) {
316 let half = ICON_SIZE * 0.15;
317 let stroke = egui::Stroke::new(1.5, color);
318
319 painter.line_segment(
320 [
321 egui::pos2(center.x - half, center.y - half * 2.0),
322 egui::pos2(center.x + half, center.y),
323 ],
324 stroke,
325 );
326 painter.line_segment(
327 [
328 egui::pos2(center.x + half, center.y),
329 egui::pos2(center.x - half, center.y + half * 2.0),
330 ],
331 stroke,
332 );
333}
334
335fn calculate_visible_pages(current: usize, total: usize, siblings: usize) -> Vec<Option<usize>> {
338 if total <= 7 {
342 return (1..=total).map(Some).collect();
344 }
345
346 let left_sibling = current.saturating_sub(siblings).max(1);
347 let right_sibling = (current + siblings).min(total);
348
349 let show_left_ellipsis = left_sibling > 2;
350 let show_right_ellipsis = right_sibling < total - 1;
351
352 let mut pages = Vec::new();
353
354 if !show_left_ellipsis && show_right_ellipsis {
355 for i in 1..=5 {
357 pages.push(Some(i));
358 }
359 pages.push(None);
360 pages.push(Some(total));
361 } else if show_left_ellipsis && !show_right_ellipsis {
362 pages.push(Some(1));
364 pages.push(None);
365 for i in (total - 4)..=total {
366 pages.push(Some(i));
367 }
368 } else if show_left_ellipsis && show_right_ellipsis {
369 pages.push(Some(1));
371 pages.push(None);
372 for i in left_sibling..=right_sibling {
373 pages.push(Some(i));
374 }
375 pages.push(None);
376 pages.push(Some(total));
377 } else {
378 for i in 1..=total {
380 pages.push(Some(i));
381 }
382 }
383
384 pages
385}