mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-23 05:19:48 -04:00
wip: cell editing
This commit is contained in:
parent
a5177bda18
commit
1aa9224c15
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -660,6 +660,7 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"tui-textarea",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -772,6 +773,17 @@ dependencies = [
|
|||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.13"
|
version = "1.0.13"
|
||||||
|
@ -13,3 +13,4 @@ csvx = "0.1.17"
|
|||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
ratatui = "0.29.0"
|
ratatui = "0.29.0"
|
||||||
thiserror = "1.0.65"
|
thiserror = "1.0.65"
|
||||||
|
tui-textarea = "0.7.0"
|
||||||
|
@ -11,40 +11,6 @@ use csvx;
|
|||||||
|
|
||||||
use std::borrow::Borrow;
|
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].
|
/// The Address in a [Tbl].
|
||||||
#[derive(Default, Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
|
#[derive(Default, Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
|
||||||
pub struct Address {
|
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<()> {
|
pub fn move_to(&mut self, addr: Address) -> Result<()> {
|
||||||
let (row, col) = self.dimensions();
|
let (row, col) = self.dimensions();
|
||||||
if addr.row >= row || addr.col >= col {
|
if addr.row >= row || addr.col >= col {
|
||||||
@ -96,8 +66,7 @@ impl Tbl {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_entry(&mut self, address: Address, value: CellValue) -> Result<()> {
|
pub fn update_entry(&mut self, address: &Address, value: String) -> Result<()> {
|
||||||
// TODO(zaphar): At some point we'll need to store the graph of computation
|
|
||||||
let (row, col) = self.dimensions();
|
let (row, col) = self.dimensions();
|
||||||
if address.row >= row {
|
if address.row >= row {
|
||||||
// then we need to add rows.
|
// then we need to add rows.
|
||||||
@ -112,7 +81,7 @@ impl Tbl {
|
|||||||
}
|
}
|
||||||
Ok(self
|
Ok(self
|
||||||
.csv
|
.csv
|
||||||
.update(address.col, address.row, value.to_csv_value())?)
|
.update(address.col, address.row, value.trim())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,11 +3,11 @@ use super::*;
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_dimensions_calculation() {
|
fn test_dimensions_calculation() {
|
||||||
let mut tbl = Tbl::new();
|
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());
|
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());
|
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());
|
assert_eq!((21, 11), tbl.dimensions());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
152
src/ui/mod.rs
152
src/ui/mod.rs
@ -8,12 +8,13 @@ use anyhow::{Context, Result};
|
|||||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
self,
|
self,
|
||||||
layout::{Constraint, Flex},
|
layout::{Constraint, Flex, Layout},
|
||||||
style::{Color, Stylize},
|
style::{Color, Modifier, Style, Stylize},
|
||||||
text::{Line, Text},
|
text::{Line, Text},
|
||||||
widgets::{Block, Cell, Row, Table, Widget},
|
widgets::{Block, Cell, Row, Table, Widget},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
use tui_textarea::TextArea;
|
||||||
|
|
||||||
#[derive(Default, Debug, PartialEq)]
|
#[derive(Default, Debug, PartialEq)]
|
||||||
pub enum Modality {
|
pub enum Modality {
|
||||||
@ -30,19 +31,25 @@ pub struct AppState {
|
|||||||
// Interaction Modalities
|
// Interaction Modalities
|
||||||
// * Navigate
|
// * Navigate
|
||||||
// * Edit
|
// * Edit
|
||||||
pub struct Workspace {
|
pub struct Workspace<'ws> {
|
||||||
name: String,
|
name: String,
|
||||||
tbl: Tbl,
|
tbl: Tbl,
|
||||||
state: AppState,
|
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 {
|
pub fn new<S: Into<String>>(tbl: Tbl, name: S) -> Self {
|
||||||
Self {
|
let mut ws = Self {
|
||||||
tbl,
|
tbl,
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
state: AppState::default(),
|
state: AppState::default(),
|
||||||
}
|
text_area: reset_text_area("".to_owned()),
|
||||||
|
dirty: false,
|
||||||
|
};
|
||||||
|
ws.handle_movement_change();
|
||||||
|
ws
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(path: &PathBuf) -> Result<Self> {
|
pub fn load(path: &PathBuf) -> Result<Self> {
|
||||||
@ -64,13 +71,13 @@ impl Workspace {
|
|||||||
// TODO(jwall): Add a row automatically if necessary?
|
// TODO(jwall): Add a row automatically if necessary?
|
||||||
let mut loc = self.tbl.location.clone();
|
let mut loc = self.tbl.location.clone();
|
||||||
let (row, _) = self.tbl.dimensions();
|
let (row, _) = self.tbl.dimensions();
|
||||||
if loc.row < row-1 {
|
if loc.row < row - 1 {
|
||||||
loc.row += 1;
|
loc.row += 1;
|
||||||
self.tbl.move_to(loc)?;
|
self.tbl.move_to(loc)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_up(&mut self) -> Result<()> {
|
pub fn move_up(&mut self) -> Result<()> {
|
||||||
let mut loc = self.tbl.location.clone();
|
let mut loc = self.tbl.location.clone();
|
||||||
if loc.row > 0 {
|
if loc.row > 0 {
|
||||||
@ -79,7 +86,7 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_left(&mut self) -> Result<()> {
|
pub fn move_left(&mut self) -> Result<()> {
|
||||||
let mut loc = self.tbl.location.clone();
|
let mut loc = self.tbl.location.clone();
|
||||||
if loc.col > 0 {
|
if loc.col > 0 {
|
||||||
@ -88,13 +95,13 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_right(&mut self) -> Result<()> {
|
pub fn move_right(&mut self) -> Result<()> {
|
||||||
// TODO(jwall): Add a column automatically if necessary?
|
// TODO(jwall): Add a column automatically if necessary?
|
||||||
let mut loc = self.tbl.location.clone();
|
let mut loc = self.tbl.location.clone();
|
||||||
let (_, col) = self.tbl.dimensions();
|
let (_, col) = self.tbl.dimensions();
|
||||||
if loc.col < col-1 {
|
if loc.col < col - 1 {
|
||||||
loc.col += 1;
|
loc.col += 1;
|
||||||
self.tbl.move_to(loc)?;
|
self.tbl.move_to(loc)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -112,54 +119,53 @@ impl Workspace {
|
|||||||
|
|
||||||
fn handle_edit_event(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
|
fn handle_edit_event(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
|
||||||
if key.kind == KeyEventKind::Press {
|
if key.kind == KeyEventKind::Press {
|
||||||
match key.code {
|
if let KeyCode::Esc = key.code {
|
||||||
KeyCode::Esc => {
|
self.state.modality = Modality::Navigate;
|
||||||
self.state.modality = Modality::Navigate;
|
self.text_area.set_cursor_line_style(Style::default());
|
||||||
},
|
self.text_area.set_cursor_style(Style::default());
|
||||||
KeyCode::Char('j') => {
|
let contents = self.text_area.lines().join("\n");
|
||||||
self.move_down()?;
|
if self.dirty {
|
||||||
},
|
let loc = self.tbl.location.clone();
|
||||||
KeyCode::Char('k') => {
|
self.tbl.update_entry(&loc, contents)?;
|
||||||
self.move_up()?;
|
|
||||||
},
|
|
||||||
KeyCode::Char('h') => {
|
|
||||||
self.move_left()?;
|
|
||||||
},
|
|
||||||
KeyCode::Char('l') => {
|
|
||||||
self.move_right()?;
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
// noop
|
|
||||||
}
|
}
|
||||||
|
return Ok(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if self.text_area.input(key) {
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_navigation_event(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
|
fn handle_navigation_event(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
|
||||||
if key.kind == KeyEventKind::Press {
|
if key.kind == KeyEventKind::Press {
|
||||||
match key.code {
|
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') => {
|
KeyCode::Char('e') => {
|
||||||
self.state.modality = Modality::CellEdit;
|
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
|
// noop
|
||||||
}
|
}
|
||||||
@ -168,23 +174,48 @@ impl Workspace {
|
|||||||
return Ok(None);
|
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)
|
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
let block = Block::bordered()
|
let outer_block = Block::bordered()
|
||||||
.title(Line::from(self.name.as_str()))
|
.title(Line::from(self.name.as_str()))
|
||||||
.title_bottom(match &self.state.modality {
|
.title_bottom(match &self.state.modality {
|
||||||
Modality::Navigate => "navigate",
|
Modality::Navigate => "navigate",
|
||||||
Modality::CellEdit => "edit",
|
Modality::CellEdit => "edit",
|
||||||
})
|
})
|
||||||
.title_bottom(Line::from(format!("{},{}", self.tbl.location.row, self.tbl.location.col)).right_aligned());
|
.title_bottom(
|
||||||
let table = Table::from(&self.tbl).block(block);
|
Line::from(format!(
|
||||||
table.render(area, buf);
|
"{},{}",
|
||||||
|
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 content = format!("{}", v);
|
||||||
let cell = Cell::new(Text::raw(content));
|
let cell = Cell::new(Text::raw(content));
|
||||||
match (value.location.row == ri, value.location.col == ci) {
|
match (value.location.row == ri, value.location.col == ci) {
|
||||||
(true, true) =>
|
(true, true) => cell.fg(Color::White).underlined(),
|
||||||
cell
|
_ => cell
|
||||||
.fg(Color::White).underlined(),
|
.bg(if ri % 2 == 0 {
|
||||||
_ =>
|
|
||||||
cell.bg(if ri % 2 == 0 {
|
|
||||||
Color::Rgb(57, 61, 71)
|
Color::Rgb(57, 61, 71)
|
||||||
} else {
|
} else {
|
||||||
Color::Rgb(165, 169, 160)
|
Color::Rgb(165, 169, 160)
|
||||||
@ -223,7 +252,8 @@ impl<'t> From<&Tbl> for Table<'t> {
|
|||||||
} else {
|
} else {
|
||||||
Color::Rgb(31, 32, 34)
|
Color::Rgb(31, 32, 34)
|
||||||
}),
|
}),
|
||||||
}.bold()
|
}
|
||||||
|
.bold()
|
||||||
}));
|
}));
|
||||||
Row::new(cells)
|
Row::new(cells)
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user