bubbletea_widgets/paginator.rs
1//! A paginator component for bubbletea-rs, ported from the Go version.
2//!
3//! This component is used for calculating pagination and rendering pagination info.
4//! Note that this package does not render actual pages of content; it's purely
5//! for handling the state and view of the pagination control itself.
6
7use crate::key::{self, KeyMap as KeyMapTrait};
8use bubbletea_rs::{KeyMsg, Msg};
9
10/// The type of pagination to display.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum Type {
13 /// Display pagination as Arabic numerals (e.g., "1/5").
14 #[default]
15 Arabic,
16 /// Display pagination as dots (e.g., "● ○ ○ ○ ○").
17 Dots,
18}
19
20/// Key bindings for different actions within the paginator.
21///
22/// This structure defines the key bindings that control pagination navigation.
23/// It implements the `KeyMap` trait to provide help information for the
24/// paginator component.
25///
26/// # Examples
27///
28/// ```rust
29/// use bubbletea_widgets::paginator::PaginatorKeyMap;
30/// use bubbletea_widgets::key;
31///
32/// let keymap = PaginatorKeyMap::default();
33///
34/// // Create custom key bindings
35/// let custom_keymap = PaginatorKeyMap {
36/// prev_page: key::new_binding(vec![
37/// key::with_keys_str(&["a", "left"]),
38/// key::with_help("a/←", "previous page"),
39/// ]),
40/// next_page: key::new_binding(vec![
41/// key::with_keys_str(&["d", "right"]),
42/// key::with_help("d/→", "next page"),
43/// ]),
44/// };
45/// ```
46#[derive(Debug, Clone)]
47pub struct PaginatorKeyMap {
48 /// Key binding for navigating to the previous page.
49 /// Default keys: PageUp, Left Arrow, 'h'
50 pub prev_page: key::Binding,
51 /// Key binding for navigating to the next page.
52 /// Default keys: PageDown, Right Arrow, 'l'
53 pub next_page: key::Binding,
54}
55
56impl Default for PaginatorKeyMap {
57 /// Creates default key bindings for paginator navigation.
58 ///
59 /// The default key bindings are:
60 /// - **Previous page**: PageUp, Left Arrow, 'h'
61 /// - **Next page**: PageDown, Right Arrow, 'l'
62 ///
63 /// These bindings are commonly used in terminal applications and provide
64 /// both arrow key navigation and vim-style 'h'/'l' keys.
65 ///
66 /// # Examples
67 ///
68 /// ```rust
69 /// use bubbletea_widgets::paginator::PaginatorKeyMap;
70 /// use bubbletea_widgets::key::KeyMap;
71 ///
72 /// let keymap = PaginatorKeyMap::default();
73 /// let help = keymap.short_help();
74 /// assert_eq!(help.len(), 2); // prev and next bindings
75 /// ```
76 fn default() -> Self {
77 Self {
78 prev_page: key::new_binding(vec![
79 key::with_keys_str(&["pgup", "left", "h"]),
80 key::with_help("←/h", "prev page"),
81 ]),
82 next_page: key::new_binding(vec![
83 key::with_keys_str(&["pgdown", "right", "l"]),
84 key::with_help("→/l", "next page"),
85 ]),
86 }
87 }
88}
89
90impl KeyMapTrait for PaginatorKeyMap {
91 /// Returns key bindings for the short help view.
92 ///
93 /// This provides the essential pagination key bindings that will be
94 /// displayed in compact help views.
95 ///
96 /// # Returns
97 ///
98 /// A vector containing references to the previous page and next page bindings.
99 fn short_help(&self) -> Vec<&key::Binding> {
100 vec![&self.prev_page, &self.next_page]
101 }
102
103 /// Returns key bindings for the full help view.
104 ///
105 /// This organizes all pagination key bindings into columns for display
106 /// in expanded help views. Since pagination only has two keys, they're
107 /// grouped together in a single column.
108 ///
109 /// # Returns
110 ///
111 /// A vector of vectors, where each inner vector represents a column
112 /// of related key bindings.
113 fn full_help(&self) -> Vec<Vec<&key::Binding>> {
114 vec![vec![&self.prev_page, &self.next_page]]
115 }
116}
117
118/// A paginator model for handling pagination state and rendering.
119///
120/// This component manages pagination state including current page, total pages,
121/// and pagination display style. It can render pagination in two modes:
122/// - **Arabic**: Shows page numbers (e.g., "3/10")
123/// - **Dots**: Shows dots representing pages (e.g., "○ ○ ● ○ ○")
124///
125/// The paginator handles key bindings for navigation and provides helper methods
126/// for calculating slice bounds and page information.
127///
128/// # Examples
129///
130/// ## Basic Usage
131///
132/// ```rust
133/// use bubbletea_widgets::paginator::{Model, Type};
134///
135/// let mut paginator = Model::new()
136/// .with_per_page(10)
137/// .with_total_items(150); // Creates 15 pages
138///
139/// assert_eq!(paginator.total_pages, 15);
140/// assert!(paginator.on_first_page());
141///
142/// paginator.next_page();
143/// assert_eq!(paginator.page, 1);
144/// ```
145///
146/// ## Different Display Types
147///
148/// ```rust
149/// use bubbletea_widgets::paginator::{Model, Type};
150///
151/// let mut paginator = Model::new()
152/// .with_total_items(50)
153/// .with_per_page(10);
154///
155/// // Arabic mode (default): "1/5"
156/// paginator.paginator_type = Type::Arabic;
157/// let arabic_view = paginator.view();
158///
159/// // Dots mode: "● ○ ○ ○ ○"
160/// paginator.paginator_type = Type::Dots;
161/// let dots_view = paginator.view();
162/// ```
163///
164/// ## Integration with bubbletea-rs
165///
166/// ```rust
167/// use bubbletea_widgets::paginator::Model as Paginator;
168/// use bubbletea_rs::{Model, Cmd, Msg};
169///
170/// struct App {
171/// paginator: Paginator,
172/// items: Vec<String>,
173/// }
174///
175/// impl Model for App {
176/// fn init() -> (Self, Option<Cmd>) {
177/// let items: Vec<String> = (1..=100).map(|i| format!("Item {}", i)).collect();
178/// let paginator = Paginator::new()
179/// .with_per_page(10)
180/// .with_total_items(items.len());
181///
182/// (Self { paginator, items }, None)
183/// }
184///
185/// fn update(&mut self, msg: Msg) -> Option<Cmd> {
186/// self.paginator.update(&msg);
187/// None
188/// }
189///
190/// fn view(&self) -> String {
191/// let (start, end) = self.paginator.get_slice_bounds(self.items.len());
192/// let page_items: Vec<String> = self.items[start..end].to_vec();
193///
194/// format!(
195/// "Items:\n{}\n\nPage: {}",
196/// page_items.join("\n"),
197/// self.paginator.view()
198/// )
199/// }
200/// }
201/// ```
202#[derive(Debug, Clone)]
203pub struct Model {
204 /// The type of pagination to display (Dots or Arabic).
205 pub paginator_type: Type,
206 /// The current page.
207 pub page: usize,
208 /// The number of items per page.
209 pub per_page: usize,
210 /// The total number of pages.
211 pub total_pages: usize,
212
213 /// The character to use for the active page in Dots mode.
214 pub active_dot: String,
215 /// The character to use for inactive pages in Dots mode.
216 pub inactive_dot: String,
217 /// The format string for Arabic mode (e.g., "%d/%d").
218 pub arabic_format: String,
219
220 /// Key bindings.
221 pub keymap: PaginatorKeyMap,
222}
223
224impl Default for Model {
225 /// Creates a paginator with default settings.
226 ///
227 /// Default configuration:
228 /// - Type: Arabic ("1/5" style)
229 /// - Current page: 0 (first page)
230 /// - Items per page: 1
231 /// - Total pages: 1
232 /// - Active dot: "•" (for dots mode)
233 /// - Inactive dot: "○" (for dots mode)
234 /// - Arabic format: "%d/%d" (current/total)
235 /// - Default key bindings
236 ///
237 /// # Examples
238 ///
239 /// ```rust
240 /// use bubbletea_widgets::paginator::{Model, Type};
241 ///
242 /// let paginator = Model::default();
243 /// assert_eq!(paginator.paginator_type, Type::Arabic);
244 /// assert_eq!(paginator.page, 0);
245 /// assert_eq!(paginator.per_page, 1);
246 /// assert_eq!(paginator.total_pages, 1);
247 /// ```
248 fn default() -> Self {
249 Self {
250 paginator_type: Type::default(),
251 page: 0,
252 per_page: 1,
253 total_pages: 1,
254 active_dot: "•".to_string(),
255 inactive_dot: "○".to_string(),
256 arabic_format: "%d/%d".to_string(),
257 keymap: PaginatorKeyMap::default(),
258 }
259 }
260}
261
262impl Model {
263 /// Creates a new paginator model with default settings.
264 ///
265 /// This is equivalent to calling `Model::default()` but provides a more
266 /// conventional constructor-style API.
267 ///
268 /// # Examples
269 ///
270 /// ```rust
271 /// use bubbletea_widgets::paginator::Model;
272 ///
273 /// let paginator = Model::new();
274 /// assert_eq!(paginator.page, 0);
275 /// assert_eq!(paginator.total_pages, 1);
276 /// ```
277 pub fn new() -> Self {
278 Self::default()
279 }
280
281 /// Sets the total number of items and calculates total pages (builder pattern).
282 ///
283 /// This method automatically calculates the total number of pages based on
284 /// the total items and the current `per_page` setting. If the current page
285 /// becomes out of bounds, it will be adjusted to the last valid page.
286 ///
287 /// # Arguments
288 ///
289 /// * `items` - The total number of items to paginate
290 ///
291 /// # Examples
292 ///
293 /// ```rust
294 /// use bubbletea_widgets::paginator::Model;
295 ///
296 /// let paginator = Model::new()
297 /// .with_per_page(10)
298 /// .with_total_items(95); // Will create 10 pages (95/10 = 9.5 -> 10)
299 ///
300 /// assert_eq!(paginator.total_pages, 10);
301 /// ```
302 pub fn with_total_items(mut self, items: usize) -> Self {
303 self.set_total_items(items);
304 self
305 }
306
307 /// Sets the number of items per page (builder pattern).
308 ///
309 /// The minimum value is 1; any value less than 1 will be clamped to 1.
310 /// This setting affects how total pages are calculated when using
311 /// `set_total_items()` or `with_total_items()`.
312 ///
313 /// # Arguments
314 ///
315 /// * `per_page` - Number of items to display per page (minimum 1)
316 ///
317 /// # Examples
318 ///
319 /// ```rust
320 /// use bubbletea_widgets::paginator::Model;
321 ///
322 /// let paginator = Model::new()
323 /// .with_per_page(25)
324 /// .with_total_items(100); // Will create 4 pages
325 ///
326 /// assert_eq!(paginator.per_page, 25);
327 /// assert_eq!(paginator.total_pages, 4);
328 ///
329 /// // Values less than 1 are clamped to 1
330 /// let clamped = Model::new().with_per_page(0);
331 /// assert_eq!(clamped.per_page, 1);
332 /// ```
333 pub fn with_per_page(mut self, per_page: usize) -> Self {
334 self.per_page = per_page.max(1);
335 self
336 }
337
338 /// Sets the number of items per page (mutable version).
339 ///
340 /// The minimum value is 1; any value less than 1 will be clamped to 1.
341 /// This method modifies the paginator in place.
342 ///
343 /// # Arguments
344 ///
345 /// * `per_page` - Number of items to display per page (minimum 1)
346 ///
347 /// # Examples
348 ///
349 /// ```rust
350 /// use bubbletea_widgets::paginator::Model;
351 ///
352 /// let mut paginator = Model::new();
353 /// paginator.set_per_page(15);
354 /// assert_eq!(paginator.per_page, 15);
355 ///
356 /// // Values less than 1 are clamped to 1
357 /// paginator.set_per_page(0);
358 /// assert_eq!(paginator.per_page, 1);
359 /// ```
360 pub fn set_per_page(&mut self, per_page: usize) {
361 self.per_page = per_page.max(1);
362 }
363
364 /// Sets the active dot character for dots mode (builder pattern).
365 ///
366 /// # Arguments
367 ///
368 /// * `dot` - The character or styled string to use for the active page
369 ///
370 /// # Examples
371 ///
372 /// ```rust
373 /// use bubbletea_widgets::paginator::Model;
374 ///
375 /// let paginator = Model::new().with_active_dot("●");
376 /// assert_eq!(paginator.active_dot, "●");
377 /// ```
378 pub fn with_active_dot(mut self, dot: &str) -> Self {
379 self.active_dot = dot.to_string();
380 self
381 }
382
383 /// Sets the inactive dot character for dots mode (builder pattern).
384 ///
385 /// # Arguments
386 ///
387 /// * `dot` - The character or styled string to use for inactive pages
388 ///
389 /// # Examples
390 ///
391 /// ```rust
392 /// use bubbletea_widgets::paginator::Model;
393 ///
394 /// let paginator = Model::new().with_inactive_dot("○");
395 /// assert_eq!(paginator.inactive_dot, "○");
396 /// ```
397 pub fn with_inactive_dot(mut self, dot: &str) -> Self {
398 self.inactive_dot = dot.to_string();
399 self
400 }
401
402 /// Sets the active dot character for dots mode (mutable version).
403 ///
404 /// # Arguments
405 ///
406 /// * `dot` - The character or styled string to use for the active page
407 ///
408 /// # Examples
409 ///
410 /// ```rust
411 /// use bubbletea_widgets::paginator::Model;
412 ///
413 /// let mut paginator = Model::new();
414 /// paginator.set_active_dot("●");
415 /// assert_eq!(paginator.active_dot, "●");
416 /// ```
417 pub fn set_active_dot(&mut self, dot: &str) {
418 self.active_dot = dot.to_string();
419 }
420
421 /// Sets the inactive dot character for dots mode (mutable version).
422 ///
423 /// # Arguments
424 ///
425 /// * `dot` - The character or styled string to use for inactive pages
426 ///
427 /// # Examples
428 ///
429 /// ```rust
430 /// use bubbletea_widgets::paginator::Model;
431 ///
432 /// let mut paginator = Model::new();
433 /// paginator.set_inactive_dot("○");
434 /// assert_eq!(paginator.inactive_dot, "○");
435 /// ```
436 pub fn set_inactive_dot(&mut self, dot: &str) {
437 self.inactive_dot = dot.to_string();
438 }
439
440 /// Sets the total number of pages directly.
441 ///
442 /// The minimum value is 1; any value less than 1 will be clamped to 1.
443 /// If the current page becomes out of bounds after setting the total pages,
444 /// it will be adjusted to the last valid page.
445 ///
446 /// **Note**: This method sets pages directly. If you want to calculate pages
447 /// based on total items, use `set_total_items()` instead.
448 ///
449 /// # Arguments
450 ///
451 /// * `pages` - The total number of pages (minimum 1)
452 ///
453 /// # Examples
454 ///
455 /// ```rust
456 /// use bubbletea_widgets::paginator::Model;
457 ///
458 /// let mut paginator = Model::new();
459 /// paginator.set_total_pages(10);
460 /// assert_eq!(paginator.total_pages, 10);
461 ///
462 /// // If current page is out of bounds, it gets adjusted
463 /// paginator.page = 15; // Out of bounds
464 /// paginator.set_total_pages(5);
465 /// assert_eq!(paginator.page, 4); // Adjusted to last page (0-indexed)
466 /// ```
467 pub fn set_total_pages(&mut self, pages: usize) {
468 self.total_pages = pages.max(1);
469 // Ensure the current page is not out of bounds
470 if self.page >= self.total_pages {
471 self.page = self.total_pages.saturating_sub(1);
472 }
473 }
474
475 /// Calculates and sets the total number of pages based on the total items.
476 ///
477 /// This method divides the total number of items by the current `per_page`
478 /// setting to calculate the total pages. The result is always at least 1,
479 /// even for 0 items. If the current page becomes out of bounds after
480 /// recalculation, it will be adjusted to the last valid page.
481 ///
482 /// # Arguments
483 ///
484 /// * `items` - The total number of items to paginate
485 ///
486 /// # Examples
487 ///
488 /// ```rust
489 /// use bubbletea_widgets::paginator::Model;
490 ///
491 /// let mut paginator = Model::new().with_per_page(10);
492 ///
493 /// // 95 items with 10 per page = 10 pages (95/10 = 9.5 -> 10)
494 /// paginator.set_total_items(95);
495 /// assert_eq!(paginator.total_pages, 10);
496 ///
497 /// // 0 items still results in 1 page minimum
498 /// paginator.set_total_items(0);
499 /// assert_eq!(paginator.total_pages, 1);
500 ///
501 /// // Exact division
502 /// paginator.set_total_items(100);
503 /// assert_eq!(paginator.total_pages, 10);
504 /// ```
505 pub fn set_total_items(&mut self, items: usize) {
506 if items == 0 {
507 self.total_pages = 1;
508 } else {
509 self.total_pages = items.div_ceil(self.per_page);
510 }
511
512 // Ensure the current page is not out of bounds
513 if self.page >= self.total_pages {
514 self.page = self.total_pages.saturating_sub(1);
515 }
516 }
517
518 /// Returns the number of items on the current page.
519 ///
520 /// This method calculates how many items are actually present on the
521 /// current page, which may be less than `per_page` on the last page
522 /// or when there are fewer total items than `per_page`.
523 ///
524 /// # Arguments
525 ///
526 /// * `total_items` - The total number of items being paginated
527 ///
528 /// # Returns
529 ///
530 /// The number of items on the current page, or 0 if there are no items.
531 ///
532 /// # Examples
533 ///
534 /// ```rust
535 /// use bubbletea_widgets::paginator::Model;
536 ///
537 /// let mut paginator = Model::new().with_per_page(10);
538 ///
539 /// // Full page
540 /// assert_eq!(paginator.items_on_page(100), 10);
541 ///
542 /// // Partial last page
543 /// paginator.page = 9; // Last page (0-indexed)
544 /// assert_eq!(paginator.items_on_page(95), 5); // Only 5 items on page 10
545 ///
546 /// // No items
547 /// assert_eq!(paginator.items_on_page(0), 0);
548 /// ```
549 pub fn items_on_page(&self, total_items: usize) -> usize {
550 if total_items == 0 {
551 return 0;
552 }
553 let (start, end) = self.get_slice_bounds(total_items);
554 end - start
555 }
556
557 /// Calculates slice bounds for the current page.
558 ///
559 /// This is a helper function for paginating slices. Given the total length
560 /// of your data, it returns the start and end indices for the current page.
561 /// The returned bounds can be used directly with slice notation.
562 ///
563 /// # Arguments
564 ///
565 /// * `length` - The total length of the data being paginated
566 ///
567 /// # Returns
568 ///
569 /// A tuple `(start, end)` where:
570 /// - `start` is the inclusive start index for the current page
571 /// - `end` is the exclusive end index for the current page
572 ///
573 /// # Examples
574 ///
575 /// ```rust
576 /// use bubbletea_widgets::paginator::Model;
577 ///
578 /// let items: Vec<i32> = (1..=100).collect();
579 /// let mut paginator = Model::new().with_per_page(10);
580 ///
581 /// // First page (0)
582 /// let (start, end) = paginator.get_slice_bounds(items.len());
583 /// assert_eq!((start, end), (0, 10));
584 /// let page_items = &items[start..end]; // Items 1-10
585 ///
586 /// // Third page (2)
587 /// paginator.page = 2;
588 /// let (start, end) = paginator.get_slice_bounds(items.len());
589 /// assert_eq!((start, end), (20, 30));
590 /// let page_items = &items[start..end]; // Items 21-30
591 /// ```
592 pub fn get_slice_bounds(&self, length: usize) -> (usize, usize) {
593 let start = self.page * self.per_page;
594 let end = (start + self.per_page).min(length);
595 (start, end)
596 }
597
598 /// Returns slice bounds assuming maximum possible data length.
599 ///
600 /// This is a convenience method that calls `get_slice_bounds()` with
601 /// the maximum possible data length (`per_page * total_pages`). It's
602 /// useful when you know your data exactly fills the pagination structure.
603 ///
604 /// # Returns
605 ///
606 /// A tuple `(start, end)` representing slice bounds for the current page.
607 ///
608 /// # Examples
609 ///
610 /// ```rust
611 /// use bubbletea_widgets::paginator::Model;
612 ///
613 /// let mut paginator = Model::new()
614 /// .with_per_page(10)
615 /// .with_total_items(100); // Exactly 10 pages
616 ///
617 /// paginator.page = 3;
618 /// let (start, end) = paginator.start_index_end_index();
619 /// assert_eq!((start, end), (30, 40));
620 /// ```
621 pub fn start_index_end_index(&self) -> (usize, usize) {
622 self.get_slice_bounds(self.per_page * self.total_pages)
623 }
624
625 /// Navigates to the previous page.
626 ///
627 /// If the paginator is already on the first page (page 0), this method
628 /// has no effect. The page number will not go below 0.
629 ///
630 /// # Examples
631 ///
632 /// ```rust
633 /// use bubbletea_widgets::paginator::Model;
634 ///
635 /// let mut paginator = Model::new().with_per_page(10).with_total_items(100);
636 /// paginator.page = 5;
637 ///
638 /// paginator.prev_page();
639 /// assert_eq!(paginator.page, 4);
640 ///
641 /// // Won't go below 0
642 /// paginator.page = 0;
643 /// paginator.prev_page();
644 /// assert_eq!(paginator.page, 0);
645 /// ```
646 pub fn prev_page(&mut self) {
647 if self.page > 0 {
648 self.page -= 1;
649 }
650 }
651
652 /// Navigates to the next page.
653 ///
654 /// If the paginator is already on the last page, this method has no effect.
655 /// The page number will not exceed `total_pages - 1`.
656 ///
657 /// # Examples
658 ///
659 /// ```rust
660 /// use bubbletea_widgets::paginator::Model;
661 ///
662 /// let mut paginator = Model::new().with_per_page(10).with_total_items(100);
663 /// // total_pages = 10, so last page is 9 (0-indexed)
664 ///
665 /// paginator.page = 5;
666 /// paginator.next_page();
667 /// assert_eq!(paginator.page, 6);
668 ///
669 /// // Won't go beyond last page
670 /// paginator.page = 8; // Second to last page
671 /// paginator.next_page();
672 /// assert_eq!(paginator.page, 9); // Should go to last page (9 is the last valid page)
673 /// paginator.next_page();
674 /// assert_eq!(paginator.page, 9); // Should stay at last page
675 /// ```
676 pub fn next_page(&mut self) {
677 if !self.on_last_page() {
678 self.page += 1;
679 }
680 }
681
682 /// Returns true if the paginator is on the first page.
683 ///
684 /// The first page is always page 0 in the 0-indexed pagination system.
685 ///
686 /// # Examples
687 ///
688 /// ```rust
689 /// use bubbletea_widgets::paginator::Model;
690 ///
691 /// let mut paginator = Model::new().with_per_page(10).with_total_items(100);
692 ///
693 /// assert!(paginator.on_first_page());
694 ///
695 /// paginator.next_page();
696 /// assert!(!paginator.on_first_page());
697 /// ```
698 pub fn on_first_page(&self) -> bool {
699 self.page == 0
700 }
701
702 /// Returns true if the paginator is on the last page.
703 ///
704 /// The last page is `total_pages - 1` in the 0-indexed pagination system.
705 ///
706 /// # Examples
707 ///
708 /// ```rust
709 /// use bubbletea_widgets::paginator::Model;
710 ///
711 /// let mut paginator = Model::new().with_per_page(10).with_total_items(90);
712 /// // Creates 9 pages (0-8), so last page is 8
713 ///
714 /// assert!(!paginator.on_last_page());
715 ///
716 /// paginator.page = 8; // Last page
717 /// assert!(paginator.on_last_page());
718 /// ```
719 pub fn on_last_page(&self) -> bool {
720 self.page == self.total_pages.saturating_sub(1)
721 }
722
723 /// Updates the paginator based on received messages.
724 ///
725 /// This method should be called from your application's `update()` method
726 /// to handle pagination key presses. It automatically responds to the
727 /// configured key bindings for next/previous page navigation.
728 ///
729 /// # Arguments
730 ///
731 /// * `msg` - The message to process, typically containing key press events
732 ///
733 /// # Examples
734 ///
735 /// ```rust
736 /// use bubbletea_widgets::paginator::Model as Paginator;
737 /// use bubbletea_rs::{Model, Msg};
738 ///
739 /// struct App {
740 /// paginator: Paginator,
741 /// }
742 ///
743 /// impl Model for App {
744 /// fn update(&mut self, msg: Msg) -> Option<bubbletea_rs::Cmd> {
745 /// // Forward messages to paginator
746 /// self.paginator.update(&msg);
747 /// None
748 /// }
749 ///
750 /// // ... other methods
751 /// # fn init() -> (Self, Option<bubbletea_rs::Cmd>) { (Self { paginator: Paginator::new() }, None) }
752 /// # fn view(&self) -> String { String::new() }
753 /// }
754 /// ```
755 pub fn update(&mut self, msg: &Msg) {
756 if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
757 if self.keymap.next_page.matches(key_msg) {
758 self.next_page();
759 } else if self.keymap.prev_page.matches(key_msg) {
760 self.prev_page();
761 }
762 }
763 }
764
765 /// Renders the paginator as a string.
766 ///
767 /// The output format depends on the `paginator_type` setting:
768 /// - **Arabic**: Shows "current/total" (e.g., "3/10")
769 /// - **Dots**: Shows dots with active page highlighted (e.g., "○ ○ ● ○ ○")
770 ///
771 /// # Returns
772 ///
773 /// A string representation of the current pagination state.
774 ///
775 /// # Examples
776 ///
777 /// ```rust
778 /// use bubbletea_widgets::paginator::{Model, Type};
779 ///
780 /// let mut paginator = Model::new().with_per_page(10).with_total_items(50);
781 /// // Creates 5 pages, currently on page 0
782 ///
783 /// // Arabic mode (default)
784 /// paginator.paginator_type = Type::Arabic;
785 /// assert_eq!(paginator.view(), "1/5"); // 1-indexed for display
786 ///
787 /// // Dots mode
788 /// paginator.paginator_type = Type::Dots;
789 /// assert_eq!(paginator.view(), "•○○○○"); // Active page shows filled bullet, others are hollow
790 ///
791 /// // Move to page 2
792 /// paginator.page = 2;
793 /// assert_eq!(paginator.view(), "○○•○○"); // Third bullet filled (active page), others hollow
794 /// ```
795 pub fn view(&self) -> String {
796 match self.paginator_type {
797 Type::Arabic => self.arabic_view(),
798 Type::Dots => self.dots_view(),
799 }
800 }
801
802 fn arabic_view(&self) -> String {
803 self.arabic_format
804 .replacen("%d", &(self.page + 1).to_string(), 1)
805 .replacen("%d", &self.total_pages.to_string(), 1)
806 }
807
808 fn dots_view(&self) -> String {
809 let mut s = String::new();
810 for i in 0..self.total_pages {
811 if i == self.page {
812 s.push_str(&self.active_dot);
813 } else {
814 s.push_str(&self.inactive_dot);
815 }
816 // Remove spacing between dots to match Go version (••••)
817 // Go version shows compact dots without spaces
818 }
819 s
820 }
821}