Skip to main content

gpui_kit_grid/state/
style.rs

1//! Cell styling: the delegate hooks, what they are told about a cell, and
2//! the theme palette the body is planned with.
3
4use gpui_kit::component::menu::PopupMenu;
5use gpui_kit::{AnyElement, App, Context, FontWeight, Hsla, SharedString, Window};
6
7use super::{
8    drag::{DragPayload, RowDragContext},
9    edit::{EditError, EditMessageContext, EditState, EditValidateContext, EditableContext},
10    groups::{CaptionTag, GroupCaptionContext},
11    menu::GridMenuContext,
12    progress::ProgressContext,
13    tooltip::{CellTooltip, CellTooltipContext, HeaderTooltipContext},
14    widgets::CellElementContext,
15};
16use crate::{
17    engine::GroupInfo,
18    highlight::{ChangeDirection, RowChange},
19    model::{CellValue, ColumnIx, GridRow},
20    store::{GridProgress, PinnedEdge},
21};
22
23/// Style of one cell, filled by the grid's change paint
24/// ([`ChangePaint`](crate::ChangePaint)) and then by
25/// [`LiveGridDelegate::cell_style`]. Unset fields use the theme.
26#[derive(Clone, Copy, Debug, Default)]
27pub struct CellStyle {
28    pub(super) background: Option<Hsla>,
29    pub(super) foreground: Option<Hsla>,
30    pub(super) font_weight: Option<FontWeight>,
31    pub(super) trend: Option<ChangeDirection>,
32    pub(super) error_mark: bool,
33}
34
35impl CellStyle {
36    /// Sets the background.
37    pub fn set_background(&mut self, color: Hsla) {
38        self.background = Some(color);
39    }
40
41    /// Sets the text colour.
42    pub fn set_foreground(&mut self, color: Hsla) {
43        self.foreground = Some(color);
44    }
45
46    /// The background, if set.
47    pub fn background(&self) -> Option<Hsla> {
48        self.background
49    }
50
51    /// The text colour, if set.
52    pub fn foreground(&self) -> Option<Hsla> {
53        self.foreground
54    }
55
56    /// Sets the weight of the cell's text, such as `FontWeight::BOLD` for a
57    /// total. Each weight is cached apart, so a bold cell does not make
58    /// other cells shape their text again.
59    pub fn set_font_weight(&mut self, weight: FontWeight) {
60        self.font_weight = Some(weight);
61    }
62
63    /// The weight of the cell's text; `None` is the grid's font.
64    pub fn font_weight(&self) -> Option<FontWeight> {
65        self.font_weight
66    }
67
68    /// Shows an arrow for the direction of the last change, or none.
69    /// [`DirectionalChanges`](crate::DirectionalChanges) sets it for a hot
70    /// cell that rose or fell; a style may set or clear it. The arrow takes
71    /// a lane on the side opposite the text's alignment: the text does not
72    /// move, it only has less room.
73    pub fn set_trend(&mut self, trend: Option<ChangeDirection>) {
74        self.trend = trend;
75    }
76
77    /// The direction the cell shows an arrow for.
78    pub fn trend(&self) -> Option<ChangeDirection> {
79        self.trend
80    }
81
82    /// Shows or hides the error mark of a refused edit: an icon in the trend
83    /// arrow's lane, which it takes from the arrow. The grid sets it on a
84    /// cell whose commit failed ([`EditState::Failed`]); `false` hides it.
85    pub fn set_error_mark(&mut self, shown: bool) {
86        self.error_mark = shown;
87    }
88
89    /// Returns true if the error mark of a refused edit shows.
90    pub fn has_error_mark(&self) -> bool {
91        self.error_mark
92    }
93}
94
95/// A highlighted change of a cell: a data cell's, a pinned cell's, an
96/// aggregate cell's, or every cell of a removed row while it fades out
97/// ([`RowChange::Removed`], with
98/// [`HighlightPolicy::with_removal_fade`](crate::HighlightPolicy::with_removal_fade)).
99#[derive(Clone, Copy, Debug, PartialEq)]
100pub struct CellChange {
101    pub(super) intensity: f32,
102    pub(super) change: RowChange,
103    pub(super) direction: Option<ChangeDirection>,
104    pub(super) column_ix: Option<ColumnIx>,
105}
106
107impl CellChange {
108    /// From 1.0 (just changed) down to the last fade step; for a removed
109    /// row, down the steps of its removal fade.
110    pub fn intensity(&self) -> f32 {
111        self.intensity
112    }
113
114    /// What happened to the row. [`RowChange::Removed`] for every cell of a
115    /// removed row while it fades out: the row is no longer in the store,
116    /// the cell shows its last value.
117    pub fn change(&self) -> RowChange {
118        self.change
119    }
120
121    /// The direction of an update, as [`GridRow::change_direction`] gave it
122    /// when the store wrote the change, or as two numeric aggregates
123    /// compare; `None` for an insert, an update without a direction (a
124    /// text, `mark_changed`, a group's new row count) and a removed row.
125    pub fn direction(&self) -> Option<ChangeDirection> {
126        self.direction
127    }
128
129    /// The column of the cell: a data or pinned cell's, an aggregate
130    /// cell's. `None` for a caption's own change (a new group, a new row
131    /// count), which tints the caption lane across the columns. Compare it
132    /// with `MyColumn::Price.ix()` to paint some columns only.
133    pub fn column_ix(&self) -> Option<ColumnIx> {
134        self.column_ix
135    }
136
137    /// `color` faded by the current intensity.
138    pub fn fade(&self, color: Hsla) -> Hsla {
139        color.opacity(self.intensity)
140    }
141}
142
143/// What [`LiveGridDelegate::cell_style`] knows about a cell.
144pub struct CellStyleContext<'a, R: GridRow> {
145    pub(super) row: &'a R,
146    pub(super) column: R::Column,
147    pub(super) value: CellValue<'a>,
148    pub(super) change: Option<CellChange>,
149    pub(super) display_ix: usize,
150    pub(super) pinned: Option<PinnedEdge>,
151    pub(super) selected: bool,
152    pub(super) focused: bool,
153    pub(super) edit_state: Option<&'a EditState>,
154}
155
156impl<'a, R: GridRow> CellStyleContext<'a, R> {
157    /// The row.
158    pub fn row(&self) -> &'a R {
159        self.row
160    }
161
162    /// The column.
163    pub fn column(&self) -> R::Column {
164        self.column
165    }
166
167    /// The value shown in the cell.
168    pub fn value(&self) -> CellValue<'a> {
169        self.value
170    }
171
172    /// The highlighted change, if the cell changed recently.
173    pub fn change(&self) -> Option<CellChange> {
174        self.change
175    }
176
177    /// The display position of the row; for a pinned row, its position in
178    /// its band.
179    pub fn display_ix(&self) -> usize {
180        self.display_ix
181    }
182
183    /// The band of a pinned row, or `None` for a data row. Pinned rows are
184    /// painted with the theme's `table_foot` colors; a style can tell them
185    /// apart here, e.g. to set off a total row.
186    pub fn pinned_edge(&self) -> Option<PinnedEdge> {
187        self.pinned
188    }
189
190    /// Returns true if the cell is selected: in the row modes, if its row
191    /// is. Pinned rows are never selected.
192    pub fn is_selected(&self) -> bool {
193        self.selected
194    }
195
196    /// Returns true if this is the focused cell (the keyboard target),
197    /// whether or not the grid has keyboard focus.
198    pub fn is_focused(&self) -> bool {
199        self.focused
200    }
201
202    /// The cell's edits: a value whose commit waits for an answer
203    /// (`Pending`: the grid paints it muted) or for the store (`Accepted`),
204    /// or a refused commit (`Failed`: the grid shows the error mark). `None`
205    /// for a cell with no edits. The style may paint over the grid's look.
206    pub fn edit_state(&self) -> Option<&'a EditState> {
207        self.edit_state
208    }
209}
210
211/// The line of an aggregate cell, as [`LiveGridDelegate::aggregate_style`]
212/// sees it.
213#[derive(Clone, Copy, Debug)]
214#[non_exhaustive]
215pub enum AggregateLine<'a> {
216    /// A group caption.
217    Caption(GroupInfo<'a>),
218    /// A group footer.
219    Footer(GroupInfo<'a>),
220    /// The totals row in the band at its edge.
221    Totals(PinnedEdge),
222}
223
224/// What [`LiveGridDelegate::aggregate_style`] knows about a visible
225/// aggregate cell.
226pub struct AggregateStyleContext<'a, R: GridRow> {
227    pub(super) line: AggregateLine<'a>,
228    pub(super) column: R::Column,
229    pub(super) value: Option<CellValue<'a>>,
230    pub(super) change: Option<CellChange>,
231    pub(super) focused: bool,
232}
233
234impl<'a, R: GridRow> AggregateStyleContext<'a, R> {
235    /// The line the cell is on.
236    pub fn line(&self) -> AggregateLine<'a> {
237        self.line
238    }
239
240    /// The aggregated column.
241    pub fn column(&self) -> R::Column {
242        self.column
243    }
244
245    /// The aggregate's value, as the cell shows it.
246    pub fn value(&self) -> Option<CellValue<'a>> {
247        self.value
248    }
249
250    /// The highlighted change of the aggregate, if it changed recently:
251    /// [`RowChange::Updated`], with the direction of two numbers, against
252    /// the projection shown before (matched by the group's path). The grid's
253    /// change paint has painted it already, as for a data cell.
254    pub fn change(&self) -> Option<CellChange> {
255        self.change
256    }
257
258    /// Returns true if the cell's line has the focus: a focused caption.
259    pub fn is_focused(&self) -> bool {
260        self.focused
261    }
262}
263
264/// Presentation and command hooks of a grid.
265///
266/// Every hook takes `&self` and must not read, update or notify the grid
267/// entity: the grid is borrowed while it calls them, so that panics.
268/// Commands leave the grid as Actions and `LiveGridEvent`s instead.
269///
270/// | Hook | Runs | Rule |
271/// |---|---|---|
272/// | [`cell_style`](Self::cell_style) | In prepaint, per visible cell, every frame | No allocation, no lock |
273/// | [`group_caption`](Self::group_caption), [`group_caption_tag`](Self::group_caption_tag) | In prepaint, per visible group caption, every frame | No allocation (write into `out`), no lock |
274/// | [`aggregate_style`](Self::aggregate_style) | In prepaint, per visible aggregate cell, every frame | No allocation, no lock |
275/// | [`tooltip`](Self::tooltip) | In prepaint, for the hovered cell and, while a screen reader listens, the focused one: at most twice a frame | May allocate |
276/// | [`header_tooltip`](Self::header_tooltip) | In `render`, for the hovered header cell | May allocate |
277/// | [`primary_command`](Self::primary_command), [`context_menu`](Self::context_menu) | Once per menu opening, from `window.defer` after the press | May allocate |
278/// | [`render_progress`](Self::render_progress) | In `render`, while progress is shown | May allocate |
279/// | [`render_cell`](Self::render_cell) | In prepaint, per visible cell of an `Element` or `Control` column, every frame | May allocate: an element is one |
280/// | [`is_editable`](Self::is_editable) | Once per edit start, per commit with no field, and per `+`/`-` on a cell with an editor | Command hook, outside prepaint |
281/// | [`validate_edit`](Self::validate_edit) | Once per commit attempt | May allocate |
282/// | [`render_edit_message`](Self::render_edit_message) | In prepaint, once a frame while the open editor shows a message | May allocate |
283/// | [`drag_payload`](Self::drag_payload) | Once per row drag, when it starts | Command hook |
284pub trait LiveGridDelegate: Sized + 'static {
285    /// The row type.
286    type Row: GridRow;
287
288    /// Adjusts the style of a visible cell, after the grid's change paint.
289    /// A removed row that fades out is planned too, with
290    /// [`RowChange::Removed`] as every cell's change and its last version
291    /// as the row.
292    fn cell_style(
293        &self,
294        _cell: &CellStyleContext<'_, Self::Row>,
295        _style: &mut CellStyle,
296        _cx: &App,
297    ) {
298    }
299
300    /// Writes the text of a visible group caption into `out`, which is
301    /// empty on entry and reused between captions. Default:
302    /// [`GroupCaptionContext::default_caption`], "Instrument: AAPL (120)".
303    fn group_caption(
304        &self,
305        caption: &GroupCaptionContext<'_, Self::Row>,
306        out: &mut String,
307        _cx: &App,
308    ) {
309        caption.default_caption(out);
310    }
311
312    /// Writes the text of a tag shown before a visible group caption's
313    /// text into `out`, which is empty on entry and reused between
314    /// captions, and returns its colours: a pill such as a transit line's,
315    /// in the line's colour. `None`, or no text, shows no tag. A screen
316    /// reader hears the tag before the caption. In prepaint, per visible
317    /// caption, every frame: no allocation, no lock. A custom key kept as
318    /// itself ([`OwnedValue::custom`](crate::OwnedValue::custom)) gives
319    /// its data through [`GroupCaptionContext::value`]. Default: `None`.
320    fn group_caption_tag(
321        &self,
322        _caption: &GroupCaptionContext<'_, Self::Row>,
323        _out: &mut String,
324        _cx: &App,
325    ) -> Option<CaptionTag> {
326        None
327    }
328
329    /// Adjusts the style of a visible aggregate cell: on a group caption,
330    /// on a group footer and on the totals row. Called
331    /// after the grid's change paint, like [`Self::cell_style`].
332    fn aggregate_style(
333        &self,
334        _cell: &AggregateStyleContext<'_, Self::Row>,
335        _style: &mut CellStyle,
336        _cx: &App,
337    ) {
338    }
339
340    /// The label of the primary command: the first, bold item of a cell's
341    /// or a group caption's context menu, shown when the command scope is
342    /// not empty. It
343    /// dispatches `Confirm`, so the item, Enter and a double click all emit
344    /// one `LiveGridEvent::Confirm`. Default: no item (Enter and a double
345    /// click still confirm).
346    fn primary_command(
347        &self,
348        _menu: &GridMenuContext<'_, Self::Row>,
349        _cx: &App,
350    ) -> Option<SharedString> {
351        None
352    }
353
354    /// Builds the context menu below the primary command. Items do their
355    /// work later, through Actions dispatched to the grid (or its
356    /// ancestors) or `on_click` handlers; a handler reads the command scope
357    /// when it runs, not [`GridMenuContext::selection`]. Default:
358    /// [`GridMenuContext::standard_items`]. An empty menu is not shown.
359    fn context_menu(
360        &self,
361        menu_context: &GridMenuContext<'_, Self::Row>,
362        menu: PopupMenu,
363        window: &mut Window,
364        cx: &mut Context<PopupMenu>,
365    ) -> PopupMenu {
366        menu_context.standard_items(menu, window, cx)
367    }
368
369    /// The tooltip of the hovered body cell, asked again every frame so a
370    /// shown tooltip follows live values in place. Default:
371    /// [`CellTooltipContext::default_tooltip`], the full text of a
372    /// truncated cell.
373    ///
374    /// While the accessibility tree is built, it is also asked for the
375    /// focused cell (once a frame; once in all if that cell is hovered): a
376    /// [`CellTooltip::text`] describes the cell to a screen reader after its
377    /// column title, unless it only repeats the cell's text. Write tooltip
378    /// text that reads without the screen.
379    fn tooltip(&self, cell: &CellTooltipContext<'_, Self::Row>, _cx: &App) -> Option<CellTooltip> {
380        cell.default_tooltip()
381    }
382
383    /// The tooltip of the hovered header cell. Default:
384    /// [`HeaderTooltipContext::default_tooltip`], the full title of a
385    /// truncated one.
386    fn header_tooltip(
387        &self,
388        header: &HeaderTooltipContext<'_, <Self::Row as GridRow>::Column>,
389        _cx: &App,
390    ) -> Option<CellTooltip> {
391        header.default_tooltip()
392    }
393
394    /// Presents the shown progress ([`LiveGridState::progress`]) over the
395    /// body, below the header. The element fills the body and has no
396    /// hitbox of its own unless the delegate adds one: presses and the
397    /// wheel should reach the rows. Default:
398    /// [`ProgressContext::default_overlay`], a thin bar under the header and,
399    /// on an empty grid, the label.
400    ///
401    /// [`LiveGridState::progress`]: super::LiveGridState::progress
402    fn render_progress(
403        &self,
404        progress: &GridProgress,
405        overlay: &ProgressContext,
406        window: &mut Window,
407        cx: &mut App,
408    ) -> AnyElement {
409        overlay.default_overlay(progress, window, cx)
410    }
411
412    /// Builds the element of a visible cell of an
413    /// [`Element`](super::CellContent::Element) or
414    /// [`Control`](super::CellContent::Control) column; `None` paints the
415    /// value as text. In prepaint, per visible cell of such a column, every
416    /// frame: it may allocate (an element is an allocation), and must not
417    /// read, update or notify the grid entity.
418    /// The element's own handlers run later, outside the grid's borrow, and
419    /// may write to the store. An element that can take the keyboard focus
420    /// sets `tab_stop(false)`: the grid is one Tab stop. A removed row that
421    /// fades out is asked too, with
422    /// [`RowChange::Removed`] as the cell's change: fade the element by
423    /// [`CellChange::intensity`]; it takes no press. Default: `None`.
424    fn render_cell(
425        &self,
426        _cell: &CellElementContext<'_, Self::Row>,
427        _window: &mut Window,
428        _cx: &mut App,
429    ) -> Option<AnyElement> {
430        None
431    }
432
433    /// Returns true if a cell of a column with an editor takes an edit now:
434    /// a filled order's price does not. Asked once per edit start, per
435    /// commit with no field and per `+`/`-` on such a cell, outside
436    /// prepaint. Default: `true`.
437    fn is_editable(&self, _cell: &EditableContext<'_, Self::Row>, _cx: &App) -> bool {
438        true
439    }
440
441    /// Checks a value before its `EditCommit`: `Err` keeps the editor open
442    /// with the message. Once per commit attempt, outside prepaint; may
443    /// allocate. Default: every value the editor parsed.
444    fn validate_edit(
445        &self,
446        _edit: &EditValidateContext<'_, Self::Row>,
447        _cx: &App,
448    ) -> Result<(), EditError> {
449        Ok(())
450    }
451
452    /// Presents the open editor's message: a refused draft or a conflict
453    /// with the store. In prepaint, once a frame while a message is shown;
454    /// may allocate, as `tooltip`. `None` shows none (an application that
455    /// shows `LiveGridState::edit_message` itself). Default:
456    /// [`EditMessageContext::default_popup`], a line under the cell.
457    fn render_edit_message(
458        &self,
459        message: &EditMessageContext<'_>,
460        window: &mut Window,
461        cx: &mut App,
462    ) -> Option<AnyElement> {
463        Some(message.default_popup(window, cx))
464    }
465
466    /// The application's part of a row drag that is about to start: data
467    /// for the drop target and the ghost's label. `None` refuses the drag.
468    /// Called once per drag, when the pointer passes the drag threshold,
469    /// inside the grid's update: it must not read or notify the grid.
470    /// Default: an empty payload, so the keys alone are dragged.
471    fn drag_payload(
472        &self,
473        _drag: &RowDragContext<'_, Self::Row>,
474        _cx: &App,
475    ) -> Option<DragPayload> {
476        Some(DragPayload::new())
477    }
478}
479
480#[derive(Clone, Copy)]
481pub(super) struct Palette {
482    pub(super) foreground: Hsla,
483    pub(super) row_border: Hsla,
484    pub(super) stripe: Hsla,
485    pub(super) selected: Hsla,
486    pub(super) ring: Hsla,
487    pub(super) divider: Hsla,
488    /// The background and text of pinned rows.
489    pub(super) pinned: Hsla,
490    pub(super) pinned_foreground: Hsla,
491    /// The text of a value whose commit waits, and the error mark.
492    pub(super) muted: Hsla,
493    pub(super) danger: Hsla,
494    /// The text of a link value.
495    pub(super) link: Hsla,
496}