rosace_widgets/tree/
data_table.rs1use std::sync::Arc;
19use rosace_render::Color;
20use super::{BoxedWidget, LayoutCtx, PaintCtx, Widget};
21use super::table::{Table, TableColumn};
22use super::text::Text;
23use super::checkbox::Checkbox;
24use super::pressable::Pressable;
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum SortDirection {
28 Ascending,
29 Descending,
30}
31
32#[derive(Clone)]
34pub struct DataTableColumn {
35 label: String,
36 sizing: TableColumn,
37}
38
39impl DataTableColumn {
40 pub fn new(label: impl Into<String>) -> Self {
41 Self { label: label.into(), sizing: TableColumn::auto() }
42 }
43 pub fn fixed_width(mut self, px: f32) -> Self { self.sizing = TableColumn::fixed(px); self }
44 pub fn flex(mut self, factor: f32) -> Self { self.sizing = TableColumn::flex(factor); self }
45}
46
47pub struct DataTable {
50 columns: Vec<DataTableColumn>,
51 rows: Vec<Vec<String>>,
55 sort_col: Option<usize>,
56 sort_dir: SortDirection,
57 selectable: bool,
58 selected_rows: Vec<bool>,
59 row_striping: Option<Color>,
60 on_sort: Option<Arc<dyn Fn(usize, SortDirection) + Send + Sync>>,
61 on_select: Option<Arc<dyn Fn(usize, bool) + Send + Sync>>,
62}
63
64impl DataTable {
65 pub fn new(columns: Vec<DataTableColumn>) -> Self {
66 Self {
67 columns,
68 rows: Vec::new(),
69 sort_col: None,
70 sort_dir: SortDirection::Ascending,
71 selectable: false,
72 selected_rows: Vec::new(),
73 row_striping: None,
74 on_sort: None,
75 on_select: None,
76 }
77 }
78
79 pub fn row(mut self, cells: Vec<impl Into<String>>) -> Self {
81 self.rows.push(cells.into_iter().map(Into::into).collect());
82 self
83 }
84 pub fn row_builder<S: Into<String>>(mut self, count: usize, builder: impl Fn(usize) -> Vec<S>) -> Self {
88 for i in 0..count {
89 self.rows.push(builder(i).into_iter().map(Into::into).collect());
90 }
91 self
92 }
93
94 pub fn sorted_by(mut self, col: usize, dir: SortDirection) -> Self {
97 self.sort_col = Some(col);
98 self.sort_dir = dir;
99 self
100 }
101 pub fn row_striping(mut self, c: Color) -> Self { self.row_striping = Some(c); self }
102
103 pub fn selectable(mut self, selected: Vec<bool>) -> Self {
105 self.selectable = true;
106 self.selected_rows = selected;
107 self
108 }
109
110 pub fn on_sort(mut self, f: impl Fn(usize, SortDirection) + Send + Sync + 'static) -> Self {
111 self.on_sort = Some(Arc::new(f));
112 self
113 }
114 pub fn on_select(mut self, f: impl Fn(usize, bool) + Send + Sync + 'static) -> Self {
115 self.on_select = Some(Arc::new(f));
116 self
117 }
118
119 fn build_table(&self) -> Table {
125 let mut table = Table::new();
126 if self.selectable {
127 table = table.column(TableColumn::fixed(32.0));
128 }
129 table = table.columns(self.columns.iter().map(|c| c.sizing).collect());
130
131 let mut header: Vec<BoxedWidget> = Vec::new();
133 if self.selectable {
134 header.push(Box::new(Text::new("")));
135 }
136 for (i, col) in self.columns.iter().enumerate() {
137 let label = if self.sort_col == Some(i) {
138 let arrow = match self.sort_dir { SortDirection::Ascending => "^", SortDirection::Descending => "v" };
139 format!("{} {arrow}", col.label)
140 } else {
141 col.label.clone()
142 };
143 let cell: BoxedWidget = match &self.on_sort {
144 Some(f) => {
145 let f = f.clone();
146 let next_dir = if self.sort_col == Some(i) && self.sort_dir == SortDirection::Ascending {
147 SortDirection::Descending
148 } else {
149 SortDirection::Ascending
150 };
151 Box::new(Pressable::new(Text::new(label).weight(rosace_render::FontWeight::Bold), move || f(i, next_dir)))
152 }
153 None => Box::new(Text::new(label).weight(rosace_render::FontWeight::Bold)),
154 };
155 header.push(cell);
156 }
157 table = table.row(header);
158
159 for (r, cells) in self.rows.iter().enumerate() {
161 let mut row_widgets: Vec<BoxedWidget> = Vec::new();
162 if self.selectable {
163 let checked = self.selected_rows.get(r).copied().unwrap_or(false);
164 let cb = Checkbox::new(checked);
165 let cb: BoxedWidget = match &self.on_select {
166 Some(f) => {
167 let f = f.clone();
168 Box::new(cb.on_change(move |v| f(r, v)))
169 }
170 None => Box::new(cb),
171 };
172 row_widgets.push(cb);
173 }
174 for text in cells {
175 row_widgets.push(Box::new(Text::new(text.clone())));
176 }
177 table = table.row(row_widgets);
178 }
179
180 if let Some(c) = self.row_striping {
181 table = table.row_background(c);
182 }
183 table.cell_padding(8.0).divider(1.0)
184 }
185}
186
187impl Widget for DataTable {
188 fn layout(&self, ctx: &LayoutCtx) -> rosace_core::types::Size {
189 self.build_table().layout(ctx)
190 }
191
192 fn paint(&self, ctx: &mut PaintCtx) {
193 self.build_table().paint(ctx);
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use rosace_layout::Constraints;
201
202 #[test]
203 fn layout_delegates_to_underlying_table() {
204 let font = rosace_render::FontCache::embedded();
205 let theme = rosace_theme::built_in::dark_theme();
206 let ctx = LayoutCtx::new(Constraints::loose(400.0, 400.0), &font, &theme);
207 let dt = DataTable::new(vec![DataTableColumn::new("Name"), DataTableColumn::new("Qty")])
208 .row(vec!["Widget", "3"]);
209 let size = dt.layout(&ctx);
210 assert!(size.width > 0.0 && size.height > 0.0);
211 }
212
213 #[test]
214 fn sorted_by_toggles_direction_on_repeat_click() {
215 let dt = DataTable::new(vec![DataTableColumn::new("Name")])
216 .sorted_by(0, SortDirection::Ascending);
217 assert_eq!(dt.sort_col, Some(0));
218 assert_eq!(dt.sort_dir, SortDirection::Ascending);
219 }
220
221 #[test]
222 fn layout_is_stable_across_repeated_calls() {
223 let font = rosace_render::FontCache::embedded();
226 let theme = rosace_theme::built_in::dark_theme();
227 let ctx = LayoutCtx::new(Constraints::loose(400.0, 400.0), &font, &theme);
228 let dt = DataTable::new(vec![DataTableColumn::new("Name")]).row(vec!["A"]).row(vec!["B"]);
229 let s1 = dt.layout(&ctx);
230 let s2 = dt.layout(&ctx);
231 assert_eq!((s1.width, s1.height), (s2.width, s2.height));
232 }
233}