Skip to main content

rusty_bubbles/
paginator.rs

1//! Cleanroom Rust port of upstream Go source file: `paginator/paginator.go`
2//! Upstream Target Tag / Version: `v2.1.0`
3//!
4//! <public-docs>
5//! # Paginator
6//!
7//! A Bubble Tea package for calculating pagination and rendering pagination
8//! info. Note that this package does not render actual pages: it's purely for
9//! handling keystrokes related to pagination, and rendering pagination status.
10//! </public-docs>
11
12use crate::key::{self, Binding};
13use rusty_bubbletea::key::{Key, KeyPressMsg};
14use rusty_bubbletea::model::{Cmd, Msg};
15
16/// Type specifies the way we render pagination.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum Type {
19    /// Arabic numerals, e.g. "3/7".
20    #[default]
21    Arabic,
22    /// Dot indicators.
23    Dots,
24}
25
26/// KeyMap is the key bindings for different actions within the paginator.
27#[derive(Debug, Clone)]
28pub struct KeyMap {
29    /// Binding to go to the previous page.
30    pub prev_page: Binding,
31    /// Binding to go to the next page.
32    pub next_page: Binding,
33}
34
35/// DefaultKeyMap is the default set of key bindings for navigating and acting
36/// upon the paginator.
37pub fn default_key_map() -> KeyMap {
38    KeyMap {
39        prev_page: key::new_binding(vec![key::with_keys(&["pgup", "left", "h"])]),
40        next_page: key::new_binding(vec![key::with_keys(&["pgdown", "right", "l"])]),
41    }
42}
43
44/// Model is the Bubble Tea model for this user interface.
45#[derive(Debug, Clone)]
46pub struct Model {
47    /// Type configures how the pagination is rendered (Arabic, Dots).
48    pub type_: Type,
49    /// Page is the current page number.
50    pub page: usize,
51    /// PerPage is the number of items per page.
52    pub per_page: usize,
53    /// TotalPages is the total number of pages.
54    pub total_pages: usize,
55    /// ActiveDot is used to mark the current page under the Dots display type.
56    pub active_dot: String,
57    /// InactiveDot is used to mark inactive pages under the Dots display type.
58    pub inactive_dot: String,
59    /// ArabicFormat is the printf-style format to use for the Arabic display type.
60    pub arabic_format: String,
61
62    /// KeyMap encodes the keybindings recognized by the widget.
63    pub key_map: KeyMap,
64}
65
66impl Model {
67    /// SetTotalPages is a helper function for calculating the total number of
68    /// pages from a given number of items. Its use is optional since this
69    /// pager can be used for other things beyond navigating sets. Note that
70    /// it both returns the number of total pages and alters the model.
71    pub fn set_total_pages(&mut self, items: usize) -> usize {
72        if items < 1 {
73            return self.total_pages;
74        }
75        let mut n = items / self.per_page;
76        if !items.is_multiple_of(self.per_page) {
77            n += 1;
78        }
79        self.total_pages = n;
80        n
81    }
82
83    /// ItemsOnPage is a helper function for returning the number of items on
84    /// the current page given the total number of items passed as an
85    /// argument.
86    pub fn items_on_page(&self, total_items: usize) -> usize {
87        if total_items < 1 {
88            return 0;
89        }
90        let (start, end) = self.get_slice_bounds(total_items);
91        end - start
92    }
93
94    /// GetSliceBounds is a helper function for paginating slices. Pass the
95    /// length of the slice you're rendering and you'll receive the start and
96    /// end bounds corresponding to the pagination. For example:
97    ///
98    /// ```rust
99    /// # use rusty_bubbles::paginator;
100    /// # let mut model = paginator::new(vec![]);
101    /// # model.per_page = 2;
102    /// let bunch_of_stuff = vec![1, 2, 3, 4, 5];
103    /// let (start, end) = model.get_slice_bounds(bunch_of_stuff.len());
104    /// let slice_to_render = &bunch_of_stuff[start..end];
105    /// ```
106    pub fn get_slice_bounds(&self, length: usize) -> (usize, usize) {
107        let start = self.page * self.per_page;
108        let end = (self.page * self.per_page + self.per_page).min(length);
109        (start, end)
110    }
111
112    /// PrevPage is a helper function for navigating one page backward. It
113    /// will not page beyond the first page (i.e. page 0).
114    pub fn prev_page(&mut self) {
115        if self.page > 0 {
116            self.page -= 1;
117        }
118    }
119
120    /// NextPage is a helper function for navigating one page forward. It
121    /// will not page beyond the last page (i.e. totalPages - 1).
122    pub fn next_page(&mut self) {
123        if !self.on_last_page() {
124            self.page += 1;
125        }
126    }
127
128    /// OnLastPage returns whether or not we're on the last page.
129    pub fn on_last_page(&self) -> bool {
130        self.page == self.total_pages - 1
131    }
132
133    /// OnFirstPage returns whether or not we're on the first page.
134    pub fn on_first_page(&self) -> bool {
135        self.page == 0
136    }
137
138    /// Update is the Tea update function which binds keystrokes to
139    /// pagination.
140    pub fn update(&mut self, msg: &dyn Msg) -> Cmd {
141        if let Some(m) = msg.as_any().downcast_ref::<KeyPressMsg>() {
142            let k: &Key = &m.0;
143            if key::matches(k, std::slice::from_ref(&self.key_map.next_page)) {
144                self.next_page();
145            } else if key::matches(k, std::slice::from_ref(&self.key_map.prev_page)) {
146                self.prev_page();
147            }
148        }
149        None
150    }
151
152    /// View renders the pagination to a string.
153    pub fn view(&self) -> String {
154        match self.type_ {
155            Type::Dots => self.dots_view(),
156            Type::Arabic => self.arabic_view(),
157        }
158    }
159
160    fn dots_view(&self) -> String {
161        let mut s = String::new();
162        for i in 0..self.total_pages {
163            if i == self.page {
164                s += &self.active_dot;
165                continue;
166            }
167            s += &self.inactive_dot;
168        }
169        s
170    }
171
172    fn arabic_view(&self) -> String {
173        // %d/%d with Go's fmt.Sprintf semantics; only the two integer
174        // placeholders used by default are supported.
175        let format = self.arabic_format.clone();
176        if format == "%d/%d" {
177            format!("{}/{}", self.page + 1, self.total_pages)
178        } else {
179            let s = format.replace("%d", &(self.page + 1).to_string());
180            s.replace("%d", &self.total_pages.to_string())
181        }
182    }
183}
184
185/// Option is used to set options in [`new`].
186pub type Option = Box<dyn FnOnce(&mut Model)>;
187
188/// New creates a new model with defaults.
189pub fn new(opts: Vec<Option>) -> Model {
190    let mut m = Model {
191        type_: Type::Arabic,
192        page: 0,
193        per_page: 1,
194        total_pages: 1,
195        key_map: default_key_map(),
196        active_dot: "•".to_string(),
197        inactive_dot: "○".to_string(),
198        arabic_format: "%d/%d".to_string(),
199    };
200
201    for opt in opts {
202        opt(&mut m);
203    }
204
205    m
206}
207
208/// WithTotalPages sets the total pages.
209pub fn with_total_pages(total_pages: usize) -> Option {
210    Box::new(move |m: &mut Model| {
211        m.total_pages = total_pages;
212    })
213}
214
215/// WithPerPage sets the total pages.
216pub fn with_per_page(per_page: usize) -> Option {
217    Box::new(move |m: &mut Model| {
218        m.per_page = per_page;
219    })
220}