mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-23 13:29:48 -04:00
Compare commits
7 Commits
1aa90255e4
...
a85c5121ee
Author | SHA1 | Date | |
---|---|---|---|
a85c5121ee | |||
4b3e25895d | |||
61a6e93515 | |||
5f6f45141c | |||
bb5d81106e | |||
c62fd08043 | |||
626748db0f |
@ -22,11 +22,14 @@ Options:
|
||||
|
||||
## User Interface
|
||||
|
||||
The sheetui user interface is loosely inspired by vim. It is a modal interface that is entirely keyboard driven. At nearly any time you can type `Alt-h` to get some context sensitive help.
|
||||
The sheetui user interface is loosely inspired by vim. It is a modal interface
|
||||
that is entirely keyboard driven. At nearly any time you can type `Alt-h` to
|
||||
get some context sensitive help.
|
||||
|
||||
### Navigation Mode
|
||||
|
||||
The interface will start out in navigation mode. You can navigate around the table and between the sheets using the following keybinds:
|
||||
The interface will start out in navigation mode. You can navigate around the
|
||||
table and between the sheets using the following keybinds:
|
||||
|
||||
**Cell Navigation**
|
||||
|
||||
@ -44,7 +47,9 @@ Sheet navigation moving will loop around when you reach the ends.
|
||||
|
||||
**Numeric prefixes**
|
||||
|
||||
You can prefix each of the keybinds above with a numeric prefix to do them that many times. So typing `123h` will move to the left 123 times. Hitting `Esc` will clear the numeric prefix if you want to cancel it.
|
||||
You can prefix each of the keybinds above with a numeric prefix to do them that
|
||||
many times. So typing `123h` will move to the left 123 times. Hitting `Esc`
|
||||
will clear the numeric prefix if you want to cancel it.
|
||||
|
||||
**Modifying the Sheet or Cells**
|
||||
|
||||
@ -54,33 +59,51 @@ You can prefix each of the keybinds above with a numeric prefix to do them that
|
||||
|
||||
**Other Keybindings**
|
||||
|
||||
* `Ctrl-r` will enter range selection mode
|
||||
* `Ctrl-s` will save the sheet.
|
||||
* `q` will exit the application.
|
||||
* `:` will enter CommandMode.
|
||||
|
||||
<aside>Note that for `q` this will not currently prompt you if the sheet is not saved.</aside>
|
||||
Range selections made from navigation mode will be available to paste into a Cell Edit.
|
||||
|
||||
<aside>Note that for `q` this will not currently prompt you if the sheet is not
|
||||
saved.</aside>
|
||||
|
||||
### CellEdit Mode
|
||||
|
||||
You enter CellEdit mode by hitting `e` or `i` while in navigation mode. Type what you want into the cell.
|
||||
You enter CellEdit mode by hitting `e` or `i` while in navigation mode. Type
|
||||
what you want into the cell.
|
||||
|
||||
Starting with:
|
||||
|
||||
* `=` will treat what you type as a formula.
|
||||
* `$` will treat it as us currency.
|
||||
|
||||
Typing a number will treat the contents as a number. While typing non-numeric text will treat it as text content. <aside>We do not yet support modifying the type of a cell after the fact. We may add this in the future.</aside>
|
||||
Typing a number will treat the contents as a number. While typing non-numeric
|
||||
text will treat it as text content.
|
||||
|
||||
For the most part this should work the same way you expect a spreadsheet to work.
|
||||
<aside>We do not yet support modifying the type of a cell after the fact. We
|
||||
may add this in the future.</aside>
|
||||
|
||||
For the most part this should work the same way you expect a spreadsheet to
|
||||
work.
|
||||
|
||||
* `Enter` will update the cell contents.
|
||||
* `Esc` will cancel editing the cell and leave it unedited.
|
||||
* `Ctrl-p` will paste the range selection if it exists into the cell.
|
||||
|
||||
You can find the functions we support documented here: [ironcalc docs](https://docs.ironcalc.com/functions/lookup-and-reference.html)
|
||||
`Ctrl-r` will enter range select mode when editing a formula. You can navigate
|
||||
around the sheet and hit space to select that cell in the sheet to set the
|
||||
start of the range. Navigate some more and hit space to set the end of the
|
||||
range.
|
||||
|
||||
You can find the functions we support documented here:
|
||||
[ironcalc docs](https://docs.ironcalc.com/functions/lookup-and-reference.html)
|
||||
|
||||
### Command Mode
|
||||
|
||||
You enter command mode by typing `:` while in navigation mode. You can then type a command and hit `Enter` to execute it or `Esc` to cancel.
|
||||
You enter command mode by typing `:` while in navigation mode. You can then
|
||||
type a command and hit `Enter` to execute it or `Esc` to cancel.
|
||||
|
||||
The currently supported commands are:
|
||||
|
||||
@ -93,4 +116,22 @@ The currently supported commands are:
|
||||
* `edit <path>` Edit a new spreadsheet at the current path. `e` is a shorthand alias for this command.
|
||||
* `quit` Quits the application. `q` is a shorthand alias for this command.
|
||||
|
||||
<aside>Note that in the case of `quit` and `edit` that we do not currently prompt you if the current spreadsheet has not been saved yet. So your changes will be discarded if you have not saved first.</aside>
|
||||
<aside>Note that in the case of `quit` and `edit` that we do not currently
|
||||
prompt you if the current spreadsheet has not been saved yet. So your changes
|
||||
will be discarded if you have not saved first.</aside>
|
||||
|
||||
### Range Select Mode
|
||||
|
||||
Range Select mode copies a range reference for use later. You can enter range
|
||||
select mode from CellEdit mode with `CTRL-r`.
|
||||
|
||||
* `h`, `j`, `k`, `l` will navigate around the sheet.
|
||||
* `Ctrl-n`, `Ctrl-p` will navigate between sheets.
|
||||
* `The spacebar will select the start and end of the range respectively.
|
||||
|
||||
When you have selected the end of the range you will exit range select mode and
|
||||
the range reference will be placed into the cell contents you are editing.
|
||||
|
||||
<aside>We only support continuous ranges for the moment. Planned for
|
||||
discontinuous ranges still needs the interaction interface to be
|
||||
determined.</aside>
|
||||
|
@ -287,6 +287,13 @@ impl Book {
|
||||
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?)
|
||||
}
|
||||
|
||||
pub(crate) fn get_sheet_name_by_idx(&self, idx: usize) -> Result<&str> {
|
||||
Ok(&self
|
||||
.model
|
||||
.workbook
|
||||
.worksheet(idx as u32)
|
||||
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?.name)
|
||||
}
|
||||
pub(crate) fn get_sheet_by_idx_mut(&mut self, idx: usize) -> Result<&mut Worksheet> {
|
||||
Ok(self
|
||||
.model
|
||||
|
227
src/ui/mod.rs
227
src/ui/mod.rs
@ -30,6 +30,40 @@ pub enum Modality {
|
||||
CellEdit,
|
||||
Command,
|
||||
Dialog,
|
||||
RangeSelect,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct RangeSelection {
|
||||
pub original_location: Option<Address>,
|
||||
pub original_sheet: Option<u32>,
|
||||
pub sheet: Option<u32>,
|
||||
pub start: Option<Address>,
|
||||
pub end: Option<Address>,
|
||||
}
|
||||
|
||||
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)]
|
||||
@ -38,6 +72,7 @@ pub struct AppState<'ws> {
|
||||
pub viewport_state: ViewportState,
|
||||
pub command_state: TextState<'ws>,
|
||||
pub numeric_prefix: Vec<char>,
|
||||
pub range_select: RangeSelection,
|
||||
dirty: bool,
|
||||
popup: Vec<String>,
|
||||
}
|
||||
@ -49,6 +84,7 @@ impl<'ws> Default for AppState<'ws> {
|
||||
viewport_state: Default::default(),
|
||||
command_state: Default::default(),
|
||||
numeric_prefix: Default::default(),
|
||||
range_select: Default::default(),
|
||||
dirty: Default::default(),
|
||||
popup: Default::default(),
|
||||
}
|
||||
@ -97,6 +133,19 @@ 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 {
|
||||
@ -149,6 +198,26 @@ impl<'ws> Workspace<'ws> {
|
||||
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();
|
||||
@ -197,6 +266,7 @@ impl<'ws> Workspace<'ws> {
|
||||
Modality::CellEdit => self.handle_edit_input(key)?,
|
||||
Modality::Command => self.handle_command_input(key)?,
|
||||
Modality::Dialog => self.handle_dialog_input(key)?,
|
||||
Modality::RangeSelect => self.handle_range_select_input(key)?,
|
||||
};
|
||||
return Ok(result);
|
||||
}
|
||||
@ -217,13 +287,14 @@ impl<'ws> Workspace<'ws> {
|
||||
"* CTRl-h: Shrink column width by 1".to_string(),
|
||||
"* CTRl-n: Next sheet. Starts over at beginning if at end.".to_string(),
|
||||
"* CTRl-p: Previous sheet. Starts over at end if at beginning.".to_string(),
|
||||
"* CTRl-?: Previous sheet. Starts over at end if at beginning.".to_string(),
|
||||
"* ALT-h: Previous sheet. Starts over at end if at beginning.".to_string(),
|
||||
"* q exit".to_string(),
|
||||
"* Ctrl-S Save sheet".to_string(),
|
||||
],
|
||||
Modality::CellEdit => vec![
|
||||
"Edit Mode:".to_string(),
|
||||
"* ENTER/RETURN: Exit edit mode and save changes".to_string(),
|
||||
"* Ctrl-r: Enter Range Selection mode".to_string(),
|
||||
"* ESC: Exit edit mode and discard changes".to_string(),
|
||||
"Otherwise edit as normal".to_string(),
|
||||
],
|
||||
@ -233,6 +304,14 @@ impl<'ws> Workspace<'ws> {
|
||||
"* CTRL-?: Exit command mode".to_string(),
|
||||
"* ENTER/RETURN: run command and exit command mode".to_string(),
|
||||
],
|
||||
Modality::RangeSelect => vec![
|
||||
"Range Selection Mode:".to_string(),
|
||||
"* ESC: Exit command mode".to_string(),
|
||||
"* h,j,k,l: vim style navigation".to_string(),
|
||||
"* Spacebar: Select start and end of range".to_string(),
|
||||
"* CTRl-n: Next sheet. Starts over at beginning if at end.".to_string(),
|
||||
"* CTRl-p: Previous sheet. Starts over at end if at beginning.".to_string(),
|
||||
],
|
||||
_ => vec!["General help".to_string()],
|
||||
}
|
||||
}
|
||||
@ -276,6 +355,17 @@ impl<'ws> Workspace<'ws> {
|
||||
self.enter_dialog_mode(self.render_help_text());
|
||||
return Ok(None);
|
||||
}
|
||||
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
|
||||
self.enter_range_select_mode();
|
||||
return Ok(None);
|
||||
}
|
||||
KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => {
|
||||
self.text_area
|
||||
.set_yank_text(self.selected_range_to_string());
|
||||
self.text_area.paste();
|
||||
self.state.dirty = true;
|
||||
return Ok(None);
|
||||
}
|
||||
KeyCode::Enter => self.exit_edit_mode(true)?,
|
||||
KeyCode::Esc => self.exit_edit_mode(false)?,
|
||||
_ => {
|
||||
@ -363,6 +453,91 @@ impl<'ws> Workspace<'ws> {
|
||||
self.state.numeric_prefix.push(digit);
|
||||
}
|
||||
|
||||
fn handle_range_select_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
if self.state.numeric_prefix.len() > 0 {
|
||||
self.state.reset_n_prefix();
|
||||
} else {
|
||||
self.state.range_select.start = None;
|
||||
self.state.range_select.end = None;
|
||||
self.exit_range_select_mode()?;
|
||||
}
|
||||
}
|
||||
KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => {
|
||||
self.enter_dialog_mode(self.render_help_text());
|
||||
return Ok(None);
|
||||
}
|
||||
KeyCode::Char(d) if d.is_ascii_digit() => {
|
||||
self.handle_numeric_prefix(d);
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||
ws.move_left()?;
|
||||
Ok(())
|
||||
})?;
|
||||
self.maybe_update_range_end();
|
||||
}
|
||||
KeyCode::Char('j') => {
|
||||
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||
ws.move_down()?;
|
||||
Ok(())
|
||||
})?;
|
||||
self.maybe_update_range_end();
|
||||
}
|
||||
KeyCode::Char('k') => {
|
||||
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||
ws.move_up()?;
|
||||
Ok(())
|
||||
})?;
|
||||
self.maybe_update_range_end();
|
||||
}
|
||||
KeyCode::Char('l') => {
|
||||
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||
ws.move_right()?;
|
||||
Ok(())
|
||||
})?;
|
||||
self.maybe_update_range_end();
|
||||
}
|
||||
KeyCode::Char(' ') | KeyCode::Enter => {
|
||||
if self.state.range_select.start.is_none() {
|
||||
self.state.range_select.start = Some(self.book.location.clone());
|
||||
} else {
|
||||
self.state.range_select.end = Some(self.book.location.clone());
|
||||
self.exit_range_select_mode()?;
|
||||
}
|
||||
}
|
||||
KeyCode::Char('n') if key.modifiers == KeyModifiers::CONTROL => {
|
||||
self.state.range_select.reset_range_selection();
|
||||
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||
ws.book.select_next_sheet();
|
||||
Ok(())
|
||||
})?;
|
||||
self.state.range_select.sheet = Some(self.book.current_sheet);
|
||||
}
|
||||
KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => {
|
||||
self.state.range_select.reset_range_selection();
|
||||
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||
ws.book.select_prev_sheet();
|
||||
Ok(())
|
||||
})?;
|
||||
self.state.range_select.sheet = Some(self.book.current_sheet);
|
||||
}
|
||||
_ => {
|
||||
// moop
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn maybe_update_range_end(&mut self) {
|
||||
if self.state.range_select.start.is_some() {
|
||||
self.state.range_select.end = Some(self.book.location.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_navigation_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
@ -381,6 +556,9 @@ impl<'ws> Workspace<'ws> {
|
||||
KeyCode::Char('s') if key.modifiers == KeyModifiers::CONTROL => {
|
||||
self.save_file()?;
|
||||
}
|
||||
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
|
||||
self.enter_range_select_mode();
|
||||
}
|
||||
KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => {
|
||||
self.enter_dialog_mode(self.render_help_text());
|
||||
}
|
||||
@ -487,7 +665,10 @@ impl<'ws> Workspace<'ws> {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
fn run_with_prefix(&mut self, action: impl Fn(&mut Workspace<'_>) -> std::result::Result<(), anyhow::Error>) -> Result<(), anyhow::Error> {
|
||||
fn run_with_prefix(
|
||||
&mut self,
|
||||
action: impl Fn(&mut Workspace<'_>) -> std::result::Result<(), anyhow::Error>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
for _ in 1..=self.state.get_n_prefix() {
|
||||
action(self)?;
|
||||
}
|
||||
@ -495,10 +676,6 @@ impl<'ws> Workspace<'ws> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn enter_navigation_mode(&mut self) {
|
||||
self.state.modality_stack.push(Modality::Navigate);
|
||||
}
|
||||
|
||||
fn enter_command_mode(&mut self) {
|
||||
self.state.modality_stack.push(Modality::Command);
|
||||
self.state.command_state.truncate();
|
||||
@ -511,6 +688,15 @@ impl<'ws> Workspace<'ws> {
|
||||
self.state.modality_stack.push(Modality::Dialog);
|
||||
}
|
||||
|
||||
fn enter_range_select_mode(&mut self) {
|
||||
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_location = Some(self.book.location.clone());
|
||||
self.state.range_select.start = None;
|
||||
self.state.range_select.end = None;
|
||||
self.state.modality_stack.push(Modality::RangeSelect);
|
||||
}
|
||||
|
||||
fn enter_edit_mode(&mut self) {
|
||||
self.state.modality_stack.push(Modality::CellEdit);
|
||||
self.text_area
|
||||
@ -535,6 +721,30 @@ impl<'ws> Workspace<'ws> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exit_range_select_mode(&mut self) -> Result<()> {
|
||||
self.book.current_sheet = self
|
||||
.state
|
||||
.range_select
|
||||
.original_sheet
|
||||
.clone()
|
||||
.expect("Missing original sheet");
|
||||
self.book.location = self
|
||||
.state
|
||||
.range_select
|
||||
.original_location
|
||||
.clone()
|
||||
.expect("Missing original location after range copy");
|
||||
self.state.range_select.original_location = None;
|
||||
self.state.pop_modality();
|
||||
if self.state.modality() == &Modality::CellEdit {
|
||||
self.text_area
|
||||
.set_yank_text(self.selected_range_to_string());
|
||||
self.text_area.paste();
|
||||
self.state.dirty = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exit_edit_mode(&mut self, keep: bool) -> Result<()> {
|
||||
self.text_area.set_cursor_line_style(Style::default());
|
||||
self.text_area.set_cursor_style(Style::default());
|
||||
@ -542,11 +752,10 @@ impl<'ws> Workspace<'ws> {
|
||||
if self.state.dirty && keep {
|
||||
self.book.edit_current_cell(contents)?;
|
||||
self.book.evaluate();
|
||||
} else {
|
||||
self.text_area = reset_text_area(self.book.get_current_cell_contents()?);
|
||||
}
|
||||
self.text_area = reset_text_area(self.book.get_current_cell_contents()?);
|
||||
self.state.dirty = false;
|
||||
self.enter_navigation_mode();
|
||||
self.state.pop_modality();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,7 @@ impl<'ws> Workspace<'ws> {
|
||||
Box::new(move |rect: Rect, buf: &mut Buffer, ws: &mut Self| {
|
||||
let sheet_name = ws.book.get_sheet_name().unwrap_or("Unknown");
|
||||
let table_block = Block::bordered().title_top(sheet_name);
|
||||
let viewport = Viewport::new(&ws.book)
|
||||
let viewport = Viewport::new(&ws.book, &ws.state.range_select)
|
||||
.with_selected(ws.book.location.clone())
|
||||
.block(table_block);
|
||||
StatefulWidget::render(viewport, rect, buf, &mut ws.state.viewport_state);
|
||||
@ -95,6 +95,7 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
|
||||
Modality::CellEdit => "edit",
|
||||
Modality::Command => "command",
|
||||
Modality::Dialog => "",
|
||||
Modality::RangeSelect => "range-copy",
|
||||
})
|
||||
.title_bottom(
|
||||
Line::from(format!(
|
||||
|
@ -1,15 +1,23 @@
|
||||
use ironcalc::base::Model;
|
||||
|
||||
use crate::ui::AppState;
|
||||
|
||||
use super::{Address, Book, Viewport, ViewportState};
|
||||
|
||||
#[test]
|
||||
fn test_viewport_get_visible_columns() {
|
||||
let mut state = ViewportState::default();
|
||||
let book = Book::new(Model::new_empty("test", "en", "America/New_York").expect("Failed to make model"));
|
||||
let book = Book::new(
|
||||
Model::new_empty("test", "en", "America/New_York").expect("Failed to make model"),
|
||||
);
|
||||
let default_size = book.get_col_size(1).expect("Failed to get column size");
|
||||
let width = dbg!(dbg!(default_size) * 12 / 2);
|
||||
let viewport = Viewport::new(&book).with_selected(Address { row: 1, col: 17 });
|
||||
let cols = viewport.get_visible_columns((width + 5) as u16, &mut state).expect("Failed to get visible columns");
|
||||
let app_state = AppState::default();
|
||||
let viewport =
|
||||
Viewport::new(&book, &app_state.range_select).with_selected(Address { row: 1, col: 17 });
|
||||
let cols = viewport
|
||||
.get_visible_columns((width + 5) as u16, &mut state)
|
||||
.expect("Failed to get visible columns");
|
||||
assert_eq!(5, cols.len());
|
||||
assert_eq!(17, cols.last().expect("Failed to get last column").idx);
|
||||
}
|
||||
@ -17,32 +25,50 @@ fn test_viewport_get_visible_columns() {
|
||||
#[test]
|
||||
fn test_viewport_get_visible_rows() {
|
||||
let mut state = dbg!(ViewportState::default());
|
||||
let book = Book::new(Model::new_empty("test", "en", "America/New_York").expect("Failed to make model"));
|
||||
let book = Book::new(
|
||||
Model::new_empty("test", "en", "America/New_York").expect("Failed to make model"),
|
||||
);
|
||||
let height = 6;
|
||||
let viewport = Viewport::new(&book).with_selected(Address { row: 17, col: 1 });
|
||||
let app_state = AppState::default();
|
||||
let viewport =
|
||||
Viewport::new(&book, &app_state.range_select).with_selected(Address { row: 17, col: 1 });
|
||||
let rows = dbg!(viewport.get_visible_rows(height as u16, &mut state));
|
||||
assert_eq!(height - 1, rows.len());
|
||||
assert_eq!(17 - (height - 2), *rows.first().expect("Failed to get first row"));
|
||||
assert_eq!(
|
||||
17 - (height - 2),
|
||||
*rows.first().expect("Failed to get first row")
|
||||
);
|
||||
assert_eq!(17, *rows.last().expect("Failed to get last row"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_viewport_visible_columns_after_length_change() {
|
||||
let mut state = ViewportState::default();
|
||||
let mut book = Book::new(Model::new_empty("test", "en", "America/New_York").expect("Failed to make model"));
|
||||
let mut book = Book::new(
|
||||
Model::new_empty("test", "en", "America/New_York").expect("Failed to make model"),
|
||||
);
|
||||
let default_size = book.get_col_size(1).expect("Failed to get column size");
|
||||
let width = dbg!(dbg!(default_size) * 12 / 2);
|
||||
{
|
||||
let viewport = Viewport::new(&book).with_selected(Address { row: 1, col: 17 });
|
||||
let cols = viewport.get_visible_columns((width + 5) as u16, &mut state).expect("Failed to get visible columns");
|
||||
let app_state = AppState::default();
|
||||
let viewport = Viewport::new(&book, &app_state.range_select)
|
||||
.with_selected(Address { row: 1, col: 17 });
|
||||
let cols = viewport
|
||||
.get_visible_columns((width + 5) as u16, &mut state)
|
||||
.expect("Failed to get visible columns");
|
||||
assert_eq!(5, cols.len());
|
||||
assert_eq!(17, cols.last().expect("Failed to get last column").idx);
|
||||
}
|
||||
|
||||
book.set_col_size(1, default_size * 6).expect("Failed to set column size");
|
||||
book.set_col_size(1, default_size * 6)
|
||||
.expect("Failed to set column size");
|
||||
{
|
||||
let viewport = Viewport::new(&book).with_selected(Address { row: 1, col: 1 });
|
||||
let cols = viewport.get_visible_columns((width + 5) as u16, &mut state).expect("Failed to get visible columns");
|
||||
let app_state = AppState::default();
|
||||
let viewport =
|
||||
Viewport::new(&book, &app_state.range_select).with_selected(Address { row: 1, col: 1 });
|
||||
let cols = viewport
|
||||
.get_visible_columns((width + 5) as u16, &mut state)
|
||||
.expect("Failed to get visible columns");
|
||||
assert_eq!(1, cols.len());
|
||||
assert_eq!(1, cols.last().expect("Failed to get last column").idx);
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use ratatui::{
|
||||
widgets::{Block, Cell, Row, StatefulWidget, Table, Widget},
|
||||
};
|
||||
|
||||
use super::{Address, Book};
|
||||
use super::{Address, Book, RangeSelection};
|
||||
|
||||
// TODO(zaphar): Move this to the book module.
|
||||
// NOTE(zaphar): This is stolen from ironcalc but ironcalc doesn't expose it
|
||||
@ -34,10 +34,11 @@ pub struct ViewportState {
|
||||
}
|
||||
|
||||
/// A renderable viewport over a book.
|
||||
pub struct Viewport<'book> {
|
||||
pub struct Viewport<'ws> {
|
||||
pub(crate) selected: Address,
|
||||
book: &'book Book,
|
||||
block: Option<Block<'book>>,
|
||||
book: &'ws Book,
|
||||
range_selection: &'ws RangeSelection,
|
||||
block: Option<Block<'ws>>,
|
||||
}
|
||||
|
||||
pub(crate) const COLNAMES: [&'static str; 26] = [
|
||||
@ -45,10 +46,11 @@ pub(crate) const COLNAMES: [&'static str; 26] = [
|
||||
"T", "U", "V", "W", "X", "Y", "Z",
|
||||
];
|
||||
|
||||
impl<'book> Viewport<'book> {
|
||||
pub fn new(book: &'book Book) -> Self {
|
||||
impl<'ws> Viewport<'ws> {
|
||||
pub fn new(book: &'ws Book, app_state: &'ws RangeSelection) -> Self {
|
||||
Self {
|
||||
book,
|
||||
range_selection: app_state,
|
||||
selected: Default::default(),
|
||||
block: None,
|
||||
}
|
||||
@ -127,7 +129,7 @@ impl<'book> Viewport<'book> {
|
||||
return Ok(visible);
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'book>) -> Self {
|
||||
pub fn block(mut self, block: Block<'ws>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
@ -158,7 +160,17 @@ impl<'book> Viewport<'book> {
|
||||
.book
|
||||
.get_cell_addr_rendered(&Address { row: ri, col: *ci })
|
||||
.unwrap();
|
||||
let cell = Cell::new(Text::raw(content));
|
||||
let mut cell = Cell::new(Text::raw(content));
|
||||
if let Some((start, end)) = &self.range_selection.get_range() {
|
||||
if ri >= start.row
|
||||
&& ri <= end.row
|
||||
&& *ci >= start.col
|
||||
&& *ci <= end.col
|
||||
{
|
||||
// This is a selected range
|
||||
cell = cell.fg(Color::Black).bg(Color::LightBlue)
|
||||
}
|
||||
}
|
||||
match (self.book.location.row == ri, self.book.location.col == *ci) {
|
||||
(true, true) => cell.fg(Color::White).bg(Color::Rgb(57, 61, 71)),
|
||||
_ => cell,
|
||||
@ -197,7 +209,7 @@ impl<'book> Viewport<'book> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'book> StatefulWidget for Viewport<'book> {
|
||||
impl<'ws> StatefulWidget for Viewport<'ws> {
|
||||
type State = ViewportState;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
use crate::ui::Modality;
|
||||
use crate::ui::{Address, Modality};
|
||||
|
||||
use super::cmd::{parse, Cmd};
|
||||
use super::Workspace;
|
||||
@ -351,3 +351,77 @@ fn test_navigation_tab_next_numeric_prefix()
|
||||
.expect("Failed to handle 'Ctrl-n' key event");
|
||||
assert_eq!("Sheet1", ws.book.get_sheet_name().expect("Failed to get sheet name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_copy() {
|
||||
let mut ws =
|
||||
Workspace::new_empty("en", "America/New_York").expect("Failed to get empty workbook");
|
||||
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
||||
|
||||
ws.book.move_to(&Address { row: 1, col: 1, }).expect("Failed to move to row");
|
||||
let original_loc = ws.book.location.clone();
|
||||
ws.handle_input(construct_modified_key_event(KeyCode::Char('r'), KeyModifiers::CONTROL))
|
||||
.expect("Failed to handle 'Ctrl-r' key event");
|
||||
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last());
|
||||
assert_eq!(Some(original_loc.clone()), ws.state.range_select.original_location);
|
||||
assert!(ws.state.range_select.start.is_none());
|
||||
assert!(ws.state.range_select.end.is_none());
|
||||
|
||||
ws.handle_input(construct_key_event(KeyCode::Char('l')))
|
||||
.expect("Failed to handle 'l' key event");
|
||||
ws.handle_input(construct_key_event(KeyCode::Char(' ')))
|
||||
.expect("Failed to handle ' ' key event");
|
||||
assert_eq!(Some(Address {row:1, col:2, }), ws.state.range_select.start);
|
||||
|
||||
ws.handle_input(construct_key_event(KeyCode::Char('j')))
|
||||
.expect("Failed to handle 'j' key event");
|
||||
ws.handle_input(construct_key_event(KeyCode::Char(' ')))
|
||||
.expect("Failed to handle ' ' key event");
|
||||
|
||||
assert!(ws.state.range_select.original_location.is_none());
|
||||
assert_eq!(Some(Address {row:1, col:2, }), ws.state.range_select.start);
|
||||
assert_eq!(Some(Address {row:2, col:2, }), ws.state.range_select.end);
|
||||
assert_eq!(original_loc, ws.book.location);
|
||||
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
||||
|
||||
ws.book.move_to(&Address { row: 5, col: 5, }).expect("Failed to move to row");
|
||||
let original_loc_2 = ws.book.location.clone();
|
||||
assert_eq!(Address { row: 5, col: 5 }, original_loc_2);
|
||||
|
||||
ws.handle_input(construct_modified_key_event(KeyCode::Char('r'), KeyModifiers::CONTROL))
|
||||
.expect("Failed to handle 'Ctrl-r' key event");
|
||||
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last());
|
||||
assert_eq!(Some(original_loc_2.clone()), ws.state.range_select.original_location);
|
||||
assert!(ws.state.range_select.start.is_none());
|
||||
assert!(ws.state.range_select.end.is_none());
|
||||
|
||||
ws.handle_input(construct_key_event(KeyCode::Char('h')))
|
||||
.expect("Failed to handle 'h' key event");
|
||||
ws.handle_input(construct_key_event(KeyCode::Char(' ')))
|
||||
.expect("Failed to handle ' ' key event");
|
||||
assert_eq!(Some(Address {row:5, col:4, }), ws.state.range_select.start);
|
||||
|
||||
ws.handle_input(construct_key_event(KeyCode::Char('k')))
|
||||
.expect("Failed to handle 'k' key event");
|
||||
ws.handle_input(construct_key_event(KeyCode::Char(' ')))
|
||||
.expect("Failed to handle ' ' key event");
|
||||
|
||||
assert!(ws.state.range_select.original_location.is_none());
|
||||
assert_eq!(Some(Address {row:5, col:4, }), ws.state.range_select.start);
|
||||
assert_eq!(Some(Address {row:4, col:4, }), ws.state.range_select.end);
|
||||
assert_eq!(original_loc_2, ws.book.location);
|
||||
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_copy_mode_from_edit_mode() {
|
||||
let mut ws =
|
||||
Workspace::new_empty("en", "America/New_York").expect("Failed to get empty workbook");
|
||||
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
||||
ws.handle_input(construct_key_event(KeyCode::Char('e')))
|
||||
.expect("Failed to handle 'e' key event");
|
||||
assert_eq!(Some(&Modality::CellEdit), ws.state.modality_stack.last());
|
||||
ws.handle_input(construct_modified_key_event(KeyCode::Char('r'), KeyModifiers::CONTROL))
|
||||
.expect("Failed to handle 'Ctrl-r' key event");
|
||||
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last());
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user