mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-25 06:19:51 -04:00
Compare commits
No commits in common. "4ae7f357c13cf744e8234da2474186511f5e9d20" and "f0dda7e345207e51930266c0dbae8231c1e34804" have entirely different histories.
4ae7f357c1
...
f0dda7e345
@ -68,8 +68,7 @@ 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.
|
||||||
|
BIN
examples/test.icalc
Normal file
BIN
examples/test.icalc
Normal file
Binary file not shown.
Binary file not shown.
116
src/book/mod.rs
116
src/book/mod.rs
@ -18,64 +18,6 @@ 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,
|
||||||
@ -154,7 +96,7 @@ impl Book {
|
|||||||
Ok(&self.get_sheet()?.sheet_data)
|
Ok(&self.get_sheet()?.sheet_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move to a specific sheet location in the current sheet
|
/// Move to a specific sheel 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;
|
||||||
@ -162,48 +104,16 @@ 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)
|
||||||
@ -218,8 +128,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)
|
||||||
@ -235,6 +145,7 @@ 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)?)
|
||||||
@ -247,7 +158,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
|
||||||
@ -256,6 +167,7 @@ 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
|
||||||
@ -362,7 +274,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
|
||||||
@ -381,7 +293,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 {
|
||||||
@ -392,6 +304,7 @@ 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
|
||||||
@ -424,14 +337,13 @@ 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))?
|
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?.name)
|
||||||
.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
|
||||||
|
@ -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::{AddressRange, Book};
|
use crate::book::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,7 +317,6 @@ 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(),
|
||||||
],
|
],
|
||||||
@ -381,7 +380,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(false);
|
self.enter_range_select_mode();
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => {
|
KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => {
|
||||||
@ -580,12 +579,6 @@ 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
|
||||||
}
|
}
|
||||||
@ -598,19 +591,25 @@ 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((
|
||||||
start,
|
Address {
|
||||||
end,
|
row: row_start,
|
||||||
|
col: col_start,
|
||||||
|
},
|
||||||
|
Address {
|
||||||
|
row: row_end,
|
||||||
|
col: col_end,
|
||||||
|
},
|
||||||
)) => {
|
)) => {
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
for row in (AddressRange { start, end, }).as_rows() {
|
for ri in (*row_start)..=(*row_end) {
|
||||||
let mut cols = Vec::new();
|
let mut cols = Vec::new();
|
||||||
for cell in row {
|
for ci in (*col_start)..=(*col_end) {
|
||||||
cols.push(if formatted {
|
cols.push(if formatted {
|
||||||
self.book
|
self.book
|
||||||
.get_cell_addr_rendered(&cell)?
|
.get_cell_addr_rendered(&Address { row: ri, col: ci })?
|
||||||
} else {
|
} else {
|
||||||
self.book
|
self.book
|
||||||
.get_cell_addr_contents(&cell)?
|
.get_cell_addr_contents(&Address { row: ri, col: ci })?
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
rows.push(cols);
|
rows.push(cols);
|
||||||
@ -671,7 +670,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(false);
|
self.enter_range_select_mode();
|
||||||
}
|
}
|
||||||
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(
|
||||||
@ -698,7 +697,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(true)
|
self.enter_range_select_mode()
|
||||||
}
|
}
|
||||||
KeyCode::Char('p') if key.modifiers != KeyModifiers::CONTROL => {
|
KeyCode::Char('p') if key.modifiers != KeyModifiers::CONTROL => {
|
||||||
self.paste_range()?;
|
self.paste_range()?;
|
||||||
@ -811,7 +810,6 @@ 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()?;
|
||||||
@ -821,7 +819,6 @@ impl<'ws> Workspace<'ws> {
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// noop
|
// noop
|
||||||
self.state.char_queue.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -832,7 +829,6 @@ 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();
|
||||||
@ -850,7 +846,6 @@ impl<'ws> Workspace<'ws> {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.book.evaluate();
|
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// NOOP
|
// NOOP
|
||||||
@ -883,15 +878,11 @@ 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, init_start: bool) {
|
fn enter_range_select_mode(&mut self) {
|
||||||
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());
|
||||||
if init_start {
|
self.state.range_select.start = None;
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user