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#[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 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 pub fn current_page(mut self, page: usize) -> Self {
53 self.current_page = page.max(1);
54 self
55 }
56
57 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 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 pub fn compact(mut self) -> Self {
89 self.compact = true;
90 self
91 }
92
93 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}