1mod 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#[derive(Clone)]
42pub struct CellEditHandle {
43 grid: Rc<dyn CellCommit>,
44 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 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
74pub(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
87pub(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
113pub(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
128pub(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
165pub(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
183type Parse = Box<dyn Fn(&str) -> Result<OwnedValue, EditError>>;
186
187pub(super) struct InputField {
198 state: Entity<InputState>,
199 initial: RefCell<SharedString>,
202 align_right: bool,
203 decimal_separator: char,
206 parse: Parse,
207}
208
209impl InputField {
210 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 pub(super) fn with_decimal_separator(mut self, separator: char) -> Self {
230 self.decimal_separator = separator;
231 self
232 }
233
234 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 *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 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}