rat_widget/list/
edit.rs

1//!
2//! Adds inline editing support for List.
3//!
4
5use crate::event::EditOutcome;
6use crate::list::selection::RowSelection;
7use crate::list::{List, ListSelection, ListState};
8use log::warn;
9use rat_event::util::MouseFlags;
10use rat_event::{ct_event, flow, HandleEvent, MouseOnly, Outcome, Regular};
11use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
12use rat_reloc::RelocatableState;
13use rat_text::HasScreenCursor;
14use ratatui::buffer::Buffer;
15use ratatui::layout::Rect;
16use ratatui::widgets::StatefulWidget;
17
18/// Editing mode.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Mode {
21    View,
22    Edit,
23    Insert,
24}
25
26/// Edit-List widget.
27///
28/// Contains the base list and the edit-widget.
29#[derive(Debug, Default)]
30pub struct EditList<'a, E>
31where
32    E: StatefulWidget + 'a,
33{
34    list: List<'a, RowSelection>,
35    editor: E,
36}
37
38/// State & event-handling.
39///
40/// Contains `mode` to differentiate between edit/non-edit.
41/// This will lock the focus to the input line while editing.
42///
43#[derive(Debug)]
44pub struct EditListState<S> {
45    /// Editing mode.
46    pub mode: Mode,
47
48    /// List state
49    pub list: ListState<RowSelection>,
50    /// EditorState. Some indicates editing is active.
51    pub editor: S,
52
53    /// Flags for mouse interaction.
54    pub mouse: MouseFlags,
55}
56
57impl<'a, E> EditList<'a, E>
58where
59    E: StatefulWidget + 'a,
60{
61    pub fn new(list: List<'a, RowSelection>, edit: E) -> Self {
62        Self { list, editor: edit }
63    }
64}
65
66impl<'a, E> StatefulWidget for EditList<'a, E>
67where
68    E: StatefulWidget + 'a,
69{
70    type State = EditListState<E::State>;
71
72    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
73        self.list.render(area, buf, &mut state.list);
74
75        if state.mode == Mode::Insert || state.mode == Mode::Edit {
76            if let Some(row) = state.list.selected() {
77                // but it might be out of xview
78                if let Some(row_area) = state.list.row_area(row) {
79                    self.editor.render(row_area, buf, &mut state.editor);
80                }
81            } else {
82                if cfg!(debug_assertions) {
83                    warn!("no row selection, not rendering editor");
84                }
85            }
86        }
87    }
88}
89
90impl<S> Default for EditListState<S>
91where
92    S: Default,
93{
94    fn default() -> Self {
95        Self {
96            mode: Mode::View,
97            list: Default::default(),
98            editor: S::default(),
99            mouse: Default::default(),
100        }
101    }
102}
103
104impl<S> HasFocus for EditListState<S>
105where
106    S: HasFocus,
107{
108    fn build(&self, builder: &mut FocusBuilder) {
109        builder.leaf_widget(self);
110    }
111
112    fn focus(&self) -> FocusFlag {
113        match self.mode {
114            Mode::View => self.list.focus(),
115            Mode::Edit => self.editor.focus(),
116            Mode::Insert => self.editor.focus(),
117        }
118    }
119
120    fn area(&self) -> Rect {
121        self.list.area()
122    }
123}
124
125impl<S> RelocatableState for EditListState<S>
126where
127    S: RelocatableState,
128{
129    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
130        self.editor.relocate(shift, clip);
131        self.list.relocate(shift, clip);
132    }
133}
134
135impl<S> HasScreenCursor for EditListState<S>
136where
137    S: HasScreenCursor,
138{
139    fn screen_cursor(&self) -> Option<(u16, u16)> {
140        match self.mode {
141            Mode::View => None,
142            Mode::Edit | Mode::Insert => self.editor.screen_cursor(),
143        }
144    }
145}
146
147impl<S> EditListState<S> {
148    /// New state.
149    pub fn new(editor: S) -> Self {
150        Self {
151            mode: Mode::View,
152            list: Default::default(),
153            editor,
154            mouse: Default::default(),
155        }
156    }
157
158    /// New state with a named focus.
159    pub fn named(name: &str, editor: S) -> Self {
160        Self {
161            mode: Mode::View,
162            list: ListState::named(name),
163            editor,
164            mouse: Default::default(),
165        }
166    }
167}
168
169impl<S> EditListState<S>
170where
171    S: HasFocus,
172{
173    /// Editing is active?
174    pub fn is_editing(&self) -> bool {
175        self.mode == Mode::Edit || self.mode == Mode::Insert
176    }
177
178    /// Is the current edit an insert?
179    pub fn is_insert(&self) -> bool {
180        self.mode == Mode::Insert
181    }
182
183    /// Remove the item at the selected row.
184    ///
185    /// This doesn't change the actual list of items, but does
186    /// all the bookkeeping with the list-state.
187    pub fn remove(&mut self, row: usize) {
188        if self.mode != Mode::View {
189            return;
190        }
191        self.list.items_removed(row, 1);
192        if !self.list.scroll_to(row) {
193            self.list.scroll_to(row.saturating_sub(1));
194        }
195    }
196
197    /// Edit a new item inserted at the selected row.
198    ///
199    /// The editor state must be initialized to an appropriate state
200    /// beforehand.
201    ///
202    /// This does all the bookkeeping with the list-state and
203    /// switches the mode to Mode::Insert.
204    pub fn edit_new(&mut self, row: usize) {
205        if self.mode != Mode::View {
206            return;
207        }
208        self._start(row, Mode::Insert);
209    }
210
211    /// Edit the item at the selected row.
212    ///
213    /// The editor state must be initialized to an appropriate state
214    /// beforehand.
215    ///
216    /// This does all the bookkeeping with the list-state and
217    /// switches the mode to Mode::Edit.
218    pub fn edit(&mut self, row: usize) {
219        if self.mode != Mode::View {
220            return;
221        }
222        self._start(row, Mode::Edit);
223    }
224
225    fn _start(&mut self, pos: usize, mode: Mode) {
226        if self.list.is_focused() {
227            self.list.focus().set(false);
228            self.editor.focus().set(true);
229        }
230
231        self.mode = mode;
232        if self.mode == Mode::Insert {
233            self.list.items_added(pos, 1);
234        }
235        self.list.move_to(pos);
236    }
237
238    /// Cancel editing.
239    ///
240    /// This doesn't reset the edit-widget.
241    ///
242    /// But it does all the bookkeeping with the list-state and
243    /// switches the mode back to Mode::View.
244    pub fn cancel(&mut self) {
245        if self.mode == Mode::View {
246            return;
247        }
248        let Some(row) = self.list.selected() else {
249            return;
250        };
251        if self.mode == Mode::Insert {
252            self.list.items_removed(row, 1);
253        }
254        self._stop();
255    }
256
257    /// Commit the changes in the editor.
258    ///
259    /// This doesn't copy the data back from the editor to the
260    /// row-item.
261    ///
262    /// But it does all the bookkeeping with the list-state and
263    /// switches the mode back to Mode::View.
264    pub fn commit(&mut self) {
265        if self.mode == Mode::View {
266            return;
267        }
268        self._stop();
269    }
270
271    fn _stop(&mut self) {
272        self.mode = Mode::View;
273        if self.editor.is_focused() {
274            self.list.focus.set(true);
275            self.editor.focus().set(false);
276        }
277    }
278}
279
280impl<S, C> HandleEvent<crossterm::event::Event, C, EditOutcome> for EditListState<S>
281where
282    S: HandleEvent<crossterm::event::Event, C, EditOutcome>,
283    S: HandleEvent<crossterm::event::Event, MouseOnly, EditOutcome>,
284    S: HasFocus,
285{
286    fn handle(&mut self, event: &crossterm::event::Event, ctx: C) -> EditOutcome {
287        if self.mode == Mode::Edit || self.mode == Mode::Insert {
288            if self.editor.is_focused() {
289                flow!(self.editor.handle(event, ctx));
290
291                flow!(match event {
292                    ct_event!(keycode press Esc) => {
293                        EditOutcome::Cancel
294                    }
295                    ct_event!(keycode press Enter) => {
296                        EditOutcome::Commit
297                    }
298                    ct_event!(keycode press Tab) => {
299                        if self.list.selected() < Some(self.list.rows().saturating_sub(1)) {
300                            EditOutcome::CommitAndEdit
301                        } else {
302                            EditOutcome::CommitAndAppend
303                        }
304                    }
305                    ct_event!(keycode press Up) => {
306                        EditOutcome::Commit
307                    }
308                    ct_event!(keycode press Down) => {
309                        EditOutcome::Commit
310                    }
311                    _ => EditOutcome::Continue,
312                });
313            } else {
314                flow!(self.editor.handle(event, MouseOnly));
315            }
316        } else {
317            flow!(match event {
318                ct_event!(mouse any for m) if self.mouse.doubleclick(self.list.inner, m) => {
319                    if self.list.row_at_clicked((m.column, m.row)).is_some() {
320                        EditOutcome::Edit
321                    } else {
322                        EditOutcome::Continue
323                    }
324                }
325                _ => EditOutcome::Continue,
326            });
327
328            if self.list.is_focused() {
329                flow!(match event {
330                    ct_event!(keycode press Insert) => {
331                        EditOutcome::Insert
332                    }
333                    ct_event!(keycode press Delete) => {
334                        EditOutcome::Remove
335                    }
336                    ct_event!(keycode press Enter) | ct_event!(keycode press F(2)) => {
337                        EditOutcome::Edit
338                    }
339                    ct_event!(keycode press Down) => {
340                        if let Some(row) = self.list.selection.lead_selection() {
341                            if row == self.list.rows().saturating_sub(1) {
342                                EditOutcome::Append
343                            } else {
344                                EditOutcome::Continue
345                            }
346                        } else {
347                            EditOutcome::Continue
348                        }
349                    }
350                    _ => {
351                        EditOutcome::Continue
352                    }
353                });
354                flow!(match self.list.handle(event, Regular) {
355                    Outcome::Continue => EditOutcome::Continue,
356                    Outcome::Unchanged => EditOutcome::Unchanged,
357                    Outcome::Changed => EditOutcome::Changed,
358                });
359            }
360
361            flow!(self.list.handle(event, MouseOnly));
362        }
363
364        EditOutcome::Continue
365    }
366}
367
368/// Handle extended edit-events.
369///
370/// List events are only handled if focus is true.
371/// Mouse events are processed if they are in range.
372///
373/// The qualifier indicates which event-handler for EState will
374/// be called. Or it can be used to pass in some context.
375pub fn handle_edit_events<S, C>(
376    state: &mut EditListState<S>,
377    focus: bool,
378    event: &crossterm::event::Event,
379    qualifier: C,
380) -> EditOutcome
381where
382    S: HandleEvent<crossterm::event::Event, C, EditOutcome>,
383    S: HandleEvent<crossterm::event::Event, MouseOnly, EditOutcome>,
384    S: HasFocus,
385{
386    state.list.focus.set(focus);
387    state.handle(event, qualifier)
388}