Skip to main content

iced_cube/navigation/
pagination.rs

1//! Moves through a long list one page at a time.
2//!
3//! [`State`] holds the current page and the page count, both counted from
4//! one. Its [`update`](State::update) handles selecting a page, stepping
5//! to the next or previous one and jumping to either end, and never leaves
6//! the range. [`slots`] decides which page numbers to show, so the control
7//! keeps the same width as the current page moves.
8//!
9//! Keyboard shortcuts resolve through a [`Keymap`] of [`Action`]s; see
10//! [`default_keymap`] and [`crate::keys`].
11
12use 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/// How the control is laid out.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
27pub enum Variant {
28    /// Previous and next buttons around page numbers, with gaps for the
29    /// pages left out.
30    #[default]
31    Numbers,
32    /// "Page 3 of 12" between first, previous, next and last buttons, for
33    /// narrow spaces.
34    Compact,
35}
36
37impl Variant {
38    pub const ALL: [Variant; 2] = [Variant::Numbers, Variant::Compact];
39}
40
41/// One place in the row of page numbers.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum Slot {
44    Page(usize),
45    /// Pages left out between two shown ones.
46    Gap,
47}
48
49/// The page numbers to show for `page` of `pages`, with `siblings` pages on
50/// each side of the current one.
51///
52/// The first and last pages are always shown. A gap only replaces two or
53/// more pages, since a gap for a single page takes as much room as the page
54/// itself. Once there are enough pages to need a gap, the result always has
55/// `2 * siblings + 5` slots, so the control does not change width as the
56/// current page moves.
57pub 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    // Slots on the side without a gap: the end page, the siblings, the
68    // current page, and the two slots the missing gap and end page free up.
69    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/// Changes to the current page.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub enum Event {
93    /// Goes to a page, counted from 1. Pages out of range are ignored.
94    Select(usize),
95    Next,
96    Previous,
97    First,
98    Last,
99}
100
101/// The current page and how many there are, both counted from 1. There is
102/// always at least one page.
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
104pub struct State {
105    page: usize,
106    pages: usize,
107}
108
109impl State {
110    /// Starts on page 1 of `pages`, or of one page when `pages` is 0.
111    pub fn new(pages: usize) -> Self {
112        Self {
113            page: 1,
114            pages: pages.max(1),
115        }
116    }
117
118    /// Enough pages for `items` at `per_page` each, starting on page 1.
119    pub fn for_items(items: usize, per_page: usize) -> Self {
120        Self::new(items.div_ceil(per_page.max(1)))
121    }
122
123    /// Starts on `page`, kept within range.
124    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    /// Changes the page count, such as after a filter, and moves the
146    /// current page back into range. Returns the new page if it moved.
147    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    /// The indices of the items on the current page, out of `items` at
158    /// `per_page` each. Empty when the page is past the end.
159    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    /// Applies an event and returns the new page, if it changed.
166    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/// What a pagination keyboard shortcut does.
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
184pub enum Action {
185    Next,
186    Previous,
187    First,
188    Last,
189}
190
191impl Action {
192    /// The [`Event`] this action sends to `state`, or `None` when the page
193    /// is already where the action would take it.
194    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
232/// The default pagination shortcuts:
233///
234/// | Keys | Action |
235/// | --- | --- |
236/// | `PageDown` | [`Action::Next`] |
237/// | `PageUp` | [`Action::Previous`] |
238/// | `Ctrl+Home` | [`Action::First`] |
239/// | `Ctrl+End` | [`Action::Last`] |
240///
241/// These follow document viewers, and leave the arrow keys, Home and End to
242/// tabs, sliders and lists on the same screen.
243pub 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
251/// A pagination builder. Convert it into an [`Element`] to render.
252pub 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
260/// Renders the controls for `state`. Without
261/// [`on_event`](Pagination::on_event) every button renders disabled.
262pub 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    /// The button size: `Sm` is 32 pixels square, `Md` 36 and `Lg` 40.
291    pub fn size(mut self, size: Size) -> Self {
292        self.size = size;
293        self
294    }
295
296    /// How many pages to show on each side of the current one. Defaults to
297    /// 1, which shows at most seven page slots.
298    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        // Drops sibling pages until the row fits, then falls back to the
327        // compact layout, so a phone never cuts the control off.
328        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
343/// The width of the numbered layout: two arrows, the page slots and the
344/// gaps between them.
345pub 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
425/// A square button with a chevron, for moving by one page or to an end.
426fn 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
450/// The width of a page button: square up to two digits, wider for longer
451/// numbers so they keep some padding.
452pub 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
460/// A page number, centred in its button.
461fn 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
480/// The ellipsis that stands in for pages left out.
481fn 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
489/// The style of a page button: outlined for the current page, ghost for
490/// the rest and for the arrows. A disabled arrow fades like any button.
491/// Selecting the current page does nothing, so it keeps its outlined
492/// look under the pointer instead of turning into a hovered page.
493pub 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        // Two arrows and seven square slots, with eight gaps between nine
752        // buttons.
753        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}