Skip to main content

vantage_table/
pagination.rs

1use vantage_expressions::traits::selectable::Selectable;
2
3/// Pagination configuration for tables
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct Pagination {
6    page: i64,
7    items_per_page: i64,
8    /// Explicit skip for a random-access `[offset, offset+limit)` window. When
9    /// set, `skip()` returns it verbatim instead of deriving it from `page` —
10    /// so the offset need not be a multiple of the page size.
11    offset: Option<i64>,
12}
13
14impl Pagination {
15    /// Create new pagination with page number and items per page
16    pub fn new(page: i64, items_per_page: i64) -> Self {
17        Self {
18            page: page.max(1),
19            items_per_page: items_per_page.max(1),
20            offset: None,
21        }
22    }
23
24    /// Create pagination for an explicit `[offset, offset + limit)` window
25    /// (random access). Unlike [`new`](Self::new), the offset is independent of
26    /// the page size, so any absolute row range maps directly onto a query.
27    pub fn window(offset: i64, limit: i64) -> Self {
28        Self {
29            page: 1,
30            items_per_page: limit.max(1),
31            offset: Some(offset.max(0)),
32        }
33    }
34
35    /// Set the current page number (1-based)
36    pub fn set_page(&mut self, page: i64) {
37        self.page = page.max(1);
38    }
39
40    /// Set items per page
41    /// When changing page size, adjusts current page to keep focused item visible
42    pub fn set_ipp(&mut self, items_per_page: i64) {
43        let items_per_page = items_per_page.max(1);
44
45        // Calculate which item is currently at the top of the page
46        let first_item_index = (self.page - 1) * self.items_per_page;
47
48        // Calculate which page that item would be on with the new page size
49        self.page = (first_item_index / items_per_page) + 1;
50        self.items_per_page = items_per_page;
51    }
52
53    /// Get the current page number (1-based)
54    pub fn get_page(&self) -> i64 {
55        self.page
56    }
57
58    /// Get items per page
59    pub fn get_ipp(&self) -> i64 {
60        self.items_per_page
61    }
62
63    /// Calculate limit value for queries
64    pub fn limit(&self) -> i64 {
65        self.items_per_page
66    }
67
68    /// Calculate skip/offset value for queries
69    pub fn skip(&self) -> i64 {
70        self.offset.unwrap_or((self.page - 1) * self.items_per_page)
71    }
72
73    /// Apply pagination to a select query
74    pub fn apply_on_select<S, E>(&self, select: &mut S)
75    where
76        S: Selectable<E>,
77    {
78        select.set_limit(Some(self.limit()), Some(self.skip()));
79    }
80}
81
82impl Default for Pagination {
83    fn default() -> Self {
84        Self {
85            page: 1,
86            items_per_page: 50,
87            offset: None,
88        }
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_new_pagination() {
98        let p = Pagination::new(2, 25);
99        assert_eq!(p.get_page(), 2);
100        assert_eq!(p.get_ipp(), 25);
101        assert_eq!(p.limit(), 25);
102        assert_eq!(p.skip(), 25);
103    }
104
105    #[test]
106    fn test_pagination_bounds() {
107        let p = Pagination::new(0, 0);
108        assert_eq!(p.get_page(), 1);
109        assert_eq!(p.get_ipp(), 1);
110    }
111
112    #[test]
113    fn test_set_page() {
114        let mut p = Pagination::new(1, 10);
115        p.set_page(3);
116        assert_eq!(p.get_page(), 3);
117        assert_eq!(p.skip(), 20);
118    }
119
120    #[test]
121    fn test_set_ipp_keeps_focus() {
122        let mut p = Pagination::new(3, 10);
123        // Page 3 with 10 items means items 20-29 are visible
124        // First item is at index 20
125
126        p.set_ipp(5);
127        // With 5 items per page, item 20 should be on page 5 (items 20-24)
128        assert_eq!(p.get_page(), 5);
129        assert_eq!(p.get_ipp(), 5);
130        assert_eq!(p.skip(), 20);
131    }
132
133    #[test]
134    fn test_set_ipp_larger_page_size() {
135        let mut p = Pagination::new(5, 5);
136        // Page 5 with 5 items means items 20-24 are visible
137
138        p.set_ipp(25);
139        // With 25 items per page, item 20 should be on page 1 (items 0-24)
140        assert_eq!(p.get_page(), 1);
141        assert_eq!(p.get_ipp(), 25);
142        assert_eq!(p.skip(), 0);
143    }
144
145    #[test]
146    fn test_default_pagination() {
147        let p = Pagination::default();
148        assert_eq!(p.get_page(), 1);
149        assert_eq!(p.get_ipp(), 50);
150        assert_eq!(p.skip(), 0);
151    }
152}