feat: wire in ironcalc to the ui

This commit is contained in:
Jeremy Wall 2024-11-16 10:51:38 -05:00
parent a27d2871c2
commit c55b1cdac3
6 changed files with 310 additions and 252 deletions

View File

@ -1,35 +1,51 @@
use std::path::PathBuf; use std::cmp::max;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use ironcalc::{ use ironcalc::{
base::{ base::{
locale, types::{SheetData, Worksheet}, Model types::{SheetData, Worksheet},
worksheet::WorksheetDimension,
Model,
}, },
export::save_to_xlsx, export::save_to_xlsx,
import::load_from_xlsx, import::load_from_xlsx,
}; };
use crate::ui::Address;
#[cfg(test)]
mod test;
/// A spreadsheet book with some internal state tracking. /// A spreadsheet book with some internal state tracking.
pub struct Book { pub struct Book {
model: Model, pub(crate) model: Model,
current_sheet: u32, pub current_sheet: u32,
current_location: (i32, i32), pub location: crate::ui::Address,
// TODO(zaphar): Because the ironcalc model is sparse we need to track our render size
// separately
} }
impl Book { impl Book {
/// Construct a new book from a Model /// Construct a new book from a Model
pub fn new(model: Model) -> Self { pub fn new(model: Model) -> Self {
Self { Self {
model, model,
current_sheet: 0, current_sheet: 0,
current_location: (0, 0), location: Address::default(),
} }
} }
pub fn new_from_xlsx(path: &str) -> Result<Self> {
Ok(Self::new(load_from_xlsx(path, "en", "America/New_York")?))
}
pub fn evaluate(&mut self) {
self.model.evaluate();
}
// TODO(zaphar): Should I support ICalc? // TODO(zaphar): Should I support ICalc?
/// Construct a new book from a path. /// Construct a new book from a path.
pub fn new_from_xlsx(path: &str, locale: &str, tz: &str) -> Result<Self> { pub fn new_from_xlsx_with_locale(path: &str, locale: &str, tz: &str) -> Result<Self> {
Ok(Self::new(load_from_xlsx(path, locale, tz)?)) Ok(Self::new(load_from_xlsx(path, locale, tz)?))
} }
@ -39,6 +55,15 @@ impl Book {
Ok(()) Ok(())
} }
pub fn get_all_sheets_identifiers(&self) -> Vec<(String, u32)> {
self.model
.workbook
.worksheets
.iter()
.map(|sheet| (sheet.get_name(), sheet.get_sheet_id()))
.collect()
}
/// Get the currently set sheets name. /// Get the currently set sheets name.
pub fn get_sheet_name(&self) -> Result<&str> { pub fn get_sheet_name(&self) -> Result<&str> {
Ok(&self.get_sheet()?.name) Ok(&self.get_sheet()?.name)
@ -49,49 +74,90 @@ impl Book {
Ok(&self.get_sheet()?.sheet_data) Ok(&self.get_sheet()?.sheet_data)
} }
pub fn move_to(&mut self, loc: Address) -> Result<()> {
// FIXME(zaphar): Check that this is safe first.
self.location.row = loc.row;
self.location.col = loc.col;
Ok(())
}
/// Get a cells formatted content. /// Get a cells formatted content.
pub fn get_cell_rendered(&self) -> Result<String> { pub fn get_current_cell_rendered(&self) -> Result<String> {
Ok(self.get_cell_addr_rendered(self.location.row, self.location.col)?)
}
// TODO(zaphar): Use Address here too
pub fn get_cell_addr_rendered(&self, row: usize, col: usize) -> Result<String> {
Ok(self Ok(self
.model .model
.get_formatted_cell_value( .get_formatted_cell_value(self.current_sheet, row as i32, col as i32)
self.current_sheet,
self.current_location.0,
self.current_location.1,
)
.map_err(|s| anyhow!("Unable to format cell {}", s))?) .map_err(|s| anyhow!("Unable to format cell {}", s))?)
} }
/// Get a cells actual content as a string. /// Get a cells actual content as a string.
pub fn get_cell_contents(&self) -> Result<String> { pub fn get_current_cell_contents(&self) -> Result<String> {
Ok(self Ok(self
.model .model
.get_cell_content( .get_cell_content(
self.current_sheet, self.current_sheet,
self.current_location.0, self.location.row as i32,
self.current_location.1, self.location.col as i32,
) )
.map_err(|s| anyhow!("Unable to format cell {}", s))?) .map_err(|s| anyhow!("Unable to format cell {}", s))?)
} }
pub fn edit_cell(&mut self, value: String) -> Result<()> { /// Update the current cell in a book.
/// This update won't be reflected until you call `Book::evaluate`.
pub fn edit_current_cell<S: Into<String>>(&mut self, value: S) -> Result<()> {
self.update_entry(&self.location.clone(), value)?;
Ok(())
}
/// Update an entry in the current sheet for a book.
/// This update won't be reflected until you call `Book::evaluate`.
pub fn update_entry<S: Into<String>>(&mut self, location: &Address, value: S) -> Result<()> {
self.model self.model
.set_user_input( .set_user_input(
self.current_sheet, self.current_sheet,
self.current_location.0, location.row as i32,
self.current_location.1, location.col as i32,
value, value.into(),
) )
.map_err(|e| anyhow!("Invalid cell contents: {}", e))?; .map_err(|e| anyhow!("Invalid cell contents: {}", e))?;
Ok(()) Ok(())
} }
pub fn insert_rows(&mut self, row_idx: usize, count: usize) -> Result<()> {
Ok(self
.model
.insert_rows(self.current_sheet, row_idx as i32, count as i32)
.map_err(|e| anyhow!("Unable to insert row(s): {}", e))?)
}
pub fn insert_columns(&mut self, col_idx: usize, count: usize) -> Result<()> {
Ok(self
.model
.insert_columns(self.current_sheet, col_idx as i32, count as i32)
.map_err(|e| anyhow!("Unable to insert column(s): {}", e))?)
}
/// Get the current sheets dimensions. This is a somewhat expensive calculation. /// Get the current sheets dimensions. This is a somewhat expensive calculation.
pub fn get_dimensions(&self) -> Result<(usize, usize)> { pub fn get_dimensions(&self) -> Result<WorksheetDimension> {
let dimensions = self.get_sheet()?.dimension(); Ok(self.get_sheet()?.dimension())
Ok(( }
dimensions.max_row.try_into()?,
dimensions.max_column.try_into()?, // Get the size of the current sheet as a `(row_count, column_count)`
)) pub fn get_size(&self) -> Result<(usize, usize)> {
let sheet = &self.get_sheet()?.sheet_data;
let mut row_count = 0 as i32;
let mut col_count = 0 as i32;
for (ri, cols) in sheet.iter() {
row_count = max(*ri, row_count);
for (ci, _) in cols.iter() {
col_count = max(*ci, col_count);
}
}
Ok((row_count as usize, col_count as usize))
} }
/// Select a sheet by name. /// Select a sheet by name.
@ -129,19 +195,17 @@ impl Book {
false false
} }
fn get_sheet(&self) -> Result<&Worksheet> { pub(crate) fn get_sheet(&self) -> Result<&Worksheet> {
Ok(self Ok(self
.model .model
.workbook .workbook
.worksheet(self.current_sheet) .worksheet(self.current_sheet)
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?) .map_err(|s| anyhow!("Invalid Worksheet: {}", s))?)
} }
}
fn get_sheet_mut(&mut self) -> Result<&mut Worksheet> { impl Default for Book {
Ok(self fn default() -> Self {
.model Book::new(Model::new_empty("default_name", "en", "America/New_York").unwrap())
.workbook
.worksheet_mut(self.current_sheet)
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?)
} }
} }

