1use crate::_private::NonExhaustive;
11use crate::edit::{Mode, TableEditor, TableEditorState};
12use crate::event::{EditOutcome, TableOutcome};
13use crate::rowselection::RowSelection;
14use crate::{Table, TableSelection, TableState};
15use log::warn;
16use rat_cursor::HasScreenCursor;
17use rat_event::util::MouseFlags;
18use rat_event::{ct_event, flow, HandleEvent, Regular};
19use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
20use rat_reloc::RelocatableState;
21use ratatui::buffer::Buffer;
22use ratatui::layout::Rect;
23use ratatui::prelude::StatefulWidget;
24#[cfg(feature = "unstable-widget-ref")]
25use ratatui::widgets::StatefulWidgetRef;
26use std::fmt::{Debug, Formatter};
27
28#[derive(Debug)]
34pub struct EditableTable<'a, E>
35where
36 E: TableEditor + 'a,
37{
38 table: Table<'a, RowSelection>,
39 editor: E,
40}
41
42pub struct EditableTableState<S> {
48 pub mode: Mode,
50
51 pub table: TableState<RowSelection>,
53 pub editor: S,
55
56 pub mouse: MouseFlags,
57
58 pub non_exhaustive: NonExhaustive,
59}
60
61impl<'a, E> EditableTable<'a, E>
62where
63 E: TableEditor + 'a,
64{
65 pub fn new(table: Table<'a, RowSelection>, editor: E) -> Self {
66 Self { table, editor }
67 }
68}
69
70#[cfg(feature = "unstable-widget-ref")]
71impl<'a, E> StatefulWidgetRef for EditableTable<'a, E>
72where
73 E: TableEditor + 'a,
74{
75 type State = EditableTableState<E::State>;
76
77 fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
78 self.table.render_ref(area, buf, &mut state.table);
79
80 if state.mode == Mode::Edit || state.mode == Mode::Insert {
81 if let Some(row) = state.table.selected_checked() {
82 if let Some((row_area, cell_areas)) = state.table.row_cells(row) {
84 self.editor
85 .render(row_area, &cell_areas, buf, &mut state.editor);
86 }
87 } else {
88 if cfg!(debug_assertions) {
89 warn!("no row selection, not rendering editor");
90 }
91 }
92 }
93 }
94}
95
96impl<'a, E> StatefulWidget for EditableTable<'a, E>
97where
98 E: TableEditor + 'a,
99{
100 type State = EditableTableState<E::State>;
101
102 #[allow(clippy::collapsible_else_if)]
103 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
104 self.table.render(area, buf, &mut state.table);
105
106 if state.mode == Mode::Insert || state.mode == Mode::Edit {
107 if let Some(row) = state.table.selected_checked() {
108 if let Some((row_area, cell_areas)) = state.table.row_cells(row) {
110 self.editor
111 .render(row_area, &cell_areas, buf, &mut state.editor);
112 }
113 } else {
114 if cfg!(debug_assertions) {
115 warn!("no row selection, not rendering editor");
116 }
117 }
118 }
119 }
120}
121
122impl<S> Default for EditableTableState<S>
123where
124 S: Default,
125{
126 fn default() -> Self {
127 Self {
128 mode: Mode::View,
129 table: Default::default(),
130 editor: S::default(),
131 mouse: Default::default(),
132 non_exhaustive: NonExhaustive,
133 }
134 }
135}
136
137impl<S> Debug for EditableTableState<S>
138where
139 S: Debug,
140{
141 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
142 f.debug_struct("EditTableState")
143 .field("mode", &self.mode)
144 .field("table", &self.table)
145 .field("editor", &self.editor)
146 .field("mouse", &self.mouse)
147 .finish()
148 }
149}
150
151impl<S> HasFocus for EditableTableState<S> {
152 fn build(&self, builder: &mut FocusBuilder) {
153 builder.leaf_widget(self);
154 }
155
156 fn focus(&self) -> FocusFlag {
157 self.table.focus()
158 }
159
160 fn area(&self) -> Rect {
161 self.table.area()
162 }
163
164 fn navigable(&self) -> Navigation {
165 match self.mode {
166 Mode::View => self.table.navigable(),
167 Mode::Edit | Mode::Insert => Navigation::Lock,
168 }
169 }
170
171 fn is_focused(&self) -> bool {
172 self.table.is_focused()
173 }
174
175 fn lost_focus(&self) -> bool {
176 self.table.lost_focus()
177 }
178
179 fn gained_focus(&self) -> bool {
180 self.table.gained_focus()
181 }
182}
183
184impl<S> HasScreenCursor for EditableTableState<S>
185where
186 S: HasScreenCursor,
187{
188 fn screen_cursor(&self) -> Option<(u16, u16)> {
189 match self.mode {
190 Mode::View => None,
191 Mode::Edit | Mode::Insert => self.editor.screen_cursor(),
192 }
193 }
194}
195
196impl<S> RelocatableState for EditableTableState<S>
197where
198 S: TableEditorState + RelocatableState,
199{
200 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
201 match self.mode {
202 Mode::View => {}
203 Mode::Edit | Mode::Insert => {
204 self.editor.relocate(shift, clip);
205 }
206 }
207 }
208}
209
210impl<S> EditableTableState<S> {
211 pub fn new(editor: S) -> Self {
213 Self {
214 mode: Mode::View,
215 table: TableState::new(),
216 editor,
217 mouse: Default::default(),
218 non_exhaustive: NonExhaustive,
219 }
220 }
221
222 pub fn named(name: &str, editor: S) -> Self {
224 Self {
225 mode: Mode::View,
226 table: TableState::named(name),
227 editor,
228 mouse: Default::default(),
229 non_exhaustive: NonExhaustive,
230 }
231 }
232}
233
234impl<S> EditableTableState<S>
235where
236 S: TableEditorState,
237{
238 pub fn is_editing(&self) -> bool {
240 self.mode == Mode::Edit || self.mode == Mode::Insert
241 }
242
243 pub fn is_insert(&self) -> bool {
245 self.mode == Mode::Insert
246 }
247
248 pub fn remove(&mut self, row: usize) {
253 if self.mode != Mode::View {
254 return;
255 }
256 self.table.items_removed(row, 1);
257 if !self.table.scroll_to_row(row) {
258 self.table.scroll_to_row(row.saturating_sub(1));
259 }
260 }
261
262 pub fn edit_new(&mut self, row: usize) {
273 if self.mode != Mode::View {
274 return;
275 }
276 self._start(row, Mode::Insert);
277 }
278
279 pub fn edit(&mut self, row: usize) {
290 if self.mode != Mode::View {
291 return;
292 }
293 self._start(row, Mode::Edit);
294 }
295
296 fn _start(&mut self, pos: usize, mode: Mode) {
297 if self.table.is_focused() {
298 FocusBuilder::build_for(&self.editor).first();
299 }
300
301 self.mode = mode;
302 if self.mode == Mode::Insert {
303 self.table.items_added(pos, 1);
304 }
305 self.table.move_to(pos);
306 self.table.scroll_to_col(0);
307 }
308
309 pub fn cancel(&mut self) {
319 if self.mode == Mode::View {
320 return;
321 }
322 let Some(row) = self.table.selected_checked() else {
323 return;
324 };
325 if self.mode == Mode::Insert {
326 self.table.items_removed(row, 1);
327 }
328 self._stop();
329 }
330
331 pub fn commit(&mut self) {
342 if self.mode == Mode::View {
343 return;
344 }
345 self._stop();
346 }
347
348 fn _stop(&mut self) {
349 self.mode = Mode::View;
350 self.table.scroll_to_col(0);
351 }
352}
353
354impl<'a, S> HandleEvent<crossterm::event::Event, S::Context<'a>, EditOutcome>
355 for EditableTableState<S>
356where
357 S: HandleEvent<crossterm::event::Event, S::Context<'a>, EditOutcome>,
358 S: TableEditorState,
359{
360 fn handle(&mut self, event: &crossterm::event::Event, ctx: S::Context<'a>) -> EditOutcome {
361 if self.mode == Mode::Edit || self.mode == Mode::Insert {
362 if self.table.is_focused() {
363 flow!(match self.editor.handle(event, ctx.clone()) {
364 EditOutcome::Continue => EditOutcome::Continue,
365 EditOutcome::Unchanged => EditOutcome::Unchanged,
366 r => {
367 if let Some(col) = self.editor.focused_col() {
368 self.table.scroll_to_col(col);
369 }
370 r
371 }
372 });
373
374 flow!(match event {
375 ct_event!(keycode press Esc) => {
376 EditOutcome::Cancel
377 }
378 ct_event!(keycode press Enter) => {
379 if self.table.selected_checked() < Some(self.table.rows().saturating_sub(1))
380 {
381 EditOutcome::CommitAndEdit
382 } else {
383 EditOutcome::CommitAndAppend
384 }
385 }
386 ct_event!(keycode press Up) => {
387 EditOutcome::Commit
388 }
389 ct_event!(keycode press Down) => {
390 EditOutcome::Commit
391 }
392 _ => EditOutcome::Continue,
393 });
394 }
395 EditOutcome::Continue
396 } else {
397 flow!(match event {
398 ct_event!(mouse any for m) if self.mouse.doubleclick(self.table.table_area, m) => {
399 if self.table.cell_at_clicked((m.column, m.row)).is_some() {
400 EditOutcome::Edit
401 } else {
402 EditOutcome::Continue
403 }
404 }
405 _ => EditOutcome::Continue,
406 });
407
408 if self.table.is_focused() {
409 flow!(match event {
410 ct_event!(keycode press Insert) => {
411 EditOutcome::Insert
412 }
413 ct_event!(keycode press Delete) => {
414 EditOutcome::Remove
415 }
416 ct_event!(keycode press Enter) | ct_event!(keycode press F(2)) => {
417 EditOutcome::Edit
418 }
419 ct_event!(keycode press Down) => {
420 if let Some((_column, row)) = self.table.selection.lead_selection() {
421 if row == self.table.rows().saturating_sub(1) {
422 EditOutcome::Append
423 } else {
424 EditOutcome::Continue
425 }
426 } else {
427 EditOutcome::Continue
428 }
429 }
430 _ => {
431 EditOutcome::Continue
432 }
433 });
434 }
435
436 match self.table.handle(event, Regular) {
437 TableOutcome::Continue => EditOutcome::Continue,
438 TableOutcome::Unchanged => EditOutcome::Unchanged,
439 TableOutcome::Changed => EditOutcome::Changed,
440 TableOutcome::Selected => EditOutcome::Changed,
441 }
442 }
443 }
444}