use ratatui::{ crossterm::event::{KeyCode, KeyEvent}, prelude::*, style::Stylize, text::ToLine, widgets::{Paragraph, Widget}, }; use crate::{ config::GlobalConfig, cursor::CursorMove, lua, sheet::cell::Cell, state::{view::mode::Mode, window::Window, GlobalState}, }; use super::statusbar::StatusBar; const DEFAULT_COLUMN_WIDTH: u16 = 10; #[derive(Default)] pub struct SheetView { bar: StatusBar, scroll: (usize, usize), } 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), } } fn set_mode(&self, mode: Mode) { GlobalState::instance_mut().sheetview.mode = mode; } fn move_cursor(&self, cm: CursorMove) { GlobalState::instance_mut() .sheetview .cursor .move_checked(cm) } fn cursor(&self) -> (usize, usize) { let state = GlobalState::instance(); (state.sheetview.cursor.y(), state.sheetview.cursor.x()) } fn start_selection(&self) { let mut state = GlobalState::instance_mut(); state.sheetview.selection_anchor = Some(self.cursor()); state.sheetview.mode = Mode::Visual; } fn open_editor(&self) { let mut state = GlobalState::instance_mut(); state.editor.buffer.set_lines_from_string( r#"require('neosheet') .state .view .active:map(function(cell) return "" end)"#, ); state.set_focus(Window::Editor) } pub fn handle_key_event(&mut self, event: KeyEvent) { let mode = { GlobalState::instance().sheetview.mode }; match mode { Mode::Normal => match event.code { KeyCode::Char('j') => self.move_cursor(CursorMove::Down(1)), KeyCode::Char('k') => self.move_cursor(CursorMove::Up(1)), KeyCode::Char('h') => self.move_cursor(CursorMove::Left(1)), KeyCode::Char('l') => self.move_cursor(CursorMove::Right(1)), KeyCode::Char('v') => self.start_selection(), KeyCode::Char('s') => self.open_editor(), KeyCode::Char(':') => { self.set_mode(Mode::Command); self.bar.set_input_mode(true); } KeyCode::Enter => { self.set_mode(Mode::Insert); self.bar.set_input_mode(true); } _ => {} }, Mode::Insert => match event.code { KeyCode::Enter => { let mut state = GlobalState::instance_mut(); let lock = state.sheetview.active_sheet().unwrap(); let mut sheet = lock.write().unwrap(); state .sheetview .selection_or_cursor() .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()), } }) }); state.sheetview.cancel_mode(); self.bar.set_input_mode(false); } KeyCode::Esc => { GlobalState::instance_mut().sheetview.cancel_mode(); self.bar.set_input_mode(false); } _ => self.bar.handle_keyevent(event), }, Mode::Visual => match event.code { KeyCode::Char('j') => self.move_cursor(CursorMove::Down(1)), KeyCode::Char('k') => self.move_cursor(CursorMove::Up(1)), KeyCode::Char('h') => self.move_cursor(CursorMove::Left(1)), KeyCode::Char('l') => self.move_cursor(CursorMove::Right(1)), KeyCode::Char('v') => self.start_selection(), KeyCode::Char('s') => self.open_editor(), KeyCode::Char(':') => { self.set_mode(Mode::Command); self.bar.set_input_mode(true); } KeyCode::Enter => { self.set_mode(Mode::Insert); self.bar.set_input_mode(true); } _ => {} }, Mode::Command => match event.code { KeyCode::Enter => { if let Err(_error) = lua::get().load(self.bar.input().unwrap_or("")).exec() { // TODO: push errors to buffer } GlobalState::instance_mut().sheetview.cancel_mode(); self.bar.set_input_mode(false); } KeyCode::Esc => { GlobalState::instance_mut().sheetview.cancel_mode(); self.bar.set_input_mode(false); } _ => self.bar.handle_keyevent(event), }, } } } 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 state = GlobalState::instance(); let lock = state.sheetview.active_sheet().unwrap(); let sheet = lock.read().unwrap(); let cursor = &state.sheetview.cursor; let sheet_area_inner = self.bar.area(area); 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 { self.scroll.0 = (cursor.y() - viewport_rows) + 1; } else if cursor.y() < self.scroll.0 { self.scroll.0 = cursor.y(); } if cursor.x() >= viewport_columns + self.scroll.1 as usize { self.scroll.1 = (cursor.x() - viewport_columns) + 1; } else if cursor.x() < self.scroll.1 { self.scroll.1 = cursor.x(); } theme .background .get((), &lua::get()) .unwrap_or_default() .apply(Paragraph::default()) .render(area, buf); for row in 0..viewport_rows { for column in 0..(viewport_columns + 1) { 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(), cursor.x()) { theme .cursor .get(cell_ref, &lua::get()) .unwrap_or_default() .apply(cell.to_line()) } else if state.sheetview.selection_contains(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 as u16) * DEFAULT_COLUMN_WIDTH, sheet_area_inner.y + (row as u16), (sheet_area_inner.width - (column as u16) * DEFAULT_COLUMN_WIDTH) .min(DEFAULT_COLUMN_WIDTH), 1, ); line.render(rect, buf); } } } match state.sheetview.mode { Mode::Command => { self.bar.set_left(" COMMAND "); } Mode::Insert => { self.bar.set_left(" INSERT "); } _ => { if state.sheetview.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(area, buf); } }