Skip to main content

armas_basic/layout/
table.rs

1//! Table component following shadcn/ui design patterns
2//!
3//! A responsive table with minimal styling, borders, and hover states.
4
5use crate::Theme;
6use egui;
7
8// Constants matching shadcn/ui spacing
9const CELL_PADDING: f32 = 8.0; // p-2 = 0.5rem = 8px
10const HEADER_HEIGHT: f32 = 40.0; // h-10 = 2.5rem = 40px
11const CELL_SPACING: f32 = 0.0;
12
13/// Get the current theme from UI context
14fn get_theme(ui: &egui::Ui) -> Theme {
15    ui.ctx().data(|d| {
16        d.get_temp::<Theme>(egui::Id::new("armas_theme"))
17            .unwrap_or_else(Theme::dark)
18    })
19}
20
21/// Draw a horizontal border line spanning all columns
22fn draw_full_width_border(ui: &mut egui::Ui, _theme: &Theme, num_columns: usize) {
23    for _ in 0..num_columns {
24        ui.add(egui::Separator::default().horizontal().spacing(0.0));
25    }
26    ui.end_row();
27}
28
29/// A table component following shadcn/ui design
30///
31/// # Example
32///
33/// ```rust,ignore
34/// table(ui, |mut rows| {
35///     header_row(&mut rows, |cells| {
36///         cell(cells, "Name");
37///         cell(cells, "Status");
38///     });
39///     row(&mut rows, |cells| {
40///         cell(cells, "Alice");
41///         cell(cells, "Active");
42///     });
43/// });
44/// ```
45pub fn table<R>(ui: &mut egui::Ui, content: impl FnOnce(&mut TableRows) -> R) -> R {
46    let theme = get_theme(ui);
47
48    egui::Grid::new(ui.id().with("table"))
49        .spacing([CELL_SPACING, CELL_SPACING])
50        .min_col_width(0.0)
51        .show(ui, |ui| {
52            let mut table_state = TableState {
53                theme,
54                num_columns: 0,
55            };
56
57            let mut rows = TableRows {
58                ui,
59                state: &mut table_state,
60            };
61
62            content(&mut rows)
63        })
64        .inner
65}
66
67/// Table state tracking
68struct TableState {
69    theme: Theme,
70    num_columns: usize,
71}
72
73/// Builder for table rows
74pub struct TableRows<'a> {
75    ui: &'a mut egui::Ui,
76    state: &'a mut TableState,
77}
78
79/// Builder for table cells
80pub struct TableCells<'a> {
81    ui: &'a mut egui::Ui,
82    theme: &'a Theme,
83    is_header: bool,
84    cell_index: usize,
85}
86
87/// Add a header row to the table
88pub fn header_row<R>(rows: &mut TableRows, content: impl FnOnce(&mut TableCells) -> R) -> R {
89    let result = render_row(rows, true, content);
90
91    rows.ui.end_row();
92
93    // Draw border after header across full width
94    draw_full_width_border(rows.ui, &rows.state.theme, rows.state.num_columns);
95
96    result
97}
98
99/// Add a data row to the table
100pub fn row<R>(rows: &mut TableRows, content: impl FnOnce(&mut TableCells) -> R) -> R {
101    let result = render_row(rows, false, content);
102
103    rows.ui.end_row();
104
105    // Draw border after row across full width
106    draw_full_width_border(rows.ui, &rows.state.theme, rows.state.num_columns);
107
108    result
109}
110
111/// Render a single row (header or data)
112fn render_row<R>(
113    rows: &mut TableRows,
114    is_header: bool,
115    content: impl FnOnce(&mut TableCells) -> R,
116) -> R {
117    let mut cells = TableCells {
118        ui: rows.ui,
119        theme: &rows.state.theme,
120        is_header,
121        cell_index: 0,
122    };
123
124    let result = content(&mut cells);
125
126    // Update column count from first row
127    if rows.state.num_columns == 0 {
128        rows.state.num_columns = cells.cell_index;
129    }
130
131    result
132}
133
134/// Add a text cell to the current row
135pub fn cell(cells: &mut TableCells, text: impl Into<String>) {
136    render_cell(cells, |ui, theme, is_header| {
137        let text = text.into();
138        let label = create_label(&text, theme, is_header);
139        ui.add(label);
140    });
141}
142
143/// Add a custom cell with custom content
144pub fn cell_ui<R>(cells: &mut TableCells, content: impl FnOnce(&mut egui::Ui) -> R) -> R {
145    render_cell(cells, |ui, _theme, _is_header| content(ui))
146}
147
148/// Render a single cell with padding and styling
149fn render_cell<R>(
150    cells: &mut TableCells,
151    content: impl FnOnce(&mut egui::Ui, &Theme, bool) -> R,
152) -> R {
153    let frame = egui::Frame::new().inner_margin(egui::Margin::same(CELL_PADDING as i8));
154
155    let result = frame
156        .show(cells.ui, |ui| {
157            // Set consistent min height for all cells in the row
158            let min_height = if cells.is_header {
159                HEADER_HEIGHT - CELL_PADDING * 2.0
160            } else {
161                0.0
162            };
163
164            if min_height > 0.0 {
165                ui.set_min_height(min_height);
166            }
167
168            // Use horizontal layout to center content vertically
169            ui.horizontal_centered(|ui| content(ui, cells.theme, cells.is_header))
170                .inner
171        })
172        .inner;
173
174    cells.cell_index += 1;
175    result
176}
177
178/// Create a styled label for table cell
179fn create_label(text: &str, theme: &Theme, is_header: bool) -> egui::Label {
180    if is_header {
181        egui::Label::new(egui::RichText::new(text).strong().color(theme.foreground()))
182    } else {
183        egui::Label::new(egui::RichText::new(text).color(theme.muted_foreground()))
184    }
185}