73
src/book/test.rs Normal file
View File

@ -0,0 +1,73 @@
use ironcalc::base::worksheet::WorksheetDimension;
use crate::ui::Address;
use super::Book;
#[test]
fn test_book_default() {
let mut book = Book::default();
let WorksheetDimension {
min_row,
max_row,
min_column,
max_column,
} = book.get_dimensions().expect("couldn't get dimensions");
assert_eq!((1, 1, 1, 1), (min_row, max_row, min_column, max_column));
assert_eq!((0, 0), book.get_size().expect("Failed to get size"));
let cell = book
.get_current_cell_contents()
.expect("couldn't get contents");
assert_eq!("", cell);
book.edit_current_cell("1").expect("failed to edit cell");
book.evaluate();
let cell = book
.get_current_cell_contents()
.expect("couldn't get contents");
assert_eq!("1", cell);
let cell = book
.get_current_cell_rendered()
.expect("couldn't get contents");
assert_eq!("1", cell);
let sheets = book.get_all_sheets_identifiers();
dbg!(&sheets);
assert_eq!(1, sheets.len());
}
#[test]
fn test_insert_rows() {
let mut book = Book::default();
}
#[test]
fn test_book_insert_cell_new_row() {
let mut book = Book::default();
book.update_entry(&Address { row: 2, col: 1 }, "1")
.expect("failed to edit cell");
book.evaluate();
dbg!(book.get_sheet().expect("Failed to get sheet"));
let WorksheetDimension {
min_row,
max_row,
min_column,
max_column,
} = book.get_dimensions().expect("couldn't get dimensions");
assert_eq!((2, 2, 1, 1), (min_row, max_row, min_column, max_column));
assert_eq!((2, 1), book.get_size().expect("Failed to get size"));
}
#[test]
fn test_book_insert_cell_new_column() {
let mut book = Book::default();
book.insert_columns(1, 1).expect("couldn't insert rows");
book.update_entry(&Address { row: 1, col: 2 }, "1")
.expect("failed to edit cell");
let WorksheetDimension {
min_row,
max_row,
min_column,
max_column,
} = book.get_dimensions().expect("couldn't get dimensions");
assert_eq!((1, 1, 2, 2), (min_row, max_row, min_column, max_column));
assert_eq!((1, 2), book.get_size().expect("Failed to get size"));
}

