Skip to main content

gpui_kit_grid/
grid.rs

1//! `LiveGrid`: the element applications place in their layout, following
2//! gpui-kit's `DataTable` pattern (a `RenderOnce` wrapper over the state).
3
4use gpui_kit::{
5    App, Entity, IntoElement, ParentElement as _, RenderOnce, SharedString, Styled as _, Window,
6    component::{ActiveTheme as _, Sizable, Size, ThemeStyled as _},
7    div,
8    prelude::FluentBuilder as _,
9};
10
11use crate::state::{LiveGridDelegate, LiveGridState};
12
13/// A live grid element over a [`LiveGridState`].
14///
15/// ```ignore
16/// LiveGrid::new(&grid)
17///     .accessibility_label("Trades")
18///     .xsmall()
19///     .bordered(false)
20///     .group_panel(true)
21/// ```
22///
23/// Sizes follow gpui-kit tables: `xsmall` 26 px rows, `small` 30, `medium`
24/// (default) 32, `large` 40, or an explicit `Size::Size(px)`.
25///
26/// The cells take the font of the element the grid is placed in: its
27/// family, size and OpenType features, and so do the ghosts its drags show
28/// under the pointer (a header, a group level, rows), though GPUI draws them
29/// outside that element. The grid adds tabular figures (`tnum`) so digits
30/// line up, and keeps the rest, so a feature set on an ancestor reaches the
31/// cells:
32///
33/// ```ignore
34/// div()
35///     .font_family("IBM Plex Mono")
36///     // A slashed zero in every cell.
37///     .font_features(FontFeatures(Arc::new(vec![("zero".into(), 1)])))
38///     .child(LiveGrid::new(&grid))
39/// ```
40///
41/// Give every grid an accessible name with [`Self::accessibility_label`]:
42/// a screen reader announces the grid by it (Design Guides, "controls have
43/// accessible names").
44#[derive(IntoElement)]
45pub struct LiveGrid<D: LiveGridDelegate> {
46    state: Entity<LiveGridState<D>>,
47    size: Size,
48    bordered: bool,
49    accessibility_label: Option<SharedString>,
50    group_panel: bool,
51}
52
53impl<D: LiveGridDelegate> LiveGrid<D> {
54    /// A grid element for `state`.
55    pub fn new(state: &Entity<LiveGridState<D>>) -> Self {
56        Self {
57            state: state.clone(),
58            size: Size::default(),
59            bordered: true,
60            accessibility_label: None,
61            group_panel: false,
62        }
63    }
64
65    /// The accessible name of the grid, e.g. "Trades": a screen reader
66    /// names the table by it. No name by default.
67    pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
68        self.accessibility_label = Some(label.into());
69        self
70    }
71
72    /// Draws a border with the theme radius around the grid (default true).
73    pub fn bordered(mut self, bordered: bool) -> Self {
74        self.bordered = bordered;
75        self
76    }
77
78    /// Shows the group panel above the header (default false): the
79    /// grouping levels as chips, and a drop zone for column headers. The
80    /// user drags a header into it to group by the column, drags a chip to
81    /// reorder the levels or out onto the grid to remove one, clicks a chip
82    /// to reverse its groups, and removes it with its button. The panel is
83    /// one header row tall; the rows get that much less room.
84    ///
85    /// The application decides when it shows, as with [`Self::bordered`]:
86    /// always, only while the grid is grouped, or behind a toolbar toggle.
87    pub fn group_panel(mut self, shown: bool) -> Self {
88        self.group_panel = shown;
89        self
90    }
91}
92
93impl<D: LiveGridDelegate> Sizable for LiveGrid<D> {
94    fn with_size(mut self, size: impl Into<Size>) -> Self {
95        self.size = size.into();
96        self
97    }
98}
99
100impl<D: LiveGridDelegate> RenderOnce for LiveGrid<D> {
101    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
102        let row_height = self.size.table_row_height();
103        let label = self.accessibility_label;
104        let (size, group_panel) = (self.size, self.group_panel);
105        self.state.update(cx, |state, cx| {
106            state.set_row_height(row_height);
107            state.set_accessibility_label(label);
108            state.set_group_panel_shown(group_panel, size, cx);
109        });
110        // The rows have the focus on the root or in an open editor.
111        let is_body_focused = self.state.read(cx).is_body_focused(window, cx);
112        let theme = cx.theme();
113        // The grid view clips its own content; this wrapper does not clip,
114        // so the focus ring outside its border stays visible.
115        div()
116            .size_full()
117            .when(self.bordered, |this| {
118                this.border_1()
119                    .border_color(theme.border)
120                    .rounded(theme.radius)
121            })
122            // Keyboard focus only, as CSS `:focus-visible` (as gpui-kit's
123            // `DataTable`): a click focuses the grid without a ring.
124            .when(
125                is_body_focused && window.last_input_was_keyboard(),
126                |this| this.focus_ring_style(window, cx),
127            )
128            .child(self.state)
129    }
130}