Skip to main content

gpui_kit_grid/state/
change.rs

1//! How a highlighted change paints a cell: the `ChangePaint` seam. The
2//! frame paints a hot cell's default style with it, then the delegate's
3//! `cell_style` adjusts the result.
4
5use std::{fmt, sync::Arc};
6
7use gpui_kit::{Context, Hsla, component::Theme};
8
9use super::{CellChange, CellStyle, LiveGridDelegate, LiveGridState};
10use crate::{
11    highlight::{ChangeDirection, HighlightPolicy, RowChange, RowStamps, Stamp},
12    model::ColumnIx,
13};
14
15/// The opacity of a change's background at full intensity: the highlight
16/// tints the row's own background instead of hiding it.
17const CHANGE_OPACITY: f32 = 0.45;
18
19/// The theme colours a [`ChangePainter`] works with, read once per frame.
20/// No colours of its own: semantic tokens of the theme.
21#[derive(Clone, Copy, Debug)]
22pub struct ChangePalette {
23    inserted: Hsla,
24    updated: Hsla,
25    up: Hsla,
26    down: Hsla,
27    removed: Hsla,
28    removed_foreground: Hsla,
29}
30
31impl ChangePalette {
32    /// The palette of a theme.
33    pub(super) fn new(theme: &Theme) -> Self {
34        Self {
35            inserted: theme.success,
36            updated: theme.warning,
37            up: theme.success,
38            down: theme.danger,
39            removed: theme.danger,
40            removed_foreground: theme.muted_foreground,
41        }
42    }
43
44    /// An inserted row: the theme's `success`.
45    pub fn inserted(&self) -> Hsla {
46        self.inserted
47    }
48
49    /// An updated cell: the theme's `warning`.
50    pub fn updated(&self) -> Hsla {
51        self.updated
52    }
53
54    /// A value that rose: the theme's `success`.
55    pub fn up(&self) -> Hsla {
56        self.up
57    }
58
59    /// A value that fell: the theme's `danger`.
60    pub fn down(&self) -> Hsla {
61        self.down
62    }
63
64    /// A removed row while it fades out: the theme's `danger`.
65    pub fn removed(&self) -> Hsla {
66        self.removed
67    }
68
69    /// The text of a removed row while it fades out: the theme's
70    /// `muted_foreground`.
71    pub fn removed_foreground(&self) -> Hsla {
72        self.removed_foreground
73    }
74}
75
76/// Paints a highlighted change into a cell's default style, before the
77/// delegate's `cell_style` adjusts it.
78///
79/// Called in prepaint for every hot visible cell, every frame: it must not
80/// allocate or lock. [`CellChange::column_ix`] tells which column the cell is
81/// in, so a painter can flash some columns and not others:
82///
83/// ```
84/// use gpui_kit_grid::{
85///     CellChange, CellStyle, ChangePainter, ChangePalette, ColumnIx, RowChange, UniformChanges,
86/// };
87///
88/// /// Stage 1's colours on the price only; inserts and removals whole.
89/// struct PriceOnly(ColumnIx);
90///
91/// impl ChangePainter for PriceOnly {
92///     fn paint(&self, change: &CellChange, palette: &ChangePalette, style: &mut CellStyle) {
93///         if change.change() != RowChange::Updated || change.column_ix() == Some(self.0) {
94///             UniformChanges.paint(change, palette, style);
95///         }
96///     }
97/// }
98/// ```
99pub trait ChangePainter: 'static {
100    /// Paints `change` into `style`.
101    fn paint(&self, change: &CellChange, palette: &ChangePalette, style: &mut CellStyle);
102}
103
104/// The background of a change: `color` at the change's opacity.
105fn change_background(change: &CellChange, color: Hsla) -> Hsla {
106    color.opacity(CHANGE_OPACITY * change.intensity())
107}
108
109/// The highlighted change a stamp shows at the global tick `tick`, while
110/// it is hot: a data cell's, an aggregate cell's or a caption's.
111#[inline]
112pub(super) fn stamp_change(
113    stamp: Stamp,
114    column: Option<ColumnIx>,
115    policy: &HighlightPolicy,
116    tick: u32,
117) -> Option<CellChange> {
118    policy.intensity(stamp, tick).map(|intensity| CellChange {
119        intensity,
120        change: stamp.change(),
121        direction: stamp.direction(),
122        column_ix: column,
123    })
124}
125
126/// Where the cells of a planned row take their highlighted changes from.
127#[derive(Clone, Copy, Debug)]
128pub(super) enum RowChanges<'a> {
129    /// A live row: its cells' stamps, if any cell is hot.
130    Stamps(Option<RowStamps<'a>>),
131    /// A removed row while it fades out: every cell shows its removal.
132    Removed(CellChange),
133}
134
135impl RowChanges<'_> {
136    /// The highlighted change of the cell of `column` at the global tick
137    /// `tick`, if it is hot.
138    #[inline]
139    pub(super) fn cell(
140        self,
141        column: ColumnIx,
142        policy: &HighlightPolicy,
143        tick: u32,
144    ) -> Option<CellChange> {
145        match self {
146            Self::Stamps(stamps) => stamps
147                .and_then(|stamps| stamp_change(stamps.get(column), Some(column), policy, tick)),
148            Self::Removed(change) => Some(CellChange {
149                column_ix: Some(column),
150                ..change
151            }),
152        }
153    }
154}
155
156/// Paints a change by its kind alone: an inserted row in `success`, an
157/// updated cell in `warning`, a removed row in `danger` with muted text, as
158/// a background whose opacity fades with the change.
159/// The text colour stays, so the text cache is not split by fade step. The
160/// default.
161#[derive(Clone, Copy, Debug, Default)]
162pub struct UniformChanges;
163
164impl ChangePainter for UniformChanges {
165    fn paint(&self, change: &CellChange, palette: &ChangePalette, style: &mut CellStyle) {
166        match change.change() {
167            RowChange::Inserted => {
168                style.set_background(change_background(change, palette.inserted()));
169            }
170            RowChange::Updated => {
171                style.set_background(change_background(change, palette.updated()));
172            }
173            RowChange::Removed => {
174                style.set_background(change_background(change, palette.removed()));
175                style.set_foreground(palette.removed_foreground());
176            }
177        }
178    }
179}
180
181/// Paints the direction of an updated number: a rise in `success`, a fall
182/// in `danger`, each with an arrow
183/// ([`CellStyle::set_trend`]); an update without a direction in `warning`
184/// without one. Inserted and removed rows as [`UniformChanges`].
185#[derive(Clone, Copy, Debug, Default)]
186pub struct DirectionalChanges;
187
188impl ChangePainter for DirectionalChanges {
189    fn paint(&self, change: &CellChange, palette: &ChangePalette, style: &mut CellStyle) {
190        match (change.change(), change.direction()) {
191            (RowChange::Updated, Some(direction)) => {
192                let color = match direction {
193                    ChangeDirection::Up => palette.up(),
194                    ChangeDirection::Down => palette.down(),
195                };
196                style.set_background(change_background(change, color));
197                style.set_trend(Some(direction));
198            }
199            _ => UniformChanges.paint(change, palette, style),
200        }
201    }
202}
203
204/// How a grid paints highlighted changes: the built-in painters,
205/// dispatched without a virtual call on the hot path, or the
206/// application's. The default is [`UniformChanges`].
207#[derive(Clone)]
208#[non_exhaustive]
209pub enum ChangePaint {
210    /// [`UniformChanges`]: one colour per kind of change.
211    Uniform(UniformChanges),
212    /// [`DirectionalChanges`]: rise and fall, with arrows.
213    Directional(DirectionalChanges),
214    /// The application's painter.
215    Custom(Arc<dyn ChangePainter>),
216}
217
218impl Default for ChangePaint {
219    fn default() -> Self {
220        Self::Uniform(UniformChanges)
221    }
222}
223
224impl fmt::Debug for ChangePaint {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        match self {
227            Self::Uniform(painter) => f.debug_tuple("Uniform").field(painter).finish(),
228            Self::Directional(painter) => f.debug_tuple("Directional").field(painter).finish(),
229            Self::Custom(_) => f.write_str("Custom(..)"),
230        }
231    }
232}
233
234impl ChangePainter for ChangePaint {
235    #[inline]
236    fn paint(&self, change: &CellChange, palette: &ChangePalette, style: &mut CellStyle) {
237        match self {
238            Self::Uniform(painter) => painter.paint(change, palette, style),
239            Self::Directional(painter) => painter.paint(change, palette, style),
240            Self::Custom(painter) => painter.paint(change, palette, style),
241        }
242    }
243}
244
245impl<D: LiveGridDelegate> LiveGridState<D> {
246    /// Sets how highlighted changes paint cells (default
247    /// [`ChangePaint::Uniform`]).
248    pub fn with_change_paint(mut self, paint: ChangePaint) -> Self {
249        self.change_paint = paint;
250        self
251    }
252
253    /// Replaces how highlighted changes paint cells, as a "Show direction"
254    /// switch does. The next frame paints with it; nothing is rebuilt.
255    pub fn set_change_paint(&mut self, paint: ChangePaint, cx: &mut Context<Self>) {
256        self.change_paint = paint;
257        cx.notify();
258    }
259
260    /// How highlighted changes paint cells.
261    pub fn change_paint(&self) -> &ChangePaint {
262        &self.change_paint
263    }
264}