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}