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, event_flow, 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                event_flow!({
429                    let r = self.editor.handle(event, ctx)?;
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                try_flow!(match event {
442                    ct_event!(keycode press Esc) => {
443                        self.cancel();
444                        Outcome::Changed
445                    }
446                    ct_event!(keycode press Enter) => {
447                        if self.table.selected_checked() < Some(self.table.rows().saturating_sub(1))
448                        {
449                            self.commit_and_edit(ctx)?;
450                            Outcome::Changed
451                        } else {
452                            self.commit_and_append(ctx)?;
453                            Outcome::Changed
454                        }
455                    }
456                    ct_event!(keycode press Up) => {
457                        self.commit(ctx)?;
458                        if self.data.is_empty() {
459                            self.edit_new(0, ctx)?;
460                        } else if let Some(row) = self.table.selected_checked()
461                            && row > 0
462                        {
463                            self.table.select(Some(row));
464                        }
465                        Outcome::Changed
466                    }
467                    ct_event!(keycode press Down) => {
468                        self.commit(ctx)?;
469                        if self.data.is_empty() {
470                            self.edit_new(0, ctx)?;
471                        } else if let Some(row) = self.table.selected_checked()
472                            && row + 1 < self.data.len()
473                        {
474                            self.table.select(Some(row + 1));
475                        }
476                        Outcome::Changed
477                    }
478                    _ => Outcome::Continue,
479                });
480            }
481
482            Ok(Outcome::Continue)
483        } else {
484            if self.table.gained_focus() {
485                if self.data.is_empty() {
486                    self.edit_new(0, ctx)?;
487                }
488            }
489
490            try_flow!(match event {
491                ct_event!(mouse any for m) if self.mouse.doubleclick(self.table.table_area, m) => {
492                    if let Some((col, row)) = self.table.cell_at_clicked((m.column, m.row)) {
493                        self.edit(col, row, ctx)?;
494                        Outcome::Changed
495                    } else {
496                        Outcome::Continue
497                    }
498                }
499                _ => Outcome::Continue,
500            });
501
502            if self.is_focused() {
503                try_flow!(match event {
504                    ct_event!(keycode press Insert) => {
505                        if let Some(row) = self.table.selected_checked() {
506                            self.edit_new(row, ctx)?;
507                        }
508                        Outcome::Changed
509                    }
510                    ct_event!(keycode press Delete) => {
511                        if let Some(row) = self.table.selected_checked() {
512                            self.remove(row);
513                            if self.data.is_empty() {
514                                self.edit_new(0, ctx)?;
515                            }
516                        }
517                        Outcome::Changed
518                    }
519                    ct_event!(keycode press Enter) | ct_event!(keycode press F(2)) => {
520                        if let Some(row) = self.table.selected_checked() {
521                            self.edit(0, row, ctx)?;
522                            Outcome::Changed
523                        } else if self.table.rows() == 0 {
524                            self.edit_new(0, ctx)?;
525                            Outcome::Changed
526                        } else {
527                            Outcome::Continue
528                        }
529                    }
530                    ct_event!(keycode press Down) => {
531                        if let Some(row) = self.table.selected_checked() {
532                            if row == self.table.rows().saturating_sub(1) {
533                                self.edit_new(row + 1, ctx)?;
534                                Outcome::Changed
535                            } else {
536                                Outcome::Continue
537                            }
538                        } else if self.table.rows() == 0 {
539                            self.edit_new(0, ctx)?;
540                            Outcome::Changed
541                        } else {
542                            Outcome::Continue
543                        }
544                    }
545                    _ => {
546                        Outcome::Continue
547                    }
548                });
549            }
550
551            try_flow!(self.table.handle(event, Regular));
552
553            Ok(Outcome::Continue)
554        }
555    }
556}