Skip to main content

gpui_base/
table.rs

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/// An unstyled semantic table root.
68#[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    /// Create an unstyled semantic table root.
80    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    /// Sets the total number of rows, including rows outside the rendered
92    /// range. Assistive technology needs this to announce "row 5 of 200" for a
93    /// virtualized table whose rendered rows are only a window onto the data.
94    pub fn row_count(mut self, count: usize) -> Self {
95        self.row_count = Some(count);
96        self
97    }
98
99    /// Sets the total number of columns, including columns outside the
100    /// rendered range.
101    pub fn column_count(mut self, count: usize) -> Self {
102        self.column_count = Some(count);
103        self
104    }
105
106    /// Sets the table's accessible name.
107    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/// An unstyled semantic table row.
161#[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    /// Create an unstyled semantic table row with a one-based accessibility index.
171    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/// An unstyled table caption slot.
284#[derive(IntoElement)]
285pub struct TableCaption {
286    base: crate::ObservedElement<Stateful<Div>>,
287    style: StyleRefinement,
288    children: Vec<AnyElement>,
289}
290
291impl TableCaption {
292    /// Create an unstyled table caption slot.
293    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}