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}