aetna_core/widgets/
table.rs1use std::panic::Location;
9
10use super::text::text;
11use crate::metrics::MetricsRole;
12use crate::tokens;
13use crate::tree::*;
14
15#[track_caller]
16pub fn table<I, E>(children: I) -> El
17where
18 I: IntoIterator<Item = E>,
19 E: Into<El>,
20{
21 El::new(Kind::Custom("table"))
22 .at_loc(Location::caller())
23 .children(children)
24 .axis(Axis::Column)
25 .width(Size::Fill(1.0))
26 .height(Size::Hug)
27 .align(Align::Stretch)
28 .clip()
29}
30
31#[track_caller]
32pub fn table_header<I, E>(rows: I) -> El
33where
34 I: IntoIterator<Item = E>,
35 E: Into<El>,
36{
37 let mut header = El::new(Kind::Custom("table_header"))
38 .at_loc(Location::caller())
39 .children(rows)
40 .axis(Axis::Column)
41 .width(Size::Fill(1.0))
42 .height(Size::Hug)
43 .align(Align::Stretch);
44
45 for row in &mut header.children {
49 if row.metrics_role == Some(MetricsRole::TableRow) {
50 row.metrics_role = Some(MetricsRole::TableHeader);
51 if !row.explicit_radius {
52 row.radius = crate::tree::Corners::ZERO;
53 }
54 }
55 }
56
57 header
58}
59
60#[track_caller]
61pub fn table_body<I, E>(rows: I) -> El
62where
63 I: IntoIterator<Item = E>,
64 E: Into<El>,
65{
66 El::new(Kind::Custom("table_body"))
67 .at_loc(Location::caller())
68 .children(rows)
69 .axis(Axis::Column)
70 .width(Size::Fill(1.0))
71 .height(Size::Hug)
72 .align(Align::Stretch)
73}
74
75#[track_caller]
76pub fn table_row<I, E>(cells: I) -> El
77where
78 I: IntoIterator<Item = E>,
79 E: Into<El>,
80{
81 row(cells)
82 .at_loc(Location::caller())
83 .metrics_role(MetricsRole::TableRow)
84 .width(Size::Fill(1.0))
85 .height(Size::Hug)
86 .align(Align::Stretch)
87 .default_gap(0.0)
88 .default_radius(0.0)
89}
90
91#[track_caller]
92pub fn table_head(label: impl Into<String>) -> El {
93 table_head_el(text(label))
94}
95
96#[track_caller]
97pub fn table_head_el(content: impl Into<El>) -> El {
98 let mut el = content
99 .into()
100 .at_loc(Location::caller())
101 .ellipsis()
102 .width(Size::Fill(1.0))
103 .height(Size::Hug)
104 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
105 .fill(tokens::MUTED)
106 .stroke(tokens::BORDER)
107 .radius(0.0);
108 apply_head_style(&mut el);
109 el
110}
111
112#[track_caller]
113pub fn table_cell(content: impl Into<El>) -> El {
114 content
115 .into()
116 .at_loc(Location::caller())
117 .ellipsis()
118 .width(Size::Fill(1.0))
119 .height(Size::Hug)
120 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
121 .stroke(tokens::BORDER)
122 .radius(0.0)
123}
124
125fn apply_head_style(el: &mut El) {
126 if el.kind == Kind::Text {
127 el.text_role = TextRole::Caption;
128 if el.font_weight == FontWeight::Regular {
129 el.font_weight = FontWeight::Medium;
130 }
131 el.text_color = Some(tokens::MUTED_FOREGROUND);
132 }
133 for child in &mut el.children {
134 apply_head_style(child);
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn table_header_promotes_direct_table_rows() {
144 let header = table_header([table_row([table_head("Name")])]);
145
146 assert_eq!(header.children.len(), 1);
147 assert_eq!(
148 header.children[0].metrics_role,
149 Some(MetricsRole::TableHeader)
150 );
151 assert_eq!(header.children[0].align, Align::Stretch);
152 }
153
154 #[test]
155 fn table_head_el_styles_rich_text_children() {
156 let head = table_head_el(text_runs([text("Rich "), text("head").bold()]));
157
158 assert_eq!(head.kind, Kind::Inlines);
159 assert_eq!(head.children[0].text_role, TextRole::Caption);
160 assert_eq!(head.children[0].font_weight, FontWeight::Medium);
161 assert_eq!(head.children[1].text_role, TextRole::Caption);
162 assert_eq!(head.children[1].font_weight, FontWeight::Bold);
163 assert_eq!(head.children[1].text.as_deref(), Some("head"));
164 }
165
166 #[test]
167 fn table_cells_carry_grid_chrome() {
168 let body = table_cell(text("Ada"));
169 assert_eq!(body.padding, Sides::xy(tokens::SPACE_3, tokens::SPACE_2));
170 assert_eq!(body.stroke, Some(tokens::BORDER));
171 assert_eq!(body.stroke_width, 1.0);
172 assert_eq!(body.radius, Corners::ZERO);
173
174 let head = table_head("Name");
175 assert_eq!(head.fill, Some(tokens::MUTED));
176 assert_eq!(head.stroke, Some(tokens::BORDER));
177 }
178
179 #[test]
180 fn table_header_text_emits_glyph_run_after_layout() {
181 use crate::Rect;
182 use crate::draw_ops::draw_ops;
183 use crate::ir::DrawOp;
184 use crate::layout::layout;
185 use crate::state::UiState;
186
187 let mut tree = table([
188 table_header([table_row([table_head("Name"), table_head("Role")])]),
189 table_body([table_row([
190 table_cell(text("Ada")),
191 table_cell(text("dev")),
192 ])]),
193 ]);
194 let mut state = UiState::new();
195 layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 320.0, 200.0));
196
197 let ops = draw_ops(&tree, &state);
198 assert!(
199 ops.iter().any(|op| matches!(
200 op,
201 DrawOp::GlyphRun { text, .. } if text == "Name"
202 )),
203 "expected header text to be painted; ops were {ops:?}"
204 );
205 let border_quads = ops
206 .iter()
207 .filter(|op| matches!(op, DrawOp::Quad { id, .. } if id.contains("text")))
208 .count();
209 assert!(
210 border_quads >= 4,
211 "expected cell chrome quads for the table cells, got {border_quads}"
212 );
213 }
214}