1use crate::theme::Theme;
17use egui::{
18 Align, Color32, CornerRadius, Frame, Layout, Margin, Response, Sense, Stroke, Ui, Vec2, vec2,
19};
20use std::hash::Hash;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
27pub enum TableSize {
28 Size1,
29 #[default]
30 Size2,
31 Size3,
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
35pub enum TableVariant {
36 #[default]
37 Default,
38 Muted,
39}
40
41#[derive(Clone, Debug)]
46pub struct TableProps {
47 pub size: TableSize,
48 pub variant: TableVariant,
49}
50
51impl Default for TableProps {
52 fn default() -> Self {
53 Self {
54 size: TableSize::Size2,
55 variant: TableVariant::Default,
56 }
57 }
58}
59
60impl TableProps {
61 pub fn new() -> Self {
62 Self::default()
63 }
64
65 pub fn size(mut self, size: TableSize) -> Self {
66 self.size = size;
67 self
68 }
69
70 pub fn variant(mut self, variant: TableVariant) -> Self {
71 self.variant = variant;
72 self
73 }
74}
75
76#[derive(Clone, Copy, Debug)]
77pub struct TableRowProps<IdSource> {
78 pub id_source: IdSource,
79 pub selected: bool,
80 pub hoverable: bool,
81}
82
83impl<IdSource> TableRowProps<IdSource> {
84 pub fn new(id_source: IdSource) -> Self {
85 Self {
86 id_source,
87 selected: false,
88 hoverable: true,
89 }
90 }
91
92 pub fn selected(mut self, selected: bool) -> Self {
93 self.selected = selected;
94 self
95 }
96
97 pub fn hoverable(mut self, hoverable: bool) -> Self {
98 self.hoverable = hoverable;
99 self
100 }
101}
102
103#[derive(Clone, Copy, Debug, Default)]
104pub struct TableCellProps {
105 pub checkbox: bool,
106 pub fill: bool,
107}
108
109impl TableCellProps {
110 pub fn new() -> Self {
111 Self::default()
112 }
113
114 pub fn checkbox(mut self, checkbox: bool) -> Self {
115 self.checkbox = checkbox;
116 self
117 }
118
119 pub fn fill(mut self, fill: bool) -> Self {
120 self.fill = fill;
121 self
122 }
123}
124
125#[derive(Clone, Copy, Debug)]
130pub struct TableContext {
131 pub size: TableSize,
132 pub variant: TableVariant,
133 tokens: TableTokens,
134 metrics: TableMetrics,
135}
136
137#[derive(Clone, Copy, Debug)]
138struct TableTokens {
139 border: Color32,
140 text: Color32,
141 text_muted: Color32,
142 hover_bg: Color32,
143 selected_bg: Color32,
144 footer_bg: Color32,
145 container_bg: Color32,
146}
147
148#[derive(Clone, Copy, Debug)]
149struct TableMetrics {
150 row_height: f32,
151 cell_padding: Margin,
152 checkbox_padding: Margin,
153 caption_gap: f32,
154}
155
156fn table_tokens(theme: &Theme, variant: TableVariant) -> TableTokens {
157 let palette = &theme.palette;
158 let container_bg = match variant {
159 TableVariant::Default => Color32::TRANSPARENT,
160 TableVariant::Muted => palette.muted.gamma_multiply(0.2),
161 };
162 TableTokens {
163 border: palette.border,
164 text: palette.foreground,
165 text_muted: palette.muted_foreground,
166 hover_bg: palette.muted.gamma_multiply(0.5),
167 selected_bg: palette.muted.gamma_multiply(0.7),
168 footer_bg: palette.muted.gamma_multiply(0.5),
169 container_bg,
170 }
171}
172
173fn table_metrics(size: TableSize) -> TableMetrics {
174 match size {
175 TableSize::Size1 => TableMetrics {
176 row_height: 32.0,
177 cell_padding: Margin::symmetric(6, 4),
178 checkbox_padding: Margin::symmetric(6, 4),
179 caption_gap: 12.0,
180 },
181 TableSize::Size2 => TableMetrics {
182 row_height: 40.0,
183 cell_padding: Margin::symmetric(8, 6),
184 checkbox_padding: Margin::symmetric(8, 6),
185 caption_gap: 16.0,
186 },
187 TableSize::Size3 => TableMetrics {
188 row_height: 48.0,
189 cell_padding: Margin::symmetric(10, 8),
190 checkbox_padding: Margin::symmetric(10, 8),
191 caption_gap: 20.0,
192 },
193 }
194}
195
196pub fn table<R>(
201 ui: &mut Ui,
202 theme: &Theme,
203 props: TableProps,
204 add_contents: impl FnOnce(&mut Ui, &TableContext) -> R,
205) -> R {
206 let tokens = table_tokens(theme, props.variant);
207 let metrics = table_metrics(props.size);
208 let ctx = TableContext {
209 size: props.size,
210 variant: props.variant,
211 tokens,
212 metrics,
213 };
214
215 Frame::NONE
216 .fill(tokens.container_bg)
217 .show(ui, |table_ui| {
218 table_ui.visuals_mut().override_text_color = Some(tokens.text);
219 table_ui.spacing_mut().item_spacing = vec2(0.0, 0.0);
220 add_contents(table_ui, &ctx)
221 })
222 .inner
223}
224
225pub fn table_header<R>(
226 ui: &mut Ui,
227 _ctx: &TableContext,
228 add_contents: impl FnOnce(&mut Ui) -> R,
229) -> R {
230 ui.vertical(|header_ui| {
231 header_ui.spacing_mut().item_spacing = vec2(0.0, 0.0);
232 add_contents(header_ui)
233 })
234 .inner
235}
236
237pub fn table_body<R>(
238 ui: &mut Ui,
239 _ctx: &TableContext,
240 add_contents: impl FnOnce(&mut Ui) -> R,
241) -> R {
242 ui.vertical(|body_ui| {
243 body_ui.spacing_mut().item_spacing = vec2(0.0, 0.0);
244 add_contents(body_ui)
245 })
246 .inner
247}
248
249pub fn table_footer<R>(
250 ui: &mut Ui,
251 ctx: &TableContext,
252 add_contents: impl FnOnce(&mut Ui) -> R,
253) -> R {
254 Frame::NONE
255 .fill(ctx.tokens.footer_bg)
256 .show(ui, |footer_ui| {
257 footer_ui.spacing_mut().item_spacing = vec2(0.0, 0.0);
258 add_contents(footer_ui)
259 })
260 .inner
261}
262
263pub fn table_row<R, IdSource: Hash>(
264 ui: &mut Ui,
265 ctx: &TableContext,
266 props: TableRowProps<IdSource>,
267 add_contents: impl FnOnce(&mut Ui) -> R,
268) -> TableRowResponse<R> {
269 let row_height = ctx.metrics.row_height;
270 let desired_size = Vec2::new(ui.available_width(), row_height);
271 let row_id = ui.make_persistent_id(props.id_source);
272
273 let inner = ui.allocate_ui_with_layout(
274 desired_size,
275 Layout::left_to_right(Align::Center),
276 |row_ui| {
277 row_ui.spacing_mut().item_spacing = vec2(0.0, 0.0);
278 let rect = row_ui.max_rect();
279 let response = row_ui.interact(rect, row_id, Sense::hover());
280 let hover = props.hoverable && response.hovered();
281 let fill = if props.selected {
282 ctx.tokens.selected_bg
283 } else if hover {
284 ctx.tokens.hover_bg
285 } else {
286 Color32::TRANSPARENT
287 };
288
289 if fill != Color32::TRANSPARENT {
290 row_ui
291 .painter()
292 .rect_filled(rect, CornerRadius::same(0), fill);
293 }
294 row_ui.painter().line_segment(
295 [rect.left_bottom(), rect.right_bottom()],
296 Stroke::new(1.0, ctx.tokens.border),
297 );
298
299 let contents = add_contents(row_ui);
300 (contents, response)
301 },
302 );
303
304 TableRowResponse {
305 inner: inner.inner.0,
306 response: inner.inner.1,
307 }
308}
309
310pub struct TableRowResponse<R> {
311 pub inner: R,
312 pub response: Response,
313}
314
315pub fn table_head<R>(
316 ui: &mut Ui,
317 ctx: &TableContext,
318 props: TableCellProps,
319 add_contents: impl FnOnce(&mut Ui) -> R,
320) -> R {
321 let padding = if props.checkbox {
322 ctx.metrics.checkbox_padding
323 } else {
324 ctx.metrics.cell_padding
325 };
326 let render = |cell_ui: &mut Ui| {
327 Frame::NONE
328 .inner_margin(padding)
329 .show(cell_ui, |inner_ui| {
330 inner_ui.visuals_mut().override_text_color = Some(ctx.tokens.text_muted);
331 inner_ui
332 .with_layout(Layout::left_to_right(Align::Center), |inner_ui| {
333 add_contents(inner_ui)
334 })
335 .inner
336 })
337 .inner
338 };
339
340 if props.fill {
341 let desired = vec2(ui.available_width(), ui.available_height());
342 ui.allocate_ui_with_layout(desired, Layout::left_to_right(Align::Center), |cell_ui| {
343 render(cell_ui)
344 })
345 .inner
346 } else {
347 render(ui)
348 }
349}
350
351pub fn table_cell<R>(
352 ui: &mut Ui,
353 ctx: &TableContext,
354 props: TableCellProps,
355 add_contents: impl FnOnce(&mut Ui) -> R,
356) -> R {
357 let padding = if props.checkbox {
358 ctx.metrics.checkbox_padding
359 } else {
360 ctx.metrics.cell_padding
361 };
362 let render = |cell_ui: &mut Ui| {
363 Frame::NONE
364 .inner_margin(padding)
365 .show(cell_ui, |inner_ui| {
366 inner_ui
367 .with_layout(Layout::left_to_right(Align::Center), |inner_ui| {
368 add_contents(inner_ui)
369 })
370 .inner
371 })
372 .inner
373 };
374
375 if props.fill {
376 let desired = vec2(ui.available_width(), ui.available_height());
377 ui.allocate_ui_with_layout(desired, Layout::left_to_right(Align::Center), |cell_ui| {
378 render(cell_ui)
379 })
380 .inner
381 } else {
382 render(ui)
383 }
384}
385
386pub fn table_caption(ui: &mut Ui, ctx: &TableContext, text: &str) -> Response {
387 ui.add_space(ctx.metrics.caption_gap);
388 ui.label(egui::RichText::new(text).color(ctx.tokens.text_muted))
389}