Compare commits

...

2 Commits

5 changed files with 131 additions and 33 deletions

View File

@ -68,7 +68,8 @@ will clear the numeric prefix if you want to cancel it.
**Other Keybindings** **Other Keybindings**
* `Ctrl-r` will enter range selection mode * `Ctrl-r` will enter range selection mode.
* `v` will enter range selection mode with the start of the range already selected.
* `Ctrl-s` will save the sheet. * `Ctrl-s` will save the sheet.
* `Ctrl-c`, `y` Copy the cell or range contents. * `Ctrl-c`, `y` Copy the cell or range contents.
* `Ctrl-v`, `p` Paste into the sheet. * `Ctrl-v`, `p` Paste into the sheet.

Binary file not shown.

Binary file not shown.

View File

@ -18,6 +18,64 @@ mod test;
const COL_PIXELS: f64 = 5.0; const COL_PIXELS: f64 = 5.0;
#[derive(Debug, Clone)]
pub struct AddressRange<'book> {
pub start: &'book Address,
pub end: &'book Address,
}
impl<'book> AddressRange<'book> {
pub fn as_rows(&self) -> Vec<Vec<Address>> {
let (row_range, col_range) = self.get_ranges();
let mut rows = Vec::with_capacity(row_range.len());
for ri in row_range.iter() {
let mut row = Vec::with_capacity(col_range.len());
for ci in col_range.iter() {
row.push(Address { row: *ri, col: *ci });
}
rows.push(row);
}
rows
}
pub fn as_series(&self) -> Vec<Address> {
let (row_range, col_range) = self.get_ranges();
let mut rows = Vec::with_capacity(row_range.len() * col_range.len());
for ri in row_range.iter() {
for ci in col_range.iter() {
rows.push(Address { row: *ri, col: *ci });
}
}
rows
}
fn get_ranges(&self) -> (Vec<usize>, Vec<usize>) {
let row_range = if self.start.row <= self.end.row {
(self.start.row..=self.end.row)
.into_iter()
.collect::<Vec<usize>>()
} else {
let mut v = (self.start.row..=self.end.row)
.into_iter()
.collect::<Vec<usize>>();
v.reverse();
v
};
let col_range = if self.start.col <= self.end.col {
(self.start.col..=self.end.col)
.into_iter()
.collect::<Vec<usize>>()
} else {
let mut v = (self.start.col..=self.end.col)
.into_iter()
.collect::<Vec<usize>>();
v.reverse();
v
};
(row_range, col_range)
}
}
/// A spreadsheet book with some internal state tracking. /// A spreadsheet book with some internal state tracking.
pub struct Book { pub struct Book {
pub(crate) model: Model, pub(crate) model: Model,
@ -96,7 +154,7 @@ impl Book {
Ok(&self.get_sheet()?.sheet_data) Ok(&self.get_sheet()?.sheet_data)
} }
/// Move to a specific sheel location in the current sheet /// Move to a specific sheet location in the current sheet
pub fn move_to(&mut self, Address { row, col }: &Address) -> Result<()> { pub fn move_to(&mut self, Address { row, col }: &Address) -> Result<()> {
// FIXME(zaphar): Check that this is safe first. // FIXME(zaphar): Check that this is safe first.
self.location.row = *row; self.location.row = *row;
@ -104,16 +162,48 @@ impl Book {
Ok(()) Ok(())
} }
/// Extend a cell to the rest of the range.
pub fn extend_to(&mut self, from: &Address, to: &Address) -> Result<()> {
for cell in (AddressRange {
start: from,
end: to,
})
.as_series()
.iter()
.skip(1)
{
let contents = self
.model
.extend_to(
self.current_sheet,
from.row as i32,
from.col as i32,
cell.row as i32,
cell.col as i32,
)
.map_err(|e| anyhow!(e))?;
self.model
.set_user_input(
self.current_sheet,
cell.row as i32,
cell.col as i32,
contents,
)
.map_err(|e| anyhow!(e))?;
}
self.evaluate();
Ok(())
}
pub fn clear_current_cell(&mut self) -> Result<()> { pub fn clear_current_cell(&mut self) -> Result<()> {
self.clear_cell_contents(self.current_sheet as u32, self.location.clone()) self.clear_cell_contents(self.current_sheet as u32, self.location.clone())
} }
pub fn clear_current_cell_all(&mut self) -> Result<()> { pub fn clear_current_cell_all(&mut self) -> Result<()> {
self.clear_cell_all(self.current_sheet as u32, self.location.clone()) self.clear_cell_all(self.current_sheet as u32, self.location.clone())
} }
pub fn clear_cell_contents(&mut self, sheet: u32, Address { row, col }: Address) -> Result<()> {
pub fn clear_cell_contents(&mut self, sheet: u32, Address { row, col, }: Address) -> Result<()> {
Ok(self Ok(self
.model .model
.cell_clear_contents(sheet, row as i32, col as i32) .cell_clear_contents(sheet, row as i32, col as i32)
@ -128,8 +218,8 @@ impl Book {
} }
Ok(()) Ok(())
} }
pub fn clear_cell_all(&mut self, sheet: u32, Address { row, col, }: Address) -> Result<()> { pub fn clear_cell_all(&mut self, sheet: u32, Address { row, col }: Address) -> Result<()> {
Ok(self Ok(self
.model .model
.cell_clear_all(sheet, row as i32, col as i32) .cell_clear_all(sheet, row as i32, col as i32)
@ -145,7 +235,6 @@ impl Book {
Ok(()) Ok(())
} }
/// Get a cells formatted content. /// Get a cells formatted content.
pub fn get_current_cell_rendered(&self) -> Result<String> { pub fn get_current_cell_rendered(&self) -> Result<String> {
Ok(self.get_cell_addr_rendered(&self.location)?) Ok(self.get_cell_addr_rendered(&self.location)?)
@ -158,7 +247,7 @@ impl Book {
.get_formatted_cell_value(self.current_sheet, *row as i32, *col as i32) .get_formatted_cell_value(self.current_sheet, *row as i32, *col as i32)
.map_err(|s| anyhow!("Unable to format cell {}", s))?) .map_err(|s| anyhow!("Unable to format cell {}", s))?)
} }
/// Get a cells actual content unformatted as a string. /// Get a cells actual content unformatted as a string.
pub fn get_cell_addr_contents(&self, Address { row, col }: &Address) -> Result<String> { pub fn get_cell_addr_contents(&self, Address { row, col }: &Address) -> Result<String> {
Ok(self Ok(self
@ -167,7 +256,6 @@ impl Book {
.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_current_cell_contents(&self) -> Result<String> { pub fn get_current_cell_contents(&self) -> Result<String> {
Ok(self Ok(self
@ -274,7 +362,7 @@ impl Book {
.enumerate() .enumerate()
.find(|(_idx, sheet)| sheet.name == name) .find(|(_idx, sheet)| sheet.name == name)
{ {
self.current_sheet =idx as u32; self.current_sheet = idx as u32;
return true; return true;
} }
false false
@ -293,7 +381,7 @@ impl Book {
} }
self.current_sheet = next; self.current_sheet = next;
} }
pub fn select_prev_sheet(&mut self) { pub fn select_prev_sheet(&mut self) {
let len = self.model.workbook.worksheets.len() as u32; let len = self.model.workbook.worksheets.len() as u32;
let next = if self.current_sheet == 0 { let next = if self.current_sheet == 0 {
@ -304,7 +392,6 @@ impl Book {
self.current_sheet = next; self.current_sheet = next;
} }
/// Select a sheet by id. /// Select a sheet by id.
pub fn select_sheet_by_id(&mut self, id: u32) -> bool { pub fn select_sheet_by_id(&mut self, id: u32) -> bool {
if let Some((idx, _sheet)) = self if let Some((idx, _sheet)) = self
@ -337,13 +424,14 @@ impl Book {
.worksheet_mut(self.current_sheet) .worksheet_mut(self.current_sheet)
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?) .map_err(|s| anyhow!("Invalid Worksheet: {}", s))?)
} }
pub(crate) fn get_sheet_name_by_idx(&self, idx: usize) -> Result<&str> { pub(crate) fn get_sheet_name_by_idx(&self, idx: usize) -> Result<&str> {
Ok(&self Ok(&self
.model .model
.workbook .workbook
.worksheet(idx as u32) .worksheet(idx as u32)
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?.name) .map_err(|s| anyhow!("Invalid Worksheet: {}", s))?
.name)
} }
pub(crate) fn get_sheet_by_idx_mut(&mut self, idx: usize) -> Result<&mut Worksheet> { pub(crate) fn get_sheet_by_idx_mut(&mut self, idx: usize) -> Result<&mut Worksheet> {
Ok(self Ok(self

View File

@ -1,7 +1,7 @@
//! Ui rendering logic //! Ui rendering logic
use std::{path::PathBuf, process::ExitCode}; use std::{path::PathBuf, process::ExitCode};
use crate::book::Book; use crate::book::{AddressRange, Book};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
@ -317,6 +317,7 @@ impl<'ws> Workspace<'ws> {
"Edit Mode:".to_string(), "Edit Mode:".to_string(),
"* ENTER/RETURN: Exit edit mode and save changes".to_string(), "* ENTER/RETURN: Exit edit mode and save changes".to_string(),
"* Ctrl-r: Enter Range Selection mode".to_string(), "* Ctrl-r: Enter Range Selection mode".to_string(),
"* v: Enter Range Selection mode with the start of the range already selected".to_string(),
"* ESC: Exit edit mode and discard changes".to_string(), "* ESC: Exit edit mode and discard changes".to_string(),
"Otherwise edit as normal".to_string(), "Otherwise edit as normal".to_string(),
], ],
@ -380,7 +381,7 @@ impl<'ws> Workspace<'ws> {
return Ok(None); return Ok(None);
} }
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => { KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
self.enter_range_select_mode(); self.enter_range_select_mode(false);
return Ok(None); return Ok(None);
} }
KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => { KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => {
@ -579,6 +580,12 @@ impl<'ws> Workspace<'ws> {
self.copy_range(false)?; self.copy_range(false)?;
} }
KeyCode::Char('y') => self.copy_range(false)?, KeyCode::Char('y') => self.copy_range(false)?,
KeyCode::Char('x') => {
if let (Some(from), Some(to)) = (self.state.range_select.start.as_ref(), self.state.range_select.end.as_ref()) {
self.book.extend_to(from, to)?;
}
self.exit_range_select_mode()?;
}
_ => { _ => {
// moop // moop
} }
@ -591,25 +598,19 @@ impl<'ws> Workspace<'ws> {
self.update_range_selection()?; self.update_range_selection()?;
match &self.state.range_select.get_range() { match &self.state.range_select.get_range() {
Some(( Some((
Address { start,
row: row_start, end,
col: col_start,
},
Address {
row: row_end,
col: col_end,
},
)) => { )) => {
let mut rows = Vec::new(); let mut rows = Vec::new();
for ri in (*row_start)..=(*row_end) { for row in (AddressRange { start, end, }).as_rows() {
let mut cols = Vec::new(); let mut cols = Vec::new();
for ci in (*col_start)..=(*col_end) { for cell in row {
cols.push(if formatted { cols.push(if formatted {
self.book self.book
.get_cell_addr_rendered(&Address { row: ri, col: ci })? .get_cell_addr_rendered(&cell)?
} else { } else {
self.book self.book
.get_cell_addr_contents(&Address { row: ri, col: ci })? .get_cell_addr_contents(&cell)?
}); });
} }
rows.push(cols); rows.push(cols);
@ -670,7 +671,7 @@ impl<'ws> Workspace<'ws> {
self.enter_edit_mode(); self.enter_edit_mode();
} }
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => { KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
self.enter_range_select_mode(); self.enter_range_select_mode(false);
} }
KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => { KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {
self.state.clipboard = Some(ClipboardContents::Cell( self.state.clipboard = Some(ClipboardContents::Cell(
@ -697,7 +698,7 @@ impl<'ws> Workspace<'ws> {
)); ));
} }
KeyCode::Char('v') if key.modifiers != KeyModifiers::CONTROL => { KeyCode::Char('v') if key.modifiers != KeyModifiers::CONTROL => {
self.enter_range_select_mode() self.enter_range_select_mode(true)
} }
KeyCode::Char('p') if key.modifiers != KeyModifiers::CONTROL => { KeyCode::Char('p') if key.modifiers != KeyModifiers::CONTROL => {
self.paste_range()?; self.paste_range()?;
@ -810,6 +811,7 @@ impl<'ws> Workspace<'ws> {
})?; })?;
} }
KeyCode::Char('g') => { KeyCode::Char('g') => {
// TODO(zaphar): This really needs a better state machine.
if self.state.char_queue.first().map(|c| *c == 'g').unwrap_or(false) { if self.state.char_queue.first().map(|c| *c == 'g').unwrap_or(false) {
self.state.char_queue.pop(); self.state.char_queue.pop();
self.move_to_top()?; self.move_to_top()?;
@ -819,6 +821,7 @@ impl<'ws> Workspace<'ws> {
} }
_ => { _ => {
// noop // noop
self.state.char_queue.clear();
} }
} }
} }
@ -829,6 +832,7 @@ impl<'ws> Workspace<'ws> {
match &self.state.clipboard { match &self.state.clipboard {
Some(ClipboardContents::Cell(contents)) => { Some(ClipboardContents::Cell(contents)) => {
self.book.edit_current_cell(contents)?; self.book.edit_current_cell(contents)?;
self.book.evaluate();
} }
Some(ClipboardContents::Range(ref rows)) => { Some(ClipboardContents::Range(ref rows)) => {
let Address { row, col } = self.book.location.clone(); let Address { row, col } = self.book.location.clone();
@ -846,6 +850,7 @@ impl<'ws> Workspace<'ws> {
)?; )?;
} }
} }
self.book.evaluate();
} }
None => { None => {
// NOOP // NOOP
@ -878,11 +883,15 @@ impl<'ws> Workspace<'ws> {
self.state.modality_stack.push(Modality::Dialog); self.state.modality_stack.push(Modality::Dialog);
} }
fn enter_range_select_mode(&mut self) { fn enter_range_select_mode(&mut self, init_start: bool) {
self.state.range_select.sheet = Some(self.book.current_sheet); self.state.range_select.sheet = Some(self.book.current_sheet);
self.state.range_select.original_sheet = Some(self.book.current_sheet); self.state.range_select.original_sheet = Some(self.book.current_sheet);
self.state.range_select.original_location = Some(self.book.location.clone()); self.state.range_select.original_location = Some(self.book.location.clone());
self.state.range_select.start = None; if init_start {
self.state.range_select.start = Some(self.book.location.clone());
} else {
self.state.range_select.start = None;
}
self.state.range_select.end = None; self.state.range_select.end = None;
self.state.modality_stack.push(Modality::RangeSelect); self.state.modality_stack.push(Modality::RangeSelect);
} }