summaryrefslogtreecommitdiff
path: root/src/widgets/sheetview.rs
diff options
context:
space:
mode:
authorNathan Reiner <nathan@nathanreiner.xyz>2024-07-25 00:40:37 +0200
committerNathan Reiner <nathan@nathanreiner.xyz>2024-07-25 00:40:37 +0200
commit58b305b9f6d13007d2ea62775054c95177f81092 (patch)
tree2123cb5379aa69476b1e549e0bcacc08d67fba71 /src/widgets/sheetview.rs
first sketch
Diffstat (limited to 'src/widgets/sheetview.rs')
-rw-r--r--src/widgets/sheetview.rs360
1 files changed, 360 insertions, 0 deletions
diff --git a/src/widgets/sheetview.rs b/src/widgets/sheetview.rs
new file mode 100644
index 0000000..57894a2
--- /dev/null
+++ b/src/widgets/sheetview.rs
@@ -0,0 +1,360 @@
+use std::{
+ sync::{Arc, Mutex},
+ thread::JoinHandle,
+};
+
+use layout::Offset;
+use mlua::Function;
+use ratatui::{
+ crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
+ prelude::*,
+ style::Stylize,
+ text::ToLine,
+ widgets::{block::BlockExt, Block, Clear, Gauge, Widget},
+};
+
+use crate::sheet::{
+ cell::Cell,
+ register::{Register, SheetId},
+};
+
+use super::luaeditor::LuaEditor;
+
+const DEFAULT_COLUMN_WIDTH: u16 = 10;
+
+#[derive(Debug)]
+pub struct SheetView<'a> {
+ block: Option<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<Option<String>>>,
+ process_progress: Arc<Mutex<usize>>,
+}
+
+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,
+ process_progress: Arc::new(Mutex::new(0)),
+ }
+ }
+
+ 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() {
+ self.error_window = handle.join().unwrap();
+ 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();
+ *self.process_progress.lock().unwrap() = 0;
+
+ let lua = crate::lua::new_instance().unwrap();
+ let result = lua
+ .load(script.clone())
+ .set_name("Temp Script")
+ .eval::<Function>();
+ match result {
+ Ok(_) => {
+ let (width, height) = {
+ let lock = Register::get(self.sheet).unwrap();
+ let sheet = lock.write().unwrap();
+ (sheet.width(), sheet.height())
+ };
+
+ let mut cells = Vec::new();
+
+ if let Some(_) = self.selection {
+ cells = self.selection()
+ } else {
+ for row in 0..height {
+ for column in 0..width {
+ cells.push((row as u16, column as u16));
+ }
+ }
+ }
+
+ {
+ let process_progress = Arc::clone(&self.process_progress);
+ let sheet = self.sheet;
+ self.process_handle = Some(std::thread::spawn(move || {
+ let lua = crate::lua::new_instance().unwrap();
+ let func = lua
+ .load(script)
+ .set_name("Temp Script")
+ .eval::<Function>()
+ .unwrap();
+ for (i, (row, column)) in cells.iter().enumerate() {
+ *process_progress.lock().unwrap() = i * 100 / cells.len();
+ let cellref = {
+ let lock = Register::get(sheet).unwrap();
+ let sheet = lock.read().unwrap();
+ sheet.get_ref(*row as usize, *column as usize)
+ };
+ if let Err(error) = func.call::<_, Cell>(cellref) {
+ return Some(error.to_string());
+ }
+ }
+
+ None
+ }));
+ }
+ }
+ Err(error) => {
+ self.error_window = Some(error.to_string());
+ }
+ }
+ }
+ 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\t\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 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::<Vec<_>>();
+ 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);
+ }
+ }
+
+ 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 progress = *self.process_progress.lock().unwrap();
+ 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);
+ }
+ }
+}