table.rs

  1use std::{ops::Range, time::Duration};
  2
  3use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide};
  4use gpui::{
  5    AppContext, Axis, Context, Entity, FocusHandle, FontWeight, Length, MouseButton, Task,
  6    UniformListScrollHandle, WeakEntity, uniform_list,
  7};
  8use settings::Settings as _;
  9use ui::{
 10    ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component,
 11    ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator,
 12    InteractiveElement as _, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce,
 13    Scrollbar, ScrollbarState, StatefulInteractiveElement as _, Styled, StyledTypography, Window,
 14    div, example_group_with_title, px, single_example, v_flex,
 15};
 16
 17struct UniformListData {
 18    render_item_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<AnyElement>>,
 19    element_id: ElementId,
 20    row_count: usize,
 21}
 22
 23enum TableContents<const COLS: usize> {
 24    Vec(Vec<[AnyElement; COLS]>),
 25    UniformList(UniformListData),
 26}
 27
 28impl<const COLS: usize> TableContents<COLS> {
 29    fn rows_mut(&mut self) -> Option<&mut Vec<[AnyElement; COLS]>> {
 30        match self {
 31            TableContents::Vec(rows) => Some(rows),
 32            TableContents::UniformList(_) => None,
 33        }
 34    }
 35
 36    fn len(&self) -> usize {
 37        match self {
 38            TableContents::Vec(rows) => rows.len(),
 39            TableContents::UniformList(data) => data.row_count,
 40        }
 41    }
 42}
 43
 44pub struct TableInteractionState {
 45    pub focus_handle: FocusHandle,
 46    pub scroll_handle: UniformListScrollHandle,
 47    pub horizontal_scrollbar: ScrollbarProperties,
 48    pub vertical_scrollbar: ScrollbarProperties,
 49}
 50
 51impl TableInteractionState {
 52    pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
 53        cx.new(|cx| {
 54            let focus_handle = cx.focus_handle();
 55
 56            cx.on_focus_out(&focus_handle, window, |this: &mut Self, _, window, cx| {
 57                this.hide_scrollbars(window, cx);
 58            })
 59            .detach();
 60
 61            let scroll_handle = UniformListScrollHandle::new();
 62            let vertical_scrollbar = ScrollbarProperties {
 63                axis: Axis::Vertical,
 64                state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
 65                show_scrollbar: false,
 66                show_track: false,
 67                auto_hide: false,
 68                hide_task: None,
 69            };
 70
 71            let horizontal_scrollbar = ScrollbarProperties {
 72                axis: Axis::Horizontal,
 73                state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
 74                show_scrollbar: false,
 75                show_track: false,
 76                auto_hide: false,
 77                hide_task: None,
 78            };
 79
 80            let mut this = Self {
 81                focus_handle,
 82                scroll_handle,
 83                horizontal_scrollbar,
 84                vertical_scrollbar,
 85            };
 86
 87            this.update_scrollbar_visibility(cx);
 88            this
 89        })
 90    }
 91
 92    fn update_scrollbar_visibility(&mut self, cx: &mut Context<Self>) {
 93        let show_setting = EditorSettings::get_global(cx).scrollbar.show;
 94
 95        let scroll_handle = self.scroll_handle.0.borrow();
 96
 97        let autohide = |show: ShowScrollbar, cx: &mut Context<Self>| match show {
 98            ShowScrollbar::Auto => true,
 99            ShowScrollbar::System => cx
100                .try_global::<ScrollbarAutoHide>()
101                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
102            ShowScrollbar::Always => false,
103            ShowScrollbar::Never => false,
104        };
105
106        let longest_item_width = scroll_handle.last_item_size.and_then(|size| {
107            (size.contents.width > size.item.width).then_some(size.contents.width)
108        });
109
110        // is there an item long enough that we should show a horizontal scrollbar?
111        let item_wider_than_container = if let Some(longest_item_width) = longest_item_width {
112            longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0)
113        } else {
114            true
115        };
116
117        let show_scrollbar = match show_setting {
118            ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true,
119            ShowScrollbar::Never => false,
120        };
121        let show_vertical = show_scrollbar;
122
123        let show_horizontal = item_wider_than_container && show_scrollbar;
124
125        let show_horizontal_track =
126            show_horizontal && matches!(show_setting, ShowScrollbar::Always);
127
128        // TODO: we probably should hide the scroll track when the list doesn't need to scroll
129        let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always);
130
131        self.vertical_scrollbar = ScrollbarProperties {
132            axis: self.vertical_scrollbar.axis,
133            state: self.vertical_scrollbar.state.clone(),
134            show_scrollbar: show_vertical,
135            show_track: show_vertical_track,
136            auto_hide: autohide(show_setting, cx),
137            hide_task: None,
138        };
139
140        self.horizontal_scrollbar = ScrollbarProperties {
141            axis: self.horizontal_scrollbar.axis,
142            state: self.horizontal_scrollbar.state.clone(),
143            show_scrollbar: show_horizontal,
144            show_track: show_horizontal_track,
145            auto_hide: autohide(show_setting, cx),
146            hide_task: None,
147        };
148
149        cx.notify();
150    }
151
152    fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
153        self.horizontal_scrollbar.hide(window, cx);
154        self.vertical_scrollbar.hide(window, cx);
155    }
156
157    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> impl IntoElement {
158        div()
159            .id("keymap-editor-vertical-scroll")
160            .occlude()
161            .flex_none()
162            .h_full()
163            .cursor_default()
164            .absolute()
165            .right_0()
166            .top_0()
167            .bottom_0()
168            .w(px(12.))
169            .on_mouse_move(cx.listener(|_, _, _, cx| {
170                cx.notify();
171                cx.stop_propagation()
172            }))
173            .on_hover(|_, _, cx| {
174                cx.stop_propagation();
175            })
176            .on_mouse_up(
177                MouseButton::Left,
178                cx.listener(|this, _, window, cx| {
179                    if !this.vertical_scrollbar.state.is_dragging()
180                        && !this.focus_handle.contains_focused(window, cx)
181                    {
182                        this.vertical_scrollbar.hide(window, cx);
183                        cx.notify();
184                    }
185
186                    cx.stop_propagation();
187                }),
188            )
189            .on_any_mouse_down(|_, _, cx| {
190                cx.stop_propagation();
191            })
192            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
193                cx.notify();
194            }))
195            .children(Scrollbar::vertical(self.vertical_scrollbar.state.clone()))
196    }
197
198    /// Renders the horizontal scrollbar.
199    ///
200    /// The right offset is used to determine how far to the right the
201    /// scrollbar should extend to, useful for ensuring it doesn't collide
202    /// with the vertical scrollbar when visible.
203    fn render_horizontal_scrollbar(
204        &self,
205        right_offset: Pixels,
206        cx: &mut Context<Self>,
207    ) -> impl IntoElement {
208        div()
209            .id("keymap-editor-horizontal-scroll")
210            .occlude()
211            .flex_none()
212            .w_full()
213            .cursor_default()
214            .absolute()
215            .bottom_neg_px()
216            .left_0()
217            .right_0()
218            .pr(right_offset)
219            .on_mouse_move(cx.listener(|_, _, _, cx| {
220                cx.notify();
221                cx.stop_propagation()
222            }))
223            .on_hover(|_, _, cx| {
224                cx.stop_propagation();
225            })
226            .on_any_mouse_down(|_, _, cx| {
227                cx.stop_propagation();
228            })
229            .on_mouse_up(
230                MouseButton::Left,
231                cx.listener(|this, _, window, cx| {
232                    if !this.horizontal_scrollbar.state.is_dragging()
233                        && !this.focus_handle.contains_focused(window, cx)
234                    {
235                        this.horizontal_scrollbar.hide(window, cx);
236                        cx.notify();
237                    }
238
239                    cx.stop_propagation();
240                }),
241            )
242            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
243                cx.notify();
244            }))
245            .children(Scrollbar::horizontal(
246                // percentage as f32..end_offset as f32,
247                self.horizontal_scrollbar.state.clone(),
248            ))
249    }
250}
251
252/// A table component
253#[derive(RegisterComponent, IntoElement)]
254pub struct Table<const COLS: usize = 3> {
255    striped: bool,
256    width: Length,
257    headers: Option<[AnyElement; COLS]>,
258    rows: TableContents<COLS>,
259    interaction_state: Option<WeakEntity<TableInteractionState>>,
260}
261
262impl<const COLS: usize> Table<COLS> {
263    pub fn uniform_list(
264        id: impl Into<ElementId>,
265        row_count: usize,
266        render_item_fn: impl Fn(Range<usize>, &mut Window, &mut App) -> Vec<AnyElement> + 'static,
267    ) -> Self {
268        Table {
269            striped: false,
270            width: Length::Auto,
271            headers: None,
272            rows: TableContents::UniformList(UniformListData {
273                element_id: id.into(),
274                row_count: row_count,
275                render_item_fn: Box::new(render_item_fn),
276            }),
277            interaction_state: None,
278        }
279    }
280
281    /// number of headers provided.
282    pub fn new() -> Self {
283        Table {
284            striped: false,
285            width: Length::Auto,
286            headers: None,
287            rows: TableContents::Vec(Vec::new()),
288            interaction_state: None,
289        }
290    }
291
292    /// Enables row striping.
293    pub fn striped(mut self) -> Self {
294        self.striped = true;
295        self
296    }
297
298    /// Sets the width of the table.
299    pub fn width(mut self, width: impl Into<Length>) -> Self {
300        self.width = width.into();
301        self
302    }
303
304    pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
305        self.interaction_state = Some(interaction_state.downgrade());
306        self
307    }
308
309    pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self {
310        self.headers = Some(headers.map(IntoElement::into_any_element));
311        self
312    }
313
314    pub fn row(mut self, items: [impl IntoElement; COLS]) -> Self {
315        if let Some(rows) = self.rows.rows_mut() {
316            rows.push(items.map(IntoElement::into_any_element));
317        }
318        self
319    }
320
321    pub fn render_row(&self, items: [impl IntoElement; COLS], cx: &mut App) -> AnyElement {
322        return render_row(0, items, self.rows.len(), self.striped, cx);
323    }
324
325    pub fn render_header(
326        &self,
327        headers: [impl IntoElement; COLS],
328        cx: &mut App,
329    ) -> impl IntoElement {
330        render_header(headers, cx)
331    }
332}
333
334fn base_cell_style(cx: &App) -> Div {
335    div()
336        .px_1p5()
337        .flex_1()
338        .justify_start()
339        .text_ui(cx)
340        .whitespace_nowrap()
341        .text_ellipsis()
342        .overflow_hidden()
343}
344
345pub fn render_row<const COLS: usize>(
346    row_index: usize,
347    items: [impl IntoElement; COLS],
348    row_count: usize,
349    striped: bool,
350    cx: &App,
351) -> AnyElement {
352    let is_last = row_index == row_count - 1;
353    let bg = if row_index % 2 == 1 && striped {
354        Some(cx.theme().colors().text.opacity(0.05))
355    } else {
356        None
357    };
358    div()
359        .w_full()
360        .flex()
361        .flex_row()
362        .items_center()
363        .justify_between()
364        .px_1p5()
365        .py_1()
366        .when_some(bg, |row, bg| row.bg(bg))
367        .when(!is_last, |row| {
368            row.border_b_1().border_color(cx.theme().colors().border)
369        })
370        .children(
371            items
372                .map(IntoElement::into_any_element)
373                .map(|cell| base_cell_style(cx).child(cell)),
374        )
375        .into_any_element()
376}
377
378pub fn render_header<const COLS: usize>(
379    headers: [impl IntoElement; COLS],
380    cx: &mut App,
381) -> impl IntoElement {
382    div()
383        .flex()
384        .flex_row()
385        .items_center()
386        .justify_between()
387        .w_full()
388        .p_2()
389        .border_b_1()
390        .border_color(cx.theme().colors().border)
391        .children(headers.into_iter().map(|h| {
392            base_cell_style(cx)
393                .font_weight(FontWeight::SEMIBOLD)
394                .child(h)
395        }))
396}
397
398impl<const COLS: usize> RenderOnce for Table<COLS> {
399    fn render(mut self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
400        // match self.ro
401        let row_count = self.rows.len();
402        let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
403        div()
404            .id("todo! how to have id")
405            .w(self.width)
406            .overflow_hidden()
407            .when_some(self.headers.take(), |this, headers| {
408                this.child(render_header(headers, cx))
409            })
410            .when_some(interaction_state, |this, interaction_state| {
411                this.track_focus(&interaction_state.read(cx).focus_handle)
412                    .on_hover({
413                        let interaction_state = interaction_state.downgrade();
414                        move |hovered, window, cx| {
415                            interaction_state
416                                .update(cx, |interaction_state, cx| {
417                                    if *hovered {
418                                        interaction_state.horizontal_scrollbar.show(cx);
419                                        interaction_state.vertical_scrollbar.show(cx);
420                                        cx.notify();
421                                    } else if !interaction_state
422                                        .focus_handle
423                                        .contains_focused(window, cx)
424                                    {
425                                        interaction_state.hide_scrollbars(window, cx);
426                                    }
427                                })
428                                .ok(); // todo! handle error?
429                        }
430                    })
431            })
432            .map(|div| match self.rows {
433                TableContents::Vec(items) => div.children(
434                    items
435                        .into_iter()
436                        .enumerate()
437                        .map(|(index, row)| render_row(index, row, row_count, self.striped, cx)),
438                ),
439                TableContents::UniformList(uniform_list_data) => div.child(uniform_list(
440                    uniform_list_data.element_id,
441                    uniform_list_data.row_count,
442                    uniform_list_data.render_item_fn,
443                )),
444            })
445    }
446}
447
448// computed state related to how to render scrollbars
449// one per axis
450// on render we just read this off the keymap editor
451// we update it when
452// - settings change
453// - on focus in, on focus out, on hover, etc.
454#[derive(Debug)]
455pub struct ScrollbarProperties {
456    axis: Axis,
457    show_scrollbar: bool,
458    show_track: bool,
459    auto_hide: bool,
460    hide_task: Option<Task<()>>,
461    state: ScrollbarState,
462}
463
464impl ScrollbarProperties {
465    // Shows the scrollbar and cancels any pending hide task
466    fn show(&mut self, cx: &mut Context<TableInteractionState>) {
467        if !self.auto_hide {
468            return;
469        }
470        self.show_scrollbar = true;
471        self.hide_task.take();
472        cx.notify();
473    }
474
475    fn hide(&mut self, window: &mut Window, cx: &mut Context<TableInteractionState>) {
476        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
477
478        if !self.auto_hide {
479            return;
480        }
481
482        let axis = self.axis;
483        self.hide_task = Some(cx.spawn_in(window, async move |keymap_editor, cx| {
484            cx.background_executor()
485                .timer(SCROLLBAR_SHOW_INTERVAL)
486                .await;
487
488            if let Some(keymap_editor) = keymap_editor.upgrade() {
489                keymap_editor
490                    .update(cx, |keymap_editor, cx| {
491                        match axis {
492                            Axis::Vertical => {
493                                keymap_editor.vertical_scrollbar.show_scrollbar = false
494                            }
495                            Axis::Horizontal => {
496                                keymap_editor.horizontal_scrollbar.show_scrollbar = false
497                            }
498                        }
499                        cx.notify();
500                    })
501                    .ok();
502            }
503        }));
504    }
505}
506
507impl Component for Table<3> {
508    fn scope() -> ComponentScope {
509        ComponentScope::Layout
510    }
511
512    fn description() -> Option<&'static str> {
513        Some("A table component for displaying data in rows and columns with optional styling.")
514    }
515
516    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
517        Some(
518            v_flex()
519                .gap_6()
520                .children(vec![
521                    example_group_with_title(
522                        "Basic Tables",
523                        vec![
524                            single_example(
525                                "Simple Table",
526                                Table::new()
527                                    .width(px(400.))
528                                    .header(["Name", "Age", "City"])
529                                    .row(["Alice", "28", "New York"])
530                                    .row(["Bob", "32", "San Francisco"])
531                                    .row(["Charlie", "25", "London"])
532                                    .into_any_element(),
533                            ),
534                            single_example(
535                                "Two Column Table",
536                                Table::new()
537                                    .header(["Category", "Value"])
538                                    .width(px(300.))
539                                    .row(["Revenue", "$100,000"])
540                                    .row(["Expenses", "$75,000"])
541                                    .row(["Profit", "$25,000"])
542                                    .into_any_element(),
543                            ),
544                        ],
545                    ),
546                    example_group_with_title(
547                        "Styled Tables",
548                        vec![
549                            single_example(
550                                "Default",
551                                Table::new()
552                                    .width(px(400.))
553                                    .header(["Product", "Price", "Stock"])
554                                    .row(["Laptop", "$999", "In Stock"])
555                                    .row(["Phone", "$599", "Low Stock"])
556                                    .row(["Tablet", "$399", "Out of Stock"])
557                                    .into_any_element(),
558                            ),
559                            single_example(
560                                "Striped",
561                                Table::new()
562                                    .width(px(400.))
563                                    .striped()
564                                    .header(["Product", "Price", "Stock"])
565                                    .row(["Laptop", "$999", "In Stock"])
566                                    .row(["Phone", "$599", "Low Stock"])
567                                    .row(["Tablet", "$399", "Out of Stock"])
568                                    .row(["Headphones", "$199", "In Stock"])
569                                    .into_any_element(),
570                            ),
571                        ],
572                    ),
573                    example_group_with_title(
574                        "Mixed Content Table",
575                        vec![single_example(
576                            "Table with Elements",
577                            Table::new()
578                                .width(px(840.))
579                                .header(["Status", "Name", "Priority", "Deadline", "Action"])
580                                .row([
581                                    Indicator::dot().color(Color::Success).into_any_element(),
582                                    "Project A".into_any_element(),
583                                    "High".into_any_element(),
584                                    "2023-12-31".into_any_element(),
585                                    Button::new("view_a", "View")
586                                        .style(ButtonStyle::Filled)
587                                        .full_width()
588                                        .into_any_element(),
589                                ])
590                                .row([
591                                    Indicator::dot().color(Color::Warning).into_any_element(),
592                                    "Project B".into_any_element(),
593                                    "Medium".into_any_element(),
594                                    "2024-03-15".into_any_element(),
595                                    Button::new("view_b", "View")
596                                        .style(ButtonStyle::Filled)
597                                        .full_width()
598                                        .into_any_element(),
599                                ])
600                                .row([
601                                    Indicator::dot().color(Color::Error).into_any_element(),
602                                    "Project C".into_any_element(),
603                                    "Low".into_any_element(),
604                                    "2024-06-30".into_any_element(),
605                                    Button::new("view_c", "View")
606                                        .style(ButtonStyle::Filled)
607                                        .full_width()
608                                        .into_any_element(),
609                                ])
610                                .into_any_element(),
611                        )],
612                    ),
613                ])
614                .into_any_element(),
615        )
616    }
617}