From 6ca07d6af8a338e76817d06c6c6c6f13e64fba9c Mon Sep 17 00:00:00 2001 From: Nathan Reiner Date: Fri, 26 Jul 2024 10:32:55 +0200 Subject: add neosheetrc support --- src/widgets/sheetview.rs | 336 ---------------------------------------- src/widgets/sheetview/mod.rs | 358 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 358 insertions(+), 336 deletions(-) delete mode 100644 src/widgets/sheetview.rs create mode 100644 src/widgets/sheetview/mod.rs (limited to 'src/widgets') diff --git a/src/widgets/sheetview.rs b/src/widgets/sheetview.rs deleted file mode 100644 index c4f7a73..0000000 --- a/src/widgets/sheetview.rs +++ /dev/null @@ -1,336 +0,0 @@ -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}, - eval::EvalFunction, Sheet, -}; - -use super::luaeditor::LuaEditor; - -const DEFAULT_COLUMN_WIDTH: u16 = 10; - -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); - } - } -} diff --git a/src/widgets/sheetview/mod.rs b/src/widgets/sheetview/mod.rs new file mode 100644 index 0000000..37e3d16 --- /dev/null +++ b/src/widgets/sheetview/mod.rs @@ -0,0 +1,358 @@ +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, sheet::{ + eval::EvalFunction, + register::{Register, SheetId}, + Sheet, +}}; + +use super::luaeditor::LuaEditor; + +const DEFAULT_COLUMN_WIDTH: u16 = 10; + +pub struct SheetView<'a> { + block: Block<'a>, + 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: Block::default() + .title_bottom(" Sheet ") + .title_style(Style::default().on_magenta().black()) + .borders(Borders::BOTTOM) + .border_style( + Style::default() + .bg(Color::Rgb(29, 32, 33)) + .fg(Color::Rgb(29, 32, 33)), + ), + luaeditor: None, + sheet, + selection: None, + cursor: (0, 0), + scroll: (0, 0), + error_window: None, + process_handle: None, + } + } + + 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"); + self.luaeditor = Some(editor) + } + _ => {} + } + } + } + + fn areas(&self, area: Rect) -> (Rect, Rect, Rect, Rect) { + let mut sheet = area; + let mut editor = Rect::default(); + let mut error = Rect::default(); + let mut progress = Rect::default(); + + if self.luaeditor.is_some() { + let layout = + Layout::horizontal([Constraint::Min(1), Constraint::Length(50)]).split(sheet); + sheet = layout[0]; + editor = layout[1]; + + if self.error_window.is_some() { + let layout = + Layout::vertical([Constraint::Min(1), Constraint::Length(10)]).split(editor); + editor = layout[0]; + error = layout[1]; + } + + if self.process_handle.is_some() { + let layout = + Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).split(editor); + editor = layout[0]; + progress = layout[1]; + } + } + + (sheet, editor, error, progress) + } +} + +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) = 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 = Register::get(self.sheet).unwrap(); + let sheet = lock.read().unwrap(); + + let mut sheet_area_inner = self.block.inner(sheet_area); + + if self.luaeditor.is_some() { + sheet_area_inner = separator.inner(sheet_area_inner); + } + + let viewport_rows = sheet.height().min(sheet_area.height as usize); + let viewport_columns = sheet + .width() + .min((sheet_area_inner.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 { + theme.cursor.apply(cell.to_line()) + } else if self.is_in_selection(cell_pos_y, cell_pos_x) { + theme.selection.apply(cell.to_line()) + } else if (row + column) % 2 == 0 { + theme.cell.0.apply(cell.to_line()) + } else { + theme.cell.1.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); + } + } + } + + self.block.clone().render(sheet_area, buf); + + if let Some(textarea) = &mut self.luaeditor { + separator.render(self.block.inner(sheet_area), buf); + textarea.render(editor_area, buf) + } + + if let Some(error_msg) = &self.error_window { + 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); + } + + progress = sheet.progress(); + } + + self.join_process_handle_on_finished(); + + if self.process_handle.is_some() { + let gauge = Gauge::default() + .gauge_style( + Style::default() + .fg(Color::White) + .bg(Color::DarkGray) + .add_modifier(Modifier::ITALIC), + ) + .percent(progress as u16); + + Clear::default().render(progress_area, buf); + gauge.render(progress_area, buf); + } + } +} -- cgit v1.2.3-70-g09d2