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