//! Ui rendering logic
use std::{path::PathBuf, process::ExitCode};
use crate::book::Book;
use anyhow::{anyhow, Result};
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use ironcalc::base::Model;
use ratatui::{
buffer::Buffer,
layout::{Constraint, Flex, Layout},
style::{Modifier, Style},
widgets::Block,
};
use tui_prompts::{State, Status, TextPrompt, TextState};
use tui_textarea::{CursorMove, TextArea};
mod cmd;
pub mod render;
#[cfg(test)]
mod test;
use cmd::Cmd;
use render::viewport::ViewportState;
#[derive(Default, Debug, PartialEq, Clone)]
pub enum Modality {
#[default]
Navigate,
CellEdit,
Command,
Dialog,
RangeSelect,
}
#[derive(Debug, Default)]
pub struct RangeSelection {
pub original_location: Option
,
pub original_sheet: Option,
pub sheet: Option,
pub start: Option,
pub end: Option,
}
impl RangeSelection {
pub fn get_range(&self) -> Option<(Address, Address)> {
if let (Some(start), Some(end)) = (&self.start, &self.end) {
return Some((
Address {
row: std::cmp::min(start.row, end.row),
col: std::cmp::min(start.col, end.col),
},
Address {
row: std::cmp::max(start.row, end.row),
col: std::cmp::max(start.col, end.col),
},
));
}
None
}
pub fn reset_range_selection(&mut self) {
self.start = None;
self.end = None;
self.sheet = None;
}
}
#[derive(Debug)]
pub struct AppState<'ws> {
pub modality_stack: Vec,
pub viewport_state: ViewportState,
pub command_state: TextState<'ws>,
pub numeric_prefix: Vec,
pub range_select: RangeSelection,
dirty: bool,
popup: Vec,
}
impl<'ws> Default for AppState<'ws> {
fn default() -> Self {
AppState {
modality_stack: vec![Modality::default()],
viewport_state: Default::default(),
command_state: Default::default(),
numeric_prefix: Default::default(),
range_select: Default::default(),
dirty: Default::default(),
popup: Default::default(),
}
}
}
impl<'ws> AppState<'ws> {
pub fn modality(&'ws self) -> &'ws Modality {
self.modality_stack.last().unwrap()
}
pub fn pop_modality(&mut self) {
if self.modality_stack.len() > 1 {
self.modality_stack.pop();
}
}
pub fn get_n_prefix(&self) -> usize {
let prefix = self
.numeric_prefix
.iter()
.map(|c| c.to_digit(10).unwrap())
.fold(Some(0 as usize), |acc, n| {
acc?.checked_mul(10)?.checked_add(n as usize)
})
.unwrap_or(1);
if prefix == 0 {
return 1;
}
prefix
}
pub fn reset_n_prefix(&mut self) {
self.numeric_prefix.clear();
}
}
// 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 }
}
pub fn to_range_part(&self) -> String {
let count = if self.col == 26 {
1
} else {
(self.col / 26) + 1
};
format!(
"{}{}",
render::viewport::COLNAMES[(self.col - 1) % 26].repeat(count),
self.row
)
}
}
impl Default for Address {
fn default() -> Self {
Address::new(1, 1)
}
}
/// A workspace defining our UI state.
pub struct Workspace<'ws> {
name: PathBuf,
book: Book,
pub(crate) state: AppState<'ws>,
text_area: TextArea<'ws>,
}
impl<'ws> Workspace<'ws> {
/// Constructs a new Workspace from an `Book` with a path for the name.
pub fn new(book: Book, name: PathBuf) -> Self {
let mut ws = Self {
book,
name,
state: AppState::default(),
text_area: reset_text_area("".to_owned()),
};
ws.handle_movement_change();
ws
}
pub fn new_empty(locale: &str, tz: &str) -> Result {
Ok(Self::new(
Book::new(Model::new_empty("", locale, tz).map_err(|e| anyhow!("{}", e))?),
PathBuf::default(),
))
}
/// Loads a workspace from a path.
pub fn load(path: &PathBuf, locale: &str, tz: &str) -> Result {
let book = load_book(path, locale, tz)?;
Ok(Workspace::new(book, path.clone()))
}
/// Loads a new `Book` into a `Workspace` from a path.
pub fn load_into>(&mut self, path: P) -> Result<()> {
let path: PathBuf = path.into();
// FIXME(zaphar): This should be managed better.
let book = load_book(&path, "en", "America/New_York")?;
self.book = book;
self.name = path;
Ok(())
}
pub fn selected_range_to_string(&self) -> String {
let state = &self.state;
if let Some((start, end)) = state.range_select.get_range() {
let a1 = format!("{}{}", start.to_range_part(), format!(":{}", end.to_range_part()));
if let Some(range_sheet) = state.range_select.sheet {
if range_sheet != self.book.current_sheet {
return format!(
"{}!{}",
self.book
.get_sheet_name_by_idx(range_sheet as usize)
.expect("No such sheet index"),
a1
);
}
}
return a1;
}
return String::new()
}
/// Move a row down in the current sheet.
pub fn move_down(&mut self) -> Result<()> {
let mut loc = self.book.location.clone();
if loc.row < render::viewport::LAST_ROW {
loc.row += 1;
self.book.move_to(&loc)?;
}
Ok(())
}
/// Move a row up in the current sheet.
pub fn move_up(&mut self) -> Result<()> {
let mut loc = self.book.location.clone();
if loc.row > 1 {
loc.row -= 1;
self.book.move_to(&loc)?;
}
Ok(())
}
/// Move a column to the left in the current sheet.
pub fn move_left(&mut self) -> Result<()> {
let mut loc = self.book.location.clone();
if loc.col > 1 {
loc.col -= 1;
self.book.move_to(&loc)?;
}
Ok(())
}
/// Move a column to the left in the current sheet.
pub fn move_right(&mut self) -> Result<()> {
let mut loc = self.book.location.clone();
if loc.col < render::viewport::LAST_COLUMN {
loc.col += 1;
self.book.move_to(&loc)?;
}
Ok(())
}
/// Handle input in our ui loop.
pub fn handle_input(&mut self, evt: Event) -> Result