View File

@ -4,7 +4,6 @@ use clap::Parser;
use ratatui; use ratatui;
use ui::Workspace; use ui::Workspace;
mod sheet;
mod ui; mod ui;
mod book; mod book;
@ -13,10 +12,15 @@ mod book;
pub struct Args { pub struct Args {
#[arg()] #[arg()]
workbook: PathBuf, workbook: PathBuf,
#[arg(default_value_t=String::from("en"), short, long)]
locale_name: String,
#[arg(default_value_t=String::from("America/New_York"), short, long)]
timezone_name: String,
} }
fn run(terminal: &mut ratatui::DefaultTerminal, name: PathBuf) -> anyhow::Result<ExitCode> { fn run(terminal: &mut ratatui::DefaultTerminal, args: Args) -> anyhow::Result<ExitCode> {
let mut ws = Workspace::load(&name)?; let mut ws = Workspace::load(&args.workbook, &args.locale_name, &args.timezone_name)?;
loop { loop {
terminal.draw(|frame| ui::draw(frame, &mut ws))?; terminal.draw(|frame| ui::draw(frame, &mut ws))?;
if let Some(code) = ws.handle_input()? { if let Some(code) = ws.handle_input()? {
@ -30,7 +34,7 @@ fn main() -> anyhow::Result<ExitCode> {
let mut terminal = ratatui::init(); let mut terminal = ratatui::init();
terminal.clear()?; terminal.clear()?;
let app_result = run(&mut terminal, args.workbook); let app_result = run(&mut terminal, args);
ratatui::restore(); ratatui::restore();
app_result app_result
} }

View File

@ -1,89 +0,0 @@
//! DataModel for a SpreadSheet
//!
//! # Overview
//!
//! Sheets can contain a [Tbl]. Tbl's contain a collection of [Address] to [Computable]
//! associations. From this we can compute the dimensions of a Tbl as well as render
//! them into a [Table] Widget.
use anyhow::{anyhow, Result};
use csvx;
use std::borrow::Borrow;
/// The Address in a [Tbl].
#[derive(Default, Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
pub struct Address {
pub row: usize,
pub col: usize,
}
impl Address {
pub fn new(row: usize, col: usize) -> Self {
Self { row, col }
}
}
/// A single table of addressable computable values.
pub struct Tbl {
pub csv: csvx::Table,
pub location: Address,
}
impl Tbl {
pub fn new() -> Self {
Self::from_str("").unwrap()
}
pub fn dimensions(&self) -> (usize, usize) {
let table = self.csv.get_raw_table();
let row_count = table.len();
if row_count > 0 {
let col_count = table.first().unwrap().len();
return (row_count, col_count);
}
return (0, 0);
}
pub fn from_str<S: Borrow<str>>(input: S) -> Result<Self> {
Ok(Self {
csv: csvx::Table::new(input)
.map_err(|e| anyhow!("Error parsing table from csv text: {}", e))?,
location: Address::default(),
})
}
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 {
return Err(anyhow!("Invalid address to move to: {:?}", addr));
}
self.location = addr;
Ok(())
}
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.
for r in row..=address.row {
self.csv.insert_y(r);
}
}
if address.col >= col {
for c in col..=address.col {
self.csv.insert_x(c);
}
}
Ok(self
.csv
.update(address.col, address.row, value.trim())?)
}
}
#[cfg(test)]
mod tests;

View File

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

View File

@ -1,16 +1,12 @@
//! Ui rendering logic //! Ui rendering logic
use std::{ use std::{path::PathBuf, process::ExitCode};
fs::File,
io::Read,
path::PathBuf,
process::ExitCode,
};
use super::sheet::{Address, Tbl}; use crate::book::Book;
use anyhow::{anyhow, Context, Result}; use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use ironcalc::base::worksheet::WorksheetDimension;
use ratatui::{ use ratatui::{
self, self,
layout::{Constraint, Flex, Layout}, layout::{Constraint, Flex, Layout},
@ -35,21 +31,41 @@ pub struct AppState {
pub table_state: TableState, pub table_state: TableState,
} }
// TODO(jwall): This should probably move to a different module.
/// The Address in a Table.
#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
pub struct Address {
pub row: usize,
pub col: usize,
}
impl Address {
pub fn new(row: usize, col: usize) -> Self {
Self { row, col }
}
}
impl Default for Address {
fn default() -> Self {
Address::new(1, 1)
}
}
// Interaction Modalities // Interaction Modalities
// * Navigate // * Navigate
// * Edit // * Edit
pub struct Workspace<'ws> { pub struct Workspace<'ws> {
name: PathBuf, name: PathBuf,
tbl: Tbl, book: Book,
state: AppState, state: AppState,
text_area: TextArea<'ws>, text_area: TextArea<'ws>,
dirty: bool, dirty: bool,
} }
impl<'ws> Workspace<'ws> { impl<'ws> Workspace<'ws> {
pub fn new(tbl: Tbl, name: PathBuf) -> Self { pub fn new(book: Book, name: PathBuf) -> Self {
let mut ws = Self { let mut ws = Self {
tbl, book,
name, name,
state: AppState::default(), state: AppState::default(),
text_area: reset_text_area("".to_owned()), text_area: reset_text_area("".to_owned()),
@ -59,61 +75,52 @@ impl<'ws> Workspace<'ws> {
ws ws
} }
pub fn load(path: &PathBuf) -> Result<Self> { pub fn load(path: &PathBuf, locale: &str, tz: &str) -> Result<Self> {
let input = if path.exists() { let book = if path.exists() {
if path.is_file() { Book::new_from_xlsx_with_locale(&path.to_string_lossy().to_string(), locale, tz)?
let mut f = File::open(path)?;
let mut buf = Vec::new();
let _ = f.read_to_end(&mut buf)?;
String::from_utf8(buf).context(format!("Error reading file: {:?}", path))?
} else {
return Err(anyhow!("Not a valid path: {}", path.to_string_lossy().to_string()));
}
} else { } else {
String::from(",,,\n,,,\n") Book::default()
}; };
let mut tbl = Tbl::from_str(input)?; //book.move_to(Address { row: 0, col: 0 })?;
tbl.move_to(Address { row: 0, col: 0 })?; Ok(Workspace::new(book, path.clone()))
Ok(Workspace::new(
tbl,
path.clone(),
))
} }
pub fn move_down(&mut self) -> Result<()> { pub fn move_down(&mut self) -> Result<()> {
let mut loc = self.tbl.location.clone(); let mut loc = self.book.location.clone();
let (row, _) = self.tbl.dimensions(); let WorksheetDimension { min_row: _, max_row, min_column: _, max_column: _ } = self.book.get_dimensions()?;
if loc.row < row - 1 { if loc.row <= max_row as usize {
loc.row += 1; loc.row += 1;
self.tbl.move_to(loc)?; self.book.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.book.location.clone();
if loc.row > 0 { let WorksheetDimension { min_row, max_row: _, min_column: _, max_column: _ } = self.book.get_dimensions()?;
if loc.row > min_row as usize {
loc.row -= 1; loc.row -= 1;
self.tbl.move_to(loc)?; self.book.move_to(loc)?;
} }
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.book.location.clone();
if loc.col > 0 { let WorksheetDimension { min_row: _, max_row: _, min_column, max_column: _ } = self.book.get_dimensions()?;
if loc.col > min_column as usize {
loc.col -= 1; loc.col -= 1;
self.tbl.move_to(loc)?; self.book.move_to(loc)?;
} }
Ok(()) Ok(())
} }
pub fn move_right(&mut self) -> Result<()> { pub fn move_right(&mut self) -> Result<()> {
let mut loc = self.tbl.location.clone(); let mut loc = self.book.location.clone();
let (_, col) = self.tbl.dimensions(); let WorksheetDimension { min_row: _, max_row: _, min_column: _, max_column} = self.book.get_dimensions()?;
if loc.col < col - 1 { if loc.col < max_column as usize {
loc.col += 1; loc.col += 1;
self.tbl.move_to(loc)?; self.book.move_to(loc)?;
} }
Ok(()) Ok(())
} }
@ -146,7 +153,8 @@ impl<'ws> Workspace<'ws> {
"* ESC: Exit edit mode".into(), "* ESC: Exit edit mode".into(),
"Otherwise edit as normal".into(), "Otherwise edit as normal".into(),
]), ]),
}).block(info_block) })
.block(info_block)
} }
fn handle_edit_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> { fn handle_edit_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
@ -157,8 +165,7 @@ impl<'ws> Workspace<'ws> {
self.text_area.set_cursor_style(Style::default()); self.text_area.set_cursor_style(Style::default());
let contents = self.text_area.lines().join("\n"); let contents = self.text_area.lines().join("\n");
if self.dirty { if self.dirty {
let loc = self.tbl.location.clone(); self.book.edit_current_cell(contents)?;
self.tbl.update_entry(&loc, contents)?;
} }
return Ok(None); return Ok(None);
} }
@ -189,19 +196,19 @@ impl<'ws> Workspace<'ws> {
self.save_file()?; self.save_file()?;
} }
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => { KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
let (row, _) = self.tbl.dimensions(); let WorksheetDimension { min_row: _, max_row, min_column: _, max_column: _ } = self.book.get_dimensions()?;
self.tbl.csv.insert_y(row); self.book.insert_rows(max_row as usize, 1)?;
let (row, _) = self.tbl.dimensions(); let WorksheetDimension { min_row: _, max_row, min_column: _, max_column: _ } = self.book.get_dimensions()?;
let mut loc = self.tbl.location.clone(); let mut loc = self.book.location.clone();
if loc.row < row - 1 { if loc.row < max_row as usize {
loc.row = row - 1; loc.row = (max_row - 1) as usize;
self.tbl.move_to(loc)?; self.book.move_to(loc)?;
} }
self.handle_movement_change(); self.handle_movement_change();
} }
KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => { KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {
let (_, col) = self.tbl.dimensions(); let WorksheetDimension { min_row: _, max_row: _, min_column: _, max_column} = self.book.get_dimensions()?;
self.tbl.csv.insert_x(col); self.book.insert_columns(max_column as usize, 1)?;
} }
KeyCode::Char('q') => { KeyCode::Char('q') => {
return Ok(Some(ExitCode::SUCCESS)); return Ok(Some(ExitCode::SUCCESS));
@ -236,13 +243,15 @@ impl<'ws> Workspace<'ws> {
} }
fn handle_movement_change(&mut self) { fn handle_movement_change(&mut self) {
let contents = self.tbl.get_raw_value(&self.tbl.location); let contents = self
.book
.get_current_cell_contents()
.expect("Unexpected failure getting current cell contents");
self.text_area = reset_text_area(contents); self.text_area = reset_text_area(contents);
} }
fn save_file(&self) -> Result<()> { fn save_file(&self) -> Result<()> {
let contents = self.tbl.csv.export_raw_table().map_err(|e| anyhow::anyhow!("Error serializing to csv: {:?}", e))?; self.book.save_to_xlsx(&self.name.to_string_lossy().to_string())?;
std::fs::write(&self.name, contents)?;
Ok(()) Ok(())
} }
} }
@ -260,9 +269,12 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
Self: Sized, Self: Sized,
{ {
let outer_block = Block::bordered() let outer_block = Block::bordered()
.title(Line::from(self.name .title(Line::from(
.file_name().map(|p| p.to_string_lossy().to_string()) self.name
.unwrap_or_else(|| String::from("Unknown")))) .file_name()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| String::from("Unknown")),
))
.title_bottom(match &self.state.modality { .title_bottom(match &self.state.modality {
Modality::Navigate => "navigate", Modality::Navigate => "navigate",
Modality::CellEdit => "edit", Modality::CellEdit => "edit",
@ -270,22 +282,26 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
.title_bottom( .title_bottom(
Line::from(format!( Line::from(format!(
"{},{}", "{},{}",
self.tbl.location.row, self.tbl.location.col self.book.location.row, self.book.location.col
)) ))
.right_aligned(), .right_aligned(),
); );
let [edit_rect, table_rect, info_rect] = let [edit_rect, table_rect, info_rect] = Layout::vertical(&[
Layout::vertical(&[Constraint::Fill(1), Constraint::Fill(30), Constraint::Fill(9)]) Constraint::Fill(1),
.vertical_margin(2) Constraint::Fill(30),
.horizontal_margin(2) Constraint::Fill(9),
.flex(Flex::Legacy) ])
.areas(area.clone()); .vertical_margin(2)
.horizontal_margin(2)
.flex(Flex::Legacy)
.areas(area.clone());
outer_block.render(area, buf); outer_block.render(area, buf);
self.text_area.render(edit_rect, buf); self.text_area.render(edit_rect, buf);
let table_block = Block::bordered(); let table_block = Block::bordered();
let table = Table::from(&self.tbl).block(table_block); let table_inner: Table = TryFrom::try_from(&self.book).expect("");
let table = table_inner.block(table_block);
// https://docs.rs/ratatui/latest/ratatui/widgets/struct.TableState.html // https://docs.rs/ratatui/latest/ratatui/widgets/struct.TableState.html
let Address { row, col } = self.tbl.location; let Address { row, col } = self.book.location;
// TODO(zaphar): Apparently scrolling by columns doesn't work? // TODO(zaphar): Apparently scrolling by columns doesn't work?
self.state.table_state.select_cell(Some((row, col))); self.state.table_state.select_cell(Some((row, col)));
self.state.table_state.select_column(Some(col)); self.state.table_state.select_column(Some(col));
@ -298,62 +314,65 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
} }
const COLNAMES: [&'static str; 26] = [ const COLNAMES: [&'static str; 26] = [
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S",
"S", "T", "U", "V", "W", "X", "Y", "Z", "T", "U", "V", "W", "X", "Y", "Z",
]; ];
impl<'t> From<&Tbl> for Table<'t> { // TODO(jwall): Maybe this should be TryFrom?
fn from(value: &Tbl) -> Self { impl<'t, 'book: 't> TryFrom<&'book Book> for Table<'t> {
let (_, cols) = value.dimensions(); fn try_from(value: &'book Book) -> std::result::Result<Self, Self::Error> {
let rows: Vec<Row> = value // TODO(zaphar): This is apparently expensive. Maybe we can cache it somehow?
.csv // We should do the correct thing here if this fails
.get_calculated_table() let WorksheetDimension { min_row, max_row, min_column, max_column } = value.get_dimensions()?;
.iter() let (row_count, col_count) = ((max_row - min_row) as usize, (max_column - min_column) as usize);
.enumerate() let rows: Vec<Row> = (1..=row_count)
.map(|(ri, r)| { .into_iter()
let cells = .map(|ri| {
vec![Cell::new(format!("{}", ri))] let cells: Vec<Cell> = (1..=col_count)
.into_iter() .into_iter()
.chain(r.iter().enumerate().map(|(ci, v)| { .map(|ci| {
let content = format!("{}", v); // TODO(zaphar): Is this safe?
let cell = Cell::new(Text::raw(content)); let content = value.get_cell_addr_rendered(ri, ci).unwrap();
match (value.location.row == ri, value.location.col == ci) { let cell = Cell::new(Text::raw(content));
(true, true) => cell.fg(Color::White).underlined(), match (value.location.row == ri, value.location.col == ci) {
_ => cell (true, true) => cell.fg(Color::White).underlined(),
.bg(if ri % 2 == 0 { _ => cell
Color::Rgb(57, 61, 71) .bg(if ri % 2 == 0 {
} else { Color::Rgb(57, 61, 71)
Color::Rgb(165, 169, 160) } else {
}) Color::Rgb(165, 169, 160)
.fg(if ri % 2 == 0 { })
Color::White .fg(if ri % 2 == 0 {
} else { Color::White
Color::Rgb(31, 32, 34) } else {
}), Color::Rgb(31, 32, 34)
} }),
.bold() }
})); .bold()
})
.collect();
Row::new(cells) Row::new(cells)
}) })
.collect(); .collect();
// TODO(zaphar): Handle the double letter column names let mut constraints: Vec<Constraint> = Vec::new();
let mut header = Vec::with_capacity(cols+1); constraints.push(Constraint::Max(5));
for _ in 0..col_count {
constraints.push(Constraint::Min(5));
}
let mut header = Vec::with_capacity(col_count as usize);
header.push(Cell::new("")); header.push(Cell::new(""));
header.extend((0..cols).map(|i| { header.extend((0..(col_count as usize)).map(|i| {
let count = (i / 26) + 1; let count = (i / 26) + 1;
Cell::new(COLNAMES[i % 26].repeat(count)) Cell::new(COLNAMES[i % 26].repeat(count))
})); }));
let mut constraints: Vec<Constraint> = Vec::new(); Ok(Table::new(rows, constraints)
constraints.push(Constraint::Max(5));
for _ in 0..cols {
constraints.push(Constraint::Min(5));
}
Table::new(rows, constraints)
.block(Block::bordered()) .block(Block::bordered())
.header(Row::new(header).underlined()) .header(Row::new(header).underlined())
.column_spacing(1) .column_spacing(1)
.flex(Flex::SpaceAround) .flex(Flex::SpaceAround))
} }
type Error = anyhow::Error;
} }
pub fn draw(frame: &mut Frame, ws: &mut Workspace) { pub fn draw(frame: &mut Frame, ws: &mut Workspace) {