Skip to main content

gpui_base/
table.rs

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/// An unstyled semantic table root.
62#[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    /// Create an unstyled semantic table root.
74    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    /// Sets the total number of rows, including rows outside the rendered
86    /// range. Assistive technology needs this to announce "row 5 of 200" for a
87    /// virtualized table whose rendered rows are only a window onto the data.
88    pub fn row_count(mut self, count: usize) -> Self {
89        self.row_count = Some(count);
90        self
91    }
92
93    /// Sets the total number of columns, including columns outside the
94    /// rendered range.
95    pub fn column_count(mut self, count: usize) -> Self {
96        self.column_count = Some(count);
97        self
98    }
99
100    /// Sets the table's accessible name.
101    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/// An unstyled semantic table row.
150#[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    /// Create an unstyled semantic table row with a one-based accessibility index.
160    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/// An unstyled table caption slot.
263#[derive(IntoElement)]
264pub struct TableCaption {
265    base: Stateful<Div>,
266    style: StyleRefinement,
267    children: Vec<AnyElement>,
268}
269
270impl TableCaption {
271    /// Create an unstyled table caption slot.
272    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}