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