rat_ftable/edit/
vec.rs

1//! Specialized editing in a table. Keeps a Vec of
2//! the row-data.
3//!
4//! A widget that renders the table and can render
5//! an edit-widget on top.
6//!
7//! __Examples__
8//! For examples go to the rat-widget crate.
9//! There is `examples/table_edit2.rs`.
10
11use crate::edit::{Mode, TableEditor, TableEditorState};
12use crate::rowselection::RowSelection;
13use crate::util::clear_buf_area;
14use crate::{Table, TableState};
15use log::warn;
16use rat_cursor::HasScreenCursor;
17use rat_event::util::MouseFlags;
18use rat_event::{HandleEvent, Outcome, Regular, ct_event, try_flow};
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/// This widget keeps a `Vec<RowData>` and modifies it.
29///
30/// It's parameterized with a `Editor` widget, that renders
31/// the input line and handles events.
32#[allow(clippy::type_complexity)]
33pub struct EditableTableVec<'a, E>
34where
35    E: TableEditor + 'a,
36{
37    table: Box<
38        dyn for<'b> Fn(
39                &'b [<<E as TableEditor>::State as TableEditorState>::Value],
40            ) -> Table<'b, RowSelection>
41            + 'a,
42    >,
43    editor: E,
44}
45
46/// State for EditTable.
47///
48/// Contains `mode` to differentiate between edit/non-edit.
49/// This will lock the focus to the input line while editing.
50///
51pub struct EditableTableVecState<S>
52where
53    S: TableEditorState,
54{
55    /// Editing mode.
56    pub mode: Mode,
57
58    /// Backing table.
59    pub table: TableState<RowSelection>,
60    /// Editor
61    pub editor: S,
62
63    /// Data store
64    pub data: Vec<S::Value>,
65
66    pub mouse: MouseFlags,
67}
68
69impl<'a, E> EditableTableVec<'a, E>
70where
71    E: TableEditor + 'a,
72{
73    /// Create a new editable table.
74    ///
75    /// A bit tricky bc lifetimes of the table-data.
76    ///
77    /// * table: constructor for the Table widget. This gets a `&[Value]` slice
78    ///   to display and returns the configured table.
79    /// * editor: editor widget.
80    pub fn new(
81        table: impl for<'b> Fn(
82            &'b [<<E as TableEditor>::State as TableEditorState>::Value],
83        ) -> Table<'b, RowSelection>
84        + 'a,
85        editor: E,
86    ) -> Self {
87        Self {
88            table: Box::new(table),
89            editor,
90        }
91    }
92}
93
94impl<'a, E> Debug for EditableTableVec<'a, E>
95where
96    E: Debug,
97    E: TableEditor + 'a,
98{
99    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
100        f.debug_struct("EditVec")
101            .field("table", &"..dyn..")
102            .field("editor", &self.editor)
103            .finish()
104    }
105}
106
107impl<'a, E> StatefulWidget for EditableTableVec<'a, E>
108where
109    E: TableEditor + 'a,
110{
111    type State = EditableTableVecState<E::State>;
112
113    #[allow(clippy::collapsible_else_if)]
114    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
115        let table = (self.table)(&state.data);
116        table.render(area, buf, &mut state.table);
117
118        if state.mode == Mode::Insert || state.mode == Mode::Edit {
119            if let Some(row) = state.table.selected() {
120                // but it might be out of view
121                if let Some((row_area, cell_areas)) = state.table.row_cells(row) {
122                    clear_buf_area(row_area, buf);
123                    self.editor
124                        .render(row_area, &cell_areas, buf, &mut state.editor);
125                }
126            } else {
127                if cfg!(feature = "perf_warnings") {
128                    warn!("no row selection, not rendering editor");
129                }
130            }
131        }
132    }
133}
134
135impl<S> Default for EditableTableVecState<S>
136where
137    S: TableEditorState + Default,
138{
139    fn default() -> Self {
140        Self {
141            mode: Mode::View,
142            table: Default::default(),
143            editor: S::default(),
144            data: Vec::default(),
145            mouse: Default::default(),
146        }
147    }
148}
149
150impl<S> Debug for EditableTableVecState<S>
151where
152    S: TableEditorState + Debug,
153    S::Value: Debug,
154{
155    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
156        f.debug_struct("EditVecState")
157            .field("mode", &self.mode)
158            .field("table", &self.table)
159            .field("editor", &self.editor)
160            .field("editor_data", &self.data)
161            .field("mouse", &self.mouse)
162            .finish()
163    }
164}
165
166impl<S> HasFocus for EditableTableVecState<S>
167where
168    S: TableEditorState,
169{
170    fn build(&self, builder: &mut FocusBuilder) {
171        builder.leaf_widget(self);
172    }
173
174    fn focus(&self) -> FocusFlag {
175        self.table.focus()
176    }
177
178    fn area(&self) -> Rect {
179        self.table.area()
180    }
181
182    fn navigable(&self) -> Navigation {
183        match self.mode {
184            Mode::View => self.table.navigable(),
185            Mode::Edit | Mode::Insert => Navigation::Lock,
186        }
187    }
188
189    fn is_focused(&self) -> bool {
190        self.table.is_focused()
191    }
192
193    fn lost_focus(&self) -> bool {
194        self.table.lost_focus()
195    }
196
197    fn gained_focus(&self) -> bool {
198        self.table.gained_focus()
199    }
200}
201
202impl<S> HasScreenCursor for EditableTableVecState<S>
203where
204    S: TableEditorState + HasScreenCursor,
205{
206    fn screen_cursor(&self) -> Option<(u16, u16)> {
207        match self.mode {
208            Mode::View => None,
209            Mode::Edit | Mode::Insert => self.editor.screen_cursor(),
210        }
211    }
212}
213
214impl<S> RelocatableState for EditableTableVecState<S>
215where
216    S: TableEditorState + RelocatableState,
217{
218    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
219        match self.mode {
220            Mode::View => {}
221            Mode::Edit | Mode::Insert => {
222                self.editor.relocate(shift, clip);
223            }
224        }
225    }
226}
227
228impl<S> EditableTableVecState<S>
229where
230    S: TableEditorState,
231{
232    pub fn new(editor: S) -> Self {
233        Self {
234            mode: Mode::View,
235            table: TableState::new(),
236            editor,
237            data: Default::default(),
238            mouse: Default::default(),
239        }
240    }
241
242    pub fn named(name: &str, editor: S) -> Self {
243        Self {
244            mode: Mode::View,
245            table: TableState::named(name),
246            editor,
247            data: Default::default(),
248            mouse: Default::default(),
249        }
250    }
251}
252
253impl<S> EditableTableVecState<S>
254where
255    S: TableEditorState,
256{
257    /// Set the edit data.
258    pub fn set_value(&mut self, data: Vec<S::Value>) {
259        self.data = data;
260    }
261
262    /// Get a clone of the edit data.
263    ///
264    /// This will not contain the data of the currently
265    /// editing row. Instead, it will contain a default value
266    /// at the current edit position.
267    pub fn value(&self) -> Vec<S::Value> {
268        self.data.clone()
269    }
270
271    /// Clear the widget.
272    pub fn clear(&mut self) {
273        self.mode = Mode::View;
274        self.table.clear_offset();
275        self.table.clear_selection();
276        self.data.clear();
277        // todo: self.editor.clear() is missing?
278    }
279
280    /// Editing is active?
281    pub fn is_editing(&self) -> bool {
282        self.mode == Mode::Edit || self.mode == Mode::Insert
283    }
284
285    /// Is the current edit an insert?
286    pub fn is_insert(&self) -> bool {
287        self.mode == Mode::Insert
288    }
289
290    /// Remove the item at the selected row.
291    pub fn remove(&mut self, row: usize) {
292        if self.mode != Mode::View {
293            return;
294        }
295        if row < self.data.len() {
296            self.data.remove(row);
297            self.table.items_removed(row, 1);
298            if !self.table.scroll_to_row(row) {
299                self.table.scroll_to_row(row.saturating_sub(1));
300            }
301        }
302    }
303
304    /// Edit a new item inserted at the selected row.
305    pub fn edit_new<'a>(&mut self, row: usize, ctx: &'a S::Context<'a>) -> Result<(), S::Err> {
306        if self.mode != Mode::View {
307            return Ok(());
308        }
309        let value = self.editor.create_value(ctx)?;
310        self.editor.set_value(value.clone(), ctx)?;
311        self.data.insert(row, value);
312        self._start(0, row, Mode::Insert);
313        Ok(())
314    }
315
316    /// Edit the item at the selected row.
317    pub fn edit<'a>(
318        &mut self,
319        col: usize,
320        row: usize,
321        ctx: &'a S::Context<'a>,
322    ) -> Result<(), S::Err> {
323        if self.mode != Mode::View {
324            return Ok(());
325        }
326        {
327            let value = &self.data[row];
328            self.editor.set_value(value.clone(), ctx)?;
329        }
330        self._start(col, row, Mode::Edit);
331        Ok(())
332    }
333
334    fn _start(&mut self, col: usize, row: usize, mode: Mode) {
335        if self.table.is_focused() {
336            // black magic
337            FocusBuilder::build_for(&self.editor).first();
338        }
339
340        self.mode = mode;
341        if self.mode == Mode::Insert {
342            self.table.items_added(row, 1);
343        }
344        self.table.move_to(row);
345        self.table.scroll_to_col(col);
346        self.editor.set_focused_col(col);
347    }
348
349    /// Cancel editing.
350    ///
351    /// Updates the state to remove the edited row.
352    pub fn cancel(&mut self) {
353        if self.mode == Mode::View {
354            return;
355        }
356        let Some(row) = self.table.selected_checked() else {
357            return;
358        };
359        if self.mode == Mode::Insert {
360            self.data.remove(row);
361            self.table.items_removed(row, 1);
362        }
363        self._stop();
364    }
365
366    /// Commit the changes in the editor.
367    pub fn commit<'a>(&mut self, ctx: &'a S::Context<'a>) -> Result<(), S::Err> {
368        if self.mode == Mode::View {
369            return Ok(());
370        }
371        let Some(row) = self.table.selected_checked() else {
372            return Ok(());
373        };
374        {
375            let value = self.editor.value(ctx)?;
376            if let Some(value) = value {
377                self.data[row] = value;
378            } else {
379                self.data.remove(row);
380                self.table.items_removed(row, 1);
381            }
382        }
383        self._stop();
384        Ok(())
385    }
386
387    pub fn commit_and_append<'a>(&mut self, ctx: &'a S::Context<'a>) -> Result<(), S::Err> {
388        self.commit(ctx)?;
389        if let Some(row) = self.table.selected_checked() {
390            self.edit_new(row + 1, ctx)?;
391        }
392        Ok(())
393    }
394
395    pub fn commit_and_edit<'a>(&mut self, ctx: &'a S::Context<'a>) -> Result<(), S::Err> {
396        let Some(row) = self.table.selected_checked() else {
397            return Ok(());
398        };
399
400        self.commit(ctx)?;
401        if row + 1 < self.data.len() {
402            self.table.select(Some(row + 1));
403            self.edit(0, row + 1, ctx)?;
404        }
405        Ok(())
406    }
407
408    fn _stop(&mut self) {
409        self.mode = Mode::View;
410        self.table.scroll_to_col(0);
411    }
412}
413
414impl<'a, S> HandleEvent<crossterm::event::Event, &'a S::Context<'a>, Result<Outcome, S::Err>>
415    for EditableTableVecState<S>
416where
417    S: HandleEvent<crossterm::event::Event, &'a S::Context<'a>, Result<Outcome, S::Err>>,
418    S: TableEditorState,
419{
420    #[allow(clippy::collapsible_if)]
421    fn handle(
422        &mut self,
423        event: &crossterm::event::Event,
424        ctx: &'a S::Context<'a>,
425    ) -> Result<Outcome, S::Err> {
426        if self.mode == Mode::Edit || self.mode == Mode::Insert {
427            if self.is_focused() {
428                try_flow!(match self.editor.handle(event, ctx)? {
429                    r => {
430                        if let Some(col) = self.editor.focused_col() {
431                            if self.table.scroll_to_col(col) {
432                                Outcome::Changed
433                            } else {
434                                r
435                            }
436                        } else {
437                            r
438                        }
439                    }
440                });
441
442                try_flow!(match event {
443                    ct_event!(keycode press Esc) => {
444                        self.cancel();
445                        Outcome::Changed
446                    }
447                    ct_event!(keycode press Enter) => {
448                        if self.table.selected_checked() < Some(self.table.rows().saturating_sub(1))
449                        {
450                            self.commit_and_edit(ctx)?;
451                            Outcome::Changed
452                        } else {
453                            self.commit_and_append(ctx)?;
454                            Outcome::Changed
455                        }
456                    }
457                    ct_event!(keycode press Up) => {
458                        self.commit(ctx)?;
459                        if self.data.is_empty() {
460                            self.edit_new(0, ctx)?;
461                        } else if let Some(row) = self.table.selected_checked()
462                            && row > 0
463                        {
464                            self.table.select(Some(row));
465                        }
466                        Outcome::Changed
467                    }
468                    ct_event!(keycode press Down) => {
469                        self.commit(ctx)?;
470                        if self.data.is_empty() {
471                            self.edit_new(0, ctx)?;
472                        } else if let Some(row) = self.table.selected_checked()
473                            && row + 1 < self.data.len()
474                        {
475                            self.table.select(Some(row + 1));
476                        }
477                        Outcome::Changed
478                    }
479                    _ => Outcome::Continue,
480                });
481            }
482
483            Ok(Outcome::Continue)
484        } else {
485            if self.table.gained_focus() {
486                if self.data.is_empty() {
487                    self.edit_new(0, ctx)?;
488                }
489            }
490
491            try_flow!(match event {
492                ct_event!(mouse any for m) if self.mouse.doubleclick(self.table.table_area, m) => {
493                    if let Some((col, row)) = self.table.cell_at_clicked((m.column, m.row)) {
494                        self.edit(col, row, ctx)?;
495                        Outcome::Changed
496                    } else {
497                        Outcome::Continue
498                    }
499                }
500                _ => Outcome::Continue,
501            });
502
503            if self.is_focused() {
504                try_flow!(match event {
505                    ct_event!(keycode press Insert) => {
506                        if let Some(row) = self.table.selected_checked() {
507                            self.edit_new(row, ctx)?;
508                        }
509                        Outcome::Changed
510                    }
511                    ct_event!(keycode press Delete) => {
512                        if let Some(row) = self.table.selected_checked() {
513                            self.remove(row);
514                            if self.data.is_empty() {
515                                self.edit_new(0, ctx)?;
516                            }
517                        }
518                        Outcome::Changed
519                    }
520                    ct_event!(keycode press Enter) | ct_event!(keycode press F(2)) => {
521                        if let Some(row) = self.table.selected_checked() {
522                            self.edit(0, row, ctx)?;
523                            Outcome::Changed
524                        } else if self.table.rows() == 0 {
525                            self.edit_new(0, ctx)?;
526                            Outcome::Changed
527                        } else {
528                            Outcome::Continue
529                        }
530                    }
531                    ct_event!(keycode press Down) => {
532                        if let Some(row) = self.table.selected_checked() {
533                            if row == self.table.rows().saturating_sub(1) {
534                                self.edit_new(row + 1, ctx)?;
535                                Outcome::Changed
536                            } else {
537                                Outcome::Continue
538                            }
539                        } else if self.table.rows() == 0 {
540                            self.edit_new(0, ctx)?;
541                            Outcome::Changed
542                        } else {
543                            Outcome::Continue
544                        }
545                    }
546                    _ => {
547                        Outcome::Continue
548                    }
549                });
550            }
551
552            try_flow!(self.table.handle(event, Regular));
553
554            Ok(Outcome::Continue)
555        }
556    }
557}