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) scroll_bounce: Option<bool>,
34 pub(super) scrollbar_visible: Edges<bool>,
35 pub(super) stripe: bool,
37 pub(super) bordered: bool,
39 pub(super) fixed_cols_border: bool,
41 pub(super) size: Size,
43 pub(super) header_row_height: Option<Pixels>,
45}
46
47impl Default for TableOptions {
48 fn default() -> Self {
49 Self {
50 scroll_bounce: None,
51 scrollbar_visible: Edges::all(true),
52 stripe: false,
53 bordered: true,
54 fixed_cols_border: true,
55 size: Size::default(),
56 header_row_height: None,
57 }
58 }
59}
60
61#[derive(IntoElement)]
99pub struct DataTable<D: TableDelegate> {
100 state: Entity<TableState<D>>,
101 options: TableOptions,
102 focus_ring_enabled: bool,
103}
104
105impl<D> DataTable<D>
106where
107 D: TableDelegate,
108{
109 pub fn new(state: &Entity<TableState<D>>) -> Self {
111 Self {
112 state: state.clone(),
113 options: TableOptions::default(),
114 focus_ring_enabled: true,
115 }
116 }
117
118 pub fn stripe(mut self, stripe: bool) -> Self {
120 self.options.stripe = stripe;
121 self
122 }
123
124 pub fn scroll_bounce(mut self, enabled: bool) -> Self {
128 self.options.scroll_bounce = Some(enabled);
129 self
130 }
131
132 pub fn bordered(mut self, bordered: bool) -> Self {
134 self.options.bordered = bordered;
135 self
136 }
137
138 pub fn header_row_height(mut self, height: impl Into<Pixels>) -> Self {
145 self.options.header_row_height = Some(height.into());
146 self
147 }
148
149 pub fn fixed_cols_border(mut self, fixed_cols_border: bool) -> Self {
155 self.options.fixed_cols_border = fixed_cols_border;
156 self
157 }
158
159 pub fn scrollbar_visible(mut self, vertical: bool, horizontal: bool) -> Self {
161 self.options.scrollbar_visible = Edges {
162 right: vertical,
163 bottom: horizontal,
164 ..Default::default()
165 };
166 self
167 }
168}
169
170impl<D> Sizable for DataTable<D>
171where
172 D: TableDelegate,
173{
174 fn with_size(mut self, size: impl Into<Size>) -> Self {
175 self.options.size = size.into();
176 self
177 }
178}
179
180impl<D> crate::FocusableExt for DataTable<D>
181where
182 D: TableDelegate,
183{
184 fn focus_ring(mut self, enabled: bool) -> Self {
185 self.focus_ring_enabled = enabled;
186 self
187 }
188
189 fn is_focus_ring_enabled(&self) -> bool {
190 self.focus_ring_enabled
191 }
192}
193
194impl<D> RenderOnce for DataTable<D>
195where
196 D: TableDelegate,
197{
198 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
199 let bordered = self.options.bordered;
200 let focus_ring_enabled = self.focus_ring_enabled;
201 let focus_handle = self.state.focus_handle(cx);
202 self.state.update(cx, |state, _| {
203 state.options = self.options;
204 });
205
206 div()
207 .id("table")
208 .test_support()
209 .size_full()
210 .key_context(CONTEXT)
211 .track_focus(&focus_handle)
212 .on_action(window.listener_for(&self.state, TableState::action_cancel))
213 .on_action(window.listener_for(&self.state, TableState::action_select_next))
214 .on_action(window.listener_for(&self.state, TableState::action_select_prev))
215 .on_action(window.listener_for(&self.state, TableState::action_select_next_col))
216 .on_action(window.listener_for(&self.state, TableState::action_select_prev_col))
217 .on_action(window.listener_for(&self.state, TableState::action_select_first_column))
218 .on_action(window.listener_for(&self.state, TableState::action_select_last_column))
219 .on_action(window.listener_for(&self.state, TableState::action_select_page_up))
220 .on_action(window.listener_for(&self.state, TableState::action_select_page_down))
221 .bg(cx.theme().tokens.table)
222 .when(bordered, |this| {
223 this.rounded(cx.theme().radius)
224 .border_1()
225 .border_color(cx.theme().border)
226 })
227 .child(self.state)
228 .when(
231 focus_ring_enabled
232 && focus_handle.is_focused(window)
233 && window.last_input_was_keyboard(),
234 |this| this.focus_ring_style(window, cx),
235 )
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use gpui::{
242 AppContext as _, Context, Entity, Render, TestAppContext, VisualTestContext, Window, div,
243 px,
244 };
245
246 use super::*;
247 use crate::{
248 FocusableExt as _,
249 table::{Column, TableState},
250 };
251
252 struct RowsDelegate;
253
254 impl TableDelegate for RowsDelegate {
255 fn columns_count(&self, _: &App) -> usize {
256 1
257 }
258 fn rows_count(&self, _: &App) -> usize {
259 3
260 }
261 fn column(&self, _: usize, _: &App) -> Column {
262 Column::new("name", "Name")
263 }
264 fn render_td(
265 &mut self,
266 row: usize,
267 _: usize,
268 _: &mut Window,
269 _: &mut Context<TableState<Self>>,
270 ) -> impl IntoElement {
271 div().child(row.to_string())
272 }
273 }
274
275 struct FocusRingHarness {
276 state: Entity<TableState<RowsDelegate>>,
277 focus_ring: bool,
278 }
279
280 impl Render for FocusRingHarness {
281 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
282 div()
283 .w(px(240.))
284 .h(px(160.))
285 .child(DataTable::new(&self.state).focus_ring(self.focus_ring))
286 }
287 }
288
289 fn keyboard_focused_table(cx: &mut TestAppContext, focus_ring: bool) -> &mut VisualTestContext {
292 cx.update(crate::init);
293 let (view, cx) = cx.add_window_view(move |window, cx| FocusRingHarness {
294 state: cx.new(|cx| TableState::new(RowsDelegate, window, cx)),
295 focus_ring,
296 });
297 let state = view.read_with(cx, |view, _| view.state.clone());
298 cx.update(|window, cx| {
299 let handle = state.focus_handle(cx);
300 window.focus(&handle, cx);
301 });
302 cx.simulate_keystrokes("down");
303 cx.update(|window, cx| window.draw(cx).clear(cx));
304 cx
305 }
306
307 #[gpui::test]
308 fn test_data_table_builder(cx: &mut TestAppContext) {
309 cx.update(crate::init);
310 let (view, cx) = cx.add_window_view(|window, cx| FocusRingHarness {
311 state: cx.new(|cx| TableState::new(RowsDelegate, window, cx)),
312 focus_ring: true,
313 });
314 let state = view.read_with(cx, |view, _| view.state.clone());
315 assert!(DataTable::new(&state).is_focus_ring_enabled());
316
317 let table = DataTable::new(&state)
318 .stripe(true)
319 .bordered(false)
320 .scroll_bounce(false)
321 .header_row_height(px(32.))
322 .fixed_cols_border(false)
323 .scrollbar_visible(false, true)
324 .with_size(Size::Small)
325 .focus_ring(false);
326 assert!(table.options.stripe);
327 assert!(!table.options.bordered);
328 assert_eq!(table.options.scroll_bounce, Some(false));
329 assert_eq!(table.options.header_row_height, Some(px(32.)));
330 assert!(!table.options.fixed_cols_border);
331 assert!(!table.options.scrollbar_visible.right);
332 assert!(table.options.scrollbar_visible.bottom);
333 assert_eq!(table.options.size, Size::Small);
334 assert!(!table.is_focus_ring_enabled());
335 assert!(table.focus_ring(true).is_focus_ring_enabled());
336 }
337
338 #[gpui::test]
339 fn data_table_draws_the_focus_ring_by_default(cx: &mut TestAppContext) {
340 let cx = keyboard_focused_table(cx, true);
341 assert!(
342 cx.debug_bounds("focus-ring").is_some(),
343 "a keyboard-focused table draws its focus ring"
344 );
345 }
346
347 #[gpui::test]
348 fn data_table_focus_ring_can_be_turned_off(cx: &mut TestAppContext) {
349 let cx = keyboard_focused_table(cx, false);
350 assert!(
351 cx.debug_bounds("focus-ring").is_none(),
352 "`focus_ring(false)` must not draw a ring"
353 );
354 }
355}