Skip to main content

gpui_component/table/
data_table.rs

1use crate::{
2    ActiveTheme, Sizable, Size, ThemeStyled as _,
3    actions::{
4        Cancel, SelectDown, SelectFirst, SelectLast, SelectNextColumn, SelectPageDown,
5        SelectPageUp, SelectPrevColumn, SelectUp,
6    },
7    table::{TableDelegate, TableState},
8};
9use gpui::{
10    App, Edges, Entity, Focusable, InteractiveElement, IntoElement, KeyBinding, ParentElement,
11    Pixels, RenderOnce, Styled, Window, div, prelude::FluentBuilder,
12};
13use gpui_base::TestSupportExt as _;
14
15const CONTEXT: &'static str = "DataTable";
16pub(super) fn init(cx: &mut App) {
17    cx.bind_keys([
18        KeyBinding::new("escape", Cancel, Some(CONTEXT)),
19        KeyBinding::new("up", SelectUp, Some(CONTEXT)),
20        KeyBinding::new("down", SelectDown, Some(CONTEXT)),
21        KeyBinding::new("left", SelectPrevColumn, Some(CONTEXT)),
22        KeyBinding::new("right", SelectNextColumn, Some(CONTEXT)),
23        KeyBinding::new("home", SelectFirst, Some(CONTEXT)),
24        KeyBinding::new("end", SelectLast, Some(CONTEXT)),
25        KeyBinding::new("pageup", SelectPageUp, Some(CONTEXT)),
26        KeyBinding::new("pagedown", SelectPageDown, Some(CONTEXT)),
27        KeyBinding::new("tab", SelectNextColumn, Some(CONTEXT)),
28        KeyBinding::new("shift-tab", SelectPrevColumn, Some(CONTEXT)),
29    ]);
30}
31
32pub(super) struct TableOptions {
33    pub(super) scrollbar_visible: Edges<bool>,
34    /// Set stripe style of the table.
35    pub(super) stripe: bool,
36    /// Set to use border style of the table.
37    pub(super) bordered: bool,
38    /// The cell size of the table.
39    pub(super) size: Size,
40    /// Height of each header row. `None` uses [`Size::table_row_height`].
41    pub(super) header_row_height: Option<Pixels>,
42}
43
44impl Default for TableOptions {
45    fn default() -> Self {
46        Self {
47            scrollbar_visible: Edges::all(true),
48            stripe: false,
49            bordered: true,
50            size: Size::default(),
51            header_row_height: None,
52        }
53    }
54}
55
56/// A table element with support for row, column, and cell selection.
57///
58/// # Features
59///
60/// - **Multiple Selection Modes**: Support for row, column, and cell selection
61/// - **Cell Selection**: Click to select individual cells, with keyboard navigation
62/// - **Virtual Scrolling**: Efficient rendering of large datasets
63/// - **Resizable Columns**: Drag column borders to resize
64/// - **Movable Columns**: Drag column headers to reorder
65/// - **Fixed Columns**: Pin columns to the left side
66/// - **Sortable Columns**: Click column headers to sort
67/// - **Context Menus**: Right-click support for rows and cells
68///
69/// # Cell Selection Mode
70///
71/// When cell selection is enabled via [`TableState::cell_selectable()`]:
72/// - Click on cells to select them
73/// - A row header column appears on the left for selecting entire rows
74///   (use [`TableState::row_header()`] to hide it)
75/// - Keyboard navigation (arrow keys, Tab, Home, End, PageUp, PageDown) works at cell level
76/// - Right-click and double-click events are supported
77///
78/// See [`TableState`] for more details on cell selection.
79///
80/// # Example
81///
82/// ```rust,ignore
83/// let table_state = cx.new(|cx| {
84///     TableState::new(delegate, cx)
85///         .cell_selectable(true)
86///         .row_selectable(true)
87/// });
88///
89/// DataTable::new(&table_state)
90///     .stripe(true)
91///     .bordered(true)
92/// ```
93#[derive(IntoElement)]
94pub struct DataTable<D: TableDelegate> {
95    state: Entity<TableState<D>>,
96    options: TableOptions,
97}
98
99impl<D> DataTable<D>
100where
101    D: TableDelegate,
102{
103    /// Create a new DataTable element with the given [`TableState`].
104    pub fn new(state: &Entity<TableState<D>>) -> Self {
105        Self {
106            state: state.clone(),
107            options: TableOptions::default(),
108        }
109    }
110
111    /// Set to use stripe style of the table, default to false.
112    pub fn stripe(mut self, stripe: bool) -> Self {
113        self.options.stripe = stripe;
114        self
115    }
116
117    /// Set to use border style of the table, default to true.
118    pub fn bordered(mut self, bordered: bool) -> Self {
119        self.options.bordered = bordered;
120        self
121    }
122
123    /// Set the height of each header row.
124    ///
125    /// Column-group rows use the same height, so the whole header is this
126    /// height times the number of header rows. Body rows keep the height from
127    /// [`Size::table_row_height`]. When this is unset, header rows use that
128    /// same height.
129    pub fn header_row_height(mut self, height: impl Into<Pixels>) -> Self {
130        self.options.header_row_height = Some(height.into());
131        self
132    }
133
134    /// Set scrollbar visibility.
135    pub fn scrollbar_visible(mut self, vertical: bool, horizontal: bool) -> Self {
136        self.options.scrollbar_visible = Edges {
137            right: vertical,
138            bottom: horizontal,
139            ..Default::default()
140        };
141        self
142    }
143}
144
145impl<D> Sizable for DataTable<D>
146where
147    D: TableDelegate,
148{
149    fn with_size(mut self, size: impl Into<Size>) -> Self {
150        self.options.size = size.into();
151        self
152    }
153}
154
155impl<D> RenderOnce for DataTable<D>
156where
157    D: TableDelegate,
158{
159    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
160        let bordered = self.options.bordered;
161        let focus_handle = self.state.focus_handle(cx);
162        self.state.update(cx, |state, _| {
163            state.options = self.options;
164        });
165
166        div()
167            .id("table")
168            .test_support()
169            .size_full()
170            .key_context(CONTEXT)
171            .track_focus(&focus_handle)
172            .on_action(window.listener_for(&self.state, TableState::action_cancel))
173            .on_action(window.listener_for(&self.state, TableState::action_select_next))
174            .on_action(window.listener_for(&self.state, TableState::action_select_prev))
175            .on_action(window.listener_for(&self.state, TableState::action_select_next_col))
176            .on_action(window.listener_for(&self.state, TableState::action_select_prev_col))
177            .on_action(window.listener_for(&self.state, TableState::action_select_first_column))
178            .on_action(window.listener_for(&self.state, TableState::action_select_last_column))
179            .on_action(window.listener_for(&self.state, TableState::action_select_page_up))
180            .on_action(window.listener_for(&self.state, TableState::action_select_page_down))
181            .bg(cx.theme().tokens.table)
182            .when(bordered, |this| {
183                this.rounded(cx.theme().radius)
184                    .border_1()
185                    .border_color(cx.theme().border)
186            })
187            .child(self.state)
188            // Clicking a row focuses the table too, so only keyboard focus
189            // shows it, as CSS `:focus-visible` does.
190            .when(
191                focus_handle.is_focused(window) && window.last_input_was_keyboard(),
192                |this| this.focus_ring_style(window, cx),
193            )
194    }
195}