use std::sync::Arc;
use crate::callback::Callback;
use crate::core::element::Element;
use crate::style::{BorderStyle, Style, StyleSlot};
use crate::widgets::button::ButtonVariant;
use crate::widgets::{Button, HStack, Text};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PaginationState {
page: usize,
per_page: usize,
total_items: usize,
}
impl PaginationState {
pub fn new(total_items: usize, per_page: usize) -> Self {
let per_page = per_page.max(1);
let mut state = Self {
page: 0,
per_page,
total_items,
};
state.clamp_page();
state
}
pub fn page(&self) -> usize {
self.page
}
pub fn per_page(&self) -> usize {
self.per_page
}
pub fn total_items(&self) -> usize {
self.total_items
}
pub fn total_pages(&self) -> usize {
self.total_items.max(1).div_ceil(self.per_page)
}
pub fn is_first_page(&self) -> bool {
self.page == 0
}
pub fn is_last_page(&self) -> bool {
self.page + 1 >= self.total_pages()
}
pub fn set_page(&mut self, page: usize) {
self.page = page;
self.clamp_page();
}
pub fn prev_page(&mut self) {
self.page = self.page.saturating_sub(1);
}
pub fn next_page(&mut self) {
if !self.is_last_page() {
self.page += 1;
}
}
pub fn first_page(&mut self) {
self.page = 0;
}
pub fn last_page(&mut self) {
self.page = self.total_pages().saturating_sub(1);
}
pub fn set_per_page(&mut self, per_page: usize) {
self.per_page = per_page.max(1);
self.clamp_page();
}
pub fn set_total_items(&mut self, total_items: usize) {
self.total_items = total_items;
self.clamp_page();
}
pub fn range(&self) -> (usize, usize) {
let start = self.page.saturating_mul(self.per_page);
let end = start.saturating_add(self.per_page).min(self.total_items);
(start.min(self.total_items), end)
}
fn clamp_page(&mut self) {
self.page = self.page.min(self.total_pages().saturating_sub(1));
}
}
impl Default for PaginationState {
fn default() -> Self {
Self::new(0, 10)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PaginationAction {
First,
Prev,
Next,
Last,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct PaginationLabels {
pub first: Arc<str>,
pub prev: Arc<str>,
pub next: Arc<str>,
pub last: Arc<str>,
}
impl Default for PaginationLabels {
fn default() -> Self {
Self {
first: "<<".into(),
prev: "<".into(),
next: ">".into(),
last: ">>".into(),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct PaginationButtonOverrides {
pub variant: Option<ButtonVariant>,
pub border_style: Option<BorderStyle>,
pub style: Option<Style>,
pub hover_style: Option<StyleSlot>,
pub focus_style: Option<StyleSlot>,
pub disabled_style: Option<Style>,
}
impl PaginationButtonOverrides {
pub fn new() -> Self {
Self::default()
}
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.variant = Some(variant);
self
}
pub fn border_style(mut self, border_style: BorderStyle) -> Self {
self.border_style = Some(border_style);
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
pub fn hover_style(mut self, style: Style) -> Self {
self.hover_style = Some(StyleSlot::Replace(style));
self
}
pub fn extend_hover_style(mut self, style: Style) -> Self {
self.hover_style = Some(StyleSlot::Extend(style));
self
}
pub fn inherit_hover_style(mut self) -> Self {
self.hover_style = Some(StyleSlot::Inherit);
self
}
pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
self.hover_style = Some(slot);
self
}
pub fn focus_style(mut self, style: Style) -> Self {
self.focus_style = Some(StyleSlot::Replace(style));
self
}
pub fn extend_focus_style(mut self, style: Style) -> Self {
self.focus_style = Some(StyleSlot::Extend(style));
self
}
pub fn inherit_focus_style(mut self) -> Self {
self.focus_style = Some(StyleSlot::Inherit);
self
}
pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
self.focus_style = Some(slot);
self
}
pub fn disabled_style(mut self, style: Style) -> Self {
self.disabled_style = Some(style);
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PaginationInfo {
pub page_index: usize,
pub page_number: usize,
pub total_pages: usize,
pub total_items: usize,
pub per_page: usize,
pub start: usize,
pub end: usize,
}
type PaginationInfoFormatter = Arc<dyn Fn(PaginationInfo) -> Arc<str>>;
#[derive(Clone)]
pub struct PaginationBar {
state: PaginationState,
labels: PaginationLabels,
show_first_last: bool,
show_range_info: bool,
gap: u16,
button_variant: ButtonVariant,
button_border_style: BorderStyle,
button_style: Style,
button_hover_style: StyleSlot,
button_focus_style: StyleSlot,
button_disabled_style: Style,
button_overrides: [PaginationButtonOverrides; 4],
info_style: Style,
info_formatter: Option<PaginationInfoFormatter>,
on_action: Option<Callback<PaginationAction>>,
}
impl PaginationBar {
pub fn new(state: PaginationState) -> Self {
Self {
state,
labels: PaginationLabels::default(),
show_first_last: true,
show_range_info: true,
gap: 1,
button_variant: ButtonVariant::Outlined,
button_border_style: BorderStyle::Plain,
button_style: Style::default(),
button_hover_style: StyleSlot::Inherit,
button_focus_style: StyleSlot::Inherit,
button_disabled_style: Style::default(),
button_overrides: [PaginationButtonOverrides::default(); 4],
info_style: Style::default(),
info_formatter: None,
on_action: None,
}
}
pub fn labels(mut self, labels: PaginationLabels) -> Self {
self.labels = labels;
self
}
pub fn first_label(mut self, label: impl Into<Arc<str>>) -> Self {
self.labels.first = label.into();
self
}
pub fn prev_label(mut self, label: impl Into<Arc<str>>) -> Self {
self.labels.prev = label.into();
self
}
pub fn next_label(mut self, label: impl Into<Arc<str>>) -> Self {
self.labels.next = label.into();
self
}
pub fn last_label(mut self, label: impl Into<Arc<str>>) -> Self {
self.labels.last = label.into();
self
}
pub fn show_first_last(mut self, show: bool) -> Self {
self.show_first_last = show;
self
}
pub fn show_range_info(mut self, show: bool) -> Self {
self.show_range_info = show;
self
}
pub fn gap(mut self, gap: u16) -> Self {
self.gap = gap;
self
}
pub fn button_variant(mut self, variant: ButtonVariant) -> Self {
self.button_variant = variant;
self
}
pub fn button_border_style(mut self, style: BorderStyle) -> Self {
self.button_border_style = style;
self
}
pub fn button_style(mut self, style: Style) -> Self {
self.button_style = style;
self
}
pub fn button_hover_style(mut self, style: Style) -> Self {
self.button_hover_style = StyleSlot::Replace(style);
self
}
pub fn extend_button_hover_style(mut self, style: Style) -> Self {
self.button_hover_style = StyleSlot::Extend(style);
self
}
pub fn inherit_button_hover_style(mut self) -> Self {
self.button_hover_style = StyleSlot::Inherit;
self
}
pub fn button_hover_style_slot(mut self, slot: StyleSlot) -> Self {
self.button_hover_style = slot;
self
}
pub fn button_focus_style(mut self, style: Style) -> Self {
self.button_focus_style = StyleSlot::Replace(style);
self
}
pub fn extend_button_focus_style(mut self, style: Style) -> Self {
self.button_focus_style = StyleSlot::Extend(style);
self
}
pub fn inherit_button_focus_style(mut self) -> Self {
self.button_focus_style = StyleSlot::Inherit;
self
}
pub fn button_focus_style_slot(mut self, slot: StyleSlot) -> Self {
self.button_focus_style = slot;
self
}
pub fn button_disabled_style(mut self, style: Style) -> Self {
self.button_disabled_style = style;
self
}
pub fn button_overrides_for(
mut self,
action: PaginationAction,
overrides: PaginationButtonOverrides,
) -> Self {
self.button_overrides[action_index(action)] = overrides;
self
}
pub fn first_button_overrides(mut self, overrides: PaginationButtonOverrides) -> Self {
self.button_overrides[action_index(PaginationAction::First)] = overrides;
self
}
pub fn prev_button_overrides(mut self, overrides: PaginationButtonOverrides) -> Self {
self.button_overrides[action_index(PaginationAction::Prev)] = overrides;
self
}
pub fn next_button_overrides(mut self, overrides: PaginationButtonOverrides) -> Self {
self.button_overrides[action_index(PaginationAction::Next)] = overrides;
self
}
pub fn last_button_overrides(mut self, overrides: PaginationButtonOverrides) -> Self {
self.button_overrides[action_index(PaginationAction::Last)] = overrides;
self
}
pub fn info_style(mut self, style: Style) -> Self {
self.info_style = style;
self
}
pub fn info_formatter<F>(mut self, formatter: F) -> Self
where
F: Fn(PaginationInfo) -> Arc<str> + 'static,
{
self.info_formatter = Some(Arc::new(formatter));
self
}
pub fn on_action(mut self, cb: Callback<PaginationAction>) -> Self {
self.on_action = Some(cb);
self
}
fn nav_button(&self, label: Arc<str>, disabled: bool, action: PaginationAction) -> Button {
let overrides = self.button_overrides[action_index(action)];
let variant = overrides.variant.unwrap_or(self.button_variant);
let border_style = overrides.border_style.unwrap_or(self.button_border_style);
let style = overrides.style.unwrap_or(self.button_style);
let hover_style = overrides.hover_style.unwrap_or(self.button_hover_style);
let focus_style = overrides.focus_style.unwrap_or(self.button_focus_style);
let disabled_style = overrides
.disabled_style
.unwrap_or(self.button_disabled_style);
let mut button = Button::new(label)
.variant(variant)
.style(style)
.hover_style_slot(hover_style)
.focus_style_slot(focus_style)
.disabled_style(disabled_style)
.disabled(disabled);
if matches!(variant, ButtonVariant::Outlined) {
button = button.border_style(border_style);
}
if let Some(cb) = self.on_action.clone() {
button = button.on_click(Callback::new(move |_| cb.emit(action)));
}
button
}
}
impl From<PaginationBar> for Element {
fn from(bar: PaginationBar) -> Self {
let mut row = HStack::new().gap(bar.gap);
if bar.show_first_last {
row = row.child(bar.nav_button(
bar.labels.first.clone(),
bar.state.is_first_page(),
PaginationAction::First,
));
}
row = row.child(bar.nav_button(
bar.labels.prev.clone(),
bar.state.is_first_page(),
PaginationAction::Prev,
));
let page = bar.state.page() + 1;
let total_pages = bar.state.total_pages();
let total = bar.state.total_items();
let (start, end) = bar.state.range();
let first_row = if total == 0 {
0
} else {
start.saturating_add(1)
};
let info_data = PaginationInfo {
page_index: bar.state.page(),
page_number: page,
total_pages,
total_items: total,
per_page: bar.state.per_page(),
start,
end,
};
let info = if let Some(formatter) = bar.info_formatter.as_ref() {
formatter(info_data)
} else if bar.show_range_info {
Arc::from(format!(
"Page {}/{} (rows {}-{} of {})",
page, total_pages, first_row, end, total
))
} else {
Arc::from(format!("Page {}/{}", page, total_pages))
};
row = row.child(Text::new(info).style(bar.info_style));
row = row.child(bar.nav_button(
bar.labels.next.clone(),
bar.state.is_last_page(),
PaginationAction::Next,
));
if bar.show_first_last {
row = row.child(bar.nav_button(
bar.labels.last.clone(),
bar.state.is_last_page(),
PaginationAction::Last,
));
}
row.into()
}
}
fn action_index(action: PaginationAction) -> usize {
match action {
PaginationAction::First => 0,
PaginationAction::Prev => 1,
PaginationAction::Next => 2,
PaginationAction::Last => 3,
}
}
#[cfg(test)]
mod tests {
use super::{PaginationLabels, PaginationState};
#[test]
fn clamps_page_to_last_after_total_change() {
let mut state = PaginationState::new(120, 10);
state.set_page(9);
state.set_total_items(15);
assert_eq!(state.page(), 1);
}
#[test]
fn range_matches_page_window() {
let mut state = PaginationState::new(53, 10);
state.set_page(2);
assert_eq!(state.range(), (20, 30));
state.last_page();
assert_eq!(state.range(), (50, 53));
}
#[test]
fn default_labels_are_ascii_navigation_arrows() {
let labels = PaginationLabels::default();
assert_eq!(labels.first.as_ref(), "<<");
assert_eq!(labels.prev.as_ref(), "<");
assert_eq!(labels.next.as_ref(), ">");
assert_eq!(labels.last.as_ref(), ">>");
}
}