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::{ct_event, flow, HandleEvent, Regular};
19use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
20use rat_reloc::RelocatableState;
21use ratatui::buffer::Buffer;
22use ratatui::layout::Rect;
23use ratatui::widgets::StatefulWidget;
24use std::fmt::{Debug, Formatter};
25
26/// Widget that supports row-wise editing of a table.
27///
28/// It's parameterized with a `Editor` widget, that renders
29/// the input line and handles events. The result of event-handling
30/// is an [EditOutcome] that can be used to do the actual editing.
31#[derive(Debug)]
32pub struct EditableTable<'a, E>
33where
34    E: TableEditor + 'a,
35{
36    table: Table<'a, RowSelection>,
37    editor: E,
38}
39
40/// State for EditTable.
41///
42/// Contains `mode` to differentiate between edit/non-edit.
43/// This will lock the focus to the input line while editing.
44///
45pub struct EditableTableState<S> {
46    /// Editing mode.
47    pub mode: Mode,
48
49    /// Backing table.
50    pub table: TableState<RowSelection>,
51    /// Editor
52    pub editor: S,
53
54    pub mouse: MouseFlags,
55
56    pub non_exhaustive: NonExhaustive,
57}
58
59impl<'a, E> EditableTable<'a, E>
60where
61    E: TableEditor + 'a,
62{
63    pub fn new(table: Table<'a, RowSelection>, editor: E) -> Self {
64        Self { table, editor }
65    }
66}
67
68impl<'a, E> StatefulWidget for &EditableTable<'a, E>
69where
70    E: TableEditor + 'a,
71{
72    type State = EditableTableState<E::State>;
73
74    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
75        (&self.table).render(area, buf, &mut state.table);
76
77        if state.mode == Mode::Edit || state.mode == Mode::Insert {
78            if let Some(row) = state.table.selected_checked() {
79                // but it might be out of view
80                if let Some((row_area, cell_areas)) = state.table.row_cells(row) {
81                    self.editor
82                        .render(row_area, &cell_areas, buf, &mut state.editor);
83                }
84            } else {
85                if cfg!(debug_assertions) {
86                    warn!("no row selection, not rendering editor");
87                }
88            }
89        }
90    }
91}
92
93impl<'a, E> StatefulWidget for EditableTable<'a, E>
94where
95    E: TableEditor + 'a,
96{
97    type State = EditableTableState<E::State>;
98
99    #[allow(clippy::collapsible_else_if)]
100    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
101        self.table.render(area, buf, &mut state.table);
102
103        if state.mode == Mode::Insert || state.mode == Mode::Edit {
104            if let Some(row) = state.table.selected_checked() {
105                // but it might be out of view
106                if let Some((row_area, cell_areas)) = state.table.row_cells(row) {
107                    self.editor
108                        .render(row_area, &cell_areas, buf, &mut state.editor);
109                }
110            } else {
111                if cfg!(debug_assertions) {
112                    warn!("no row selection, not rendering editor");
113                }
114            }
115        }
116    }
117}
118
119impl<S> Default for EditableTableState<S>
120where
121    S: Default,
122{
123    fn default() -> Self {
124        Self {
125            mode: Mode::View,
126            table: Default::default(),
127            editor: S::default(),
128            mouse: Default::default(),
129            non_exhaustive: NonExhaustive,
130        }
131    }
132}
133
134impl<S> Debug for EditableTableState<S>
135where
136    S: Debug,
137{
138    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
139        f.debug_struct("EditTableState")
140            .field("mode", &self.mode)
141            .field("table", &self.table)
142            .field("editor", &self.editor)
143            .field("mouse", &self.mouse)
144            .finish()
145    }
146}
147
148impl<S> HasFocus for EditableTableState<S> {
149    fn build(&self, builder: &mut FocusBuilder) {
150        builder.leaf_widget(self);
151    }
152
153    fn focus(&self) -> FocusFlag {
154        self.table.focus()
155    }
156
157    fn area(&self) -> Rect {
158        self.table.area()
159    }
160
161    fn navigable(&self) -> Navigation {
162        match self.mode {
163            Mode::View => self.table.navigable(),
164            Mode::Edit | Mode::Insert => Navigation::Lock,
165        }
166    }
167
168    fn is_focused(&self) -> bool {
169        self.table.is_focused()
170    }
171
172    fn lost_focus(&self) -> bool {
173        self.table.lost_focus()
174    }
175
176    fn gained_focus(&self) -> bool {
177        self.table.gained_focus()
178    }
179}
180
181impl<S> HasScreenCursor for EditableTableState<S>
182where
183    S: HasScreenCursor,
184{
185    fn screen_cursor(&self) -> Option<(u16, u16)> {
186        match self.mode {
187            Mode::View => None,
188            Mode::Edit | Mode::Insert => self.editor.screen_cursor(),
189        }
190    }
191}
192
193impl<S> RelocatableState for EditableTableState<S>
194where
195    S: TableEditorState + RelocatableState,
196{
197    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
198        match self.mode {
199            Mode::View => {}
200            Mode::Edit | Mode::Insert => {
201                self.editor.relocate(shift, clip);
202            }
203        }
204    }
205}
206
207impl<S> EditableTableState<S> {
208    /// New state.
209    pub fn new(editor: S) -> Self {
210        Self {
211            mode: Mode::View,
212            table: TableState::new(),
213            editor,
214            mouse: Default::default(),
215            non_exhaustive: NonExhaustive,
216        }
217    }
218
219    /// New state with a named focus.
220    pub fn named(name: &str, editor: S) -> Self {
221        Self {
222            mode: Mode::View,
223            table: TableState::named(name),
224            editor,
225            mouse: Default::default(),
226            non_exhaustive: NonExhaustive,
227        }
228    }
229}
230
231impl<S> EditableTableState<S>
232where
233    S: TableEditorState,
234{
235    /// Editing is active?
236    pub fn is_editing(&self) -> bool {
237        self.mode == Mode::Edit || self.mode == Mode::Insert
238    }
239
240    /// Is the current edit an insert?
241    pub fn is_insert(&self) -> bool {
242        self.mode == Mode::Insert
243    }
244
245    /// Remove the item at the selected row.
246    ///
247    /// This doesn't change the actual list of items, but does
248    /// all the bookkeeping with the table-state.
249    pub fn remove(&mut self, row: usize) {
250        if self.mode != Mode::View {
251            return;
252        }
253        self.table.items_removed(row, 1);
254        if !self.table.scroll_to_row(row) {
255            self.table.scroll_to_row(row.saturating_sub(1));
256        }
257    }
258
259    /// Edit a new item inserted at the selected row.
260    ///
261    /// The editor state must be initialized to an appropriate state
262    /// beforehand.
263    ///
264    /// __See__
265    /// [TableEditorState::set_value]
266    ///
267    /// This does all the bookkeeping with the table-state and
268    /// switches the mode to Mode::Insert.
269    pub fn edit_new(&mut self, row: usize) {
270        if self.mode != Mode::View {
271            return;
272        }
273        self._start(0, row, Mode::Insert);
274    }
275
276    /// Edit the item at the selected row.
277    ///
278    /// The editor state must be initialized to an appropriate state
279    /// beforehand.
280    ///
281    /// __See__
282    /// [TableEditorState::set_value]
283    ///
284    /// This does all the bookkeeping with the table-state and
285    /// switches the mode to Mode::Edit.
286    pub fn edit(&mut self, col: usize, row: usize) {
287        if self.mode != Mode::View {
288            return;
289        }
290        self._start(col, row, Mode::Edit);
291    }
292
293    fn _start(&mut self, col: usize, row: usize, mode: Mode) {
294        if self.table.is_focused() {
295            FocusBuilder::build_for(&self.editor).first();
296        }
297
298        self.mode = mode;
299        if self.mode == Mode::Insert {
300            self.table.items_added(row, 1);
301        }
302        self.table.move_to(row);
303        self.table.scroll_to_col(col);
304        self.editor.set_focused_col(col);
305    }
306
307    /// Cancel editing.
308    ///
309    /// This doesn't reset the edit-widget.
310    ///
311    /// __See__
312    /// [TableEditorState::set_value]
313    ///
314    /// But it does all the bookkeeping with the table-state and
315    /// switches the mode back to Mode::View.
316    pub fn cancel(&mut self) {
317        if self.mode == Mode::View {
318            return;
319        }
320        let Some(row) = self.table.selected_checked() else {
321            return;
322        };
323        if self.mode == Mode::Insert {
324            self.table.items_removed(row, 1);
325        }
326        self._stop();
327    }
328
329    /// Commit the changes in the editor.
330    ///
331    /// This doesn't copy the data back from the editor to the
332    /// row-item.
333    ///
334    /// __See__
335    /// [TableEditorState::value]
336    ///
337    /// But it does all the bookkeeping with the table-state and
338    /// switches the mode back to Mode::View.
339    pub fn commit(&mut self) {
340        if self.mode == Mode::View {
341            return;
342        }
343        self._stop();
344    }
345
346    fn _stop(&mut self) {
347        self.mode = Mode::View;
348        self.table.scroll_to_col(0);
349    }
350}
351
352impl<'a, S> HandleEvent<crossterm::event::Event, &'a S::Context<'a>, EditOutcome>
353    for EditableTableState<S>
354where
355    S: HandleEvent<crossterm::event::Event, &'a S::Context<'a>, EditOutcome>,
356    S: TableEditorState,
357{
358    fn handle(&mut self, event: &crossterm::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}