summaryrefslogtreecommitdiff
path: root/src/widgets/sheetview/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/widgets/sheetview/mod.rs')
-rw-r--r--src/widgets/sheetview/mod.rs358
1 files changed, 358 insertions, 0 deletions
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<LuaEditor<'a>>,
+ sheet: SheetId,
+ selection: Option<(u16, u16)>,
+ cursor: (u16, u16),
+ scroll: (u16, u16),
+ error_window: Option<String>,
+ process_handle: Option<JoinHandle<Result<Sheet, String>>>,
+}
+
+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::<Vec<_>>();
+
+ 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);
+ }
+ }
+}