kael_ui 0.2.0

Professional shadcn-inspired UI component library for Kael. 100+ accessible components for building beautiful, performant desktop applications.
//! Pagination component for navigating through paginated content.

use kael::{prelude::FluentBuilder as _, *};
use std::rc::Rc;

use crate::components::button::{Button, ButtonSize, ButtonVariant};
use crate::theme::use_theme;

#[derive(IntoElement)]
pub struct Pagination {
    current_page: usize,
    total_pages: usize,
    siblings: usize,
    show_edges: bool,
    on_page_change: Option<Rc<dyn Fn(usize, &mut Window, &mut App)>>,
    style: StyleRefinement,
}

impl Pagination {
    pub fn new() -> Self {
        Self {
            current_page: 1,
            total_pages: 1,
            siblings: 1,
            show_edges: true,
            on_page_change: None,
            style: StyleRefinement::default(),
        }
    }

    pub fn current_page(mut self, page: usize) -> Self {
        self.current_page = page.max(1);
        self
    }

    pub fn total_pages(mut self, total: usize) -> Self {
        self.total_pages = total.max(1);
        self
    }

    pub fn siblings(mut self, siblings: usize) -> Self {
        self.siblings = siblings;
        self
    }

    pub fn show_edges(mut self, show: bool) -> Self {
        self.show_edges = show;
        self
    }

    pub fn on_page_change<F>(mut self, handler: F) -> Self
    where
        F: Fn(usize, &mut Window, &mut App) + 'static,
    {
        self.on_page_change = Some(Rc::new(handler));
        self
    }

    fn get_page_range(&self) -> Vec<PageItem> {
        let mut items = Vec::new();
        let current = self.current_page;
        let total = self.total_pages;
        let siblings = self.siblings;

        if total <= 7 {
            for i in 1..=total {
                items.push(PageItem::Page(i));
            }
            return items;
        }

        let left_sibling = (current.saturating_sub(siblings)).max(1);
        let right_sibling = (current + siblings).min(total);

        let show_left_ellipsis = left_sibling > 2;
        let show_right_ellipsis = right_sibling < total - 1;

        if self.show_edges {
            items.push(PageItem::Page(1));
        }

        if show_left_ellipsis {
            items.push(PageItem::Ellipsis);
        } else if left_sibling == 2 {
            items.push(PageItem::Page(2));
        }

        for i in left_sibling..=right_sibling {
            if i != 1 && i != total {
                items.push(PageItem::Page(i));
            }
        }

        if show_right_ellipsis {
            items.push(PageItem::Ellipsis);
        } else if right_sibling == total - 1 {
            items.push(PageItem::Page(total - 1));
        }

        if self.show_edges {
            items.push(PageItem::Page(total));
        }

        items
    }
}

impl Default for Pagination {
    fn default() -> Self {
        Self::new()
    }
}

impl Styled for Pagination {
    fn style(&mut self) -> &mut StyleRefinement {
        &mut self.style
    }
}

#[derive(Clone, Copy, Debug)]
enum PageItem {
    Page(usize),
    Ellipsis,
}

impl RenderOnce for Pagination {
    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
        let theme = use_theme();
        let current_page = self.current_page;
        let total_pages = self.total_pages;
        let page_range = self.get_page_range();
        let on_change = self.on_page_change;
        let user_style = self.style;

        let has_prev = current_page > 1;
        let has_next = current_page < total_pages;

        div()
            .flex()
            .items_center()
            .gap(px(4.0))
            .child({
                let handler = on_change.clone();
                Button::new("previous-btn", "Previous")
                    .variant(ButtonVariant::Outline)
                    .size(ButtonSize::Sm)
                    .disabled(!has_prev)
                    .when(has_prev && handler.is_some(), |btn| {
                        let handler = handler.clone().unwrap();
                        btn.on_click(move |_, window, cx| {
                            handler(current_page - 1, window, cx);
                        })
                    })
            })
            .children({
                let on_change_for_pages = on_change.clone();
                page_range.into_iter().map(move |item| match item {
                    PageItem::Page(page) => {
                        let is_current = page == current_page;
                        let handler = on_change_for_pages.clone();

                        div()
                            .child(
                                Button::new(("page-btn", page), page.to_string())
                                    .variant(if is_current {
                                        ButtonVariant::Default
                                    } else {
                                        ButtonVariant::Outline
                                    })
                                    .size(ButtonSize::Sm)
                                    .disabled(is_current)
                                    .when(!is_current && handler.is_some(), |btn| {
                                        let handler = handler.clone().unwrap();
                                        btn.on_click(move |_, window, cx| {
                                            handler(page, window, cx);
                                        })
                                    }),
                            )
                            .into_any_element()
                    }
                    PageItem::Ellipsis => div()
                        .flex()
                        .items_center()
                        .justify_center()
                        .h(px(36.0))
                        .w(px(36.0))
                        .text_color(theme.tokens.muted_foreground)
                        .child("...")
                        .into_any_element(),
                })
            })
            .child({
                let handler = on_change;
                Button::new("next-btn", "Next")
                    .variant(ButtonVariant::Outline)
                    .size(ButtonSize::Sm)
                    .disabled(!has_next)
                    .when(has_next && handler.is_some(), |btn| {
                        let handler = handler.unwrap();
                        btn.on_click(move |_, window, cx| {
                            handler(current_page + 1, window, cx);
                        })
                    })
            })
            .map(|this| {
                let mut div = this;
                div.style().refine(&user_style);
                div
            })
    }
}