use std::{ops::Range, rc::Rc};
use gpui::{
App, ElementId, InteractiveElement as _, IntoElement, ParentElement, RenderOnce, SharedString,
StyleRefinement, Styled, Window, prelude::FluentBuilder as _,
};
use crate::{
Button, ButtonVariants as _, Disableable, DropdownMenu as _, Icon, IconName, PopupMenuItem,
Sizable, Size, StyleSized, StyledExt, Tooltip, h_flex, translate_woocraft,
};
type PageClickHandler = dyn Fn(&usize, &mut Window, &mut App);
#[derive(IntoElement)]
pub struct Pagination {
id: ElementId,
style: StyleRefinement,
size: Size,
current_page: usize,
total_pages: usize,
disabled: bool,
compact: bool,
visible_pages: usize,
on_click: Option<Rc<PageClickHandler>>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
enum PageItem {
Page(usize),
Ellipsis(Range<usize>),
}
impl Pagination {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
style: StyleRefinement::default(),
size: Size::default(),
current_page: 1,
total_pages: 1,
visible_pages: 5,
disabled: false,
compact: false,
on_click: None,
}
}
pub fn current_page(mut self, page: usize) -> Self {
self.current_page = page.max(1);
self
}
pub fn total_pages(mut self, pages: usize) -> Self {
self.total_pages = pages.max(1);
if self.current_page > self.total_pages {
self.current_page = self.total_pages;
}
self
}
pub fn on_click(mut self, handler: impl Fn(&usize, &mut Window, &mut App) + 'static) -> Self {
self.on_click = Some(Rc::new(handler));
self
}
pub fn compact(mut self) -> Self {
self.compact = true;
self
}
pub fn visible_pages(mut self, max: usize) -> Self {
self.visible_pages = max;
self
}
fn render_nav_button(&self, is_prev: bool, current_page: usize, total_pages: usize) -> Button {
let (id, label, icon, disabled) = if is_prev {
(
"prev",
SharedString::from(translate_woocraft("pagination.previous")),
IconName::ChevronLeft,
current_page <= 1,
)
} else {
(
"next",
SharedString::from(translate_woocraft("pagination.next")),
IconName::ChevronRight,
current_page >= total_pages,
)
};
let target_page = if is_prev {
current_page.saturating_sub(1)
} else {
current_page.saturating_add(1).min(total_pages)
};
Button::new(id)
.default()
.with_size(self.size)
.disabled(self.disabled || disabled)
.tooltip({
let tooltip_label = label.clone();
move |window, cx| Tooltip::new(tooltip_label.clone()).build(window, cx)
})
.when(self.compact, |this| this.icon(Icon::new(icon)))
.when(!self.compact, |this| {
if is_prev {
this.child(Icon::new(icon)).child(label.clone())
} else {
this.child(label.clone()).child(Icon::new(icon))
}
})
.when_some(self.on_click.clone(), |this, handler| {
this.on_click(move |_, window, cx| {
handler(&target_page, window, cx);
})
})
}
}
impl_disableable!(Pagination);
impl_sizable!(Pagination);
impl_styled!(Pagination);
impl RenderOnce for Pagination {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let current_page = self.current_page.max(1).min(self.total_pages.max(1));
let total_pages = self.total_pages.max(1);
let page_numbers = if !self.compact {
calculate_page_range(current_page, total_pages, self.visible_pages)
} else {
vec![]
};
let is_disabled = self.disabled;
let on_click = self.on_click.clone();
let size = self.size;
h_flex()
.id(self.id.clone())
.container_size(size)
.container_gap(size)
.items_center()
.refine_style(&self.style)
.child(self.render_nav_button(true, current_page, total_pages))
.children({
page_numbers.into_iter().map(move |item| match item {
PageItem::Page(page) => {
let is_selected = page == current_page;
Button::new(("pagination-page", page))
.with_size(size)
.map(|this| {
if is_selected {
this.primary()
} else {
this.default()
}
})
.label(page.to_string())
.disabled(is_disabled)
.when(!is_selected, |this| {
this.when_some(on_click.clone(), |this, handler| {
this.on_click(move |_, window, cx| {
handler(&page, window, cx);
})
})
})
.into_any_element()
}
PageItem::Ellipsis(range) => {
let ellipsis_id = SharedString::from(format!("ellipsis-{}-{}", range.start, range.end));
let on_click_for_popover = on_click.clone();
Button::new((ellipsis_id.clone(), 0usize))
.flat()
.with_size(size)
.disabled(is_disabled)
.icon(Icon::new(IconName::MoreHorizontal))
.dropdown_menu(move |mut menu, _, _| {
let on_click_for_page = on_click_for_popover.clone();
for page in range.clone() {
let is_selected = page == current_page;
let mut item = PopupMenuItem::new(page.to_string())
.checked(is_selected)
.disabled(is_disabled || is_selected);
if !is_selected && let Some(handler) = on_click_for_page.clone() {
item = item.on_click(move |_, window, cx| {
handler(&page, window, cx);
});
}
menu = menu.item(item);
}
menu
})
.into_any_element()
}
})
})
.child(self.render_nav_button(false, current_page, total_pages))
}
}
fn calculate_page_range(current: usize, total: usize, max_visible: usize) -> Vec<PageItem> {
if total <= 1 {
return vec![];
}
let max_visible = max_visible.max(5);
if total <= max_visible {
return (1..=total).map(PageItem::Page).collect();
}
let mut pages = vec![];
let side_pages = (max_visible - 3) / 2;
pages.push(PageItem::Page(1));
let start = if current <= side_pages + 1 {
2
} else if current > total - side_pages - 1 {
total - side_pages - 1
} else {
current - side_pages
};
if start > 2 {
pages.push(PageItem::Ellipsis(2..start));
}
let end = if current >= total - side_pages {
total - 1
} else if current <= side_pages + 1 {
side_pages + 2
} else {
current + side_pages
};
for page in start..=end {
pages.push(PageItem::Page(page));
}
if end < total - 1 {
pages.push(PageItem::Ellipsis(end + 1..total));
}
pages.push(PageItem::Page(total));
pages
}
#[cfg(test)]
mod tests {
#[test]
fn test_calculate_page_range() {
use super::{PageItem, calculate_page_range};
let result = calculate_page_range(1, 10, 7);
let expected = vec![
PageItem::Page(1),
PageItem::Page(2),
PageItem::Page(3),
PageItem::Page(4),
PageItem::Ellipsis(5..10),
PageItem::Page(10),
];
assert_eq!(result, expected);
let result = calculate_page_range(5, 10, 7);
let expected = vec![
PageItem::Page(1),
PageItem::Ellipsis(2..3),
PageItem::Page(3),
PageItem::Page(4),
PageItem::Page(5),
PageItem::Page(6),
PageItem::Page(7),
PageItem::Ellipsis(8..10),
PageItem::Page(10),
];
assert_eq!(result, expected);
let result = calculate_page_range(10, 10, 7);
let expected = vec![
PageItem::Page(1),
PageItem::Ellipsis(2..7),
PageItem::Page(7),
PageItem::Page(8),
PageItem::Page(9),
PageItem::Page(10),
];
assert_eq!(result, expected);
}
}