wip: cell editing

This commit is contained in:
Jeremy Wall 2024-11-02 21:42:17 -04:00
parent a5177bda18
commit 1aa9224c15
5 changed files with 113 additions and 101 deletions

12
Cargo.lock generated
View File

@ -660,6 +660,7 @@ dependencies = [
"futures",
"ratatui",
"thiserror",
"tui-textarea",
]
[[package]]
@ -772,6 +773,17 @@ dependencies = [
"syn",
]
[[package]]
name = "tui-textarea"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae"
dependencies = [
"crossterm",
"ratatui",
"unicode-width 0.2.0",
]
[[package]]
name = "unicode-ident"
version = "1.0.13"

View File

@ -13,3 +13,4 @@ csvx = "0.1.17"
futures = "0.3.31"
ratatui = "0.29.0"
thiserror = "1.0.65"
tui-textarea = "0.7.0"

View File

@ -11,40 +11,6 @@ use csvx;
use std::borrow::Borrow;
pub enum CellValue {
Text(String),
Float(f64),
Integer(i64),
Formula(String),
}
impl CellValue {
pub fn to_csv_value(&self) -> String {
match self {
CellValue::Text(v) => format!("\"{}\"", v),
CellValue::Float(v) => format!("{}", v),
CellValue::Integer(v) => format!("{}", v),
CellValue::Formula(v) => format!("{}", v),
}
}
pub fn text<S: Into<String>>(value: S) -> CellValue {
CellValue::Text(Into::<String>::into(value))
}
pub fn formula<S: Into<String>>(value: S) -> CellValue {
CellValue::Formula(Into::<String>::into(value))
}
pub fn float(value: f64) -> CellValue {
CellValue::Float(value)
}
pub fn int(value: i64) -> CellValue {
CellValue::Integer(value)
}
}
/// The Address in a [Tbl].
#[derive(Default, Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
pub struct Address {
@ -87,6 +53,10 @@ impl Tbl {
})
}
pub fn get_raw_value(&self, Address {row, col}: &Address) -> String {
self.csv.get_raw_table()[*row][*col].clone()
}
pub fn move_to(&mut self, addr: Address) -> Result<()> {
let (row, col) = self.dimensions();
if addr.row >= row || addr.col >= col {
@ -96,8 +66,7 @@ impl Tbl {
Ok(())
}
pub fn update_entry(&mut self, address: Address, value: CellValue) -> Result<()> {
// TODO(zaphar): At some point we'll need to store the graph of computation
pub fn update_entry(&mut self, address: &Address, value: String) -> Result<()> {
let (row, col) = self.dimensions();
if address.row >= row {
// then we need to add rows.
@ -112,7 +81,7 @@ impl Tbl {
}
Ok(self
.csv
.update(address.col, address.row, value.to_csv_value())?)
.update(address.col, address.row, value.trim())?)
}
}

View File

@ -3,11 +3,11 @@ use super::*;
#[test]
fn test_dimensions_calculation() {
let mut tbl = Tbl::new();
tbl.update_entry(Address::new(0, 0), CellValue::Text(String::new())).unwrap();
tbl.update_entry(&Address::new(0, 0), String::new()).unwrap();
assert_eq!((1, 1), tbl.dimensions());
tbl.update_entry(Address::new(0, 10), CellValue::Text(String::new())).unwrap();
tbl.update_entry(&Address::new(0, 10), String::new()).unwrap();
assert_eq!((1, 11), tbl.dimensions());
tbl.update_entry(Address::new(20, 5), CellValue::Text(String::new())).unwrap();
tbl.update_entry(&Address::new(20, 5), String::new()).unwrap();
assert_eq!((21, 11), tbl.dimensions());
}

View File

@ -8,12 +8,13 @@ use anyhow::{Context, Result};
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use ratatui::{
self,
layout::{Constraint, Flex},
style::{Color, Stylize},
layout::{Constraint, Flex, Layout},
style::{Color, Modifier, Style, Stylize},
text::{Line, Text},
widgets::{Block, Cell, Row, Table, Widget},
Frame,
};
use tui_textarea::TextArea;
#[derive(Default, Debug, PartialEq)]
pub enum Modality {
@ -30,19 +31,25 @@ pub struct AppState {
// Interaction Modalities
// * Navigate
// * Edit
pub struct Workspace {
pub struct Workspace<'ws> {
name: String,
tbl: Tbl,
state: AppState,
text_area: TextArea<'ws>,
dirty: bool,
}
impl Workspace {
impl<'ws> Workspace<'ws> {
pub fn new<S: Into<String>>(tbl: Tbl, name: S) -> Self {
Self {
let mut ws = Self {
tbl,
name: name.into(),
state: AppState::default(),
}
text_area: reset_text_area("".to_owned()),
dirty: false,
};
ws.handle_movement_change();
ws
}
pub fn load(path: &PathBuf) -> Result<Self> {
@ -64,13 +71,13 @@ impl Workspace {
// TODO(jwall): Add a row automatically if necessary?
let mut loc = self.tbl.location.clone();
let (row, _) = self.tbl.dimensions();
if loc.row < row-1 {
if loc.row < row - 1 {
loc.row += 1;
self.tbl.move_to(loc)?;
}
Ok(())
}
pub fn move_up(&mut self) -> Result<()> {
let mut loc = self.tbl.location.clone();
if loc.row > 0 {
@ -79,7 +86,7 @@ impl Workspace {
}
Ok(())
}
pub fn move_left(&mut self) -> Result<()> {
let mut loc = self.tbl.location.clone();
if loc.col > 0 {
@ -88,13 +95,13 @@ impl Workspace {
}
Ok(())
}
pub fn move_right(&mut self) -> Result<()> {
// TODO(jwall): Add a column automatically if necessary?
let mut loc = self.tbl.location.clone();
let (_, col) = self.tbl.dimensions();
if loc.col < col-1 {
loc.col += 1;
if loc.col < col - 1 {
loc.col += 1;
self.tbl.move_to(loc)?;
}
Ok(())
@ -112,54 +119,53 @@ impl Workspace {
fn handle_edit_event(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Esc => {
self.state.modality = Modality::Navigate;
},
KeyCode::Char('j') => {
self.move_down()?;
},
KeyCode::Char('k') => {
self.move_up()?;
},
KeyCode::Char('h') => {
self.move_left()?;
},
KeyCode::Char('l') => {
self.move_right()?;
},
_ => {
// noop
if let KeyCode::Esc = key.code {
self.state.modality = Modality::Navigate;
self.text_area.set_cursor_line_style(Style::default());
self.text_area.set_cursor_style(Style::default());
let contents = self.text_area.lines().join("\n");
if self.dirty {
let loc = self.tbl.location.clone();
self.tbl.update_entry(&loc, contents)?;
}
return Ok(None);
}
}
if self.text_area.input(key) {
self.dirty = true;
}
Ok(None)
}
fn handle_navigation_event(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Esc => {
self.state.modality = Modality::Navigate;
},
KeyCode::Char('q') => {
return Ok(Some(ExitCode::SUCCESS));
},
KeyCode::Char('j') => {
self.move_down()?;
},
KeyCode::Char('k') => {
self.move_up()?;
},
KeyCode::Char('h') => {
self.move_left()?;
},
KeyCode::Char('l') => {
self.move_right()?;
},
KeyCode::Char('e') => {
self.state.modality = Modality::CellEdit;
},
self.text_area
.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
self.text_area
.set_cursor_style(Style::default().add_modifier(Modifier::SLOW_BLINK));
}
KeyCode::Char('q') => {
return Ok(Some(ExitCode::SUCCESS));
}
KeyCode::Char('j') => {
self.move_down()?;
self.handle_movement_change();
}
KeyCode::Char('k') => {
self.move_up()?;
self.handle_movement_change();
}
KeyCode::Char('h') => {
self.move_left()?;
self.handle_movement_change();
}
KeyCode::Char('l') => {
self.move_right()?;
self.handle_movement_change();
}
_ => {
// noop
}
@ -168,23 +174,48 @@ impl Workspace {
return Ok(None);
}
// navigation methods left, right, up, down
fn handle_movement_change(&mut self) {
let contents = self.tbl.get_raw_value(&self.tbl.location);
self.text_area = reset_text_area(contents);
}
}
impl<'a> Widget for &'a Workspace {
fn reset_text_area<'a>(content: String) -> TextArea<'a> {
let mut text_area = TextArea::from(content.lines());
text_area.set_cursor_line_style(Style::default());
text_area.set_cursor_style(Style::default());
text_area
}
impl<'widget, 'ws: 'widget> Widget for &'widget Workspace<'ws> {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
where
Self: Sized,
{
let block = Block::bordered()
let outer_block = Block::bordered()
.title(Line::from(self.name.as_str()))
.title_bottom(match &self.state.modality {
Modality::Navigate => "navigate",
Modality::CellEdit => "edit",
})
.title_bottom(Line::from(format!("{},{}", self.tbl.location.row, self.tbl.location.col)).right_aligned());
let table = Table::from(&self.tbl).block(block);
table.render(area, buf);
.title_bottom(
Line::from(format!(
"{},{}",
self.tbl.location.row, self.tbl.location.col
))
.right_aligned(),
);
let [edit_rect, table_rect] =
Layout::vertical(&[Constraint::Fill(1), Constraint::Fill(20)])
.vertical_margin(2)
.horizontal_margin(2)
.flex(Flex::Legacy)
.areas(area.clone());
outer_block.render(area, buf);
self.text_area.render(edit_rect, buf);
let table_block = Block::bordered();
let table = Table::from(&self.tbl).block(table_block);
table.render(table_rect, buf);
}
}
@ -209,11 +240,9 @@ impl<'t> From<&Tbl> for Table<'t> {
let content = format!("{}", v);
let cell = Cell::new(Text::raw(content));
match (value.location.row == ri, value.location.col == ci) {
(true, true) =>
cell
.fg(Color::White).underlined(),
_ =>
cell.bg(if ri % 2 == 0 {
(true, true) => cell.fg(Color::White).underlined(),
_ => cell
.bg(if ri % 2 == 0 {
Color::Rgb(57, 61, 71)
} else {
Color::Rgb(165, 169, 160)
@ -223,7 +252,8 @@ impl<'t> From<&Tbl> for Table<'t> {
} else {
Color::Rgb(31, 32, 34)
}),
}.bold()
}
.bold()
}));
Row::new(cells)
})