1use std::ops::Range;
13use std::rc::Rc;
14
15use iced::keyboard::key::Named;
16use iced::widget::{self, button::Status, container, responsive, row, text};
17use iced::{Alignment, Element, Length, Padding, Theme};
18
19use crate::icon::{Glyph, opacity, themed};
20use crate::keys::{self, Chord, Keymap};
21use crate::lucide;
22use crate::primitives::button::{self, Size};
23use crate::theme::{Tokens, space};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
27pub enum Variant {
28 #[default]
31 Numbers,
32 Compact,
35}
36
37impl Variant {
38 pub const ALL: [Variant; 2] = [Variant::Numbers, Variant::Compact];
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum Slot {
44 Page(usize),
45 Gap,
47}
48
49pub fn slots(page: usize, pages: usize, siblings: usize) -> Vec<Slot> {
58 let total = 2 * siblings + 5;
59 if pages <= total {
60 return (1..=pages).map(Slot::Page).collect();
61 }
62 let page = page.clamp(1, pages);
63 let left = page.saturating_sub(siblings).max(1);
64 let right = (page + siblings).min(pages);
65 let left_gap = left > 3;
66 let right_gap = right + 2 < pages;
67 let run = total - 2;
70
71 let mut slots = Vec::with_capacity(total);
72 match (left_gap, right_gap) {
73 (false, _) => {
74 slots.extend((1..=run).map(Slot::Page));
75 slots.extend([Slot::Gap, Slot::Page(pages)]);
76 }
77 (true, false) => {
78 slots.extend([Slot::Page(1), Slot::Gap]);
79 slots.extend((pages + 1 - run..=pages).map(Slot::Page));
80 }
81 (true, true) => {
82 slots.extend([Slot::Page(1), Slot::Gap]);
83 slots.extend((left..=right).map(Slot::Page));
84 slots.extend([Slot::Gap, Slot::Page(pages)]);
85 }
86 }
87 slots
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub enum Event {
93 Select(usize),
95 Next,
96 Previous,
97 First,
98 Last,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
104pub struct State {
105 page: usize,
106 pages: usize,
107}
108
109impl State {
110 pub fn new(pages: usize) -> Self {
112 Self {
113 page: 1,
114 pages: pages.max(1),
115 }
116 }
117
118 pub fn for_items(items: usize, per_page: usize) -> Self {
120 Self::new(items.div_ceil(per_page.max(1)))
121 }
122
123 pub fn with_page(mut self, page: usize) -> Self {
125 self.page = page.clamp(1, self.pages);
126 self
127 }
128
129 pub fn page(&self) -> usize {
130 self.page
131 }
132
133 pub fn pages(&self) -> usize {
134 self.pages
135 }
136
137 pub fn is_first(&self) -> bool {
138 self.page == 1
139 }
140
141 pub fn is_last(&self) -> bool {
142 self.page == self.pages
143 }
144
145 pub fn set_pages(&mut self, pages: usize) -> Option<usize> {
148 self.pages = pages.max(1);
149 let page = self.page.min(self.pages);
150 if page == self.page {
151 return None;
152 }
153 self.page = page;
154 Some(page)
155 }
156
157 pub fn range(&self, items: usize, per_page: usize) -> Range<usize> {
160 let start = ((self.page - 1) * per_page).min(items);
161 let end = (start + per_page).min(items);
162 start..end
163 }
164
165 pub fn update(&mut self, event: Event) -> Option<usize> {
167 let target = match event {
168 Event::Select(page) => (1..=self.pages).contains(&page).then_some(page)?,
169 Event::Next => (self.page + 1).min(self.pages),
170 Event::Previous => self.page.saturating_sub(1).max(1),
171 Event::First => 1,
172 Event::Last => self.pages,
173 };
174 if target == self.page {
175 return None;
176 }
177 self.page = target;
178 Some(target)
179 }
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
184pub enum Action {
185 Next,
186 Previous,
187 First,
188 Last,
189}
190
191impl Action {
192 pub fn event(self, state: &State) -> Option<Event> {
195 match self {
196 Action::Next | Action::Last if state.is_last() => None,
197 Action::Previous | Action::First if state.is_first() => None,
198 Action::Next => Some(Event::Next),
199 Action::Previous => Some(Event::Previous),
200 Action::First => Some(Event::First),
201 Action::Last => Some(Event::Last),
202 }
203 }
204}
205
206impl keys::Action for Action {
207 const ALL: &'static [Self] = &[Action::Next, Action::Previous, Action::First, Action::Last];
208
209 fn defaults() -> Keymap<Self> {
210 default_keymap()
211 }
212
213 fn name(self) -> &'static str {
214 match self {
215 Action::Next => "Next",
216 Action::Previous => "Previous",
217 Action::First => "First",
218 Action::Last => "Last",
219 }
220 }
221
222 fn description(self) -> &'static str {
223 match self {
224 Action::Next => "Goes to the next page.",
225 Action::Previous => "Goes to the previous page.",
226 Action::First => "Goes to the first page.",
227 Action::Last => "Goes to the last page.",
228 }
229 }
230}
231
232pub fn default_keymap() -> Keymap<Action> {
244 Keymap::new()
245 .bind(Chord::named(Named::PageDown), Action::Next)
246 .bind(Chord::named(Named::PageUp), Action::Previous)
247 .bind(Chord::named(Named::Home).ctrl(), Action::First)
248 .bind(Chord::named(Named::End).ctrl(), Action::Last)
249}
250
251pub struct Pagination<'a, Message> {
253 state: &'a State,
254 variant: Variant,
255 size: Size,
256 siblings: usize,
257 on_event: Option<Box<dyn Fn(Event) -> Message + 'a>>,
258}
259
260pub fn pagination<Message>(state: &State) -> Pagination<'_, Message> {
263 Pagination {
264 state,
265 variant: Variant::default(),
266 size: Size::Md,
267 siblings: 1,
268 on_event: None,
269 }
270}
271
272impl<Message> std::fmt::Debug for Pagination<'_, Message> {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 f.debug_struct("Pagination")
275 .field("state", self.state)
276 .field("variant", &self.variant)
277 .field("size", &self.size)
278 .field("siblings", &self.siblings)
279 .field("enabled", &self.on_event.is_some())
280 .finish()
281 }
282}
283
284impl<'a, Message> Pagination<'a, Message> {
285 pub fn variant(mut self, variant: Variant) -> Self {
286 self.variant = variant;
287 self
288 }
289
290 pub fn size(mut self, size: Size) -> Self {
292 self.size = size;
293 self
294 }
295
296 pub fn siblings(mut self, siblings: usize) -> Self {
299 self.siblings = siblings;
300 self
301 }
302
303 pub fn on_event(mut self, on_event: impl Fn(Event) -> Message + 'a) -> Self {
304 self.on_event = Some(Box::new(on_event));
305 self
306 }
307}
308
309type Handler<'a, Message> = Option<Rc<dyn Fn(Event) -> Message + 'a>>;
310
311impl<'a, Message: Clone + 'a> From<Pagination<'a, Message>> for Element<'a, Message> {
312 fn from(pagination: Pagination<'a, Message>) -> Self {
313 let Pagination {
314 state,
315 variant,
316 size,
317 siblings,
318 on_event,
319 } = pagination;
320 let state = *state;
321 let on_event: Handler<'a, Message> = on_event.map(Rc::from);
322
323 if variant == Variant::Compact {
324 return compact(state, size, &on_event);
325 }
326 responsive(move |available| {
329 let fits = (0..=siblings)
330 .rev()
331 .find(|&siblings| numbers_width(state, siblings, size) <= available.width);
332 match fits {
333 Some(siblings) => numbers(state, siblings, size, &on_event),
334 None => compact(state, size, &on_event),
335 }
336 })
337 .width(Length::Shrink)
338 .height(Length::Shrink)
339 .into()
340 }
341}
342
343pub fn numbers_width(state: State, siblings: usize, size: Size) -> f32 {
346 let side = size.metrics().height;
347 let slots = slots(state.page, state.pages, siblings);
348 let pages: f32 = slots
349 .iter()
350 .map(|slot| match slot {
351 Slot::Page(page) => number_width(*page, size),
352 Slot::Gap => side,
353 })
354 .sum();
355 2.0 * side + pages + space::XS * (slots.len() + 1) as f32
356}
357
358fn send<Message>(on_event: &Handler<'_, Message>, event: Event, enabled: bool) -> Option<Message> {
359 on_event
360 .as_ref()
361 .filter(|_| enabled)
362 .map(|on_event| on_event(event))
363}
364
365fn numbers<'a, Message: Clone + 'a>(
366 state: State,
367 siblings: usize,
368 size: Size,
369 on_event: &Handler<'a, Message>,
370) -> Element<'a, Message> {
371 let height = size.metrics().height;
372 let step = |glyph: Glyph, event: Event, enabled: bool| {
373 arrow(glyph, size, send(on_event, event, enabled))
374 };
375
376 let mut controls = row![step(
377 lucide!(ChevronLeft),
378 Event::Previous,
379 !state.is_first()
380 )]
381 .spacing(space::XS)
382 .align_y(Alignment::Center);
383 for slot in slots(state.page, state.pages, siblings) {
384 controls = controls.push(match slot {
385 Slot::Page(page) => number(
386 page,
387 page == state.page,
388 size,
389 send(on_event, Event::Select(page), true),
390 ),
391 Slot::Gap => gap(height),
392 });
393 }
394 controls
395 .push(step(lucide!(ChevronRight), Event::Next, !state.is_last()))
396 .into()
397}
398
399fn compact<'a, Message: Clone + 'a>(
400 state: State,
401 size: Size,
402 on_event: &Handler<'a, Message>,
403) -> Element<'a, Message> {
404 let step = |glyph: Glyph, event: Event, enabled: bool| {
405 arrow(glyph, size, send(on_event, event, enabled))
406 };
407 let label = text(format!("Page {} of {}", state.page, state.pages))
408 .size(size.metrics().text)
409 .wrapping(text::Wrapping::None)
410 .style(|theme: &Theme| text::Style {
411 color: Some(Tokens::of(theme).foreground),
412 });
413 row![
414 step(lucide!(ChevronsLeft), Event::First, !state.is_first()),
415 step(lucide!(ChevronLeft), Event::Previous, !state.is_first()),
416 container(label).padding(Padding::from([0.0, space::SM])),
417 step(lucide!(ChevronRight), Event::Next, !state.is_last()),
418 step(lucide!(ChevronsRight), Event::Last, !state.is_last()),
419 ]
420 .spacing(space::XS)
421 .align_y(Alignment::Center)
422 .into()
423}
424
425fn arrow<'a, Message: Clone + 'a>(
427 glyph: Glyph,
428 size: Size,
429 message: Option<Message>,
430) -> Element<'a, Message> {
431 let metrics = size.metrics();
432 let enabled = message.is_some();
433 let resting = if enabled {
434 Status::Active
435 } else {
436 Status::Disabled
437 };
438 let icon = themed(glyph, metrics.icon, opacity(enabled), move |theme| {
439 page_style(&Tokens::of(theme), false, resting).text_color
440 });
441 widget::button(icon)
442 .width(Length::Fixed(metrics.height))
443 .height(Length::Fixed(metrics.height))
444 .padding(Padding::from((metrics.height - metrics.icon) / 2.0))
445 .on_press_maybe(message)
446 .style(|theme, status| page_style(&Tokens::of(theme), false, status))
447 .into()
448}
449
450pub fn number_width(page: usize, size: Size) -> f32 {
453 let metrics = size.metrics();
454 let digits = page.max(1).ilog10() as f32 + 1.0;
455 metrics
456 .height
457 .max(digits * metrics.text * 0.55 + 2.0 * space::SM)
458}
459
460fn number<'a, Message: Clone + 'a>(
462 page: usize,
463 current: bool,
464 size: Size,
465 message: Option<Message>,
466) -> Element<'a, Message> {
467 let metrics = size.metrics();
468 let label = text(page.to_string())
469 .size(metrics.text)
470 .wrapping(text::Wrapping::None);
471 widget::button(container(label).center(Length::Fill))
472 .width(Length::Fixed(number_width(page, size)))
473 .height(Length::Fixed(metrics.height))
474 .padding(0)
475 .on_press_maybe(message)
476 .style(move |theme, status| page_style(&Tokens::of(theme), current, status))
477 .into()
478}
479
480fn gap<'a, Message: 'a>(side: f32) -> Element<'a, Message> {
482 container(themed(lucide!(Ellipsis), 16.0, 1.0, |theme| {
483 Tokens::of(theme).muted_foreground
484 }))
485 .center(Length::Fixed(side))
486 .into()
487}
488
489pub fn page_style(tokens: &Tokens, current: bool, status: Status) -> widget::button::Style {
494 if !current {
495 return button::style(tokens, button::Variant::Ghost, status);
496 }
497 let status = match status {
498 Status::Hovered | Status::Pressed => Status::Active,
499 status => status,
500 };
501 button::style(tokens, button::Variant::Outline, status)
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507 use crate::theme::{dark, light};
508
509 fn pages(slots: &[Slot]) -> String {
510 slots
511 .iter()
512 .map(|slot| match slot {
513 Slot::Page(page) => page.to_string(),
514 Slot::Gap => String::from("."),
515 })
516 .collect::<Vec<_>>()
517 .join(" ")
518 }
519
520 #[test]
521 fn few_pages_are_all_shown() {
522 assert_eq!(pages(&slots(1, 1, 1)), "1");
523 assert_eq!(pages(&slots(3, 7, 1)), "1 2 3 4 5 6 7");
524 assert_eq!(pages(&slots(1, 0, 1)), "");
525 }
526
527 #[test]
528 fn gaps_appear_on_the_side_with_hidden_pages() {
529 assert_eq!(pages(&slots(1, 10, 1)), "1 2 3 4 5 . 10");
530 assert_eq!(pages(&slots(4, 10, 1)), "1 2 3 4 5 . 10");
531 assert_eq!(pages(&slots(5, 10, 1)), "1 . 4 5 6 . 10");
532 assert_eq!(pages(&slots(6, 10, 1)), "1 . 5 6 7 . 10");
533 assert_eq!(pages(&slots(7, 10, 1)), "1 . 6 7 8 9 10");
534 assert_eq!(pages(&slots(10, 10, 1)), "1 . 6 7 8 9 10");
535 }
536
537 #[test]
538 fn slot_count_stays_the_same_as_the_page_moves() {
539 for siblings in 0..4 {
540 for page in 1..=40 {
541 let slots = slots(page, 40, siblings);
542 assert_eq!(slots.len(), 2 * siblings + 5, "{page} {siblings}");
543 assert!(slots.contains(&Slot::Page(page)), "{page} {siblings}");
544 assert_eq!(slots.first(), Some(&Slot::Page(1)));
545 assert_eq!(slots.last(), Some(&Slot::Page(40)));
546 }
547 }
548 }
549
550 #[test]
551 fn a_gap_always_hides_at_least_two_pages() {
552 for page in 1..=20 {
553 let slots = slots(page, 20, 1);
554 for window in slots.windows(3) {
555 if let [Slot::Page(before), Slot::Gap, Slot::Page(after)] = window {
556 assert!(after - before > 2, "{page}: {before} . {after}");
557 }
558 }
559 }
560 }
561
562 #[test]
563 fn more_siblings_widen_the_window() {
564 assert_eq!(pages(&slots(10, 20, 2)), "1 . 8 9 10 11 12 . 20");
565 assert_eq!(pages(&slots(10, 20, 0)), "1 . 10 . 20");
566 }
567
568 #[test]
569 fn out_of_range_pages_are_clamped_in_slots() {
570 assert_eq!(slots(0, 10, 1), slots(1, 10, 1));
571 assert_eq!(slots(99, 10, 1), slots(10, 10, 1));
572 }
573
574 #[test]
575 fn new_starts_on_the_first_page_and_never_has_zero_pages() {
576 let state = State::new(5);
577 assert_eq!((state.page(), state.pages()), (1, 5));
578 assert!(state.is_first());
579 assert!(!state.is_last());
580 let empty = State::new(0);
581 assert_eq!((empty.page(), empty.pages()), (1, 1));
582 assert!(empty.is_first() && empty.is_last());
583 }
584
585 #[test]
586 fn for_items_rounds_up() {
587 assert_eq!(State::for_items(0, 10).pages(), 1);
588 assert_eq!(State::for_items(10, 10).pages(), 1);
589 assert_eq!(State::for_items(11, 10).pages(), 2);
590 assert_eq!(State::for_items(5, 0).pages(), 5);
591 }
592
593 #[test]
594 fn with_page_clamps() {
595 assert_eq!(State::new(5).with_page(3).page(), 3);
596 assert_eq!(State::new(5).with_page(0).page(), 1);
597 assert_eq!(State::new(5).with_page(9).page(), 5);
598 }
599
600 #[test]
601 fn update_moves_within_range() {
602 let mut state = State::new(3);
603 assert_eq!(state.update(Event::Previous), None);
604 assert_eq!(state.update(Event::First), None);
605 assert_eq!(state.update(Event::Next), Some(2));
606 assert_eq!(state.update(Event::Next), Some(3));
607 assert_eq!(state.update(Event::Next), None);
608 assert_eq!(state.update(Event::Last), None);
609 assert_eq!(state.update(Event::Previous), Some(2));
610 assert_eq!(state.update(Event::First), Some(1));
611 assert_eq!(state.update(Event::Last), Some(3));
612 }
613
614 #[test]
615 fn select_ignores_the_current_page_and_out_of_range_pages() {
616 let mut state = State::new(4);
617 assert_eq!(state.update(Event::Select(1)), None);
618 assert_eq!(state.update(Event::Select(0)), None);
619 assert_eq!(state.update(Event::Select(5)), None);
620 assert_eq!(state.update(Event::Select(4)), Some(4));
621 assert!(state.is_last());
622 }
623
624 #[test]
625 fn set_pages_pulls_the_page_back_into_range() {
626 let mut state = State::new(10).with_page(8);
627 assert_eq!(state.set_pages(20), None);
628 assert_eq!(state.set_pages(5), Some(5));
629 assert_eq!(state.set_pages(0), Some(1));
630 assert_eq!(state.pages(), 1);
631 }
632
633 #[test]
634 fn range_covers_the_items_on_the_page() {
635 let state = State::for_items(23, 10);
636 assert_eq!(state.range(23, 10), 0..10);
637 assert_eq!(state.with_page(3).range(23, 10), 20..23);
638 assert_eq!(state.with_page(3).range(5, 10), 5..5);
639 assert_eq!(State::new(1).range(0, 10), 0..0);
640 }
641
642 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
643 let chord: Chord = chord.parse().unwrap();
644 keymap.resolve(chord.key(), chord.modifiers())
645 }
646
647 #[test]
648 fn default_keymap_covers_every_action() {
649 let keymap = default_keymap();
650 assert_eq!(press(&keymap, "PageDown"), Some(Action::Next));
651 assert_eq!(press(&keymap, "PageUp"), Some(Action::Previous));
652 assert_eq!(press(&keymap, "Ctrl+Home"), Some(Action::First));
653 assert_eq!(press(&keymap, "Ctrl+End"), Some(Action::Last));
654 assert_eq!(press(&keymap, "Home"), None);
655 assert_eq!(press(&keymap, "ArrowRight"), None);
656 for action in <Action as keys::Action>::ALL {
657 assert!(!keymap.chords(action).is_empty(), "{action:?}");
658 }
659 assert_eq!(Keymap::<Action>::defaults(), keymap);
660 }
661
662 #[test]
663 fn keymap_overrides_and_unbinding() {
664 let keymap = default_keymap()
665 .unbind(&Chord::named(Named::PageDown))
666 .bind("Alt+N".parse().unwrap(), Action::Next);
667 assert_eq!(press(&keymap, "PageDown"), None);
668 assert_eq!(press(&keymap, "Alt+N"), Some(Action::Next));
669 assert_eq!(press(&keymap, "PageUp"), Some(Action::Previous));
670 assert!(
671 default_keymap()
672 .unbind_action(&Action::Last)
673 .chords(&Action::Last)
674 .is_empty()
675 );
676 }
677
678 #[test]
679 fn actions_map_to_events_and_stop_at_the_ends() {
680 let first = State::new(3);
681 assert_eq!(Action::Next.event(&first), Some(Event::Next));
682 assert_eq!(Action::Last.event(&first), Some(Event::Last));
683 assert_eq!(Action::Previous.event(&first), None);
684 assert_eq!(Action::First.event(&first), None);
685
686 let mut last = first.with_page(3);
687 assert_eq!(Action::Next.event(&last), None);
688 assert_eq!(Action::Last.event(&last), None);
689 assert_eq!(Action::Previous.event(&last), Some(Event::Previous));
690 let event = Action::First.event(&last).unwrap();
691 assert_eq!(last.update(event), Some(1));
692
693 let single = State::new(1);
694 for action in <Action as keys::Action>::ALL {
695 assert_eq!(action.event(&single), None, "{action:?}");
696 }
697 }
698
699 #[test]
700 fn current_page_is_outlined_and_the_rest_are_ghosts() {
701 for theme in [light(), dark()] {
702 let tokens = Tokens::of(&theme);
703 let current = page_style(&tokens, true, Status::Active);
704 let other = page_style(&tokens, false, Status::Active);
705 assert!(current.border.width > 0.0);
706 assert_eq!(other.border.width, 0.0);
707 assert!(other.background.is_none());
708 }
709 }
710
711 #[test]
712 fn page_buttons_respond_to_hover_and_fade_when_disabled() {
713 let tokens = Tokens::of(&light());
714 let rest = page_style(&tokens, false, Status::Active);
715 let hovered = page_style(&tokens, false, Status::Hovered);
716 assert_ne!(rest.background, hovered.background);
717 for current in [false, true] {
718 let rest = page_style(&tokens, current, Status::Active);
719 let disabled = page_style(&tokens, current, Status::Disabled);
720 assert!(disabled.text_color.a < rest.text_color.a, "{current}");
721 }
722 }
723
724 #[test]
725 fn the_current_page_keeps_its_outline_under_the_pointer() {
726 for theme in [light(), dark()] {
727 let tokens = Tokens::of(&theme);
728 let rest = page_style(&tokens, true, Status::Active);
729 for status in [Status::Hovered, Status::Pressed] {
730 let hovered = page_style(&tokens, true, status);
731 assert_eq!(hovered, rest, "{status:?}");
732 assert_ne!(hovered, page_style(&tokens, false, status), "{status:?}");
733 }
734 }
735 }
736
737 #[test]
738 fn page_buttons_are_square_until_the_number_needs_more_room() {
739 for size in Size::ALL {
740 let side = size.metrics().height;
741 assert_eq!(number_width(1, size), side);
742 assert_eq!(number_width(99, size), side);
743 assert!(number_width(10_000, size) > side);
744 assert!(number_width(100_000, size) > number_width(10_000, size));
745 }
746 }
747
748 #[test]
749 fn the_numbered_width_counts_arrows_slots_and_gaps() {
750 let state = State::new(10).with_page(5);
751 assert_eq!(numbers_width(state, 1, Size::Md), 9.0 * 36.0 + 8.0 * 4.0);
754 assert_eq!(numbers_width(state, 0, Size::Md), 7.0 * 36.0 + 6.0 * 4.0);
755 assert!(numbers_width(state, 1, Size::Sm) < numbers_width(state, 1, Size::Md));
756 let long = State::new(2000).with_page(1000);
757 assert!(numbers_width(long, 1, Size::Md) > numbers_width(state, 1, Size::Md));
758 }
759
760 #[test]
761 fn builder_defaults_and_options() {
762 let state = State::new(3);
763 let p: Pagination<'_, Event> = pagination(&state);
764 assert_eq!(p.variant, Variant::Numbers);
765 assert_eq!(p.size, Size::Md);
766 assert_eq!(p.siblings, 1);
767 assert!(p.on_event.is_none());
768
769 let p = p
770 .variant(Variant::Compact)
771 .size(Size::Sm)
772 .siblings(2)
773 .on_event(|event| event);
774 assert_eq!(p.variant, Variant::Compact);
775 assert_eq!(p.size, Size::Sm);
776 assert_eq!(p.siblings, 2);
777 assert!(p.on_event.is_some());
778 }
779}