Skip to main content

gpui_kit_grid/state/editors/
mod.rs

1//! The built-in cell editors: a text field and a number field on gpui-kit's
2//! single-line `Input`, the size of a field that fits a row, and the raw
3//! text a field starts from; a list of values, a calendar, a checkbox with
4//! no field, and the handle that commits from an element in a cell.
5
6mod checkbox;
7mod date;
8mod number;
9mod select;
10mod text;
11
12use std::{any::Any, cell::RefCell, fmt::Write as _, rc::Rc, sync::Arc};
13
14use gpui_kit::component::Size as ComponentSize;
15use gpui_kit::component::input::{Input, InputState};
16use gpui_kit::{
17    AnyElement, App, AppContext as _, Entity, IntoElement as _, ParentElement as _, Pixels,
18    SharedString, Styled as _, WeakEntity, Window, div, prelude::FluentBuilder as _, px,
19};
20
21pub use self::{
22    checkbox::CheckboxEditor,
23    date::DateEditor,
24    number::{NumberEditor, NumberKind},
25    select::{SelectEditor, SelectOption},
26    text::TextEditor,
27};
28use super::{
29    ColumnOf, LiveGridDelegate, LiveGridState,
30    columns::column_of,
31    edit::{EditError, EditorCell, EditorField},
32};
33use crate::model::{CellValue, ColumnIx, GridRow, OwnedValue, push_timestamp};
34
35/// Commits values of one cell from a handler of its element, such as a
36/// checkbox's `on_click` or `on_activate`: the same path as an editor's
37/// commit with no field. From
38/// [`CellElementContext::edit`](super::CellElementContext::edit). Cheap to
39/// clone. Call it from an event handler, outside the grid's update, never
40/// from `render_cell`.
41#[derive(Clone)]
42pub struct CellEditHandle {
43    grid: Rc<dyn CellCommit>,
44    /// The row as the frame showed it: its key finds it in the store.
45    row: Arc<dyn Any>,
46    column: ColumnIx,
47}
48
49impl CellEditHandle {
50    pub(super) fn new(grid: Rc<dyn CellCommit>, row: Arc<dyn Any>, column: ColumnIx) -> Self {
51        Self { grid, row, column }
52    }
53
54    /// Commits `value` as an edit of the cell: `is_editable` must take the
55    /// cell now, an unchanged value closes as an editor's does
56    /// (`EditCancel { Unchanged }`), `validate_edit` must take the value;
57    /// then `EditCommit` goes out and the cell shows the value until the
58    /// commit settles. Returns true if it was committed; a refused cell or
59    /// value emits nothing.
60    pub fn commit(&self, value: OwnedValue, window: &mut Window, cx: &mut App) -> bool {
61        self.grid
62            .commit(self.row.as_ref(), self.column, value, window, cx)
63    }
64}
65
66impl std::fmt::Debug for CellEditHandle {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        f.debug_struct("CellEditHandle")
69            .field("column", &self.column)
70            .finish_non_exhaustive()
71    }
72}
73
74/// A grid that takes commits from its cells' elements, whatever its
75/// delegate: one per grid, shared by every handle.
76pub(super) trait CellCommit {
77    fn commit(
78        &self,
79        row: &dyn Any,
80        column: ColumnIx,
81        value: OwnedValue,
82        window: &mut Window,
83        cx: &mut App,
84    ) -> bool;
85}
86
87/// [`CellCommit`] of a grid entity.
88pub(super) struct GridCommit<D: LiveGridDelegate> {
89    pub(super) grid: WeakEntity<LiveGridState<D>>,
90}
91
92impl<D: LiveGridDelegate> CellCommit for GridCommit<D> {
93    fn commit(
94        &self,
95        row: &dyn Any,
96        column: ColumnIx,
97        value: OwnedValue,
98        window: &mut Window,
99        cx: &mut App,
100    ) -> bool {
101        let Some(row) = row.downcast_ref::<D::Row>() else {
102            return false;
103        };
104        let column: ColumnOf<D> = column_of(column);
105        self.grid
106            .update(cx, |grid, cx| {
107                grid.commit_from_element(row.key(), column, value, window, cx)
108            })
109            .unwrap_or(false)
110    }
111}
112
113/// The largest gpui-kit size whose field fits a row `row_height` high with
114/// 2 px of room above and below: `xsmall` rows (26 px) take an `XSmall`
115/// field (20 px), `small` and `medium` rows (30, 32) a `Small` one (24),
116/// `large` rows (40) a `Medium` one (32).
117pub(crate) fn editor_size(row_height: Pixels) -> ComponentSize {
118    let room = row_height - px(4.);
119    [
120        (ComponentSize::Medium, px(32.)),
121        (ComponentSize::Small, px(24.)),
122    ]
123    .into_iter()
124    .find(|(_, height)| *height <= room)
125    .map_or(ComponentSize::XSmall, |(size, _)| size)
126}
127
128/// Appends the exact text of `value` a field starts from: as a raw copy
129/// writes it, not rounded as the cell shows it, with `decimal_separator`
130/// between the whole and fractional digits. A float that is not finite
131/// starts empty; `Bool` as `TRUE` or `FALSE`.
132pub(super) fn push_raw_text(value: CellValue<'_>, decimal_separator: char, out: &mut String) {
133    let start = out.len();
134    match value {
135        CellValue::Empty => {}
136        CellValue::Bool(value) => out.push_str(if value { "TRUE" } else { "FALSE" }),
137        CellValue::Int(value) => {
138            let _ = write!(out, "{value}");
139        }
140        CellValue::Float(value) if value.is_finite() => {
141            let _ = write!(out, "{value}");
142        }
143        CellValue::Float(_) => {}
144        #[cfg(feature = "decimal")]
145        CellValue::Decimal(value) => {
146            let _ = write!(out, "{value}");
147        }
148        CellValue::Text(text) => out.push_str(text),
149        CellValue::Timestamp(value) => push_timestamp(value, out),
150        CellValue::Icon(icon) => out.push_str(icon.text()),
151        CellValue::Image(image) => out.push_str(image.text()),
152        CellValue::Link(link) => out.push_str(link.text()),
153        CellValue::Custom(value) => value.write_text(out),
154    }
155    if value.is_number()
156        && decimal_separator != '.'
157        && let Some(dot) = out[start..].find('.')
158    {
159        let dot = start + dot;
160        let mut separator = [0; 4];
161        out.replace_range(dot..dot + 1, decimal_separator.encode_utf8(&mut separator));
162    }
163}
164
165/// The text a built-in field starts from: the cell's exact text for a
166/// command, a press or a call; empty for `Clear`; the typed text for
167/// `Typing`.
168pub(super) fn initial_text<R: crate::model::GridRow>(
169    start: &super::edit::EditorStart<'_, R>,
170    decimal_separator: char,
171) -> SharedString {
172    match start.input() {
173        super::edit::EditInput::Clear => SharedString::default(),
174        super::edit::EditInput::Typing(text) => text.clone(),
175        _ => {
176            let mut text = String::new();
177            push_raw_text(start.value(), decimal_separator, &mut text);
178            text.into()
179        }
180    }
181}
182
183/// The draft of a built-in editor: its text parsed as a value of the
184/// column.
185type Parse = Box<dyn Fn(&str) -> Result<OwnedValue, EditError>>;
186
187/// The field of the built-in editors: gpui-kit's single-line `Input`
188/// without its own frame (the host draws the cell's), named after the
189/// column, its text right-aligned for numbers.
190///
191/// The field opens without laying its text out. An `InputState` made
192/// outside a frame (an editor opens from a key or a click) takes the
193/// window's default text style, GPUI's `.SystemUIFont`, until its element
194/// first draws in the cell; laying text out before then resolves that
195/// family, which GPUI's web platform may lack. So the text is the state's
196/// default value, laid out in the first frame in the cell's font.
197pub(super) struct InputField {
198    state: Entity<InputState>,
199    /// The text the field opened with, or the store gave it since: a draft
200    /// that differs is dirty.
201    initial: RefCell<SharedString>,
202    align_right: bool,
203    /// What a value given to the field writes between the whole and
204    /// fractional digits.
205    decimal_separator: char,
206    parse: Parse,
207}
208
209impl InputField {
210    /// A field over `state`, which holds `initial`, parsing its drafts with
211    /// `parse`.
212    pub(super) fn new(
213        state: Entity<InputState>,
214        initial: SharedString,
215        align_right: bool,
216        parse: Parse,
217    ) -> Self {
218        Self {
219            state,
220            initial: RefCell::new(initial),
221            align_right,
222            decimal_separator: '.',
223            parse,
224        }
225    }
226
227    /// Sets the separator a value given to the field is written with, as
228    /// its editor parses it (default `.`).
229    pub(super) fn with_decimal_separator(mut self, separator: char) -> Self {
230        self.decimal_separator = separator;
231        self
232    }
233
234    /// The state of a single-line input that holds `text` with the caret
235    /// at its end, configured by `configure` (a mask, a placeholder). The
236    /// text is its default value, laid out in its first frame.
237    pub(super) fn input_state(
238        text: &SharedString,
239        configure: impl FnOnce(InputState) -> InputState,
240        window: &mut Window,
241        cx: &mut App,
242    ) -> Entity<InputState> {
243        let state = cx.new(|cx| configure(InputState::new(window, cx)).default_value(text.clone()));
244        state.update(cx, |state, cx| {
245            let end = state.value().len();
246            state.set_selected_range(end..end, cx);
247        });
248        state
249    }
250}
251
252impl EditorField for InputField {
253    fn render(&self, cell: &EditorCell<'_>, _: &mut Window, _: &mut App) -> AnyElement {
254        use gpui_kit::component::Sizable as _;
255
256        div()
257            .flex_1()
258            .min_w_0()
259            .child(
260                Input::new(&self.state)
261                    .appearance(false)
262                    .with_size(cell.size())
263                    .aria_label(cell.title().clone())
264                    .when(self.align_right, |this| this.text_right()),
265            )
266            .into_any_element()
267    }
268
269    fn focus(&self, window: &mut Window, cx: &mut App) {
270        self.state.update(cx, |state, cx| state.focus(window, cx));
271    }
272
273    fn value(&self, cx: &App) -> Result<OwnedValue, EditError> {
274        (self.parse)(&self.state.read(cx).value())
275    }
276
277    fn set_value(&self, value: &OwnedValue, window: &mut Window, cx: &mut App) {
278        let mut text = String::new();
279        push_raw_text(value.as_cell(), self.decimal_separator, &mut text);
280        let text = SharedString::from(text);
281        self.state
282            .update(cx, |state, cx| state.set_value(text.clone(), window, cx));
283        // The store's value is the new starting point: untouched again.
284        *self.initial.borrow_mut() = text;
285    }
286
287    fn is_dirty(&self, cx: &App) -> bool {
288        self.state.read(cx).value() != *self.initial.borrow()
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn test_editor_size() {
298        for (row, size) in [
299            (26., ComponentSize::XSmall),
300            (30., ComponentSize::Small),
301            (32., ComponentSize::Small),
302            (40., ComponentSize::Medium),
303            (20., ComponentSize::XSmall),
304            (60., ComponentSize::Medium),
305        ] {
306            assert_eq!(editor_size(px(row)), size, "{row}");
307        }
308    }
309
310    #[test]
311    fn test_raw_text() {
312        let text = |value: CellValue, separator: char| {
313            let mut out = String::from("x");
314            push_raw_text(value, separator, &mut out);
315            out[1..].to_string()
316        };
317        // Exact, not as the cell rounds it.
318        assert_eq!(text(CellValue::Float(101.2534), '.'), "101.2534");
319        assert_eq!(text(CellValue::Float(101.2534), ','), "101,2534");
320        assert_eq!(text(CellValue::Float(f64::NAN), '.'), "");
321        assert_eq!(text(CellValue::Int(-12), ','), "-12");
322        assert_eq!(text(CellValue::Text("a.b"), ','), "a.b");
323        assert_eq!(text(CellValue::Empty, '.'), "");
324        let placed = chrono::DateTime::from_timestamp(0, 0).expect("the epoch");
325        assert_eq!(
326            text(CellValue::Timestamp(placed), '.'),
327            "1970-01-01 00:00:00"
328        );
329    }
330}