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