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