1use std::{cell::Cell, rc::Rc};
16
17use gpui_kit::base::{Align, POPUP_PRIORITY, Placement, Positioner, actions::Cancel};
18use gpui_kit::component::{ActiveTheme as _, Icon, IconName, Size as ComponentSize};
19use gpui_kit::{
20 Anchor, AnyElement, AnyWindowHandle, App, Bounds, Context, Div, FocusHandle,
21 InteractiveElement, IntoElement, KeyDownEvent, Modifiers, MouseButton, MouseDownEvent,
22 MouseUpEvent, ParentElement as _, Pixels, Point, Role, SharedString, Stateful,
23 StatefulInteractiveElement as _, Styled as _, Subscription, Window, anchored, deferred, div,
24 point, prelude::FluentBuilder as _, px, size,
25};
26
27use rust_i18n::t;
28
29use super::{
30 ColumnOf, KeyOf, LiveGridDelegate, LiveGridEvent, LiveGridState, ViewColumnIx,
31 a11y::data_row_id_name,
32 columns::column_of,
33 editors::editor_size,
34 frame::{HostCell, RowPlanner},
35 key_hash,
36 selection::{Navigation, is_past_drag_threshold},
37};
38use crate::{
39 actions::{
40 CancelEdit, CommitEdit, CommitEditDown, CommitEditNext, CommitEditPrev, CommitEditUp,
41 EDITOR_KEY_CONTEXT, StartEdit, StartEditEmpty,
42 },
43 element::{CellHost, HostLayer, HostPlan},
44 model::{CellValue, ColumnIx, ColumnSet, GridColumns, GridRow, OwnedValue, display_text},
45 policy::{
46 AfterCommit, CellMove, DraftAction, EditCommitKey, EditEnd, EditGesture,
47 EditGestureContext, EditPolicy, EditPolicyContext, StandardEditGesture, StandardEditPolicy,
48 },
49 store::ChangeSet,
50};
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
55pub struct EditId(u64);
56
57impl EditId {
58 #[cfg_attr(not(test), expect(dead_code, reason = "the edit slot numbers commits"))]
59 pub(crate) const fn new(id: u64) -> Self {
60 Self(id)
61 }
62
63 pub const fn get(self) -> u64 {
65 self.0
66 }
67}
68
69#[derive(Clone, Debug, PartialEq, Eq)]
72pub struct EditError {
73 message: SharedString,
74}
75
76impl EditError {
77 pub fn new(message: impl Into<SharedString>) -> Self {
80 Self {
81 message: message.into(),
82 }
83 }
84
85 pub fn message(&self) -> &SharedString {
87 &self.message
88 }
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
93#[non_exhaustive]
94pub enum EditOrigin {
95 User,
97 Programmatic,
99}
100
101#[derive(Clone, Debug, PartialEq, Eq)]
103#[non_exhaustive]
104pub enum EditInput {
105 Command,
108 Clear,
110 Typing(SharedString),
112 Press {
114 click_count: usize,
116 },
117 Programmatic,
120}
121
122#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
124#[non_exhaustive]
125pub enum EditCancelReason {
126 Escape,
128 Blur,
130 Unchanged,
133 RowRemoved,
135 RowHidden,
138 Programmatic,
140}
141
142#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
144#[non_exhaustive]
145pub enum EditorMark {
146 #[default]
148 None,
149 Invalid,
152 Conflict,
154}
155
156#[derive(Clone, Debug, PartialEq, Eq)]
158#[non_exhaustive]
159pub enum EditState {
160 Pending,
162 Accepted,
164 Failed(EditError),
167}
168
169pub trait CellEditor<R: GridRow>: 'static {
172 fn open(&self, start: &EditorStart<'_, R>, window: &mut Window, cx: &mut App) -> EditorOpen;
174}
175
176#[non_exhaustive]
178pub enum EditorOpen {
179 Field(Box<dyn EditorField>),
181 Commit(OwnedValue),
183 Refuse,
186}
187
188pub trait EditorField: 'static {
190 fn render(&self, cell: &EditorCell<'_>, window: &mut Window, cx: &mut App) -> AnyElement;
193
194 fn focus(&self, window: &mut Window, cx: &mut App);
196
197 fn value(&self, cx: &App) -> Result<OwnedValue, EditError>;
200
201 fn set_value(&self, value: &OwnedValue, window: &mut Window, cx: &mut App);
203
204 fn is_dirty(&self, cx: &App) -> bool;
206}
207
208pub struct EditorStart<'a, R: GridRow> {
210 row: &'a R,
211 column: R::Column,
212 value: CellValue<'a>,
213 input: &'a EditInput,
214 size: ComponentSize,
215 title: &'a SharedString,
216}
217
218impl<'a, R: GridRow> EditorStart<'a, R> {
219 pub fn row(&self) -> &'a R {
221 self.row
222 }
223
224 pub fn key(&self) -> &'a R::Key {
226 self.row.key()
227 }
228
229 pub fn column(&self) -> R::Column {
231 self.column
232 }
233
234 pub fn value(&self) -> CellValue<'a> {
237 self.value
238 }
239
240 pub fn input(&self) -> &'a EditInput {
242 self.input
243 }
244
245 pub fn size(&self) -> ComponentSize {
247 self.size
248 }
249
250 pub fn title(&self) -> &'a SharedString {
252 self.title
253 }
254}
255
256pub struct EditorCell<'a> {
258 bounds: Bounds<Pixels>,
259 size: ComponentSize,
260 mark: EditorMark,
261 message: Option<&'a SharedString>,
262 title: &'a SharedString,
263 popup: &'a FieldPopup,
264}
265
266impl<'a> EditorCell<'a> {
267 pub fn bounds(&self) -> Bounds<Pixels> {
269 self.bounds
270 }
271
272 pub fn size(&self) -> ComponentSize {
274 self.size
275 }
276
277 pub fn mark(&self) -> EditorMark {
279 self.mark
280 }
281
282 pub fn message(&self) -> Option<&'a SharedString> {
284 self.message
285 }
286
287 pub fn title(&self) -> &'a SharedString {
289 self.title
290 }
291
292 pub fn popup(&self, content: impl IntoElement, cx: &App) -> AnyElement {
298 use gpui_kit::component::ThemeStyled as _;
299
300 self.popup.shown.set(true);
301 let bounds = self.popup.bounds.clone();
302 deferred(
303 Positioner::side(self.bounds)
304 .placement(Placement::Bottom)
305 .align(Align::Start)
306 .offset(px(2.))
307 .margin(px(8.))
308 .on_position(move |resolved| bounds.set(Some(resolved.bounds)))
309 .child(div().occlude().popover_style(cx).child(content)),
310 )
311 .with_priority(POPUP_PRIORITY)
312 .into_any_element()
313 }
314}
315
316#[derive(Default)]
319struct FieldPopup {
320 shown: Cell<bool>,
321 bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
322}
323
324pub struct EditableContext<'a, R: GridRow> {
326 row: &'a R,
327 column: R::Column,
328 value: CellValue<'a>,
329 input: &'a EditInput,
330}
331
332impl<'a, R: GridRow> EditableContext<'a, R> {
333 pub fn row(&self) -> &'a R {
335 self.row
336 }
337
338 pub fn key(&self) -> &'a R::Key {
340 self.row.key()
341 }
342
343 pub fn column(&self) -> R::Column {
345 self.column
346 }
347
348 pub fn value(&self) -> CellValue<'a> {
350 self.value
351 }
352
353 pub fn input(&self) -> &'a EditInput {
355 self.input
356 }
357}
358
359pub struct EditValidateContext<'a, R: GridRow> {
361 row: &'a R,
362 column: R::Column,
363 value: &'a OwnedValue,
364 base: &'a OwnedValue,
365 current: &'a OwnedValue,
366}
367
368impl<'a, R: GridRow> EditValidateContext<'a, R> {
369 pub fn row(&self) -> &'a R {
371 self.row
372 }
373
374 pub fn key(&self) -> &'a R::Key {
376 self.row.key()
377 }
378
379 pub fn column(&self) -> R::Column {
381 self.column
382 }
383
384 pub fn value(&self) -> &'a OwnedValue {
386 self.value
387 }
388
389 pub fn base(&self) -> &'a OwnedValue {
391 self.base
392 }
393
394 pub fn current(&self) -> &'a OwnedValue {
397 self.current
398 }
399}
400
401pub struct EditMessageContext<'a> {
404 mark: EditorMark,
405 message: &'a SharedString,
406 cell_bounds: Bounds<Pixels>,
407 size: ComponentSize,
408}
409
410impl<'a> EditMessageContext<'a> {
411 pub fn mark(&self) -> EditorMark {
413 self.mark
414 }
415
416 pub fn message(&self) -> &'a SharedString {
418 self.message
419 }
420
421 pub fn cell_bounds(&self) -> Bounds<Pixels> {
423 self.cell_bounds
424 }
425
426 pub fn size(&self) -> ComponentSize {
428 self.size
429 }
430
431 pub fn default_popup(&self, _window: &mut Window, cx: &mut App) -> AnyElement {
436 use gpui_kit::component::ThemeStyled as _;
437
438 let theme = cx.theme();
439 let (icon, color) = match self.mark {
440 EditorMark::Conflict => (IconName::TriangleAlert, theme.warning),
441 _ => (IconName::CircleAlert, theme.danger),
442 };
443 div()
444 .flex()
445 .items_center()
446 .gap(px(4.))
447 .px(px(6.))
448 .py(px(2.))
449 .text_xs()
450 .popover_style(cx)
451 .child(Icon::new(icon).size_3().text_color(color))
452 .child(self.message.clone())
453 .into_any_element()
454 }
455}
456
457struct EditSession<K, C> {
460 key: K,
461 column: C,
462 base: OwnedValue,
464 seen: OwnedValue,
467 input: EditInput,
469 field: Box<dyn EditorField>,
470 title: SharedString,
472 mark: EditorMark,
473 message: Option<SharedString>,
474 editor_focus: FocusHandle,
476 host_bounds: Cell<Option<Bounds<Pixels>>>,
479 popup_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
482 field_popup: FieldPopup,
484 reveal: Rc<dyn Fn(&mut App)>,
486 window: AnyWindowHandle,
489 _focus_out: Subscription,
490}
491
492pub(super) struct EditSlot<K, C> {
494 session: Option<EditSession<K, C>>,
495 next_edit: u64,
496 single_press: Option<(K, C, Point<Pixels>)>,
499 edited_columns: ColumnSet,
501}
502
503impl<K, C> Default for EditSlot<K, C> {
504 fn default() -> Self {
505 Self {
506 session: None,
507 next_edit: 0,
508 single_press: None,
509 edited_columns: ColumnSet::EMPTY,
510 }
511 }
512}
513
514impl<K: PartialEq, C: Copy> EditSlot<K, C> {
515 pub(super) fn has_editor(&self, column: ColumnIx) -> bool {
517 self.edited_columns.contains(column)
518 }
519
520 pub(super) fn is_open(&self) -> bool {
522 self.session.is_some()
523 }
524
525 pub(super) fn column_in(&self, key: &K) -> Option<C> {
527 self.session
528 .as_ref()
529 .and_then(|session| (session.key == *key).then_some(session.column))
530 }
531
532 pub(super) fn contains_focus(&self, window: &Window, cx: &App) -> bool {
534 self.session
535 .as_ref()
536 .is_some_and(|session| session.editor_focus.contains_focused(window, cx))
537 }
538
539 fn next_id(&mut self) -> EditId {
541 self.next_edit += 1;
542 EditId(self.next_edit)
543 }
544}
545
546#[derive(Clone, Copy, Debug, PartialEq, Eq)]
548enum CommitOutcome {
549 Committed,
551 Closed,
554 Refused,
556}
557
558pub(super) struct EditSetup<R: GridRow> {
560 editors: Vec<Option<Rc<dyn CellEditor<R>>>>,
562 gesture: Box<dyn EditGesture>,
563 policy: Box<dyn EditPolicy>,
564}
565
566impl<R: GridRow> Default for EditSetup<R> {
567 fn default() -> Self {
568 Self {
569 editors: Vec::new(),
570 gesture: Box::new(StandardEditGesture),
571 policy: Box::new(StandardEditPolicy),
572 }
573 }
574}
575
576impl<R: GridRow> EditSetup<R> {
577 fn editor(&self, column: ColumnIx) -> Option<&Rc<dyn CellEditor<R>>> {
578 self.editors.get(column.get())?.as_ref()
579 }
580
581 fn set_editor(&mut self, column: ColumnIx, editor: Option<Rc<dyn CellEditor<R>>>) {
582 if self.editors.len() <= column.get() {
583 self.editors.resize_with(column.get() + 1, || None);
584 }
585 self.editors[column.get()] = editor;
586 }
587
588 fn edited_columns(&self) -> ColumnSet {
590 let mut columns = ColumnSet::EMPTY;
591 for (ix, editor) in self.editors.iter().enumerate() {
592 if editor.is_some() {
593 columns.insert(ColumnIx::new(ix as u16));
594 }
595 }
596 columns
597 }
598
599 pub(super) fn gesture(&self) -> &dyn EditGesture {
601 &*self.gesture
602 }
603
604 pub(super) fn policy(&self) -> &dyn EditPolicy {
606 &*self.policy
607 }
608}
609
610impl<D: LiveGridDelegate> LiveGridState<D> {
611 pub fn with_editor(mut self, column: ColumnOf<D>, editor: impl CellEditor<D::Row>) -> Self {
614 self.edit_setup
615 .set_editor(column.ix(), Some(Rc::new(editor)));
616 self.edit.edited_columns = self.edit_setup.edited_columns();
617 self
618 }
619
620 pub fn set_editor(
622 &mut self,
623 column: ColumnOf<D>,
624 editor: Option<Rc<dyn CellEditor<D::Row>>>,
625 cx: &mut Context<Self>,
626 ) {
627 self.edit_setup.set_editor(column.ix(), editor);
628 self.edit.edited_columns = self.edit_setup.edited_columns();
629 cx.notify();
630 }
631
632 pub fn has_editor(&self, column: ColumnOf<D>) -> bool {
634 self.edit_setup.editor(column.ix()).is_some()
635 }
636
637 pub fn with_edit_gesture(mut self, gesture: impl EditGesture) -> Self {
640 self.edit_setup.gesture = Box::new(gesture);
641 self
642 }
643
644 pub fn with_edit_policy(mut self, policy: impl EditPolicy) -> Self {
647 self.edit_setup.policy = Box::new(policy);
648 self
649 }
650
651 pub fn start_edit(
657 &mut self,
658 key: &KeyOf<D>,
659 column: ColumnOf<D>,
660 window: &mut Window,
661 cx: &mut Context<Self>,
662 ) -> bool {
663 self.begin_edit(
664 key.clone(),
665 column,
666 EditInput::Programmatic,
667 EditOrigin::Programmatic,
668 Modifiers::default(),
669 window,
670 cx,
671 )
672 }
673
674 pub fn commit_edit(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
678 self.edit.is_open() && self.commit_field(Some(window), cx) != CommitOutcome::Refused
679 }
680
681 pub fn cancel_edit(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
684 self.cancel_field(EditCancelReason::Programmatic, Some(window), cx)
685 }
686
687 pub fn editing_cell(&self) -> Option<(&KeyOf<D>, ColumnOf<D>)> {
689 let session = self.edit.session.as_ref()?;
690 Some((&session.key, session.column))
691 }
692
693 pub fn is_editing(&self) -> bool {
695 self.edit.is_open()
696 }
697
698 pub fn edit_message(&self) -> Option<(EditorMark, &SharedString)> {
702 let session = self.edit.session.as_ref()?;
703 Some((session.mark, session.message.as_ref()?))
704 }
705
706 pub(crate) fn is_body_focused(&self, window: &Window, cx: &App) -> bool {
710 self.focus_handle.is_focused(window) || self.edit.contains_focus(window, cx)
711 }
712
713 fn focused_data_cell(&self) -> Option<(KeyOf<D>, ColumnOf<D>)> {
715 let focus = self.selection.focus()?;
716 Some((focus.row()?.clone(), focus.column()))
717 }
718
719 #[allow(clippy::too_many_arguments)]
726 fn begin_edit(
727 &mut self,
728 key: KeyOf<D>,
729 column: ColumnOf<D>,
730 input: EditInput,
731 origin: EditOrigin,
732 modifiers: Modifiers,
733 window: &mut Window,
734 cx: &mut Context<Self>,
735 ) -> bool {
736 let snapshot = self.snapshot.clone();
737 let Some(id) = snapshot.row_id(&key) else {
738 return false;
739 };
740 let Some(row) = snapshot.row(id).cloned() else {
741 return false;
742 };
743 if self.projection.display_ix(id).is_none()
744 || self.columns.view_column_ix(column.ix()).is_none()
745 {
746 return false;
747 }
748 let Some(editor) = self.edit_setup.editor(column.ix()).cloned() else {
749 return false;
750 };
751 let content = self.columns.column(column.ix()).content;
752 if matches!(input, EditInput::Typing(_) | EditInput::Press { .. })
753 && !self
754 .edit_setup
755 .gesture()
756 .starts_edit(&EditGestureContext::new(input.clone(), modifiers, content))
757 {
758 return false;
759 }
760 let shown = self.pending_edits.overlay(&key, column).cloned();
762 let value = shown
763 .as_ref()
764 .map_or_else(|| row.value(column), OwnedValue::as_cell);
765 let editable = self.delegate.is_editable(
766 &EditableContext {
767 row: &*row,
768 column,
769 value,
770 input: &input,
771 },
772 cx,
773 );
774 if !editable {
775 return false;
776 }
777 if self.edit.is_open() {
779 self.end_edit_on_blur(window, cx);
780 }
781 let base = OwnedValue::from_cell(value);
782 let title = self.columns.column(column.ix()).title.clone();
783 let opened = editor.open(
784 &EditorStart {
785 row: &*row,
786 column,
787 value,
788 input: &input,
789 size: editor_size(self.row_height),
790 title: &title,
791 },
792 window,
793 cx,
794 );
795 match opened {
796 EditorOpen::Refuse => false,
797 EditorOpen::Commit(value) => {
798 self.focus_edited_cell(&key, column, cx);
799 self.pending_edits.clear_failed(&key, column);
800 self.commit_value(key, column, value, base, &input, Some(window), cx);
801 true
802 }
803 EditorOpen::Field(field) => {
804 self.focus_edited_cell(&key, column, cx);
805 let editor_focus = cx.focus_handle();
806 let focus_out = cx.on_focus_out(&editor_focus, window, |this, _, window, cx| {
807 this.on_editor_focus_out(window, cx);
808 });
809 let reveal: Rc<dyn Fn(&mut App)> = {
810 let grid = cx.weak_entity();
811 Rc::new(move |cx: &mut App| {
812 let _ = grid.update(cx, |this, cx| {
813 if this.reveal_edited_cell() {
814 cx.notify();
815 }
816 });
817 })
818 };
819 field.focus(window, cx);
820 let (grid, refocus) = (cx.weak_entity(), editor_focus.clone());
821 window.defer(cx, move |window, cx| {
822 let _ = grid.update(cx, |this, cx| this.refocus_field(&refocus, window, cx));
823 });
824 self.pending_edits.clear_failed(&key, column);
825 self.edit.session = Some(EditSession {
826 key: key.clone(),
827 column,
828 seen: OwnedValue::from_cell(row.value(column)),
829 base,
830 input,
831 field,
832 title,
833 mark: EditorMark::None,
834 message: None,
835 editor_focus,
836 host_bounds: Cell::new(None),
837 popup_bounds: Rc::default(),
838 field_popup: FieldPopup::default(),
839 reveal,
840 window: window.window_handle(),
841 _focus_out: focus_out,
842 });
843 cx.emit(LiveGridEvent::EditStart {
844 key,
845 column,
846 origin,
847 });
848 cx.notify();
849 true
850 }
851 }
852 }
853
854 fn focus_edited_cell(&mut self, key: &KeyOf<D>, column: ColumnOf<D>, cx: &mut Context<Self>) {
857 if self.focused_data_cell().as_ref() != Some(&(key.clone(), column)) {
858 self.set_focused_cell(key, Some(column), cx);
859 }
860 self.reveal_focus();
861 }
862
863 fn reveal_edited_cell(&mut self) -> bool {
865 let Some(session) = &self.edit.session else {
866 return false;
867 };
868 let Some(display_ix) = self
869 .snapshot
870 .row_id(&session.key)
871 .and_then(|id| self.projection.display_ix(id))
872 else {
873 return false;
874 };
875 let column = session.column.ix();
876 let before = (self.scroll_row, self.scroll_offset, self.scroll_x);
877 self.reveal_row(display_ix);
878 let mut columns = ColumnSet::EMPTY;
879 columns.insert(column);
880 self.reveal_columns(columns);
881 self.clamp_scroll();
882 before != (self.scroll_row, self.scroll_offset, self.scroll_x)
883 }
884
885 fn store_value(&self, key: &KeyOf<D>, column: ColumnOf<D>) -> Option<OwnedValue> {
887 let row = self.snapshot.row(self.snapshot.row_id(key)?)?;
888 Some(OwnedValue::from_cell(row.value(column)))
889 }
890
891 fn close_field(
896 &mut self,
897 window: Option<&mut Window>,
898 cx: &mut Context<Self>,
899 ) -> Option<EditSession<KeyOf<D>, ColumnOf<D>>> {
900 let session = self.edit.session.take()?;
901 match window {
902 Some(window) => {
903 if session.editor_focus.contains_focused(window, cx) {
904 window.focus(&self.focus_handle, cx);
905 }
906 }
907 None => {
908 let (root, window) = (self.focus_handle.clone(), session.window);
909 cx.defer(move |cx| {
910 let _ = window.update(cx, |_, window, cx| {
911 if window.focused(cx).is_none() || root.contains_focused(window, cx) {
912 window.focus(&root, cx);
913 }
914 });
915 });
916 }
917 }
918 let grid = cx.weak_entity();
920 cx.defer(move |cx| {
921 let _ = grid.update(cx, |this, cx| this.reconsider_held(cx));
922 });
923 cx.notify();
924 Some(session)
925 }
926
927 #[allow(clippy::too_many_arguments)]
933 fn commit_value(
934 &mut self,
935 key: KeyOf<D>,
936 column: ColumnOf<D>,
937 value: OwnedValue,
938 base: OwnedValue,
939 input: &EditInput,
940 window: Option<&mut Window>,
941 cx: &mut Context<Self>,
942 ) -> CommitOutcome {
943 let close = |this: &mut Self,
944 reason: EditCancelReason,
945 draft: Option<OwnedValue>,
946 window: Option<&mut Window>,
947 cx: &mut Context<Self>| {
948 this.close_field(window, cx);
949 cx.emit(LiveGridEvent::EditCancel {
950 key: key.clone(),
951 column,
952 reason,
953 draft,
954 });
955 CommitOutcome::Closed
956 };
957 if value.cmp_total(&base).is_eq() && !self.edit_setup.policy().commit_unchanged() {
958 return close(self, EditCancelReason::Unchanged, None, window, cx);
959 }
960 let snapshot = self.snapshot.clone();
961 let Some(row) = snapshot.row_id(&key).and_then(|id| snapshot.row(id)) else {
962 return close(self, EditCancelReason::RowRemoved, Some(value), window, cx);
963 };
964 let current = OwnedValue::from_cell(row.value(column));
965 let editable = self.delegate.is_editable(
966 &EditableContext {
967 row: &**row,
968 column,
969 value: current.as_cell(),
970 input,
971 },
972 cx,
973 );
974 if !editable {
975 return close(
976 self,
977 EditCancelReason::Programmatic,
978 Some(value),
979 window,
980 cx,
981 );
982 }
983 let valid = self.delegate.validate_edit(
984 &EditValidateContext {
985 row: &**row,
986 column,
987 value: &value,
988 base: &base,
989 current: ¤t,
990 },
991 cx,
992 );
993 if let Err(error) = valid {
994 if let Some(session) = &mut self.edit.session {
995 session.mark = EditorMark::Invalid;
996 session.message = Some(error.message().clone());
997 cx.notify();
998 }
999 return CommitOutcome::Refused;
1000 }
1001 let edit = self.edit.next_id();
1002 self.close_field(window, cx);
1003 cx.emit(LiveGridEvent::EditCommit {
1004 edit,
1005 key: key.clone(),
1006 column,
1007 value: value.clone(),
1008 base,
1009 current,
1010 });
1011 self.begin_pending_edit(edit, key, column, value, cx);
1013 CommitOutcome::Committed
1014 }
1015
1016 pub(super) fn commit_from_element(
1022 &mut self,
1023 key: &KeyOf<D>,
1024 column: ColumnOf<D>,
1025 value: OwnedValue,
1026 window: &mut Window,
1027 cx: &mut Context<Self>,
1028 ) -> bool {
1029 if self.edit_setup.editor(column.ix()).is_none() {
1030 return false;
1031 }
1032 let snapshot = self.snapshot.clone();
1033 let Some(row) = snapshot.get(key) else {
1034 return false;
1035 };
1036 let input = EditInput::Programmatic;
1037 let shown = self.pending_edits.overlay(key, column).cloned();
1038 let now = shown
1039 .as_ref()
1040 .map_or_else(|| row.value(column), OwnedValue::as_cell);
1041 let editable = self.delegate.is_editable(
1042 &EditableContext {
1043 row: &**row,
1044 column,
1045 value: now,
1046 input: &input,
1047 },
1048 cx,
1049 );
1050 if !editable {
1051 return false;
1052 }
1053 if self.edit.is_open() {
1054 self.end_edit_on_blur(window, cx);
1055 }
1056 let base = OwnedValue::from_cell(now);
1057 self.pending_edits.clear_failed(key, column);
1058 self.commit_value(key.clone(), column, value, base, &input, Some(window), cx)
1059 == CommitOutcome::Committed
1060 }
1061
1062 fn commit_field(
1065 &mut self,
1066 window: Option<&mut Window>,
1067 cx: &mut Context<Self>,
1068 ) -> CommitOutcome {
1069 let Some(session) = &mut self.edit.session else {
1070 return CommitOutcome::Closed;
1071 };
1072 let draft = session.field.value(cx);
1073 let value = match draft {
1074 Ok(value) => value,
1075 Err(error) => {
1076 session.mark = EditorMark::Invalid;
1077 session.message = Some(error.message().clone());
1078 cx.notify();
1079 return CommitOutcome::Refused;
1080 }
1081 };
1082 let (key, column, base, input) = (
1083 session.key.clone(),
1084 session.column,
1085 session.base.clone(),
1086 session.input.clone(),
1087 );
1088 self.commit_value(key, column, value, base, &input, window, cx)
1089 }
1090
1091 fn cancel_field(
1094 &mut self,
1095 reason: EditCancelReason,
1096 window: Option<&mut Window>,
1097 cx: &mut Context<Self>,
1098 ) -> bool {
1099 let Some(session) = &self.edit.session else {
1100 return false;
1101 };
1102 let draft = session
1103 .field
1104 .value(cx)
1105 .ok()
1106 .filter(|draft| !draft.cmp_total(&session.base).is_eq());
1107 let Some(session) = self.close_field(window, cx) else {
1108 return false;
1109 };
1110 cx.emit(LiveGridEvent::EditCancel {
1111 key: session.key,
1112 column: session.column,
1113 reason,
1114 draft,
1115 });
1116 true
1117 }
1118
1119 fn end_edit_on_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1123 let Some(session) = &self.edit.session else {
1124 return;
1125 };
1126 let draft = session.field.value(cx).ok();
1127 let current = self
1128 .store_value(&session.key, session.column)
1129 .unwrap_or_else(|| session.base.clone());
1130 let end = self.edit_setup.policy().on_blur(&EditPolicyContext::new(
1131 &session.base,
1132 ¤t,
1133 draft.as_ref(),
1134 session.field.is_dirty(cx),
1135 ));
1136 if end == EditEnd::Commit
1137 && self.commit_field(Some(&mut *window), cx) != CommitOutcome::Refused
1138 {
1139 return;
1140 }
1141 self.cancel_field(EditCancelReason::Blur, Some(window), cx);
1142 }
1143
1144 fn on_editor_focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1148 if window.is_window_active() && self.edit.is_open() && !self.edit.contains_focus(window, cx)
1149 {
1150 self.end_edit_on_blur(window, cx);
1151 }
1152 }
1153
1154 fn refocus_field(&mut self, editor_focus: &FocusHandle, window: &mut Window, cx: &mut App) {
1158 if let Some(session) = &self.edit.session
1159 && session.editor_focus == *editor_focus
1160 && !session.editor_focus.contains_focused(window, cx)
1161 {
1162 session.field.focus(window, cx);
1163 }
1164 }
1165
1166 fn commit_and_move(&mut self, key: EditCommitKey, window: &mut Window, cx: &mut Context<Self>) {
1169 if !self.edit.is_open()
1170 || self.commit_field(Some(&mut *window), cx) == CommitOutcome::Refused
1171 {
1172 return;
1173 }
1174 match self.edit_setup.gesture().after_commit(key) {
1175 AfterCommit::Move(to) => {
1176 self.move_focus(to, cx);
1177 }
1178 AfterCommit::MoveAndEdit(to) => {
1179 if self.move_focus(to, cx)
1180 && let Some((key, column)) = self.focused_data_cell()
1181 {
1182 self.begin_edit(
1183 key,
1184 column,
1185 EditInput::Command,
1186 EditOrigin::User,
1187 Modifiers::default(),
1188 window,
1189 cx,
1190 );
1191 }
1192 }
1193 _ => {}
1194 }
1195 }
1196
1197 fn move_focus(&mut self, to: CellMove, cx: &mut Context<Self>) -> bool {
1201 let before = self.focused_data_cell();
1202 let navigation = match to {
1203 CellMove::Up => Navigation::Up,
1204 CellMove::Down => Navigation::Down,
1205 CellMove::Left => Navigation::PrevColumn,
1206 CellMove::Right => Navigation::NextColumn,
1207 CellMove::NextEditable | CellMove::PrevEditable => {
1208 return self.focus_editable_neighbour(to == CellMove::NextEditable, cx);
1209 }
1210 };
1211 self.navigate(navigation, false, cx);
1212 self.focused_data_cell() != before
1213 }
1214
1215 fn focus_editable_neighbour(&mut self, forward: bool, cx: &mut Context<Self>) -> bool {
1218 let Some((key, column)) = self.focused_data_cell() else {
1219 return false;
1220 };
1221 let Some(view) = self.columns.view_column_ix(column.ix()) else {
1222 return false;
1223 };
1224 let snapshot = self.snapshot.clone();
1225 let Some(row) = snapshot.row_id(&key).and_then(|id| snapshot.row(id)) else {
1226 return false;
1227 };
1228 let count = self.columns.visible_set().len();
1229 let views: Vec<usize> = if forward {
1230 (view.get() + 1..count).collect()
1231 } else {
1232 (0..view.get()).rev().collect()
1233 };
1234 let next = views.into_iter().find_map(|at| {
1235 let ix = self.columns.column_at(ViewColumnIx::new(at as u16))?;
1236 let column: ColumnOf<D> = column_of(ix);
1237 let editable = self.edit_setup.editor(ix).is_some()
1238 && self.delegate.is_editable(
1239 &EditableContext {
1240 row: &**row,
1241 column,
1242 value: row.value(column),
1243 input: &EditInput::Command,
1244 },
1245 cx,
1246 );
1247 editable.then_some(column)
1248 });
1249 match next {
1250 Some(column) => self.set_focused_cell(&key, Some(column), cx),
1251 None => false,
1252 }
1253 }
1254
1255 pub(super) fn commit_edit_on_press(
1261 &mut self,
1262 position: Point<Pixels>,
1263 window: &mut Window,
1264 cx: &mut Context<Self>,
1265 ) {
1266 let Some(session) = &self.edit.session else {
1267 return;
1268 };
1269 let inside = |bounds: Option<Bounds<Pixels>>| bounds.is_some_and(|b| b.contains(&position));
1270 if inside(session.host_bounds.get())
1271 || inside(session.popup_bounds.get())
1272 || inside(session.field_popup.bounds.get())
1273 {
1274 return;
1275 }
1276 self.end_edit_on_blur(window, cx);
1277 }
1278
1279 pub(super) fn start_edit_by_press(
1283 &mut self,
1284 click_count: usize,
1285 window: &mut Window,
1286 cx: &mut Context<Self>,
1287 ) -> bool {
1288 let Some(pointer) = self
1289 .pointer_target
1290 .as_ref()
1291 .filter(|pointer| pointer.pinned().is_none())
1292 else {
1293 return false;
1294 };
1295 let (Some(key), Some(column)) = (pointer.row().cloned(), pointer.column()) else {
1296 return false;
1297 };
1298 self.begin_edit(
1299 key,
1300 column_of(column),
1301 EditInput::Press { click_count },
1302 EditOrigin::User,
1303 Modifiers::default(),
1304 window,
1305 cx,
1306 )
1307 }
1308
1309 pub(super) fn follow_edit_rows(&mut self, _changes: &ChangeSet, cx: &mut Context<Self>) {
1315 self.follow_pending_edits(cx);
1316 let Some(session) = &self.edit.session else {
1317 return;
1318 };
1319 let Some(current) = self.store_value(&session.key, session.column) else {
1320 self.cancel_field(EditCancelReason::RowRemoved, None, cx);
1321 return;
1322 };
1323 if current.cmp_total(&session.base).is_eq() {
1324 if let Some(session) = &mut self.edit.session
1326 && session.mark == EditorMark::Conflict
1327 {
1328 session.mark = EditorMark::None;
1329 session.message = None;
1330 session.seen = current;
1331 cx.notify();
1332 }
1333 return;
1334 }
1335 if current.cmp_total(&session.seen).is_eq() {
1336 return;
1337 }
1338 let draft = session.field.value(cx).ok();
1339 let action = self
1340 .edit_setup
1341 .policy()
1342 .on_value_change(&EditPolicyContext::new(
1343 &session.base,
1344 ¤t,
1345 draft.as_ref(),
1346 session.field.is_dirty(cx),
1347 ));
1348 match action {
1349 DraftAction::Replace => {
1350 let Some(session) = &mut self.edit.session else {
1351 return;
1352 };
1353 session.seen = current.clone();
1354 session.base = current.clone();
1355 session.mark = EditorMark::None;
1356 session.message = None;
1357 let (grid, window, editor_focus) = (
1359 cx.weak_entity(),
1360 session.window,
1361 session.editor_focus.clone(),
1362 );
1363 cx.defer(move |cx| {
1364 let _ = window.update(cx, |_, window, cx| {
1365 let _ = grid.update(cx, |this, cx| {
1366 if let Some(session) = &this.edit.session
1367 && session.editor_focus == editor_focus
1368 {
1369 session.field.set_value(¤t, window, cx);
1370 cx.notify();
1371 }
1372 });
1373 });
1374 });
1375 cx.notify();
1376 }
1377 DraftAction::Cancel => {
1378 self.cancel_field(EditCancelReason::Programmatic, None, cx);
1379 }
1380 _ => {
1381 let mut text = String::new();
1382 let shown = display_text(current.as_cell(), &mut text).to_owned();
1383 let Some(session) = &mut self.edit.session else {
1384 return;
1385 };
1386 session.mark = EditorMark::Conflict;
1387 session.message = Some(t!("live_grid.edit.conflict", value = shown).into());
1388 session.seen = current;
1389 cx.notify();
1390 }
1391 }
1392 }
1393
1394 pub(super) fn end_hidden_edit(&mut self, cx: &mut Context<Self>) {
1399 let Some(session) = &self.edit.session else {
1400 return;
1401 };
1402 let shown = self
1403 .snapshot
1404 .row_id(&session.key)
1405 .and_then(|id| self.projection.display_ix(id))
1406 .is_some()
1407 && self.columns.view_column_ix(session.column.ix()).is_some();
1408 if shown {
1409 return;
1410 }
1411 let draft = session.field.value(cx).ok();
1412 let current = self
1413 .store_value(&session.key, session.column)
1414 .unwrap_or_else(|| session.base.clone());
1415 let end = self.edit_setup.policy().on_hidden(&EditPolicyContext::new(
1416 &session.base,
1417 ¤t,
1418 draft.as_ref(),
1419 session.field.is_dirty(cx),
1420 ));
1421 if end == EditEnd::Commit && self.commit_field(None, cx) != CommitOutcome::Refused {
1422 return;
1423 }
1424 self.cancel_field(EditCancelReason::RowHidden, None, cx);
1425 }
1426
1427 pub(super) fn plan_hidden_editor_host(
1431 &mut self,
1432 hosts: &mut HostPlan,
1433 painted: bool,
1434 window: &mut Window,
1435 cx: &mut Context<Self>,
1436 ) {
1437 let Some(session) = &self.edit.session else {
1438 return;
1439 };
1440 if self.columns.view_column_ix(session.column.ix()).is_none() {
1443 let grid = cx.weak_entity();
1444 cx.defer(move |cx| {
1445 let _ = grid.update(cx, |this, cx| this.end_hidden_edit(cx));
1446 });
1447 }
1448 if painted {
1449 return;
1450 }
1451 session.host_bounds.set(None);
1452 let rows = self.body_bands().rows();
1453 let line = self
1454 .snapshot
1455 .row_id(&session.key)
1456 .and_then(|id| self.projection.display_ix(id))
1457 .map_or(rows.origin.y, |display_ix| {
1458 rows.origin.y + self.row_height * (display_ix as f32 - self.scroll_row as f32)
1459 - self.scroll_offset
1460 });
1461 let line = Bounds::new(
1462 point(rows.origin.x, line),
1463 size(rows.size.width, self.row_height),
1464 );
1465 let width = self.columns.column(session.column.ix()).width;
1466 let bounds = Bounds::new(line.origin, size(width, self.row_height));
1467 let first = hosts.len();
1468 let element = editor_host(
1469 session,
1470 bounds,
1471 editor_size(self.row_height),
1472 false,
1473 None,
1474 window,
1475 cx,
1476 );
1477 hosts.push(CellHost {
1478 layer: HostLayer::Editor,
1479 column: session.column.ix(),
1480 zone: self.columns.column(session.column.ix()).zone,
1481 bounds,
1482 element,
1483 });
1484 hosts.end_row(
1485 first,
1486 data_row_id_name(None),
1487 key_hash(&session.key),
1488 line,
1489 Bounds::default(),
1490 );
1491 }
1492
1493 pub(super) fn bind_edit_actions<E: InteractiveElement>(root: E, cx: &Context<Self>) -> E {
1496 root.on_action(cx.listener(|this, _: &StartEdit, window, cx| {
1497 this.start_edit_by_key(EditInput::Command, window, cx)
1498 }))
1499 .on_action(cx.listener(|this, _: &StartEditEmpty, window, cx| {
1500 this.start_edit_by_key(EditInput::Clear, window, cx)
1501 }))
1502 .on_action(cx.listener(|this, _: &CommitEdit, window, cx| {
1503 this.commit_and_move(EditCommitKey::Enter, window, cx)
1504 }))
1505 .on_action(cx.listener(|this, _: &CommitEditUp, window, cx| {
1506 this.commit_and_move(EditCommitKey::Up, window, cx)
1507 }))
1508 .on_action(cx.listener(|this, _: &CommitEditDown, window, cx| {
1509 this.commit_and_move(EditCommitKey::Down, window, cx)
1510 }))
1511 .on_action(cx.listener(|this, _: &CommitEditNext, window, cx| {
1512 this.commit_and_move(EditCommitKey::Next, window, cx)
1513 }))
1514 .on_action(cx.listener(|this, _: &CommitEditPrev, window, cx| {
1515 this.commit_and_move(EditCommitKey::Prev, window, cx)
1516 }))
1517 .on_action(cx.listener(|this, _: &CancelEdit, window, cx| {
1518 this.cancel_field(EditCancelReason::Escape, Some(window), cx);
1519 }))
1520 .on_key_down(cx.listener(Self::type_to_edit))
1521 .on_mouse_down(MouseButton::Left, cx.listener(Self::note_single_press))
1522 .on_mouse_up(MouseButton::Left, cx.listener(Self::edit_on_release))
1523 }
1524
1525 fn note_single_press(&mut self, event: &MouseDownEvent, _: &mut Window, _: &mut Context<Self>) {
1528 self.edit.single_press = self.pointer_target.as_ref().and_then(|pointer| {
1529 let fresh = pointer.position() == event.position && event.click_count == 1;
1530 let cell = (pointer.row().cloned()?, column_of(pointer.column()?));
1531 (fresh && pointer.pinned().is_none()).then_some((cell.0, cell.1, event.position))
1532 });
1533 }
1534
1535 fn edit_on_release(
1540 &mut self,
1541 event: &MouseUpEvent,
1542 window: &mut Window,
1543 cx: &mut Context<Self>,
1544 ) {
1545 let Some((key, column, origin)) = self.edit.single_press.take() else {
1546 return;
1547 };
1548 if self.edit.is_open() || is_past_drag_threshold(origin, event.position) {
1549 return;
1550 }
1551 self.begin_edit(
1552 key,
1553 column,
1554 EditInput::Press { click_count: 1 },
1555 EditOrigin::User,
1556 event.modifiers,
1557 window,
1558 cx,
1559 );
1560 }
1561
1562 fn start_edit_by_key(&mut self, input: EditInput, window: &mut Window, cx: &mut Context<Self>) {
1565 let started = self.focus_handle.is_focused(window)
1566 && self.focused_data_cell().is_some_and(|(key, column)| {
1567 self.begin_edit(
1568 key,
1569 column,
1570 input,
1571 EditOrigin::User,
1572 Modifiers::default(),
1573 window,
1574 cx,
1575 )
1576 });
1577 if !started {
1578 cx.propagate();
1579 }
1580 }
1581
1582 fn type_to_edit(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
1587 let keystroke = &event.keystroke;
1588 let modifiers = keystroke.modifiers;
1589 if self.edit.is_open()
1590 || !self.focus_handle.is_focused(window)
1591 || modifiers.control
1592 || modifiers.platform
1593 || modifiers.function
1594 {
1595 return;
1596 }
1597 let Some(text) = keystroke
1598 .key_char
1599 .as_ref()
1600 .filter(|text| !text.is_empty() && !text.chars().any(char::is_control))
1601 else {
1602 return;
1603 };
1604 let Some((key, column)) = self.focused_data_cell() else {
1605 return;
1606 };
1607 let input = EditInput::Typing(text.clone().into());
1608 if self.begin_edit(key, column, input, EditOrigin::User, modifiers, window, cx) {
1609 cx.stop_propagation();
1610 }
1611 }
1612
1613 pub(super) fn would_type_into_focus(&self, text: &str, cx: &App) -> bool {
1618 let Some((key, column)) = self.focused_data_cell() else {
1619 return false;
1620 };
1621 let Some(row) = self
1622 .snapshot
1623 .row_id(&key)
1624 .and_then(|id| self.snapshot.row(id))
1625 else {
1626 return false;
1627 };
1628 let input = EditInput::Typing(SharedString::new(text));
1629 self.edit_setup.editor(column.ix()).is_some()
1630 && self.columns.view_column_ix(column.ix()).is_some()
1631 && self
1632 .edit_setup
1633 .gesture()
1634 .starts_edit(&EditGestureContext::new(
1635 input.clone(),
1636 Modifiers::default(),
1637 self.columns.column(column.ix()).content,
1638 ))
1639 && self.delegate.is_editable(
1640 &EditableContext {
1641 row: &**row,
1642 column,
1643 value: row.value(column),
1644 input: &input,
1645 },
1646 cx,
1647 )
1648 }
1649}
1650
1651fn editor_host<K, C>(
1656 session: &EditSession<K, C>,
1657 bounds: Bounds<Pixels>,
1658 size: ComponentSize,
1659 a11y: bool,
1660 message: Option<AnyElement>,
1661 window: &mut Window,
1662 cx: &mut App,
1663) -> Stateful<Div>
1664where
1665 C: GridColumns,
1666{
1667 session.field_popup.shown.set(false);
1668 session.field_popup.bounds.set(None);
1669 let field = session.field.render(
1670 &EditorCell {
1671 bounds,
1672 size,
1673 mark: session.mark,
1674 message: session.message.as_ref(),
1675 title: &session.title,
1676 popup: &session.field_popup,
1677 },
1678 window,
1679 cx,
1680 );
1681 let (message_anchor, message_at) = if session.field_popup.shown.get() {
1683 (Anchor::BottomLeft, point(bounds.left(), bounds.top()))
1684 } else {
1685 (Anchor::TopLeft, point(bounds.left(), bounds.bottom()))
1686 };
1687 let theme = cx.theme();
1688 let frame = match session.mark {
1689 EditorMark::Invalid => theme.danger,
1690 EditorMark::Conflict => theme.warning,
1691 _ => theme.ring,
1692 };
1693 let reveal = session.reveal.clone();
1694 let popup_bounds = session.popup_bounds.clone();
1695 popup_bounds.set(None);
1696 CellHost::host_div(HostLayer::Editor, session.column.id())
1697 .track_focus(&session.editor_focus)
1698 .key_context(EDITOR_KEY_CONTEXT)
1699 .capture_key_down(move |_, _, cx| reveal(cx))
1700 .on_action(|_: &Cancel, window, cx| {
1703 window.dispatch_action(Box::new(CancelEdit), cx);
1704 })
1705 .block_mouse_except_scroll()
1706 .flex()
1707 .items_center()
1708 .px(px(2.))
1709 .bg(theme.background)
1710 .border_2()
1711 .border_color(frame)
1712 .when(a11y, |this| this.role(Role::GridCell))
1713 .when_some(session.message.clone(), |this, message| {
1714 this.aria_description(message)
1715 })
1716 .child(field)
1717 .when_some(message, |this, message| {
1718 this.child(
1719 deferred(
1720 anchored()
1721 .anchor(message_anchor)
1722 .position(message_at)
1723 .snap_to_window_with_margin(px(4.))
1724 .child(
1725 div()
1726 .on_children_prepainted(move |children, _, _| {
1727 popup_bounds.set(children.first().copied());
1728 })
1729 .child(div().occlude().child(message)),
1730 ),
1731 )
1732 .with_priority(1),
1733 )
1734 })
1735}
1736
1737impl<D: LiveGridDelegate> RowPlanner<'_, D> {
1738 pub(super) fn plan_editor_host(
1742 &mut self,
1743 cell: &HostCell<'_, D::Row>,
1744 window: &mut Window,
1745 ) -> bool {
1746 let Some(session) = &self.edit.session else {
1747 return false;
1748 };
1749 let bounds = Bounds::new(
1750 point(cell.bounds.origin.x + cell.indent, cell.bounds.origin.y),
1751 size(
1752 (cell.bounds.size.width - cell.indent).max(px(0.)),
1753 cell.bounds.size.height,
1754 ),
1755 );
1756 let clip = self.band_clips[1];
1757 session.host_bounds.set(Some(bounds.intersect(&clip)));
1758 let size = editor_size(self.row_height);
1759 let message = session.message.as_ref().and_then(|message| {
1760 self.delegate.render_edit_message(
1761 &EditMessageContext {
1762 mark: session.mark,
1763 message,
1764 cell_bounds: bounds,
1765 size: grid_size(self.row_height),
1766 },
1767 window,
1768 self.app,
1769 )
1770 });
1771 let element = editor_host(
1772 session,
1773 bounds,
1774 size,
1775 self.a11y.is_some(),
1776 message,
1777 window,
1778 self.app,
1779 );
1780 self.hosts.push(CellHost {
1781 layer: HostLayer::Editor,
1782 column: cell.column.ix(),
1783 zone: cell.zone,
1784 bounds,
1785 element,
1786 });
1787 true
1788 }
1789}
1790
1791fn grid_size(row_height: Pixels) -> ComponentSize {
1794 [
1795 ComponentSize::XSmall,
1796 ComponentSize::Small,
1797 ComponentSize::Medium,
1798 ComponentSize::Large,
1799 ]
1800 .into_iter()
1801 .find(|size| size.table_row_height() == row_height)
1802 .unwrap_or(ComponentSize::Size(row_height))
1803}
1804
1805#[cfg(test)]
1806mod tests {
1807 use std::collections::HashSet;
1808
1809 use gpui_kit::Modifiers;
1810
1811 use super::*;
1812 use crate::{
1813 CellContent,
1814 policy::{
1815 AfterCommit, CellMove, DraftAction, EditCommitKey, EditEnd, EditGestureContext,
1816 EditPolicyContext, ReorderContext,
1817 },
1818 };
1819
1820 #[test]
1821 fn test_edit_types() {
1822 let ids: HashSet<EditId> = (0..4).map(EditId::new).collect();
1824 assert_eq!(ids.len(), 4);
1825 assert_eq!(EditId::new(7).get(), 7);
1826 let error = EditError::new("Enter a number");
1827 assert_eq!(error.message().as_ref(), "Enter a number");
1828 assert_eq!(EditorMark::default(), EditorMark::None);
1829
1830 let gesture = StandardEditGesture;
1833 let starts = |input: EditInput, content: CellContent| {
1834 gesture.starts_edit(&EditGestureContext::new(
1835 input,
1836 Modifiers::default(),
1837 content,
1838 ))
1839 };
1840 for input in [
1841 EditInput::Command,
1842 EditInput::Clear,
1843 EditInput::Typing("4".into()),
1844 EditInput::Press { click_count: 2 },
1845 EditInput::Programmatic,
1846 ] {
1847 assert!(starts(input.clone(), CellContent::Text), "{input:?}");
1848 assert!(starts(input.clone(), CellContent::Element), "{input:?}");
1849 }
1850 assert!(!starts(
1851 EditInput::Press { click_count: 1 },
1852 CellContent::Text
1853 ));
1854 assert!(!starts(
1855 EditInput::Press { click_count: 2 },
1856 CellContent::Control
1857 ));
1858 assert!(starts(EditInput::Typing("x".into()), CellContent::Control));
1859 for (key, expected) in [
1860 (EditCommitKey::Enter, CellMove::Down),
1861 (EditCommitKey::Down, CellMove::Down),
1862 (EditCommitKey::Up, CellMove::Up),
1863 (EditCommitKey::Next, CellMove::Right),
1864 (EditCommitKey::Prev, CellMove::Left),
1865 ] {
1866 assert_eq!(gesture.after_commit(key), AfterCommit::Move(expected));
1867 }
1868
1869 let policy = StandardEditPolicy;
1873 let (base, current, draft) = (OwnedValue::Int(1), OwnedValue::Int(2), OwnedValue::Int(3));
1874 let valid = EditPolicyContext::new(&base, ¤t, Some(&draft), true);
1875 let invalid = EditPolicyContext::new(&base, ¤t, None, true);
1876 assert_eq!(policy.on_blur(&valid), EditEnd::Commit);
1877 assert_eq!(policy.on_blur(&invalid), EditEnd::Cancel);
1878 assert!(!policy.commit_unchanged());
1879 assert_eq!(policy.on_value_change(&valid), DraftAction::Keep);
1880 assert_eq!(policy.on_hidden(&valid), EditEnd::Cancel);
1881 assert_eq!(policy.settle_timeout(), std::time::Duration::from_secs(5));
1882 assert_eq!(policy.error_mark_timeout(), None);
1883 assert_eq!(
1884 (valid.base(), valid.current(), valid.is_dirty()),
1885 (&base, ¤t, true)
1886 );
1887
1888 let now = web_time::Instant::now();
1890 let context = ReorderContext::new(now, now, false, false);
1891 assert!(!context.is_editing());
1892 assert!(!context.has_pending_edits());
1893 assert!(!context.is_drag_over());
1894 assert!(!context.is_column_chooser_open());
1895 assert!(context.with_editing(true).is_editing());
1896
1897 let slot = EditSlot::<u32, u16>::default();
1899 assert!(!slot.is_open());
1900 assert_eq!(slot.column_in(&1), None);
1901 let setup = EditSetup::<TestRow>::default();
1902 assert!(setup.gesture().starts_edit(&EditGestureContext::new(
1903 EditInput::Command,
1904 Modifiers::default(),
1905 CellContent::Text,
1906 )));
1907 assert_eq!(setup.policy().on_blur(&invalid), EditEnd::Cancel);
1908 }
1909
1910 struct TestRow;
1911
1912 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1913 struct TestColumn;
1914
1915 impl crate::model::GridColumns for TestColumn {
1916 fn all() -> &'static [Self] {
1917 &[Self]
1918 }
1919
1920 fn id(self) -> &'static str {
1921 "test"
1922 }
1923
1924 fn ix(self) -> ColumnIx {
1925 ColumnIx::new(0)
1926 }
1927 }
1928
1929 impl GridRow for TestRow {
1930 type Key = u32;
1931 type Column = TestColumn;
1932
1933 fn key(&self) -> &u32 {
1934 &0
1935 }
1936
1937 fn value(&self, _: TestColumn) -> CellValue<'_> {
1938 CellValue::Empty
1939 }
1940 }
1941}