Skip to main content

gpui_component/table/
table.rs

1use gpui::{
2    AnyElement, App, InteractiveElement as _, IntoElement, ParentElement, Pixels, RenderOnce,
3    SharedString, StyleRefinement, Styled, TextAlign, Window, div, prelude::FluentBuilder as _, px,
4    relative,
5};
6use gpui_base::{
7    Table as BaseTable, TableBody as BaseTableBody, TableCaption as BaseTableCaption,
8    TableCell as BaseTableCell, TableHead as BaseTableHead, TableHeader as BaseTableHeader,
9    TableRow as BaseTableRow,
10};
11
12use crate::{ActiveTheme as _, AnyChildElement, ChildElement, Sizable, Size, StyledExt as _};
13
14const MIN_CELL_WIDTH: Pixels = px(100.);
15
16/// A basic table component for directly rendering tabular data.
17///
18/// Unlike [`DataTable`], this is a simple, stateless, composable table
19/// without virtual scrolling or column management.
20///
21/// Size set via [`Sizable`] is automatically propagated to all children.
22///
23/// # Example
24///
25/// ```rust,ignore
26/// Table::new()
27///     .small()
28///     .child(TableHeader::new().child(
29///         TableRow::new()
30///             .child(TableHead::new().child("Name"))
31///             .child(TableHead::new().child("Email"))
32///     ))
33///     .child(TableBody::new()
34///         .child(TableRow::new()
35///             .child(TableCell::new().child("John"))
36///             .child(TableCell::new().child("john@example.com")))
37///     )
38///     .child(TableCaption::new().child("A list of recent invoices."))
39/// ```
40#[derive(IntoElement)]
41pub struct Table {
42    ix: usize,
43    style: StyleRefinement,
44    children: Vec<AnyChildElement>,
45    size: Size,
46    accessibility_label: Option<SharedString>,
47}
48
49impl Table {
50    pub fn new() -> Self {
51        Self {
52            ix: 0,
53            style: StyleRefinement::default(),
54            children: Vec::new(),
55            size: Size::default(),
56            accessibility_label: None,
57        }
58    }
59
60    /// Set the name a screen reader announces for the table.
61    ///
62    /// A [`TableCaption`] is visible descriptive content and is not used
63    /// automatically as the table's accessible name.
64    pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
65        self.accessibility_label = Some(label.into());
66        self
67    }
68
69    pub fn child(mut self, child: impl ChildElement + 'static) -> Self {
70        self.children.push(AnyChildElement::new(child));
71        self
72    }
73
74    pub fn children<E: ChildElement + 'static>(
75        mut self,
76        children: impl IntoIterator<Item = E>,
77    ) -> Self {
78        self.children
79            .extend(children.into_iter().map(AnyChildElement::new));
80        self
81    }
82}
83
84impl Styled for Table {
85    fn style(&mut self) -> &mut StyleRefinement {
86        &mut self.style
87    }
88}
89
90impl Sizable for Table {
91    fn with_size(mut self, size: impl Into<Size>) -> Self {
92        self.size = size.into();
93        self
94    }
95}
96
97impl ChildElement for Table {
98    fn with_ix(mut self, ix: usize) -> Self {
99        self.ix = ix;
100        self
101    }
102}
103
104impl RenderOnce for Table {
105    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
106        BaseTable::new(("table", self.ix))
107            .when_some(self.accessibility_label, |this, label| {
108                this.accessibility_label(label)
109            })
110            .w_full()
111            .text_sm()
112            .overflow_hidden()
113            .bg(cx.theme().tokens.table)
114            .refine_style(&self.style)
115            .children(
116                self.children
117                    .into_iter()
118                    .enumerate()
119                    .map(|(ix, c)| c.into_any(ix, self.size)),
120            )
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn stores_an_explicit_accessibility_label() {
130        let plain = Table::new();
131        assert_eq!(plain.accessibility_label, None);
132
133        let named = Table::new().accessibility_label("Recent invoices");
134        assert_eq!(
135            named.accessibility_label.as_deref(),
136            Some("Recent invoices")
137        );
138    }
139}
140
141/// The header section of a [`Table`], wrapping header rows.
142#[derive(IntoElement)]
143pub struct TableHeader {
144    ix: usize,
145    style: StyleRefinement,
146    children: Vec<AnyChildElement>,
147    size: Size,
148}
149
150impl TableHeader {
151    pub fn new() -> Self {
152        Self {
153            ix: 0,
154            style: StyleRefinement::default(),
155            children: Vec::new(),
156            size: Size::default(),
157        }
158    }
159
160    pub fn child(mut self, child: impl ChildElement + 'static) -> Self {
161        self.children.push(AnyChildElement::new(child));
162        self
163    }
164
165    pub fn children<E: ChildElement + 'static>(
166        mut self,
167        children: impl IntoIterator<Item = E>,
168    ) -> Self {
169        self.children
170            .extend(children.into_iter().map(AnyChildElement::new));
171        self
172    }
173}
174
175impl Styled for TableHeader {
176    fn style(&mut self) -> &mut StyleRefinement {
177        &mut self.style
178    }
179}
180
181impl ChildElement for TableHeader {
182    fn with_ix(mut self, ix: usize) -> Self {
183        self.ix = ix;
184        self
185    }
186}
187
188impl Sizable for TableHeader {
189    fn with_size(mut self, size: impl Into<Size>) -> Self {
190        self.size = size.into();
191        self
192    }
193}
194
195impl RenderOnce for TableHeader {
196    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
197        BaseTableHeader::new(("table-header", self.ix))
198            .w_full()
199            .bg(cx.theme().tokens.table_head)
200            .text_color(cx.theme().table_head_foreground)
201            .refine_style(&self.style)
202            .border_b_1()
203            .border_color(cx.theme().table_row_border)
204            .children(
205                self.children
206                    .into_iter()
207                    .enumerate()
208                    .map(|(ix, c)| c.into_any(ix, self.size)),
209            )
210    }
211}
212
213/// The body section of a [`Table`], wrapping data rows.
214#[derive(IntoElement)]
215pub struct TableBody {
216    ix: usize,
217    style: StyleRefinement,
218    children: Vec<AnyChildElement>,
219    size: Size,
220}
221
222impl TableBody {
223    pub fn new() -> Self {
224        Self {
225            ix: 0,
226            style: StyleRefinement::default(),
227            children: Vec::new(),
228            size: Size::default(),
229        }
230    }
231
232    pub fn child(mut self, child: impl ChildElement + 'static) -> Self {
233        self.children.push(AnyChildElement::new(child));
234        self
235    }
236
237    pub fn children<E: ChildElement + 'static>(
238        mut self,
239        children: impl IntoIterator<Item = E>,
240    ) -> Self {
241        self.children
242            .extend(children.into_iter().map(AnyChildElement::new));
243        self
244    }
245}
246
247impl Styled for TableBody {
248    fn style(&mut self) -> &mut StyleRefinement {
249        &mut self.style
250    }
251}
252
253impl Sizable for TableBody {
254    fn with_size(mut self, size: impl Into<Size>) -> Self {
255        self.size = size.into();
256        self
257    }
258}
259
260impl ChildElement for TableBody {
261    fn with_ix(mut self, ix: usize) -> Self {
262        self.ix = ix;
263        self
264    }
265}
266
267impl RenderOnce for TableBody {
268    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
269        BaseTableBody::new(("table-body", self.ix))
270            .w_full()
271            .refine_style(&self.style)
272            .children(
273                self.children
274                    .into_iter()
275                    .enumerate()
276                    .map(|(ix, c)| c.into_any(ix, self.size)),
277            )
278    }
279}
280
281/// The footer section of a [`Table`], wrapping footer rows.
282#[derive(IntoElement)]
283pub struct TableFooter {
284    ix: usize,
285    style: StyleRefinement,
286    children: Vec<AnyChildElement>,
287    size: Size,
288}
289
290impl TableFooter {
291    pub fn new() -> Self {
292        Self {
293            ix: 0,
294            style: StyleRefinement::default(),
295            children: Vec::new(),
296            size: Size::default(),
297        }
298    }
299
300    pub fn child(mut self, child: impl ChildElement + 'static) -> Self {
301        self.children.push(AnyChildElement::new(child));
302        self
303    }
304
305    pub fn children<E: ChildElement + 'static>(
306        mut self,
307        children: impl IntoIterator<Item = E>,
308    ) -> Self {
309        self.children
310            .extend(children.into_iter().map(AnyChildElement::new));
311        self
312    }
313}
314
315impl Styled for TableFooter {
316    fn style(&mut self) -> &mut StyleRefinement {
317        &mut self.style
318    }
319}
320
321impl Sizable for TableFooter {
322    fn with_size(mut self, size: impl Into<Size>) -> Self {
323        self.size = size.into();
324        self
325    }
326}
327
328impl ChildElement for TableFooter {
329    fn with_ix(mut self, ix: usize) -> Self {
330        self.ix = ix;
331        self
332    }
333}
334
335impl RenderOnce for TableFooter {
336    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
337        div()
338            .id(("table-footer", self.ix))
339            .w_full()
340            .bg(cx.theme().tokens.table_foot)
341            .text_color(cx.theme().table_foot_foreground)
342            .border_t_1()
343            .border_color(cx.theme().table_row_border)
344            .refine_style(&self.style)
345            .children(
346                self.children
347                    .into_iter()
348                    .enumerate()
349                    .map(|(ix, c)| c.into_any(ix, self.size)),
350            )
351    }
352}
353
354/// A row in a [`Table`].
355#[derive(IntoElement)]
356pub struct TableRow {
357    ix: usize,
358    style: StyleRefinement,
359    children: Vec<AnyChildElement>,
360    size: Size,
361}
362
363impl TableRow {
364    pub fn new() -> Self {
365        Self {
366            ix: 0,
367            style: StyleRefinement::default(),
368            children: Vec::new(),
369            size: Size::default(),
370        }
371    }
372
373    pub fn child(mut self, child: impl ChildElement + 'static) -> Self {
374        self.children.push(AnyChildElement::new(child));
375        self
376    }
377
378    pub fn children<E: ChildElement + 'static>(
379        mut self,
380        children: impl IntoIterator<Item = E>,
381    ) -> Self {
382        self.children
383            .extend(children.into_iter().map(AnyChildElement::new));
384        self
385    }
386}
387
388impl Styled for TableRow {
389    fn style(&mut self) -> &mut StyleRefinement {
390        &mut self.style
391    }
392}
393
394impl Sizable for TableRow {
395    fn with_size(mut self, size: impl Into<Size>) -> Self {
396        self.size = size.into();
397        self
398    }
399}
400
401impl ChildElement for TableRow {
402    fn with_ix(mut self, ix: usize) -> Self {
403        self.ix = ix;
404        self
405    }
406}
407
408impl RenderOnce for TableRow {
409    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
410        BaseTableRow::new(("table-row", self.ix), self.ix + 1)
411            .w_full()
412            .flex()
413            .flex_row()
414            .refine_style(&self.style)
415            .border_color(cx.theme().table_row_border)
416            .when(self.ix > 0, |this| this.border_t_1())
417            .children(
418                self.children
419                    .into_iter()
420                    .enumerate()
421                    .map(|(ix, c)| c.into_any(ix, self.size)),
422            )
423    }
424}
425
426/// A header cell in a [`TableRow`].
427#[derive(IntoElement)]
428pub struct TableHead {
429    ix: usize,
430    style: StyleRefinement,
431    children: Vec<AnyElement>,
432    col_span: usize,
433    align: TextAlign,
434    size: Size,
435}
436
437impl TableHead {
438    pub fn new() -> Self {
439        Self {
440            ix: 0,
441            style: StyleRefinement::default(),
442            children: Vec::new(),
443            col_span: 1,
444            align: TextAlign::Left,
445            size: Size::default(),
446        }
447    }
448
449    /// Set the column span of this header cell.
450    pub fn col_span(mut self, span: usize) -> Self {
451        self.col_span = span.max(1);
452        self
453    }
454
455    /// Set text alignment to center.
456    pub fn text_center(mut self) -> Self {
457        self.align = TextAlign::Center;
458        self
459    }
460
461    /// Set text alignment to right.
462    pub fn text_right(mut self) -> Self {
463        self.align = TextAlign::Right;
464        self
465    }
466}
467
468impl ParentElement for TableHead {
469    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
470        self.children.extend(elements);
471    }
472}
473
474impl Sizable for TableHead {
475    fn with_size(mut self, size: impl Into<Size>) -> Self {
476        self.size = size.into();
477        self
478    }
479}
480
481impl ChildElement for TableHead {
482    fn with_ix(mut self, ix: usize) -> Self {
483        self.ix = ix;
484        self
485    }
486}
487
488impl Styled for TableHead {
489    fn style(&mut self) -> &mut StyleRefinement {
490        &mut self.style
491    }
492}
493
494impl RenderOnce for TableHead {
495    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
496        let paddings = self.size.table_cell_padding();
497
498        BaseTableHead::new(("table-head", self.ix), self.ix + 1)
499            .flex()
500            .items_center()
501            .when(self.style.size.width.is_none(), |this| {
502                this.flex_shrink_1()
503                    .flex_basis(relative(self.col_span as f32))
504            })
505            .min_w(MIN_CELL_WIDTH * self.col_span)
506            .px(paddings.left)
507            .py(paddings.top)
508            .when(self.align == TextAlign::Center, |this| {
509                this.justify_center()
510            })
511            .when(self.align == TextAlign::Right, |this| this.justify_end())
512            .refine_style(&self.style)
513            .children(self.children)
514    }
515}
516
517/// A data cell in a [`TableRow`].
518#[derive(IntoElement)]
519pub struct TableCell {
520    ix: usize,
521    style: StyleRefinement,
522    children: Vec<AnyElement>,
523    col_span: usize,
524    align: TextAlign,
525    size: Size,
526}
527
528impl TableCell {
529    pub fn new() -> Self {
530        Self {
531            ix: 0,
532            style: StyleRefinement::default(),
533            children: Vec::new(),
534            col_span: 1,
535            align: TextAlign::Left,
536            size: Size::default(),
537        }
538    }
539
540    /// Set the column span of this cell.
541    pub fn col_span(mut self, span: usize) -> Self {
542        self.col_span = span.max(1);
543        self
544    }
545
546    /// Set text alignment to center.
547    pub fn text_center(mut self) -> Self {
548        self.align = TextAlign::Center;
549        self
550    }
551
552    /// Set text alignment to right.
553    pub fn text_right(mut self) -> Self {
554        self.align = TextAlign::Right;
555        self
556    }
557}
558
559impl ParentElement for TableCell {
560    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
561        self.children.extend(elements);
562    }
563}
564
565impl Sizable for TableCell {
566    fn with_size(mut self, size: impl Into<Size>) -> Self {
567        self.size = size.into();
568        self
569    }
570}
571
572impl ChildElement for TableCell {
573    fn with_ix(mut self, ix: usize) -> Self {
574        self.ix = ix;
575        self
576    }
577}
578
579impl Styled for TableCell {
580    fn style(&mut self) -> &mut StyleRefinement {
581        &mut self.style
582    }
583}
584
585impl RenderOnce for TableCell {
586    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
587        let paddings = self.size.table_cell_padding();
588
589        BaseTableCell::new(("table-cell", self.ix), self.ix + 1)
590            .flex()
591            .items_center()
592            .when(self.style.size.width.is_none(), |this| {
593                this.flex_shrink_1()
594                    .flex_basis(relative(self.col_span as f32))
595            })
596            .min_w(MIN_CELL_WIDTH * self.col_span)
597            .px(paddings.left)
598            .py(paddings.top)
599            .when(self.align == TextAlign::Center, |this| {
600                this.justify_center()
601            })
602            .when(self.align == TextAlign::Right, |this| this.justify_end())
603            .refine_style(&self.style)
604            .children(self.children)
605    }
606}
607
608/// A caption displayed below the [`Table`].
609#[derive(IntoElement)]
610pub struct TableCaption {
611    ix: usize,
612    style: StyleRefinement,
613    children: Vec<AnyElement>,
614    size: Size,
615}
616
617impl TableCaption {
618    pub fn new() -> Self {
619        Self {
620            ix: 0,
621            style: StyleRefinement::default(),
622            children: Vec::new(),
623            size: Size::default(),
624        }
625    }
626}
627
628impl ParentElement for TableCaption {
629    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
630        self.children.extend(elements);
631    }
632}
633
634impl Sizable for TableCaption {
635    fn with_size(mut self, size: impl Into<Size>) -> Self {
636        self.size = size.into();
637        self
638    }
639}
640
641impl ChildElement for TableCaption {
642    fn with_ix(mut self, ix: usize) -> Self {
643        self.ix = ix;
644        self
645    }
646}
647
648impl Styled for TableCaption {
649    fn style(&mut self) -> &mut StyleRefinement {
650        &mut self.style
651    }
652}
653
654impl RenderOnce for TableCaption {
655    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
656        let paddings = self.size.table_cell_padding();
657
658        BaseTableCaption::new(("table-caption", self.ix))
659            .w_full()
660            .px(paddings.left)
661            .py(paddings.top)
662            .text_sm()
663            .text_color(cx.theme().muted_foreground)
664            .text_center()
665            .refine_style(&self.style)
666            .children(self.children)
667    }
668}