Skip to main content

gpui_component/
pagination.rs

1use std::rc::Rc;
2
3use gpui::{
4    App, ElementId, IntoElement, ParentElement, RenderOnce, SharedString, StyleRefinement, Styled,
5    Window, prelude::FluentBuilder, px,
6};
7use rust_i18n::t;
8
9use gpui_base::{Pagination as BasePagination, PaginationItem as PageItem, PaginationState};
10
11use crate::{
12    Disableable, Icon, Sizable, Size, StyledExt,
13    button::{Button, ButtonVariants},
14    h_flex,
15    icon::IconName,
16    menu::{DropdownMenu as _, PopupMenuItem},
17};
18
19/// Pagination with page navigation, next and previous links.
20#[derive(IntoElement)]
21pub struct Pagination {
22    id: ElementId,
23    style: StyleRefinement,
24    size: Size,
25    current_page: usize,
26    total_pages: usize,
27    disabled: bool,
28    compact: bool,
29    visible_pages: usize,
30    on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App)>>,
31}
32
33impl Pagination {
34    /// Create a new Pagination component with the given ID.
35    pub fn new(id: impl Into<ElementId>) -> Self {
36        Self {
37            id: id.into(),
38            style: StyleRefinement::default(),
39            size: Size::default(),
40            current_page: 1,
41            total_pages: 1,
42            visible_pages: 5,
43            disabled: false,
44            compact: false,
45            on_click: None,
46        }
47    }
48
49    /// Set the current page number (1-based).
50    ///
51    /// The value will be clamped between 1 and total_pages when total_pages is set.
52    pub fn current_page(mut self, page: usize) -> Self {
53        self.current_page = page.max(1);
54        self
55    }
56
57    /// Set the total number of pages.
58    pub fn total_pages(mut self, pages: usize) -> Self {
59        self.total_pages = pages.max(1);
60        if self.current_page > self.total_pages {
61            self.current_page = self.total_pages;
62        }
63        self
64    }
65
66    /// Set the handler for page change (when clicking on page numbers, prev, or next).
67    ///
68    /// This handler receives the new page number to navigate to.
69    ///
70    /// # Examples
71    ///
72    /// ```ignore
73    /// Pagination::new("my-pagination")
74    ///     .current_page(current_page)
75    ///     .total_pages(total_pages)
76    ///     .on_click(|page, _, cx| {
77    ///         // Handle page change
78    ///     })
79    /// ```
80    pub fn on_click(mut self, handler: impl Fn(&usize, &mut Window, &mut App) + 'static) -> Self {
81        self.on_click = Some(Rc::new(handler));
82        self
83    }
84
85    /// Set to display as compact style.
86    ///
87    /// If true, only the prev, next buttons with only icon.
88    pub fn compact(mut self) -> Self {
89        self.compact = true;
90        self
91    }
92
93    /// Set viewable maximum number of page buttons, default
94    pub fn visible_pages(mut self, max: usize) -> Self {
95        self.visible_pages = max;
96        self
97    }
98
99    fn render_nav_button(&self, state: &PaginationState, is_prev: bool) -> Button {
100        let (id, label, icon) = if is_prev {
101            ("prev", t!("Pagination.previous"), IconName::ChevronLeft)
102        } else {
103            ("next", t!("Pagination.next"), IconName::ChevronRight)
104        };
105
106        let target_page = if is_prev {
107            state.previous_page()
108        } else {
109            state.next_page()
110        };
111
112        Button::new(id)
113            .ghost()
114            .compact()
115            .with_size(self.size)
116            .disabled(target_page.is_none())
117            .tooltip(label.clone())
118            .when(self.compact, |this| this.icon(icon.clone()))
119            .when(!self.compact, |this| {
120                this.child(
121                    h_flex()
122                        .w_full()
123                        .gap_2()
124                        .flex_nowrap()
125                        .when(is_prev, |this| this.flex_row_reverse())
126                        .child(SharedString::from(label))
127                        .child(Icon::new(icon)),
128                )
129            })
130            .when_some(
131                target_page.filter(|_| state.has_on_change()),
132                |this, target_page| {
133                    let state = state.clone();
134                    this.on_click(move |_, window, cx| {
135                        state.request_page(target_page, window, cx);
136                    })
137                },
138            )
139    }
140}
141
142impl Disableable for Pagination {
143    fn disabled(mut self, disabled: bool) -> Self {
144        self.disabled = disabled;
145        self
146    }
147}
148
149impl Sizable for Pagination {
150    fn with_size(mut self, size: impl Into<Size>) -> Self {
151        self.size = size.into();
152        self
153    }
154}
155
156impl Styled for Pagination {
157    fn style(&mut self) -> &mut StyleRefinement {
158        &mut self.style
159    }
160}
161
162impl RenderOnce for Pagination {
163    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
164        let mut state = PaginationState::new(self.current_page, self.total_pages)
165            .visible_pages(self.visible_pages)
166            .disabled(self.disabled);
167        if let Some(on_click) = self.on_click.clone() {
168            state = state.on_change(move |page, window, cx| on_click(&page, window, cx));
169        }
170        let page_numbers = (!self.compact).then(|| state.items()).unwrap_or_default();
171
172        let current_page = state.current_page();
173        let is_disabled = self.disabled;
174        let item_state = state.clone();
175
176        BasePagination::new(self.id.clone(), state.clone())
177            .h_flex()
178            .px_2()
179            .py_2()
180            .gap_1()
181            .items_center()
182            .refine_style(&self.style)
183            .child(self.render_nav_button(&state, true))
184            .children({
185                page_numbers.into_iter().map(|item| match item {
186                    PageItem::Page(page) => {
187                        let is_selected = page == current_page;
188
189                        Button::new(page)
190                            .with_size(self.size)
191                            .map(|this| {
192                                if is_selected {
193                                    this.outline()
194                                } else {
195                                    this.ghost()
196                                }
197                            })
198                            .label(page.to_string())
199                            .compact()
200                            .disabled(is_disabled)
201                            .when(!is_selected && item_state.has_on_change(), |this| {
202                                let state = item_state.clone();
203                                this.on_click(move |_, window, cx| {
204                                    state.request_page(page, window, cx);
205                                })
206                            })
207                            .into_any_element()
208                    }
209                    PageItem::Ellipsis(range) => Button::new(SharedString::from(format!(
210                        "ellipsis-{}-{}",
211                        range.start, range.end
212                    )))
213                    .ghost()
214                    .with_size(self.size)
215                    .compact()
216                    .disabled(self.disabled)
217                    .icon(IconName::Ellipsis)
218                    .dropdown_menu({
219                        let state = item_state.clone();
220                        move |mut menu, _, _| {
221                            for page in range.clone() {
222                                menu = menu.item(
223                                    PopupMenuItem::new(format!("{}", page))
224                                        .checked(page == current_page)
225                                        .on_click({
226                                            let state = state.clone();
227                                            move |_, window, cx| {
228                                                state.request_page(page, window, cx);
229                                            }
230                                        }),
231                                )
232                            }
233
234                            menu.min_w(px(55.)).max_h(px(240.)).scrollable(true)
235                        }
236                    })
237                    .into_any_element(),
238                })
239            })
240            .child(self.render_nav_button(&state, false))
241    }
242}