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