Skip to main content

gpui_kit_grid/state/
widgets.rs

1//! Elements in cells: what the delegate's `render_cell` knows about a cell
2//! of an `Element` or `Control` column, the host that places its element in
3//! the body, presses on it, and Space on a control.
4//!
5//! A host covers its cell for the mouse (`block_mouse_except_scroll`): the
6//! body does not see a press on it. The host reports the press itself, in
7//! two phases. In the capture phase it records the press ([`CellPress`]);
8//! in the bubble phase, unless the element stopped the press, the press is
9//! a press on the cell: selection, drag-selection, double click, menu and
10//! `Click`, as on any cell. After the press is dispatched, a press the
11//! element stopped moves only the grid's focus to the cell, and the
12//! keyboard focus goes back to the grid's root: the grid is one Tab stop.
13
14use std::{any::Any, cell::RefCell, rc::Rc, sync::Arc};
15
16use gpui_kit::component::Size as ComponentSize;
17use gpui_kit::{
18    App, Context, InteractiveElement, MouseButton, MouseDownEvent, ParentElement as _, Pixels,
19    Role, SharedString, Size, StatefulInteractiveElement as _, Styled, WeakEntity, Window, div,
20    prelude::FluentBuilder as _, px, size,
21};
22
23use super::{
24    CELL_PADDING, CellChange, ColumnAlign, LiveGridDelegate, LiveGridState,
25    a11y::column_index,
26    columns::CellContent,
27    edit::EditState,
28    editors::{CellCommit, CellEditHandle, GridCommit},
29    frame::{HostCell, RowPlanner},
30    selection::FocusedCell,
31};
32use crate::{
33    actions::ActivateCell,
34    element::{CellHost, HostLayer},
35    model::{CellValue, GridColumns as _, GridRow, display_text_in},
36    store::PinnedEdge,
37};
38
39/// What `ActivateCell` (Space on a focused `Control` cell) calls: the
40/// handler the cell's element registered in the last frame.
41pub(crate) type CellActivation = Rc<dyn Fn(&mut Window, &mut App)>;
42
43/// The handler of Space on the focused cell of a `Control` column, from
44/// the last frame, with the cell it belongs to: a key moved before the
45/// next frame does not press another cell's element.
46pub(super) struct ActiveCell<K, C> {
47    key: K,
48    column: C,
49    handler: CellActivation,
50}
51
52/// A press on a cell host, recorded in the capture phase before the
53/// element sees it.
54pub(super) struct CellPress<K, C> {
55    key: K,
56    column: C,
57    /// The band of a pinned row: a press there takes no focus.
58    pinned: Option<PinnedEdge>,
59    button: MouseButton,
60    /// The element did not stop the press: it was a press on the cell.
61    accepted: bool,
62}
63
64/// What a grid keeps for the elements in its cells: the press on a host
65/// being dispatched, and the handler `ActivateCell` calls.
66pub(super) struct WidgetSlot<K, C> {
67    press: Option<CellPress<K, C>>,
68    /// Set while the focused `Control` cell is planned; cleared when a
69    /// frame begins.
70    activation: RefCell<Option<ActiveCell<K, C>>>,
71    /// The grid as every `CellEditHandle` commits to it: made once, shared.
72    commit: RefCell<Option<Rc<dyn CellCommit>>>,
73}
74
75impl<K, C> Default for WidgetSlot<K, C> {
76    fn default() -> Self {
77        Self {
78            press: None,
79            activation: RefCell::new(None),
80            commit: RefCell::new(None),
81        }
82    }
83}
84
85impl<K, C> WidgetSlot<K, C> {
86    /// A frame begins: the last frame's handler of Space goes; the focused
87    /// control registers its own again.
88    pub(super) fn begin_frame(&self) {
89        self.activation.borrow_mut().take();
90    }
91
92    /// The grid as cell elements commit to it: made on first use.
93    fn commit<D: LiveGridDelegate>(
94        &self,
95        grid: &WeakEntity<LiveGridState<D>>,
96    ) -> Rc<dyn CellCommit> {
97        let mut commit = self.commit.borrow_mut();
98        Rc::clone(commit.get_or_insert_with(|| {
99            Rc::new(GridCommit { grid: grid.clone() }) as Rc<dyn CellCommit>
100        }))
101    }
102}
103
104/// What [`LiveGridDelegate::render_cell`] knows about a visible cell of an
105/// `Element` or `Control` column.
106pub struct CellElementContext<'a, R: GridRow> {
107    row: &'a Arc<R>,
108    column: R::Column,
109    value: CellValue<'a>,
110    text: &'a str,
111    change: Option<CellChange>,
112    size: ComponentSize,
113    cell_size: Size<Pixels>,
114    selected: bool,
115    focused: bool,
116    /// The cell's edits.
117    edit_state: Option<&'a EditState>,
118    /// Where `on_activate` keeps its handler: only for the focused cell of
119    /// a `Control` column.
120    activation: Option<&'a RefCell<Option<ActiveCell<R::Key, R::Column>>>>,
121    /// The grid, for `edit`: only in a column with an editor, off the
122    /// pinned rows.
123    commit: Option<Rc<dyn CellCommit>>,
124}
125
126impl<'a, R: GridRow> CellElementContext<'a, R> {
127    /// The row, as this frame shows it.
128    pub fn row(&self) -> &'a R {
129        self.row
130    }
131
132    /// The row, shared: for an element's handlers, which run after the
133    /// frame and outlive it.
134    pub fn shared_row(&self) -> &'a Arc<R> {
135        self.row
136    }
137
138    /// The row's key.
139    pub fn key(&self) -> &'a R::Key {
140        self.row.key()
141    }
142
143    /// The column.
144    pub fn column(&self) -> R::Column {
145        self.column
146    }
147
148    /// The cell's value: the store's, or the value a commit of the cell
149    /// still waits with.
150    pub fn value(&self) -> CellValue<'a> {
151        self.value
152    }
153
154    /// The text the body would paint for the value.
155    pub fn text(&self) -> &'a str {
156        self.text
157    }
158
159    /// The cell's highlighted change, if it is hot: for an element that
160    /// shows the change itself. On a removed row that fades out it is
161    /// [`RowChange::Removed`](crate::RowChange::Removed) at the fade's
162    /// intensity: the element is drawn until the fade ends, and takes no
163    /// press, Space or edit.
164    pub fn change(&self) -> Option<CellChange> {
165        self.change
166    }
167
168    /// The grid's size: gpui-kit elements take it through `Sizable` and fit
169    /// the row.
170    pub fn size(&self) -> ComponentSize {
171        self.size
172    }
173
174    /// The room for the element: the cell without its padding.
175    pub fn cell_size(&self) -> Size<Pixels> {
176        self.cell_size
177    }
178
179    /// Returns true if the cell is selected.
180    pub fn is_selected(&self) -> bool {
181        self.selected
182    }
183
184    /// Returns true if the cell has the grid's focus.
185    pub fn is_focused(&self) -> bool {
186        self.focused
187    }
188
189    /// The cell's edits: a commit that waits for an answer (`Pending`) or
190    /// for the store (`Accepted`) — [`Self::value`] is its value then — or a
191    /// refused one (`Failed`). `None` for a cell with no edits.
192    pub fn edit_state(&self) -> Option<&'a EditState> {
193        self.edit_state
194    }
195
196    /// A handle that commits values of this cell as edits, for the
197    /// element's handlers, such as a checkbox's `on_click` and
198    /// `on_activate`: `EditCommit`, then the value shown until the commit
199    /// settles. `None` if the column has no editor, or on a pinned row.
200    /// Whether the delegate's `is_editable` takes the cell is asked at the
201    /// commit, outside the frame.
202    pub fn edit(&self) -> Option<CellEditHandle> {
203        let commit = self.commit.as_ref()?;
204        let row: Arc<dyn Any> = Arc::clone(self.row) as Arc<dyn Any>;
205        Some(CellEditHandle::new(
206            Rc::clone(commit),
207            row,
208            self.column.ix(),
209        ))
210    }
211
212    /// Sets what Space does on the focused cell of a `Control` column
213    /// (`ActivateCell`): what a press on the element does, such as
214    /// toggling a checkbox. Kept for the focused cell only, until the next
215    /// frame; on any other cell it is dropped without being stored. It runs
216    /// after the key is handled, outside the grid's update, so it may
217    /// update the grid.
218    pub fn on_activate(&self, handler: impl Fn(&mut Window, &mut App) + 'static) {
219        if let Some(activation) = self.activation {
220            *activation.borrow_mut() = Some(ActiveCell {
221                key: self.row.key().clone(),
222                column: self.column,
223                handler: Rc::new(handler),
224            });
225        }
226    }
227}
228
229/// The component size of a grid with rows of `row_height`: what `LiveGrid`
230/// sizes its rows from (`Size::table_row_height`).
231fn component_size(row_height: Pixels) -> ComponentSize {
232    [
233        ComponentSize::XSmall,
234        ComponentSize::Small,
235        ComponentSize::Medium,
236        ComponentSize::Large,
237    ]
238    .into_iter()
239    .find(|size| size.table_row_height() == row_height)
240    .unwrap_or(ComponentSize::Size(row_height))
241}
242
243/// What a host tells a screen reader about its cell, while the tree is
244/// built: it is the cell's `GridCell` node.
245struct HostA11y {
246    /// The value's text for an `Element`; none for a `Control`, whose
247    /// element names itself and tells its state.
248    label: Option<SharedString>,
249    /// The column title, as on a synthetic cell.
250    description: SharedString,
251    column_index: usize,
252    /// Set when the selection is made of cells.
253    selected: Option<bool>,
254}
255
256impl<D: LiveGridDelegate> RowPlanner<'_, D> {
257    /// Plans the element of a cell of an `Element` or `Control` column
258    /// `render_cell`, and the host around its element.
259    /// Returns true if the cell is hosted: the body paints no text there;
260    /// false if the delegate gave no element.
261    ///
262    /// A removed row that fades out keeps its elements, so they fade with
263    /// its text (`CellElementContext::change` is its removal). They take no
264    /// press: a cover over the element passes the press to the body, which
265    /// finds no row there, as on the fading row's text cells.
266    pub(super) fn plan_cell_host(
267        &mut self,
268        cell: &HostCell<'_, D::Row>,
269        window: &mut Window,
270    ) -> bool {
271        let row = cell.row;
272        let live = row.is_live();
273        let bounds = cell.host_bounds();
274        let control = cell.content == CellContent::Control;
275        let widgets = self.widgets;
276        // Only the focused control keeps a handler of Space; a pinned row
277        // takes no focus.
278        let activation = (live && control && cell.focused && row.pinned.is_none())
279            .then_some(&widgets.activation);
280        let delegate = self.delegate;
281        self.host_text.clear();
282        let text = display_text_in(cell.value, cell.number_format, &mut self.host_text);
283        let context = CellElementContext {
284            row: row.row,
285            column: cell.column,
286            value: cell.value,
287            text,
288            change: cell.change,
289            size: component_size(self.row_height),
290            cell_size: size(
291                (bounds.size.width - CELL_PADDING * 2.).max(px(0.)),
292                bounds.size.height,
293            ),
294            selected: cell.selected,
295            focused: cell.focused,
296            edit_state: cell.edit_state,
297            activation,
298            commit: (live && self.edit.has_editor(cell.column.ix()) && row.pinned.is_none())
299                .then(|| widgets.commit(&self.grid)),
300        };
301        let Some(element) = delegate.render_cell(&context, window, self.app) else {
302            return false;
303        };
304        let a11y = self.a11y.as_ref().map(|input| HostA11y {
305            label: (!control).then(|| SharedString::from(text.to_owned())),
306            description: cell.title.clone(),
307            column_index: column_index(cell.view),
308            selected: input.cell_selected(cell.selected),
309        });
310        let column = cell.column;
311        if !live {
312            let grid = self.grid.clone();
313            self.hosts.push(CellHost {
314                layer: HostLayer::Widget,
315                column: column.ix(),
316                zone: cell.zone,
317                bounds,
318                element: CellHost::host_div(HostLayer::Widget, column.id())
319                    .relative()
320                    .flex()
321                    .items_center()
322                    .px(CELL_PADDING)
323                    .map(|this| align_host(this, cell.align))
324                    .child(element)
325                    .child(
326                        div()
327                            .absolute()
328                            .inset_0()
329                            .block_mouse_except_scroll()
330                            .on_any_mouse_down(move |event, window, cx| {
331                                let _ = grid.update(cx, |state, cx| {
332                                    state.on_body_mouse_down(event, window, cx);
333                                });
334                            }),
335                    ),
336            });
337            return true;
338        }
339        let press = HostPress {
340            grid: self.grid.clone(),
341            row: Arc::clone(row.row),
342            column,
343            pinned: row.pinned,
344        };
345        let element = CellHost::host_div(HostLayer::Widget, column.id())
346            .flex()
347            .items_center()
348            .px(CELL_PADDING)
349            .map(|this| align_host(this, cell.align))
350            .block_mouse_except_scroll()
351            .capture_any_mouse_down({
352                let press = press.clone();
353                move |event, window, cx| press.record(event, window, cx)
354            })
355            .on_any_mouse_down(move |event, window, cx| press.accept(event, window, cx))
356            .when_some(a11y, |this, a11y| {
357                this.role(Role::GridCell)
358                    .aria_column_index(a11y.column_index)
359                    .aria_description(a11y.description)
360                    .when_some(a11y.label, |this, label| this.aria_label(label))
361                    .when_some(a11y.selected, |this, selected| this.aria_selected(selected))
362            })
363            .child(element);
364        self.hosts.push(CellHost {
365            layer: HostLayer::Widget,
366            column: column.ix(),
367            zone: cell.zone,
368            bounds,
369            element,
370        });
371        true
372    }
373}
374
375/// Places a host's element by its column's alignment.
376fn align_host<E: Styled>(host: E, align: ColumnAlign) -> E {
377    match align {
378        ColumnAlign::Start => host.justify_start(),
379        ColumnAlign::End => host.justify_end(),
380        // A column of icons or checkboxes reads as a column.
381        ColumnAlign::Auto => host.justify_center(),
382    }
383}
384
385/// What a host's mouse listeners know: the grid, and the cell by key and
386/// column. Cloning it clones two handles.
387struct HostPress<D: LiveGridDelegate> {
388    grid: WeakEntity<LiveGridState<D>>,
389    row: Arc<D::Row>,
390    column: <D::Row as GridRow>::Column,
391    pinned: Option<PinnedEdge>,
392}
393
394impl<D: LiveGridDelegate> Clone for HostPress<D> {
395    fn clone(&self) -> Self {
396        Self {
397            grid: self.grid.clone(),
398            row: Arc::clone(&self.row),
399            column: self.column,
400            pinned: self.pinned,
401        }
402    }
403}
404
405impl<D: LiveGridDelegate> HostPress<D> {
406    /// The capture phase: records the press, and settles it once the press
407    /// is dispatched.
408    fn record(&self, event: &MouseDownEvent, window: &mut Window, cx: &mut App) {
409        let press = CellPress {
410            key: self.row.key().clone(),
411            column: self.column,
412            pinned: self.pinned,
413            button: event.button,
414            accepted: false,
415        };
416        if self
417            .grid
418            .update(cx, |state, _| state.widgets.press = Some(press))
419            .is_err()
420        {
421            return;
422        }
423        let grid = self.grid.clone();
424        window.defer(cx, move |window, cx| {
425            let _ = grid.update(cx, |state, cx| state.settle_cell_press(window, cx));
426        });
427    }
428
429    /// The bubble phase, reached when the element did not stop the press:
430    /// a press on the cell.
431    fn accept(&self, event: &MouseDownEvent, window: &mut Window, cx: &mut App) {
432        let _ = self.grid.update(cx, |state, cx| {
433            if let Some(press) = &mut state.widgets.press {
434                press.accepted = true;
435            }
436            state.on_body_mouse_down(event, window, cx);
437        });
438    }
439}
440
441impl<D: LiveGridDelegate> LiveGridState<D> {
442    /// Binds `ActivateCell`.
443    pub(super) fn bind_cell_actions<E: InteractiveElement>(root: E, cx: &Context<Self>) -> E {
444        root.on_action(cx.listener(Self::on_activate_cell))
445    }
446
447    /// Space on a focused cell of a `Control` column: the handler its
448    /// element registered in the last frame, after this update; without
449    /// one, the key goes on to `ToggleSelection`.
450    fn on_activate_cell(&mut self, _: &ActivateCell, window: &mut Window, cx: &mut Context<Self>) {
451        let handler = self.selection.focus().and_then(|focus| {
452            let key = focus.row()?;
453            let column = focus.column();
454            if self.columns.column(column.ix()).content != CellContent::Control {
455                return None;
456            }
457            let activation = self.widgets.activation.borrow();
458            activation
459                .as_ref()
460                .filter(|active| active.key == *key && active.column == column)
461                .map(|active| Rc::clone(&active.handler))
462        });
463        match handler {
464            Some(handler) => window.defer(cx, move |window, cx| handler(window, cx)),
465            None => cx.propagate(),
466        }
467    }
468
469    /// A press on a cell host has been dispatched. One the element stopped
470    /// moves only the focus to the cell of a data row (no selection, no
471    /// drag, no double click, no `Click`); its `FocusChange` comes on
472    /// release, at once for another button. Then the keyboard focus goes
473    /// back to the grid's root, wherever the element put it.
474    fn settle_cell_press(&mut self, window: &mut Window, cx: &mut Context<Self>) {
475        let Some(press) = self.widgets.press.take() else {
476            return;
477        };
478        if !press.accepted && press.pinned.is_none() {
479            self.focus_press(FocusedCell::new(press.key, press.column), cx);
480            if press.button != MouseButton::Left {
481                self.on_body_mouse_up(cx);
482            }
483        }
484        if !self.is_body_focused(window, cx) {
485            #[cfg(debug_assertions)]
486            self.warn_tab_stop_in_cell(window, cx);
487            window.focus(&self.focus_handle, cx);
488        }
489    }
490
491    /// Warns once, in a debug build, when an element in a cell took the
492    /// keyboard focus and is a Tab stop: Tab would stop on it.
493    #[cfg(debug_assertions)]
494    fn warn_tab_stop_in_cell(&self, window: &Window, cx: &App) {
495        use std::sync::atomic::{AtomicBool, Ordering};
496
497        static WARNED: AtomicBool = AtomicBool::new(false);
498        let in_cell = window.focused(cx).is_some_and(|focused| {
499            focused.tab_stop && self.focus_handle.contains(&focused, window)
500        });
501        if in_cell && !WARNED.swap(true, Ordering::Relaxed) {
502            log::warn!(
503                "an element in a grid cell took the keyboard focus as a Tab stop; \
504                 give it `tab_stop(false)`: the grid is one Tab stop"
505            );
506        }
507    }
508}