use std::thread::JoinHandle; use ratatui::{ crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, prelude::*, style::Stylize, text::ToLine, widgets::{Block, Borders, Clear, Gauge, Paragraph, Widget, Wrap}, }; use crate::{ config::GlobalConfig, cursor::CursorMove, lua, sheet::{ cell::Cell, eval::EvalFunction, Sheet, }, state::GlobalState, }; use super::{luaeditor::LuaEditor, statusbar::StatusBar}; const DEFAULT_COLUMN_WIDTH: u16 = 10; #[derive(Default)] enum SheetViewMode { #[default] Normal, Insert, Visual, Command, CommandError(String), Script, Processing(Option>>), EditorError(String), } #[derive(Default)] pub struct SheetView { bar: StatusBar, scroll: (u16, u16), selection_anchor: Option<(u16, u16)>, mode: SheetViewMode, editor: LuaEditor, } impl SheetView { pub fn new() -> Self { Self { bar: StatusBar::new() .left(" NORMAL ") .left_style(Style::default().on_magenta()) .middle("Sheet") .right("") .right_style(Style::default().on_red()), scroll: (0, 0), mode: SheetViewMode::Normal, selection_anchor: None, editor: LuaEditor::new(), } } pub fn move_cursor_by(&mut self, delta: (isize, isize)) { let mut state = GlobalState::instance_mut(); state .sheetview .cursor .move_checked(CursorMove::Relative(delta)); } pub fn selection(&self) -> Vec<(u16, u16)> { let mut selection = Vec::new(); if let Some((row, column)) = self.selection_range() { for i in row.0..=row.1 { for j in column.0..=column.1 { selection.push((i, j)) } } } selection } fn is_in_selection(&mut self, row: u16, column: u16) -> bool { if let Some((row_range, column_range)) = self.selection_range() { row >= row_range.0 && row <= row_range.1 && column >= column_range.0 && column <= column_range.1 } else { false } } fn selection_range(&self) -> Option<((u16, u16), (u16, u16))> { let state = GlobalState::instance(); let cursor = &state.sheetview.cursor; if let Some(selection) = self.selection_anchor { let row = if selection.0 as usize > cursor.y() { (cursor.y() as u16, selection.0) } else { (selection.0, cursor.y() as u16) }; let column = if selection.1 as usize > cursor.x() { (cursor.x() as u16, selection.1) } else { (selection.1, cursor.x() as u16) }; Some((row, column)) } else { None } } fn join_process_handle_on_finished(&mut self) { if let SheetViewMode::Processing(opt) = &mut self.mode { let handle = opt.take().unwrap(); if handle.is_finished() { match handle.join().unwrap() { Ok(sheet) => { GlobalState::instance() .sheetview .active_sheet() .unwrap() .write() .unwrap() .apply(sheet); self.mode = SheetViewMode::Script; } Err(message) => self.mode = SheetViewMode::EditorError(message), }; } else { self.mode = SheetViewMode::Processing(Some(handle)); } } } pub fn handle_key_event(&mut self, event: KeyEvent) { match &self.mode { SheetViewMode::Normal => match event.code { KeyCode::Char('j') => self.move_cursor_by((0, 1)), KeyCode::Char('k') => self.move_cursor_by((0, -1)), KeyCode::Char('h') => self.move_cursor_by((-1, 0)), KeyCode::Char('l') => self.move_cursor_by((1, 0)), KeyCode::Char('v') => { let state = GlobalState::instance(); let cursor = &state.sheetview.cursor; self.selection_anchor = Some((cursor.y() as u16, cursor.x() as u16)); self.mode = SheetViewMode::Visual } KeyCode::Char('s') => { self.editor.set_text("function(cell)\n\treturn \"\"\nend"); self.mode = SheetViewMode::Script; } KeyCode::Char(':') => { self.mode = SheetViewMode::Command; self.bar.set_input_mode(true) } KeyCode::Enter => { self.mode = SheetViewMode::Insert; self.bar.set_input_mode(true); } _ => {} }, SheetViewMode::Insert => match event.code { KeyCode::Enter => { let lock = GlobalState::instance().sheetview.active_sheet().unwrap(); let mut sheet = lock.write().unwrap(); let state = GlobalState::instance(); let cursor = &state.sheetview.cursor; if self.selection_anchor.is_some() { self.selection().iter().map(|(r, c)| (*r, *c)).collect() } else { vec![(cursor.y() as u16, cursor.x() as u16)] } .into_iter() .for_each(|(r, c)| { sheet.set_cell(r as usize, c as usize, { let s = self.bar.input().unwrap(); match s.parse() { Ok(n) => Cell::Number(n), Err(_) => Cell::String(s.to_string()), } }) }); self.bar.set_input_mode(false); if self.selection_anchor.is_some() { self.mode = SheetViewMode::Visual; } else { self.mode = SheetViewMode::Normal; } } KeyCode::Esc => { if self.selection_anchor.is_some() { self.mode = SheetViewMode::Visual; } else { self.mode = SheetViewMode::Normal; } self.bar.set_input_mode(false); } _ => self.bar.handle_keyevent(event), }, SheetViewMode::Visual => match event.code { KeyCode::Char('j') => self.move_cursor_by((0, 1)), KeyCode::Char('k') => self.move_cursor_by((0, -1)), KeyCode::Char('h') => self.move_cursor_by((-1, 0)), KeyCode::Char('l') => self.move_cursor_by((1, 0)), KeyCode::Esc | KeyCode::Char('v') => { self.selection_anchor = None; self.mode = SheetViewMode::Normal; } KeyCode::Char('s') => { self.editor.set_text("function(cell)\n\treturn \"\"\nend"); self.mode = SheetViewMode::Script; } KeyCode::Char(':') => { self.mode = SheetViewMode::Command; self.bar.set_input_mode(true) } KeyCode::Enter => { self.mode = SheetViewMode::Insert; self.bar.set_input_mode(true); } _ => {} }, SheetViewMode::Command => match event.code { KeyCode::Enter => { if let Err(error) = lua::get().load(self.bar.input().unwrap_or("")).exec() { self.mode = SheetViewMode::CommandError(error.to_string()) } else if self.selection_anchor.is_some() { self.mode = SheetViewMode::Visual; } else { self.mode = SheetViewMode::Normal; } self.bar.set_input_mode(false); } KeyCode::Esc => { if self.selection_anchor.is_some() { self.mode = SheetViewMode::Visual; } else { self.mode = SheetViewMode::Normal; } self.bar.set_input_mode(false); } _ => self.bar.handle_keyevent(event), }, SheetViewMode::CommandError(_) => match event.code { KeyCode::Esc | KeyCode::Enter => self.mode = SheetViewMode::Normal, _ => {} }, SheetViewMode::Script => match event.code { KeyCode::Char('r') if event.modifiers == KeyModifiers::CONTROL => { let script = self.editor.text(); let (width, height) = { let lock = GlobalState::instance().sheetview.active_sheet().unwrap(); let sheet = lock.read().unwrap(); (sheet.width(), sheet.height()) }; let mut cells = Vec::new(); if self.selection_anchor.is_some() { cells = self .selection() .iter() .map(|(r, c)| (*r as usize, *c as usize)) .collect() } else { for row in 0..height { for column in 0..width { cells.push((row, column)); } } } self.mode = SheetViewMode::Processing(Some( GlobalState::instance() .sheetview .active_sheet() .unwrap() .eval_function(script, cells), )); } KeyCode::Esc => { if self.selection_anchor.is_some() { self.mode = SheetViewMode::Visual; } else { self.mode = SheetViewMode::Normal; } } _ => self.editor.handle_key_event(event), }, SheetViewMode::Processing(_) => { self.join_process_handle_on_finished(); } SheetViewMode::EditorError(_) => match event.code { KeyCode::Esc | KeyCode::Enter => self.mode = SheetViewMode::Script, _ => {} }, } } fn is_editor_visible(&self) -> bool { matches!( self.mode, SheetViewMode::Script | SheetViewMode::Processing(_) | SheetViewMode::EditorError(_), ) } fn is_error_window_visible(&self) -> bool { matches!(self.mode, SheetViewMode::EditorError(_)) } fn is_progress_visible(&self) -> bool { matches!(self.mode, SheetViewMode::Processing(_)) } fn areas(&self, area: Rect) -> (Rect, Rect, Rect, Rect, Rect) { let mut sheet = area; let mut editor = Rect::default(); let mut error = Rect::default(); let mut progress = Rect::default(); let mut command_error = Rect::default(); if let SheetViewMode::CommandError(message) = &self.mode { let layout = Layout::vertical([ Constraint::Min(1), Constraint::Length(message.lines().count().min(15) as u16 + 1), ]) .split(sheet); sheet = layout[0]; command_error = layout[1]; } if self.is_editor_visible() { let layout = Layout::horizontal([Constraint::Min(1), Constraint::Length(50)]).split(sheet); sheet = layout[0]; editor = layout[1]; if self.is_error_window_visible() { let layout = Layout::vertical([Constraint::Min(1), Constraint::Length(10)]).split(editor); editor = layout[0]; error = layout[1]; } if self.is_progress_visible() { let layout = Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).split(editor); editor = layout[0]; progress = layout[1]; } } (sheet, editor, error, progress, command_error) } } impl Widget for &mut SheetView { fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) where Self: Sized, { let theme = GlobalConfig::instance().theme.sheetview.clone(); let progress; let (sheet_area, editor_area, error_area, progress_area, cmd_error_area) = self.areas(area); let separator = Block::default().borders(Borders::RIGHT).border_style( Style::default() .bg(Color::Rgb(29, 32, 33)) .fg(Color::Rgb(29, 32, 33)), ); { let lock = GlobalState::instance().sheetview.active_sheet().unwrap(); let sheet = lock.read().unwrap(); let state = GlobalState::instance(); let cursor = &state.sheetview.cursor; let mut sheet_area_inner = self.bar.area(sheet_area); if self.is_editor_visible() { sheet_area_inner = separator.inner(sheet_area_inner); } let viewport_rows = sheet.height().min(sheet_area_inner.height as usize); let viewport_columns = sheet .width() .min((sheet_area_inner.width / DEFAULT_COLUMN_WIDTH) as usize); if cursor.y() >= viewport_rows + self.scroll.0 as usize { self.scroll.0 = (cursor.y() - viewport_rows) as u16 + 1; } else if cursor.y() < self.scroll.0 as usize { self.scroll.0 = cursor.y() as u16; } if cursor.x() >= viewport_columns + self.scroll.1 as usize { self.scroll.1 = (cursor.x() - viewport_columns) as u16 + 1; } else if cursor.x() < self.scroll.1 as usize { self.scroll.1 = cursor.x() as u16; } for row in 0..viewport_rows as u16 { for column in 0..(viewport_columns + 1) as u16 { let (cell_pos_y, cell_pos_x) = (row + self.scroll.0, column + self.scroll.1); if let Some(cell_ref) = sheet.get_ref(cell_pos_y as usize, cell_pos_x as usize) { let cell = cell_ref.value().to_string() + " "; let line = if (cell_pos_y, cell_pos_x) == (cursor.y() as u16, cursor.x() as u16) { theme .cursor .get(cell_ref, &lua::get()) .unwrap_or_default() .apply(cell.to_line()) } else if self.is_in_selection(cell_pos_y, cell_pos_x) { theme .selection .get(cell_ref, &lua::get()) .unwrap_or_default() .apply(cell.to_line()) } else { theme .cell .get(cell_ref, &lua::get()) .unwrap_or_default() .apply(cell.to_line()) }; let rect = Rect::new( sheet_area_inner.x + column * DEFAULT_COLUMN_WIDTH, sheet_area_inner.y + row, (sheet_area_inner.width - column * DEFAULT_COLUMN_WIDTH) .min(DEFAULT_COLUMN_WIDTH), 1, ); line.render(rect, buf); } } } match &self.mode { SheetViewMode::Command => { self.bar.set_left(" COMMAND "); } SheetViewMode::Insert => { self.bar.set_left(" INSERT "); } _ => { if self.selection_anchor.is_some() { self.bar.set_left(" VISUAL "); } else { self.bar.set_left(" NORMAL "); self.bar.set_middle_alignment(Alignment::Center); self.bar.set_middle("Sheet"); } } } self.bar.render(sheet_area, buf); if self.is_editor_visible() { separator.render(sheet_area, buf); self.editor.render(editor_area, buf) } if let SheetViewMode::EditorError(error_msg) = &self.mode { let lines = error_msg.lines().collect::>(); let block = Block::default() .title_bottom(" Error ") .title_style(Style::default().on_red()) .border_style(Style::default().black().on_black()) .borders(Borders::BOTTOM); let error_inner = block.inner(error_area); block.render(error_area, buf); let text = Text::from_iter(lines.iter().map(|s| s.to_line().white())); Paragraph::new(text) .wrap(Wrap { trim: true }) .bg(Color::Rgb(70, 25, 25)) .render(error_inner, buf); } if let SheetViewMode::CommandError(message) = &self.mode { let lines = message.lines().collect::>(); let block = Block::default() .title_bottom(" Error ") .title_style(Style::default().on_red()) .border_style(Style::default().black().on_black()) .borders(Borders::BOTTOM); let error_inner = block.inner(cmd_error_area); block.render(cmd_error_area, buf); let text = Text::from_iter(lines.iter().map(|s| s.to_line().white())); Paragraph::new(text) .wrap(Wrap { trim: true }) .bg(Color::Rgb(70, 25, 25)) .render(error_inner, buf); } progress = sheet.progress(); } self.join_process_handle_on_finished(); if self.is_progress_visible() { let gauge = Gauge::default() .gauge_style( Style::default() .fg(Color::White) .bg(Color::DarkGray) .add_modifier(Modifier::ITALIC), ) .percent(progress as u16); Clear.render(progress_area, buf); gauge.render(progress_area, buf); } } }