Skip to main content

ui/components/
pagination.rs

1use std::rc::Rc;
2
3use crate::prelude::*;
4
5/// Prev/next + numbered page navigation.
6///
7/// Presentational only: the caller owns the current page and is notified of
8/// page changes via [`Pagination::on_change`]; this component does not hold
9/// any page state itself.
10#[derive(IntoElement, RegisterComponent)]
11pub struct Pagination {
12    id: ElementId,
13    current_page: usize,
14    total_pages: usize,
15    on_change: Option<Rc<dyn Fn(usize, &mut Window, &mut App) + 'static>>,
16}
17
18impl Pagination {
19    /// `current_page` and `total_pages` are both 1-indexed.
20    pub fn new(id: impl Into<ElementId>, current_page: usize, total_pages: usize) -> Self {
21        Self {
22            id: id.into(),
23            current_page: current_page.max(1),
24            total_pages: total_pages.max(1),
25            on_change: None,
26        }
27    }
28
29    /// Called with the newly selected page whenever the user clicks a page
30    /// number, or the prev/next buttons.
31    pub fn on_change(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self {
32        self.on_change = Some(Rc::new(handler));
33        self
34    }
35}
36
37impl RenderOnce for Pagination {
38    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
39        let total = self.total_pages;
40        let current = self.current_page.min(total);
41        let on_change = self.on_change;
42
43        let prev_disabled = current <= 1;
44        let next_disabled = current >= total;
45
46        let prev_handler = on_change.clone();
47        let next_handler = on_change.clone();
48
49        h_flex()
50            .id(self.id)
51            .items_center()
52            .gap_1()
53            .child(
54                IconButton::new("pagination-prev", IconName::ChevronLeft)
55                    .disabled(prev_disabled)
56                    .when(!prev_disabled, |this| {
57                        this.on_click(move |_, window, cx| {
58                            if let Some(handler) = prev_handler.as_ref() {
59                                handler(current - 1, window, cx);
60                            }
61                        })
62                    }),
63            )
64            .children((1..=total).map(|page| {
65                let handler = on_change.clone();
66                let is_active = page == current;
67
68                let button = Button::new(("pagination-page", page), page.to_string())
69                    .label_size(LabelSize::Small);
70                let button = if is_active {
71                    button.primary()
72                } else {
73                    button.style(ButtonStyle::Transparent)
74                };
75
76                button.when(!is_active, |this| {
77                    this.on_click(move |_, window, cx| {
78                        if let Some(handler) = handler.as_ref() {
79                            handler(page, window, cx);
80                        }
81                    })
82                })
83            }))
84            .child(
85                IconButton::new("pagination-next", IconName::ChevronRight)
86                    .disabled(next_disabled)
87                    .when(!next_disabled, |this| {
88                        this.on_click(move |_, window, cx| {
89                            if let Some(handler) = next_handler.as_ref() {
90                                handler(current + 1, window, cx);
91                            }
92                        })
93                    }),
94            )
95    }
96}
97
98impl Component for Pagination {
99    fn scope() -> ComponentScope {
100        ComponentScope::Navigation
101    }
102
103    fn description() -> Option<&'static str> {
104        Some("Prev/next and numbered page navigation; caller owns the current page.")
105    }
106
107    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
108        Some(
109            v_flex()
110                .gap_6()
111                .children(vec![
112                    example_group_with_title(
113                        "Middle Page",
114                        vec![single_example(
115                            "Page 3 of 5",
116                            Pagination::new("pagination_middle", 3, 5).into_any_element(),
117                        )],
118                    ),
119                    example_group_with_title(
120                        "Bounds",
121                        vec![
122                            single_example(
123                                "First Page",
124                                Pagination::new("pagination_first", 1, 5).into_any_element(),
125                            ),
126                            single_example(
127                                "Last Page",
128                                Pagination::new("pagination_last", 5, 5).into_any_element(),
129                            ),
130                        ],
131                    ),
132                ])
133                .into_any_element(),
134        )
135    }
136}