egui_components/
pagination.rs1use egui::{vec2, Rect, Response, Sense, Ui, Widget};
12use egui_components_theme::{mix, Theme};
13
14use crate::icon::{paint_icon, IconKind};
15
16pub struct Pagination<'a> {
17 current: &'a mut usize,
18 pages: usize,
19 siblings: usize,
20}
21
22impl<'a> Pagination<'a> {
23 pub fn new(current: &'a mut usize, pages: usize) -> Self {
24 Self {
25 current,
26 pages: pages.max(1),
27 siblings: 1,
28 }
29 }
30 pub fn siblings(mut self, n: usize) -> Self {
32 self.siblings = n;
33 self
34 }
35}
36
37impl<'a> Widget for Pagination<'a> {
38 fn ui(self, ui: &mut Ui) -> Response {
39 let pages = self.pages;
40 let cur = (*self.current).min(pages - 1);
41 let entries = layout_pages(cur, pages, self.siblings);
42
43 let mut changed = false;
44 let resp = ui
45 .horizontal(|ui| {
46 if arrow_button(ui, IconKind::ChevronLeft, cur > 0) {
47 *self.current = cur.saturating_sub(1);
48 changed = true;
49 }
50 for entry in entries {
51 match entry {
52 Some(p) => {
53 if page_button(ui, p + 1, p == cur) && p != cur {
54 *self.current = p;
55 changed = true;
56 }
57 }
58 None => ellipsis(ui),
59 }
60 }
61 if arrow_button(ui, IconKind::ChevronRight, cur + 1 < pages) {
62 *self.current = (cur + 1).min(pages - 1);
63 changed = true;
64 }
65 })
66 .response;
67
68 let mut resp = resp;
69 if changed {
70 resp.mark_changed();
71 }
72 resp
73 }
74}
75
76fn layout_pages(cur: usize, pages: usize, siblings: usize) -> Vec<Option<usize>> {
78 let mut out = Vec::new();
79 let near = |i: usize| {
80 i == 0
81 || i + 1 == pages
82 || (i as isize - cur as isize).unsigned_abs() <= siblings
83 };
84 let mut last: Option<usize> = None;
85 for i in 0..pages {
86 if near(i) {
87 if let Some(l) = last {
88 if i > l + 1 {
89 out.push(None);
90 }
91 }
92 out.push(Some(i));
93 last = Some(i);
94 }
95 }
96 out
97}
98
99fn button_size(ui: &Ui) -> f32 {
100 Theme::get(ui.ctx()).metrics.button_height_sm
101}
102
103fn arrow_button(ui: &mut Ui, icon: IconKind, enabled: bool) -> bool {
104 let theme = Theme::get(ui.ctx());
105 let c = theme.colors;
106 let s = button_size(ui);
107 let sense = if enabled { Sense::click() } else { Sense::hover() };
108 let (rect, resp) = ui.allocate_exact_size(vec2(s, s), sense);
109 if ui.is_rect_visible(rect) {
110 let painter = ui.painter();
111 if enabled && resp.hovered() {
112 painter.rect_filled(rect, theme.corner_sm(), c.accent_background);
113 }
114 let fg = if enabled {
115 c.foreground
116 } else {
117 mix(c.muted_foreground, c.background, 0.5)
118 };
119 let ir = Rect::from_center_size(rect.center(), vec2(14.0, 14.0));
120 paint_icon(painter, icon, ir, fg, 1.6);
121 if enabled && resp.hovered() {
122 ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
123 }
124 }
125 resp.clicked()
126}
127
128fn page_button(ui: &mut Ui, number: usize, selected: bool) -> bool {
129 let theme = Theme::get(ui.ctx());
130 let c = theme.colors;
131 let s = button_size(ui);
132 let (rect, resp) = ui.allocate_exact_size(vec2(s, s), Sense::click());
133 if ui.is_rect_visible(rect) {
134 let painter = ui.painter();
135 let bg = if selected {
136 c.primary_background
137 } else if resp.hovered() {
138 c.accent_background
139 } else {
140 egui::Color32::TRANSPARENT
141 };
142 if bg != egui::Color32::TRANSPARENT {
143 painter.rect_filled(rect, theme.corner_sm(), bg);
144 }
145 let fg = if selected {
146 c.primary_foreground
147 } else {
148 c.foreground
149 };
150 painter.text(
151 rect.center(),
152 egui::Align2::CENTER_CENTER,
153 number.to_string(),
154 egui::FontId::proportional(theme.metrics.font_size_sm),
155 fg,
156 );
157 if resp.hovered() {
158 ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
159 }
160 }
161 resp.clicked()
162}
163
164fn ellipsis(ui: &mut Ui) {
165 let theme = Theme::get(ui.ctx());
166 let s = button_size(ui);
167 let (rect, _) = ui.allocate_exact_size(vec2(s * 0.7, s), Sense::hover());
168 ui.painter().text(
169 rect.center(),
170 egui::Align2::CENTER_CENTER,
171 "…",
172 egui::FontId::proportional(theme.metrics.font_size_md),
173 theme.colors.muted_foreground,
174 );
175}