rat_ftable/edit/
table.rs

1//! More general editing in a table.
2//!
3//! A widget that renders the table and can render
4//! an edit-widget on top.
5//!
6//! __Examples__
7//! For examples go to the rat-widget crate.
8//! There is `examples/table_edit1.rs`.
9
10use crate::_private::NonExhaustive;
11use crate::edit::{Mode, TableEditor, TableEditorState};
12use crate::event::{EditOutcome, TableOutcome};
13use crate::rowselection::RowSelection;
14use crate::{Table, TableSelection, TableState};
15use log::warn;
16use rat_cursor::HasScreenCursor;
17use rat_event::util::MouseFlags;
18use rat_event::{HandleEvent, Regular, ct_event, flow};
19use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
20use rat_reloc::RelocatableState;
21use ratatui_core::buffer::Buffer;
22use ratatui_core::layout::Rect;
23use ratatui_core::widgets::StatefulWidget;
24use ratatui_crossterm::crossterm::event::Event;
25use std::fmt::{Debug, Formatter};
26
27/// Widget that supports row-wise editing of a table.
28///
29/// It's parameterized with a `Editor` widget, that renders
30/// the input line and handles events. The result of event-handling
31/// is an [EditOutcome] that can be used to do the actual editing.
32#[derive(Debug)]
33pub struct EditableTable<'a, E>
34where
35    E: TableEditor + 'a,
36{
37    table: Table<'a, RowSelection>,
38    editor: E,
39}
40
41/// State for EditTable.
42///
43/// Contains `mode` to differentiate between edit/non-edit.
44/// This will lock the focus to the input line while editing.
45///
46pub struct EditableTableState<S> {
47    /// Editing mode.
48    pub mode: Mode,
49
50    /// Backing table.
51    pub table: TableState<RowSelection>,
52    /// Editor
53    pub editor: S,
54
55    pub mouse: MouseFlags,
56
57    pub non_exhaustive: NonExhaustive,
58}
59
60impl<'a, E> EditableTable<'a, E>
61where
62    E: TableEditor + 'a,
63{
64    pub fn new(table: Table<'a, RowSelection>, editor: E) -> Self {
65        Self { table, editor }
66    }
67}
68
69impl<'a, E> StatefulWidget for &EditableTable<'a, E>
70where
71    E: TableEditor + 'a,
72{
73    type State = EditableTableState<E::State>;
74
75    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
76        (&self.table).render(area, buf, &mut state.table);
77
78        if state.mode == Mode::Edit || state.mode == Mode::Insert {
79            if let Some(row) = state.table.selected_checked() {
80                // but it might be out of view
81                if let Some((row_area, cell_areas)) = state.table.row_cells(row) {
82                    self.editor
83                        .render(row_area, &cell_areas, buf, &mut state.editor);
84                }
85            } else {
86                if cfg!(feature = "perf_warnings") {
87                    warn!("no row selection, not rendering editor");
88                }
89            }
90        }
91    }
92}
93
94impl<'a, E> StatefulWidget for EditableTable<'a, E>
95where
96    E: TableEditor + 'a,
97{
98    type State = EditableTableState<E::State>;
99
100    #[allow(clippy::collapsible_else_if)]
101    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
102        self.table.render(area, buf, &mut state.table);
103
104        if state.mode == Mode::Insert || state.mode == Mode::Edit {
105            if let Some(row) = state.table.selected_checked() {
106                // but it might be out of view
107                if let Some((row_area, cell_areas)) = state.table.row_cells(row) {
108                    self.editor
109                        .render(row_area, &cell_areas, buf, &mut state.editor);
110                }
111            } else {
112                if cfg!(feature = "perf_warnings") {
113                    warn!("no row selection, not rendering editor");
114                }
115            }
116        }
117    }
118}
119
120impl<S> Default for EditableTableState<S>
121where
122    S: Default,
123{
124    fn default() -> Self {
125        Self {
126            mode: Mode::View,
127            table: Default::default(),
128            editor: S::default(),
129            mouse: Default::default(),
130            non_exhaustive: NonExhaustive,
131        }
132    }
133}
134
135impl<S> Debug for EditableTableState<S>
136where
137    S: Debug,
138{
139    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
140        f.debug_struct("EditTableState")
141            .field("mode", &self.mode)
142            .field("table", &self.table)
143            .field("editor", &self.editor)
144            .field("mouse", &self.mouse)
145            .finish()
146    }
147}
148
149impl<S> HasFocus for EditableTableState<S> {
150    fn build(&self, builder: &mut FocusBuilder) {
151        builder.leaf_widget(self);
152    }
153
154    fn focus(&self) -> FocusFlag {
155        self.table.focus()
156    }
157
158    fn area(&self) -> Rect {
159        self.table.area()
160    }
161
162    fn navigable(&self) -> Navigation {
163        match self.mode {
164            Mode::View => self.table.navigable(),
165            Mode::Edit | Mode::Insert => Navigation::Lock,
166        }
167    }
168
169    fn is_focused(&self) -> bool {
170        self.table.is_focused()
171    }
172
173    fn lost_focus(&self) -> bool {
174        self.table.lost_focus()
175    }
176
177    fn gained_focus(&self) -> bool {
178        self.table.gained_focus()
179    }
180}
181
182impl<S> HasScreenCursor for EditableTableState<S>
183where
184    S: HasScreenCursor,
185{
186    fn screen_cursor(&self) -> Option<(u16, u16)> {
187        match self.mode {
188            Mode::View => None,
189            Mode::Edit | Mode::Insert => self.editor.screen_cursor(),
190        }
191    }
192}
193
194impl<S> RelocatableState for EditableTableState<S>
195where
196    S: TableEditorState + RelocatableState,
197{
198    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
199        match self.mode {
200            Mode::View => {}
201            Mode::Edit | Mode::Insert => {
202                self.editor.relocate(shift, clip);
203            }
204        }
205    }
206}
207
208impl<S> EditableTableState<S> {
209    /// New state.
210    pub fn new(editor: S) -> Self {
211        Self {
212            mode: Mode::View,
213            table: TableState::new(),
214            editor,
215            mouse: Default::default(),
216            non_exhaustive: NonExhaustive,
217        }
218    }
219
220    /// New state with a named focus.
221    pub fn named(name: &str, editor: S) -> Self {
222        Self {
223            mode: Mode::View,
224            table: TableState::named(name),
225            editor,
226            mouse: Default::default(),
227            non_exhaustive: NonExhaustive,
228        }
229    }
230}
231
232impl<S> EditableTableState<S>
233where
234    S: TableEditorState,
235{
236    /// Editing is active?
237    pub fn is_editing(&self) -> bool {
238        self.mode == Mode::Edit || self.mode == Mode::Insert
239    }
240
241    /// Is the current edit an insert?
242    pub fn is_insert(&self) -> bool {
243        self.mode == Mode::Insert
244    }
245
246    /// Remove the item at the selected row.
247    ///
248    /// This doesn't change the actual list of items, but does
249    /// all the bookkeeping with the table-state.
250    pub fn remove(&mut self, row: usize) {
251        if self.mode != Mode::View {
252            return;
253        }
254        self.table.items_removed(row, 1);
255        if !self.table.scroll_to_row(row) {
256            self.table.scroll_to_row(row.saturating_sub(1));
257        }
258    }
259
260    /// Edit a new item inserted at the selected row.
261    ///
262    /// The editor state must be initialized to an appropriate state
263    /// beforehand.
264    ///
265    /// __See__
266    /// [TableEditorState::set_value]
267    ///
268    /// This does all the bookkeeping with the table-state and
269    /// switches the mode to Mode::Insert.
270    pub fn edit_new(&mut self, row: usize) {
271        if self.mode != Mode::View {
272            return;
273        }
274        self._start(0, row, Mode::Insert);
275    }
276
277    /// Edit the item at the selected row.
278    ///
279    /// The editor state must be initialized to an appropriate state
280    /// beforehand.
281    ///
282    /// __See__
283    /// [TableEditorState::set_value]
284    ///
285    /// This does all the bookkeeping with the table-state and
286    /// switches the mode to Mode::Edit.
287    pub fn edit(&mut self, col: usize, row: usize) {
288        if self.mode != Mode::View {
289            return;
290        }
291        self._start(col, row, Mode::Edit);
292    }
293
294    fn _start(&mut self, col: usize, row: usize, mode: Mode) {
295        if self.table.is_focused() {
296            FocusBuilder::build_for(&self.editor).first();
297        }
298
299        self.mode = mode;
300        if self.mode == Mode::Insert {
301            self.table.items_added(row, 1);
302        }
303        self.table.move_to(row);
304        self.table.scroll_to_col(col);
305        self.editor.set_focused_col(col);
306    }
307
308    /// Cancel editing.
309    ///
310    /// This doesn't reset the edit-widget.
311    ///
312    /// __See__
313    /// [TableEditorState::set_value]
314    ///
315    /// But it does all the bookkeeping with the table-state and
316    /// switches the mode back to Mode::View.
317    pub fn cancel(&mut self) {
318        if self.mode == Mode::View {
319            return;
320        }
321        let Some(row) = self.table.selected_checked() else {
322            return;
323        };
324        if self.mode == Mode::Insert {
325            self.table.items_removed(row, 1);
326        }
327        self._stop();
328    }
329
330    /// Commit the changes in the editor.
331    ///
332    /// This doesn't copy the data back from the editor to the
333    /// row-item.
334    ///
335    /// __See__
336    /// [TableEditorState::value]
337    ///
338    /// But it does all the bookkeeping with the table-state and
339    /// switches the mode back to Mode::View.
340    pub fn commit(&mut self) {
341        if self.mode == Mode::View {
342            return;
343        }
344        self._stop();
345    }
346
347    fn _stop(&mut self) {
348        self.mode = Mode::View;
349        self.table.scroll_to_col(0);
350    }
351}
352
353impl<'a, S> HandleEvent<Event, &'a S::Context<'a>, EditOutcome> for EditableTableState<S>
354where
355    S: HandleEvent<Event, &'a S::Context<'a>, EditOutcome>,
356    S: TableEditorState,
357{
358    fn handle(&mut self, event: &Event, ctx: &'a S::Context<'a>) -> EditOutcome {
359        if self.mode == Mode::Edit || self.mode == Mode::Insert {
360            if self.table.is_focused() {
361                flow!(match self.editor.handle(event, ctx) {
362                    EditOutcome::Continue => EditOutcome::Continue,
363                    EditOutcome::Unchanged => EditOutcome::Unchanged,
364                    r => {
365                        if let Some(col) = self.editor.focused_col() {
366                            self.table.scroll_to_col(col);
367                        }
368                        r
369                    }
370                });
371
372                flow!(match event {
373                    ct_event!(keycode press Esc) => {
374                        EditOutcome::Cancel
375                    }
376                    ct_event!(keycode press Enter) => {
377                        if self.table.selected_checked() < Some(self.table.rows().saturating_sub(1))
378                        {
379                            EditOutcome::CommitAndEdit
380                        } else {
381                            EditOutcome::CommitAndAppend
382                        }
383                    }
384                    ct_event!(keycode press Up) => {
385                        EditOutcome::Commit
386                    }
387                    ct_event!(keycode press Down) => {
388                        EditOutcome::Commit
389                    }
390                    _ => EditOutcome::Continue,
391                });
392            }
393            EditOutcome::Continue
394        } else {
395            flow!(match event {
396                ct_event!(mouse any for m) if self.mouse.doubleclick(self.table.table_area, m) => {
397                    if self.table.cell_at_clicked((m.column, m.row)).is_some() {
398                        EditOutcome::Edit
399                    } else {
400                        EditOutcome::Continue
401                    }
402                }
403                _ => EditOutcome::Continue,
404            });
405
406            if self.table.is_focused() {
407                flow!(match event {
408                    ct_event!(keycode press Insert) => {
409                        EditOutcome::Insert
410                    }
411                    ct_event!(keycode press Delete) => {
412                        EditOutcome::Remove
413                    }
414                    ct_event!(keycode press Enter) | ct_event!(keycode press F(2)) => {
415                        EditOutcome::Edit
416                    }
417                    ct_event!(keycode press Down) => {
418                        if let Some((_column, row)) = self.table.selection.lead_selection() {
419                            if row == self.table.rows().saturating_sub(1) {
420                                EditOutcome::Append
421                            } else {
422                                EditOutcome::Continue
423                            }
424                        } else {
425                            EditOutcome::Continue
426                        }
427                    }
428                    _ => {
429                        EditOutcome::Continue
430                    }
431                });
432            }
433
434            match self.table.handle(event, Regular) {
435                TableOutcome::Continue => EditOutcome::Continue,
436                TableOutcome::Unchanged => EditOutcome::Unchanged,
437                TableOutcome::Changed => EditOutcome::Changed,
438                TableOutcome::Selected => EditOutcome::Changed,
439            }
440        }
441    }
442}