Skip to main content

tui_lipan/widgets/
pagination.rs

1//! Pagination helpers.
2
3use std::sync::Arc;
4
5use crate::callback::Callback;
6use crate::core::element::Element;
7use crate::style::{BorderStyle, Style, StyleSlot};
8use crate::widgets::button::ButtonVariant;
9use crate::widgets::{Button, HStack, Text};
10
11/// Controlled pagination state helper for composition-based UIs.
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13pub struct PaginationState {
14    page: usize,
15    per_page: usize,
16    total_items: usize,
17}
18
19impl PaginationState {
20    /// Create pagination state.
21    pub fn new(total_items: usize, per_page: usize) -> Self {
22        let per_page = per_page.max(1);
23        let mut state = Self {
24            page: 0,
25            per_page,
26            total_items,
27        };
28        state.clamp_page();
29        state
30    }
31
32    /// Current zero-based page index.
33    pub fn page(&self) -> usize {
34        self.page
35    }
36
37    /// Items per page.
38    pub fn per_page(&self) -> usize {
39        self.per_page
40    }
41
42    /// Total items.
43    pub fn total_items(&self) -> usize {
44        self.total_items
45    }
46
47    /// Total page count (at least 1).
48    pub fn total_pages(&self) -> usize {
49        self.total_items.max(1).div_ceil(self.per_page)
50    }
51
52    /// Whether this is the first page.
53    pub fn is_first_page(&self) -> bool {
54        self.page == 0
55    }
56
57    /// Whether this is the last page.
58    pub fn is_last_page(&self) -> bool {
59        self.page + 1 >= self.total_pages()
60    }
61
62    /// Set zero-based page index (clamped to bounds).
63    pub fn set_page(&mut self, page: usize) {
64        self.page = page;
65        self.clamp_page();
66    }
67
68    /// Move to previous page.
69    pub fn prev_page(&mut self) {
70        self.page = self.page.saturating_sub(1);
71    }
72
73    /// Move to next page.
74    pub fn next_page(&mut self) {
75        if !self.is_last_page() {
76            self.page += 1;
77        }
78    }
79
80    /// Move to first page.
81    pub fn first_page(&mut self) {
82        self.page = 0;
83    }
84
85    /// Move to last page.
86    pub fn last_page(&mut self) {
87        self.page = self.total_pages().saturating_sub(1);
88    }
89
90    /// Set items per page and re-clamp the current page.
91    pub fn set_per_page(&mut self, per_page: usize) {
92        self.per_page = per_page.max(1);
93        self.clamp_page();
94    }
95
96    /// Set total items and re-clamp the current page.
97    pub fn set_total_items(&mut self, total_items: usize) {
98        self.total_items = total_items;
99        self.clamp_page();
100    }
101
102    /// Current item range as `(start, end_exclusive)`.
103    pub fn range(&self) -> (usize, usize) {
104        let start = self.page.saturating_mul(self.per_page);
105        let end = start.saturating_add(self.per_page).min(self.total_items);
106        (start.min(self.total_items), end)
107    }
108
109    fn clamp_page(&mut self) {
110        self.page = self.page.min(self.total_pages().saturating_sub(1));
111    }
112}
113
114impl Default for PaginationState {
115    fn default() -> Self {
116        Self::new(0, 10)
117    }
118}
119
120/// Navigation action emitted by [`PaginationBar`].
121#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
122pub enum PaginationAction {
123    /// Jump to first page.
124    First,
125    /// Move to previous page.
126    Prev,
127    /// Move to next page.
128    Next,
129    /// Jump to last page.
130    Last,
131}
132
133/// Navigation labels for [`PaginationBar`].
134#[derive(Clone, Debug, PartialEq, Eq, Hash)]
135pub struct PaginationLabels {
136    /// Label for first-page button.
137    pub first: Arc<str>,
138    /// Label for previous-page button.
139    pub prev: Arc<str>,
140    /// Label for next-page button.
141    pub next: Arc<str>,
142    /// Label for last-page button.
143    pub last: Arc<str>,
144}
145
146impl Default for PaginationLabels {
147    fn default() -> Self {
148        Self {
149            first: "<<".into(),
150            prev: "<".into(),
151            next: ">".into(),
152            last: ">>".into(),
153        }
154    }
155}
156
157/// Per-button style overrides for [`PaginationBar`].
158#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
159pub struct PaginationButtonOverrides {
160    /// Optional variant override.
161    pub variant: Option<ButtonVariant>,
162    /// Optional border style override (for outlined variant).
163    pub border_style: Option<BorderStyle>,
164    /// Optional base style override.
165    pub style: Option<Style>,
166    /// Optional hover style override.
167    pub hover_style: Option<StyleSlot>,
168    /// Optional focus style override.
169    pub focus_style: Option<StyleSlot>,
170    /// Optional disabled style override.
171    pub disabled_style: Option<Style>,
172}
173
174impl PaginationButtonOverrides {
175    /// Create empty per-button overrides.
176    pub fn new() -> Self {
177        Self::default()
178    }
179
180    /// Override variant.
181    pub fn variant(mut self, variant: ButtonVariant) -> Self {
182        self.variant = Some(variant);
183        self
184    }
185
186    /// Override border style.
187    pub fn border_style(mut self, border_style: BorderStyle) -> Self {
188        self.border_style = Some(border_style);
189        self
190    }
191
192    /// Override base style.
193    pub fn style(mut self, style: Style) -> Self {
194        self.style = Some(style);
195        self
196    }
197
198    /// Override hover style.
199    pub fn hover_style(mut self, style: Style) -> Self {
200        self.hover_style = Some(StyleSlot::Replace(style));
201        self
202    }
203
204    /// Extend the themed hover style.
205    pub fn extend_hover_style(mut self, style: Style) -> Self {
206        self.hover_style = Some(StyleSlot::Extend(style));
207        self
208    }
209
210    /// Inherit the themed hover style.
211    pub fn inherit_hover_style(mut self) -> Self {
212        self.hover_style = Some(StyleSlot::Inherit);
213        self
214    }
215
216    /// Override hover style slot directly for composite forwarding.
217    pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
218        self.hover_style = Some(slot);
219        self
220    }
221
222    /// Override focus style.
223    pub fn focus_style(mut self, style: Style) -> Self {
224        self.focus_style = Some(StyleSlot::Replace(style));
225        self
226    }
227
228    /// Extend the themed focus style.
229    pub fn extend_focus_style(mut self, style: Style) -> Self {
230        self.focus_style = Some(StyleSlot::Extend(style));
231        self
232    }
233
234    /// Inherit the themed focus style.
235    pub fn inherit_focus_style(mut self) -> Self {
236        self.focus_style = Some(StyleSlot::Inherit);
237        self
238    }
239
240    /// Override focus style slot directly for composite forwarding.
241    pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
242        self.focus_style = Some(slot);
243        self
244    }
245
246    /// Override disabled style.
247    pub fn disabled_style(mut self, style: Style) -> Self {
248        self.disabled_style = Some(style);
249        self
250    }
251}
252
253/// Structured values available to custom pagination info formatters.
254#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
255pub struct PaginationInfo {
256    /// Current zero-based page index.
257    pub page_index: usize,
258    /// Current one-based page number.
259    pub page_number: usize,
260    /// Total number of pages.
261    pub total_pages: usize,
262    /// Total number of items.
263    pub total_items: usize,
264    /// Items per page.
265    pub per_page: usize,
266    /// Current range start (zero-based, inclusive).
267    pub start: usize,
268    /// Current range end (zero-based, exclusive).
269    pub end: usize,
270}
271
272type PaginationInfoFormatter = Arc<dyn Fn(PaginationInfo) -> Arc<str>>;
273
274/// Composable pagination controls with style personalization.
275#[derive(Clone)]
276pub struct PaginationBar {
277    state: PaginationState,
278    labels: PaginationLabels,
279    show_first_last: bool,
280    show_range_info: bool,
281    gap: u16,
282    button_variant: ButtonVariant,
283    button_border_style: BorderStyle,
284    button_style: Style,
285    button_hover_style: StyleSlot,
286    button_focus_style: StyleSlot,
287    button_disabled_style: Style,
288    button_overrides: [PaginationButtonOverrides; 4],
289    info_style: Style,
290    info_formatter: Option<PaginationInfoFormatter>,
291    on_action: Option<Callback<PaginationAction>>,
292}
293
294impl PaginationBar {
295    /// Create a new pagination bar from controlled state.
296    pub fn new(state: PaginationState) -> Self {
297        Self {
298            state,
299            labels: PaginationLabels::default(),
300            show_first_last: true,
301            show_range_info: true,
302            gap: 1,
303            button_variant: ButtonVariant::Outlined,
304            button_border_style: BorderStyle::Plain,
305            button_style: Style::default(),
306            button_hover_style: StyleSlot::Inherit,
307            button_focus_style: StyleSlot::Inherit,
308            button_disabled_style: Style::default(),
309            button_overrides: [PaginationButtonOverrides::default(); 4],
310            info_style: Style::default(),
311            info_formatter: None,
312            on_action: None,
313        }
314    }
315
316    /// Set all navigation labels at once.
317    pub fn labels(mut self, labels: PaginationLabels) -> Self {
318        self.labels = labels;
319        self
320    }
321
322    /// Set first-page button label.
323    pub fn first_label(mut self, label: impl Into<Arc<str>>) -> Self {
324        self.labels.first = label.into();
325        self
326    }
327
328    /// Set previous-page button label.
329    pub fn prev_label(mut self, label: impl Into<Arc<str>>) -> Self {
330        self.labels.prev = label.into();
331        self
332    }
333
334    /// Set next-page button label.
335    pub fn next_label(mut self, label: impl Into<Arc<str>>) -> Self {
336        self.labels.next = label.into();
337        self
338    }
339
340    /// Set last-page button label.
341    pub fn last_label(mut self, label: impl Into<Arc<str>>) -> Self {
342        self.labels.last = label.into();
343        self
344    }
345
346    /// Show or hide first/last navigation buttons.
347    pub fn show_first_last(mut self, show: bool) -> Self {
348        self.show_first_last = show;
349        self
350    }
351
352    /// Show or hide row-range information in the center label.
353    pub fn show_range_info(mut self, show: bool) -> Self {
354        self.show_range_info = show;
355        self
356    }
357
358    /// Set horizontal gap between controls.
359    pub fn gap(mut self, gap: u16) -> Self {
360        self.gap = gap;
361        self
362    }
363
364    /// Set button variant for all nav buttons.
365    pub fn button_variant(mut self, variant: ButtonVariant) -> Self {
366        self.button_variant = variant;
367        self
368    }
369
370    /// Set border style for outlined nav buttons.
371    pub fn button_border_style(mut self, style: BorderStyle) -> Self {
372        self.button_border_style = style;
373        self
374    }
375
376    /// Set base style for nav buttons.
377    pub fn button_style(mut self, style: Style) -> Self {
378        self.button_style = style;
379        self
380    }
381
382    /// Set hover style for nav buttons.
383    pub fn button_hover_style(mut self, style: Style) -> Self {
384        self.button_hover_style = StyleSlot::Replace(style);
385        self
386    }
387
388    /// Extend the themed hover style for nav buttons.
389    pub fn extend_button_hover_style(mut self, style: Style) -> Self {
390        self.button_hover_style = StyleSlot::Extend(style);
391        self
392    }
393
394    /// Inherit the themed hover style for nav buttons.
395    pub fn inherit_button_hover_style(mut self) -> Self {
396        self.button_hover_style = StyleSlot::Inherit;
397        self
398    }
399
400    /// Set hover style slot for nav buttons directly for composite forwarding.
401    pub fn button_hover_style_slot(mut self, slot: StyleSlot) -> Self {
402        self.button_hover_style = slot;
403        self
404    }
405
406    /// Set focus style for nav buttons.
407    pub fn button_focus_style(mut self, style: Style) -> Self {
408        self.button_focus_style = StyleSlot::Replace(style);
409        self
410    }
411
412    /// Extend the themed focus style for nav buttons.
413    pub fn extend_button_focus_style(mut self, style: Style) -> Self {
414        self.button_focus_style = StyleSlot::Extend(style);
415        self
416    }
417
418    /// Inherit the themed focus style for nav buttons.
419    pub fn inherit_button_focus_style(mut self) -> Self {
420        self.button_focus_style = StyleSlot::Inherit;
421        self
422    }
423
424    /// Set focus style slot for nav buttons directly for composite forwarding.
425    pub fn button_focus_style_slot(mut self, slot: StyleSlot) -> Self {
426        self.button_focus_style = slot;
427        self
428    }
429
430    /// Set disabled style for nav buttons.
431    pub fn button_disabled_style(mut self, style: Style) -> Self {
432        self.button_disabled_style = style;
433        self
434    }
435
436    /// Set per-button style overrides for one action.
437    pub fn button_overrides_for(
438        mut self,
439        action: PaginationAction,
440        overrides: PaginationButtonOverrides,
441    ) -> Self {
442        self.button_overrides[action_index(action)] = overrides;
443        self
444    }
445
446    /// Set style overrides for first-page button.
447    pub fn first_button_overrides(mut self, overrides: PaginationButtonOverrides) -> Self {
448        self.button_overrides[action_index(PaginationAction::First)] = overrides;
449        self
450    }
451
452    /// Set style overrides for previous-page button.
453    pub fn prev_button_overrides(mut self, overrides: PaginationButtonOverrides) -> Self {
454        self.button_overrides[action_index(PaginationAction::Prev)] = overrides;
455        self
456    }
457
458    /// Set style overrides for next-page button.
459    pub fn next_button_overrides(mut self, overrides: PaginationButtonOverrides) -> Self {
460        self.button_overrides[action_index(PaginationAction::Next)] = overrides;
461        self
462    }
463
464    /// Set style overrides for last-page button.
465    pub fn last_button_overrides(mut self, overrides: PaginationButtonOverrides) -> Self {
466        self.button_overrides[action_index(PaginationAction::Last)] = overrides;
467        self
468    }
469
470    /// Set style for pagination info text.
471    pub fn info_style(mut self, style: Style) -> Self {
472        self.info_style = style;
473        self
474    }
475
476    /// Set a custom formatter for the center info label.
477    pub fn info_formatter<F>(mut self, formatter: F) -> Self
478    where
479        F: Fn(PaginationInfo) -> Arc<str> + 'static,
480    {
481        self.info_formatter = Some(Arc::new(formatter));
482        self
483    }
484
485    /// Callback fired when a nav button is clicked.
486    pub fn on_action(mut self, cb: Callback<PaginationAction>) -> Self {
487        self.on_action = Some(cb);
488        self
489    }
490
491    fn nav_button(&self, label: Arc<str>, disabled: bool, action: PaginationAction) -> Button {
492        let overrides = self.button_overrides[action_index(action)];
493        let variant = overrides.variant.unwrap_or(self.button_variant);
494        let border_style = overrides.border_style.unwrap_or(self.button_border_style);
495        let style = overrides.style.unwrap_or(self.button_style);
496        let hover_style = overrides.hover_style.unwrap_or(self.button_hover_style);
497        let focus_style = overrides.focus_style.unwrap_or(self.button_focus_style);
498        let disabled_style = overrides
499            .disabled_style
500            .unwrap_or(self.button_disabled_style);
501
502        let mut button = Button::new(label)
503            .variant(variant)
504            .style(style)
505            .hover_style_slot(hover_style)
506            .focus_style_slot(focus_style)
507            .disabled_style(disabled_style)
508            .disabled(disabled);
509
510        if matches!(variant, ButtonVariant::Outlined) {
511            button = button.border_style(border_style);
512        }
513
514        if let Some(cb) = self.on_action.clone() {
515            button = button.on_click(Callback::new(move |_| cb.emit(action)));
516        }
517
518        button
519    }
520}
521
522impl From<PaginationBar> for Element {
523    fn from(bar: PaginationBar) -> Self {
524        let mut row = HStack::new().gap(bar.gap);
525
526        if bar.show_first_last {
527            row = row.child(bar.nav_button(
528                bar.labels.first.clone(),
529                bar.state.is_first_page(),
530                PaginationAction::First,
531            ));
532        }
533
534        row = row.child(bar.nav_button(
535            bar.labels.prev.clone(),
536            bar.state.is_first_page(),
537            PaginationAction::Prev,
538        ));
539
540        let page = bar.state.page() + 1;
541        let total_pages = bar.state.total_pages();
542        let total = bar.state.total_items();
543        let (start, end) = bar.state.range();
544        let first_row = if total == 0 {
545            0
546        } else {
547            start.saturating_add(1)
548        };
549        let info_data = PaginationInfo {
550            page_index: bar.state.page(),
551            page_number: page,
552            total_pages,
553            total_items: total,
554            per_page: bar.state.per_page(),
555            start,
556            end,
557        };
558        let info = if let Some(formatter) = bar.info_formatter.as_ref() {
559            formatter(info_data)
560        } else if bar.show_range_info {
561            Arc::from(format!(
562                "Page {}/{}  (rows {}-{} of {})",
563                page, total_pages, first_row, end, total
564            ))
565        } else {
566            Arc::from(format!("Page {}/{}", page, total_pages))
567        };
568        row = row.child(Text::new(info).style(bar.info_style));
569
570        row = row.child(bar.nav_button(
571            bar.labels.next.clone(),
572            bar.state.is_last_page(),
573            PaginationAction::Next,
574        ));
575
576        if bar.show_first_last {
577            row = row.child(bar.nav_button(
578                bar.labels.last.clone(),
579                bar.state.is_last_page(),
580                PaginationAction::Last,
581            ));
582        }
583
584        row.into()
585    }
586}
587
588fn action_index(action: PaginationAction) -> usize {
589    match action {
590        PaginationAction::First => 0,
591        PaginationAction::Prev => 1,
592        PaginationAction::Next => 2,
593        PaginationAction::Last => 3,
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::{PaginationLabels, PaginationState};
600
601    #[test]
602    fn clamps_page_to_last_after_total_change() {
603        let mut state = PaginationState::new(120, 10);
604        state.set_page(9);
605        state.set_total_items(15);
606        assert_eq!(state.page(), 1);
607    }
608
609    #[test]
610    fn range_matches_page_window() {
611        let mut state = PaginationState::new(53, 10);
612        state.set_page(2);
613        assert_eq!(state.range(), (20, 30));
614
615        state.last_page();
616        assert_eq!(state.range(), (50, 53));
617    }
618
619    #[test]
620    fn default_labels_are_ascii_navigation_arrows() {
621        let labels = PaginationLabels::default();
622        assert_eq!(labels.first.as_ref(), "<<");
623        assert_eq!(labels.prev.as_ref(), "<");
624        assert_eq!(labels.next.as_ref(), ">");
625        assert_eq!(labels.last.as_ref(), ">>");
626    }
627}