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}