1use gpui::{
2 AnyElement, App, Div, ElementId, InteractiveElement, Interactivity, IntoElement, ParentElement,
3 RenderOnce, Role, SharedString, Stateful, StatefulInteractiveElement, StyleRefinement, Styled,
4 Window, div, prelude::FluentBuilder as _,
5};
6
7use crate::StyledExt as _;
8
9macro_rules! table_part {
10 ($name:ident, $role:expr, $docs:literal) => {
11 #[doc = $docs]
12 #[derive(IntoElement)]
13 pub struct $name {
14 base: Stateful<Div>,
15 style: StyleRefinement,
16 children: Vec<AnyElement>,
17 }
18
19 impl $name {
20 #[doc = concat!("Create ", $docs)]
21 pub fn new(id: impl Into<ElementId>) -> Self {
22 Self {
23 base: div().id(id),
24 style: StyleRefinement::default(),
25 children: Vec::new(),
26 }
27 }
28 }
29
30 impl Styled for $name {
31 fn style(&mut self) -> &mut StyleRefinement {
32 &mut self.style
33 }
34 }
35
36 impl ParentElement for $name {
37 fn extend(&mut self, children: impl IntoIterator<Item = AnyElement>) {
38 self.children.extend(children);
39 }
40 }
41
42 impl InteractiveElement for $name {
43 fn interactivity(&mut self) -> &mut Interactivity {
44 self.base.interactivity()
45 }
46 }
47
48 impl StatefulInteractiveElement for $name {}
49
50 impl RenderOnce for $name {
51 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
52 self.base
53 .role($role)
54 .children(self.children)
55 .refine_style(&self.style)
56 }
57 }
58 };
59}
60
61#[derive(IntoElement)]
63pub struct Table {
64 base: Stateful<Div>,
65 style: StyleRefinement,
66 children: Vec<AnyElement>,
67 row_count: Option<usize>,
68 column_count: Option<usize>,
69 accessibility_label: Option<SharedString>,
70}
71
72impl Table {
73 pub fn new(id: impl Into<ElementId>) -> Self {
75 Self {
76 base: div().id(id),
77 style: StyleRefinement::default(),
78 children: Vec::new(),
79 row_count: None,
80 column_count: None,
81 accessibility_label: None,
82 }
83 }
84
85 pub fn row_count(mut self, count: usize) -> Self {
89 self.row_count = Some(count);
90 self
91 }
92
93 pub fn column_count(mut self, count: usize) -> Self {
96 self.column_count = Some(count);
97 self
98 }
99
100 pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
102 self.accessibility_label = Some(label.into());
103 self
104 }
105}
106
107impl Styled for Table {
108 fn style(&mut self) -> &mut StyleRefinement {
109 &mut self.style
110 }
111}
112
113impl ParentElement for Table {
114 fn extend(&mut self, children: impl IntoIterator<Item = AnyElement>) {
115 self.children.extend(children);
116 }
117}
118
119impl InteractiveElement for Table {
120 fn interactivity(&mut self) -> &mut Interactivity {
121 self.base.interactivity()
122 }
123}
124
125impl StatefulInteractiveElement for Table {}
126
127impl RenderOnce for Table {
128 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
129 self.base
130 .role(Role::Table)
131 .when_some(self.accessibility_label, |this, label| {
132 this.aria_label(label)
133 })
134 .when_some(self.row_count, |this, count| this.aria_row_count(count))
135 .when_some(self.column_count, |this, count| {
136 this.aria_column_count(count)
137 })
138 .children(self.children)
139 .refine_style(&self.style)
140 }
141}
142table_part!(
143 TableHeader,
144 Role::RowGroup,
145 "An unstyled table header group."
146);
147table_part!(TableBody, Role::RowGroup, "An unstyled table body group.");
148
149#[derive(IntoElement)]
151pub struct TableRow {
152 base: Stateful<Div>,
153 style: StyleRefinement,
154 row_index: usize,
155 children: Vec<AnyElement>,
156}
157
158impl TableRow {
159 pub fn new(id: impl Into<ElementId>, row_index: usize) -> Self {
161 Self {
162 base: div().id(id),
163 style: StyleRefinement::default(),
164 row_index,
165 children: Vec::new(),
166 }
167 }
168}
169
170impl Styled for TableRow {
171 fn style(&mut self) -> &mut StyleRefinement {
172 &mut self.style
173 }
174}
175
176impl ParentElement for TableRow {
177 fn extend(&mut self, children: impl IntoIterator<Item = AnyElement>) {
178 self.children.extend(children);
179 }
180}
181
182impl InteractiveElement for TableRow {
183 fn interactivity(&mut self) -> &mut Interactivity {
184 self.base.interactivity()
185 }
186}
187
188impl StatefulInteractiveElement for TableRow {}
189
190impl RenderOnce for TableRow {
191 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
192 self.base
193 .role(Role::Row)
194 .aria_row_index(self.row_index)
195 .children(self.children)
196 .refine_style(&self.style)
197 }
198}
199
200macro_rules! table_cell {
201 ($name:ident, $role:expr, $docs:literal) => {
202 #[doc = $docs]
203 #[derive(IntoElement)]
204 pub struct $name {
205 base: Stateful<Div>,
206 style: StyleRefinement,
207 column_index: usize,
208 children: Vec<AnyElement>,
209 }
210
211 impl $name {
212 #[doc = concat!("Create ", $docs, " with a one-based accessibility index.")]
213 pub fn new(id: impl Into<ElementId>, column_index: usize) -> Self {
214 Self {
215 base: div().id(id),
216 style: StyleRefinement::default(),
217 column_index,
218 children: Vec::new(),
219 }
220 }
221 }
222
223 impl Styled for $name {
224 fn style(&mut self) -> &mut StyleRefinement {
225 &mut self.style
226 }
227 }
228
229 impl ParentElement for $name {
230 fn extend(&mut self, children: impl IntoIterator<Item = AnyElement>) {
231 self.children.extend(children);
232 }
233 }
234
235 impl InteractiveElement for $name {
236 fn interactivity(&mut self) -> &mut Interactivity {
237 self.base.interactivity()
238 }
239 }
240
241 impl StatefulInteractiveElement for $name {}
242
243 impl RenderOnce for $name {
244 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
245 self.base
246 .role($role)
247 .aria_column_index(self.column_index)
248 .children(self.children)
249 .refine_style(&self.style)
250 }
251 }
252 };
253}
254
255table_cell!(
256 TableHead,
257 Role::ColumnHeader,
258 "An unstyled table column header."
259);
260table_cell!(TableCell, Role::Cell, "An unstyled table data cell.");
261
262#[derive(IntoElement)]
264pub struct TableCaption {
265 base: Stateful<Div>,
266 style: StyleRefinement,
267 children: Vec<AnyElement>,
268}
269
270impl TableCaption {
271 pub fn new(id: impl Into<ElementId>) -> Self {
273 Self {
274 base: div().id(id),
275 style: StyleRefinement::default(),
276 children: Vec::new(),
277 }
278 }
279}
280
281impl Styled for TableCaption {
282 fn style(&mut self) -> &mut StyleRefinement {
283 &mut self.style
284 }
285}
286
287impl ParentElement for TableCaption {
288 fn extend(&mut self, children: impl IntoIterator<Item = AnyElement>) {
289 self.children.extend(children);
290 }
291}
292
293impl InteractiveElement for TableCaption {
294 fn interactivity(&mut self) -> &mut Interactivity {
295 self.base.interactivity()
296 }
297}
298
299impl StatefulInteractiveElement for TableCaption {}
300
301impl RenderOnce for TableCaption {
302 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
303 self.base.children(self.children).refine_style(&self.style)
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use gpui::{Context, Element as _, Modifiers, Render, TestAppContext, accesskit, point, px};
311 use std::{cell::Cell, rc::Rc};
312
313 #[gpui::test]
314 fn table_projects_its_accessible_name(cx: &mut TestAppContext) {
315 let window = cx.add_empty_window();
316 window.update(|window, cx| {
317 let mut node = accesskit::Node::new(Role::Table);
318 Table::new("positions")
319 .accessibility_label("Open positions")
320 .render(window, cx)
321 .into_element()
322 .write_a11y_info(&mut node);
323
324 assert_eq!(node.label(), Some("Open positions"));
325 });
326 }
327
328 #[gpui::test]
329 fn row_and_cells_project_accessibility_indices(cx: &mut TestAppContext) {
330 let window = cx.add_empty_window();
331 window.update(|window, cx| {
332 let mut row = accesskit::Node::new(Role::Row);
333 TableRow::new("row", 3)
334 .render(window, cx)
335 .into_element()
336 .write_a11y_info(&mut row);
337 assert_eq!(row.row_index(), Some(3));
338
339 let mut head = accesskit::Node::new(Role::GenericContainer);
340 TableHead::new("head", 2)
341 .render(window, cx)
342 .into_element()
343 .write_a11y_info(&mut head);
344 assert_eq!(head.column_index(), Some(2));
345
346 let mut cell = accesskit::Node::new(Role::Cell);
347 TableCell::new("cell", 4)
348 .render(window, cx)
349 .into_element()
350 .write_a11y_info(&mut cell);
351 assert_eq!(cell.column_index(), Some(4));
352 });
353 }
354
355 struct TableHarness {
356 clicks: Rc<Cell<usize>>,
357 }
358
359 impl Render for TableHarness {
360 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
361 let clicks = self.clicks.clone();
362 Table::new("table")
363 .debug_selector(|| "base-table".into())
364 .w(px(120.))
365 .h(px(60.))
366 .on_click(move |_, _, _| clicks.set(clicks.get() + 1))
367 .child(
368 TableBody::new("body").child(
369 TableRow::new("row", 1)
370 .child(TableCell::new("cell", 1).child(
371 div().debug_selector(|| "table-child".into()).size(px(20.)),
372 )),
373 ),
374 )
375 }
376 }
377
378 #[gpui::test]
379 fn table_forwards_children_instance_style_and_pointer_interaction(cx: &mut TestAppContext) {
380 let clicks = Rc::new(Cell::new(0));
381 let (_, cx) = cx.add_window_view({
382 let clicks = clicks.clone();
383 move |_, _| TableHarness { clicks }
384 });
385 cx.update(|window, cx| window.draw(cx).clear(cx));
386
387 let table = cx.debug_bounds("base-table").expect("table is rendered");
388 assert_eq!(table.size.width, px(120.));
389 assert_eq!(table.size.height, px(60.));
390 let child = cx.debug_bounds("table-child").expect("child is rendered");
391 assert_eq!(child.size.width, px(20.));
392 assert_eq!(child.size.height, px(20.));
393
394 cx.simulate_click(point(px(10.), px(10.)), Modifiers::default());
395 cx.simulate_click(point(px(100.), px(50.)), Modifiers::default());
396 assert_eq!(clicks.get(), 2);
397 }
398}