use std::thread::JoinHandle; use layout::Offset; use ratatui::{ crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, prelude::*, style::Stylize, text::ToLine, widgets::{block::BlockExt, Block, Clear, Gauge, Widget}, }; use crate::sheet::{ register::{Register, SheetId}, EvalFunction, Sheet, }; use super::luaeditor::LuaEditor; const DEFAULT_COLUMN_WIDTH: u16 = 10; #[derive(Debug)] pub struct SheetView<'a> { block: Option>, luaeditor: Option>, sheet: SheetId, selection: Option<(u16, u16)>, cursor: (u16, u16), scroll: (u16, u16), error_window: Option, process_handle: Option>>, } impl<'a> SheetView<'a> { pub fn new(sheet: SheetId) -> Self { Self { block: None, luaeditor: None, sheet, selection: None, cursor: (0, 0), scroll: (0, 0), error_window: None, process_handle: None, } } pub fn block(mut self, block: Block<'a>) -> Self { self.block = Some(block); self } pub fn sheet(mut self, sheet: SheetId) -> Self { self.sheet = sheet; self } pub fn move_cursor_by(&mut self, delta: (i32, i32)) { let lock = Register::get(self.sheet).unwrap(); let sheet = lock.read().unwrap(); self.cursor.0 = ((self.cursor.0 as i32) + delta.0) .max(0) .min(sheet.height() as i32 - 1) as u16; self.cursor.1 = ((self.cursor.1 as i32) + delta.1) .max(0) .min(sheet.width() as i32 - 1) as u16; } pub fn select_mode(&mut self, flag: bool) { if flag { self.selection = Some(self.cursor); } else { self.selection = None; } } pub fn is_select_mode(&self) -> bool { self.selection.is_some() } pub fn toggle_select_mode(&mut self) { self.select_mode(!self.is_select_mode()); } 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))> { if let Some(selection) = self.selection { let row = if selection.0 > self.cursor.0 { (self.cursor.0, selection.0) } else { (selection.0, self.cursor.0) }; let column = if selection.1 > self.cursor.1 { (self.cursor.1, selection.1) } else { (selection.1, self.cursor.1) }; Some((row, column)) } else { None } } fn join_process_handle_on_finished(&mut self) { if let Some(handle) = self.process_handle.take() { if handle.is_finished() { match handle.join().unwrap() { Ok(sheet) => Register::get(self.sheet) .unwrap() .write() .unwrap() .apply(sheet), Err(message) => self.error_window = Some(message), }; self.process_handle = None; } else { self.process_handle = Some(handle); } } } pub fn handle_key_event(&mut self, event: KeyEvent) { if self.process_handle.is_some() { self.join_process_handle_on_finished() } else if let Some(_) = &self.error_window { self.error_window = None; } else if let Some(textarea) = &mut self.luaeditor { match event.code { KeyCode::Char('r') if event.modifiers == KeyModifiers::CONTROL => { let script = textarea.text(); let (width, height) = { let lock = Register::get(self.sheet).unwrap(); let sheet = lock.read().unwrap(); (sheet.width(), sheet.height()) }; let mut cells = Vec::new(); if let Some(_) = self.selection { 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.process_handle = Some( Register::get(self.sheet) .unwrap() .eval_function(script, cells), ); } KeyCode::Esc => { self.luaeditor = None; } _ => { textarea.handle_key_event(event); } } } else { match event.code { KeyCode::Char('j') => self.move_cursor_by((1, 0)), KeyCode::Char('k') => self.move_cursor_by((-1, 0)), KeyCode::Char('h') => self.move_cursor_by((0, -1)), KeyCode::Char('l') => self.move_cursor_by((0, 1)), KeyCode::Char('v') => self.toggle_select_mode(), KeyCode::Char('s') => { let editor = LuaEditor::new("function(cell)\n\treturn \"\"\nend") .block(Some(Block::bordered().title(" Temp Script "))); self.luaeditor = Some(editor) } _ => {} } } } } impl Widget for &mut SheetView<'_> { fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) where Self: Sized, { let progress; { let lock = Register::get(self.sheet).unwrap(); let sheet = lock.read().unwrap(); let splits = Layout::horizontal([Constraint::Min(1), Constraint::Length(50)]).split(area); let block_area = if self.luaeditor.is_some() { splits[0] } else { area }; let sheet_area = self.block.inner_if_some(block_area); let viewport_rows = sheet.height().min(sheet_area.height as usize); let viewport_columns = sheet .width() .min((sheet_area.width / DEFAULT_COLUMN_WIDTH) as usize); if self.cursor.0 >= viewport_rows as u16 + self.scroll.0 { self.scroll.0 = self.cursor.0 - viewport_rows as u16 + 1; } else if self.cursor.0 < self.scroll.0 { self.scroll.0 = self.cursor.0; } if self.cursor.1 >= viewport_columns as u16 + self.scroll.1 { self.scroll.1 = self.cursor.1 - viewport_columns as u16 + 1; } else if self.cursor.1 < self.scroll.1 { self.scroll.1 = self.cursor.1; } 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) = sheet.get_cell(cell_pos_y as usize, cell_pos_x as usize) { let cell = cell.to_string() + " "; let line = if (cell_pos_y, cell_pos_x) == self.cursor { cell.to_line().bg(Color::Rgb(120, 90, 90)).white() } else if self.is_in_selection(cell_pos_y, cell_pos_x) { cell.to_line().bg(Color::Rgb(120, 120, 90)).white() } else if (row + column) % 2 == 0 { cell.to_line().bg(Color::Rgb(30, 30, 30)).white() } else { cell.to_line().bg(Color::Rgb(50, 50, 50)).white() }; let rect = Rect::new( sheet_area.x + column * DEFAULT_COLUMN_WIDTH, sheet_area.y + row, (sheet_area.width - column * DEFAULT_COLUMN_WIDTH) .min(DEFAULT_COLUMN_WIDTH), 1, ); line.render(rect, buf); } } } self.block.render(block_area, buf); if let Some(textarea) = &mut self.luaeditor { textarea.render(splits[1], buf) } if let Some(error_msg) = &self.error_window { let lines = error_msg.lines().collect::>(); let height = lines.len() as u16 + 2; let width = lines.iter().map(|s| s.len()).max().unwrap_or(0) as u16 + 2; let centered = Rect::new( (area.width - width) / 2, (area.height - height) / 2, width, height, ); let block = Block::bordered().red().on_black().bold().title(" Error "); let inner_centered = block.inner(centered); Clear::default().render(centered, buf); block.render(centered, buf); for (i, line) in lines.iter().enumerate() { let line = line.replace('\t', " ").red().on_black().bold(); line.render(inner_centered.offset(Offset { x: 0, y: i as i32 }), buf); } } progress = sheet.progress(); } self.join_process_handle_on_finished(); if self.process_handle.is_some() { let height = 3; let width = area.width / 2; let centered = Rect::new( (area.width - width) / 2, (area.height - height) / 2, width, height, ); let gauge = Gauge::default() .block(Block::bordered().title("Progress").on_black()) .gauge_style( Style::default() .fg(Color::White) .bg(Color::Black) .add_modifier(Modifier::ITALIC), ) .percent(progress as u16); Clear::default().render(centered, buf); gauge.render(centered, buf); } } }