Skip to main content

gpui_component/table/
data_table.rs

1use crate::{
2    ActiveTheme, Sizable, Size,
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    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}
41
42impl Default for TableOptions {
43    fn default() -> Self {
44        Self {
45            scrollbar_visible: Edges::all(true),
46            stripe: false,
47            bordered: true,
48            size: Size::default(),
49        }
50    }
51}
52
53/// A table element with support for row, column, and cell selection.
54///
55/// # Features
56///
57/// - **Multiple Selection Modes**: Support for row, column, and cell selection
58/// - **Cell Selection**: Click to select individual cells, with keyboard navigation
59/// - **Virtual Scrolling**: Efficient rendering of large datasets
60/// - **Resizable Columns**: Drag column borders to resize
61/// - **Movable Columns**: Drag column headers to reorder
62/// - **Fixed Columns**: Pin columns to the left side
63/// - **Sortable Columns**: Click column headers to sort
64/// - **Context Menus**: Right-click support for rows and cells
65///
66/// # Cell Selection Mode
67///
68/// When cell selection is enabled via [`TableState::cell_selectable()`]:
69/// - Click on cells to select them
70/// - A row header column appears on the left for selecting entire rows
71///   (use [`TableState::row_header()`] to hide it)
72/// - Keyboard navigation (arrow keys, Tab, Home, End, PageUp, PageDown) works at cell level
73/// - Right-click and double-click events are supported
74///
75/// See [`TableState`] for more details on cell selection.
76///
77/// # Example
78///
79/// ```rust,ignore
80/// let table_state = cx.new(|cx| {
81///     TableState::new(delegate, cx)
82///         .cell_selectable(true)
83///         .row_selectable(true)
84/// });
85///
86/// DataTable::new(&table_state)
87///     .stripe(true)
88///     .bordered(true)
89/// ```
90#[derive(IntoElement)]
91pub struct DataTable<D: TableDelegate> {
92    state: Entity<TableState<D>>,
93    options: TableOptions,
94}
95
96impl<D> DataTable<D>
97where
98    D: TableDelegate,
99{
100    /// Create a new DataTable element with the given [`TableState`].
101    pub fn new(state: &Entity<TableState<D>>) -> Self {
102        Self {
103            state: state.clone(),
104            options: TableOptions::default(),
105        }
106    }
107
108    /// Set to use stripe style of the table, default to false.
109    pub fn stripe(mut self, stripe: bool) -> Self {
110        self.options.stripe = stripe;
111        self
112    }
113
114    /// Set to use border style of the table, default to true.
115    pub fn bordered(mut self, bordered: bool) -> Self {
116        self.options.bordered = bordered;
117        self
118    }
119
120    /// Set scrollbar visibility.
121    pub fn scrollbar_visible(mut self, vertical: bool, horizontal: bool) -> Self {
122        self.options.scrollbar_visible = Edges {
123            right: vertical,
124            bottom: horizontal,
125            ..Default::default()
126        };
127        self
128    }
129}
130
131impl<D> Sizable for DataTable<D>
132where
133    D: TableDelegate,
134{
135    fn with_size(mut self, size: impl Into<Size>) -> Self {
136        self.options.size = size.into();
137        self
138    }
139}
140
141impl<D> RenderOnce for DataTable<D>
142where
143    D: TableDelegate,
144{
145    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
146        let bordered = self.options.bordered;
147        let focus_handle = self.state.focus_handle(cx);
148        self.state.update(cx, |state, _| {
149            state.options = self.options;
150        });
151
152        div()
153            .id("table")
154            .test_support()
155            .size_full()
156            .key_context(CONTEXT)
157            .track_focus(&focus_handle)
158            .on_action(window.listener_for(&self.state, TableState::action_cancel))
159            .on_action(window.listener_for(&self.state, TableState::action_select_next))
160            .on_action(window.listener_for(&self.state, TableState::action_select_prev))
161            .on_action(window.listener_for(&self.state, TableState::action_select_next_col))
162            .on_action(window.listener_for(&self.state, TableState::action_select_prev_col))
163            .on_action(window.listener_for(&self.state, TableState::action_select_first_column))
164            .on_action(window.listener_for(&self.state, TableState::action_select_last_column))
165            .on_action(window.listener_for(&self.state, TableState::action_select_page_up))
166            .on_action(window.listener_for(&self.state, TableState::action_select_page_down))
167            .bg(cx.theme().tokens.table)
168            .when(bordered, |this| {
169                this.rounded(cx.theme().radius)
170                    .border_1()
171                    .border_color(cx.theme().border)
172            })
173            .child(self.state)
174    }
175}