Compare commits

..

7 Commits

Author SHA1 Message Date
a85c5121ee wip: handle range resets when switching sheets 2024-12-05 17:59:46 -05:00
4b3e25895d chore: cleanup unused imports 2024-12-05 17:59:38 -05:00
61a6e93515 wip: bits of polish
* show what you are selecting in range mode
* Fix error with cell edit not updating after Enter
2024-12-05 17:32:58 -05:00
5f6f45141c fix: various bugs around cell edit state and docs 2024-12-05 14:36:26 -05:00
bb5d81106e docs: help text, spelling 2024-12-05 14:20:29 -05:00
c62fd08043 feat: range copy mode 2024-12-05 14:20:29 -05:00
626748db0f chore: cleanup navigation mode 2024-12-05 14:18:26 -05:00
6 changed files with 206 additions and 105 deletions

View File

@ -22,11 +22,14 @@ Options:
## User Interface ## 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 ### 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** **Cell Navigation**
@ -44,7 +47,9 @@ Sheet navigation moving will loop around when you reach the ends.
**Numeric prefixes** **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** **Modifying the Sheet or Cells**
@ -54,36 +59,51 @@ You can prefix each of the keybinds above with a numeric prefix to do them that
**Other Keybindings** **Other Keybindings**
* `Ctrl-r` will enter range selection mode
* `Ctrl-s` will save the sheet. * `Ctrl-s` will save the sheet.
* `q` will exit the application. * `q` will exit the application.
* `:` will enter CommandMode. * `:` 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 ### 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: Starting with:
* `=` will treat what you type as a formula. * `=` will treat what you type as a formula.
* `$` will treat it as us currency. * `$` 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. * `Enter` will update the cell contents.
* `Esc` will cancel editing the cell and leave it unedited. * `Esc` will cancel editing the cell and leave it unedited.
* `Ctrl-p` will paste the range selection if it exists into the cell.
`Ctrl-r` will enter range select mode when editing a formula. You can navigate around the `Ctrl-r` will enter range select mode when editing a formula. You can navigate
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. 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) You can find the functions we support documented here:
[ironcalc docs](https://docs.ironcalc.com/functions/lookup-and-reference.html)
### Command Mode ### 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: The currently supported commands are:
@ -96,16 +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. * `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. * `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
Range Select mode copies a range reference for use later. You can enter range select mode from CellEdit mode with `CTRL-r`. 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. * `h`, `j`, `k`, `l` will navigate around the sheet.
* `Ctrl-n`, `Ctrl-p` will navigate between sheets. * `Ctrl-n`, `Ctrl-p` will navigate between sheets.
* ` ` the spacebar will select the start and end of the range respectively. * `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. 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> <aside>We only support continuous ranges for the moment. Planned for
discontinuous ranges still needs the interaction interface to be
determined.</aside>

View File

@ -33,17 +33,46 @@ pub enum Modality {
RangeSelect, 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)] #[derive(Debug)]
pub struct AppState<'ws> { pub struct AppState<'ws> {
pub modality_stack: Vec<Modality>, pub modality_stack: Vec<Modality>,
pub viewport_state: ViewportState, pub viewport_state: ViewportState,
pub command_state: TextState<'ws>, pub command_state: TextState<'ws>,
pub numeric_prefix: Vec<char>, pub numeric_prefix: Vec<char>,
pub original_location: Option<Address>, pub range_select: RangeSelection,
pub original_sheet: Option<u32>,
pub range_sheet: Option<u32>,
pub start_range: Option<Address>,
pub end_range: Option<Address>,
dirty: bool, dirty: bool,
popup: Vec<String>, popup: Vec<String>,
} }
@ -55,11 +84,7 @@ impl<'ws> Default for AppState<'ws> {
viewport_state: Default::default(), viewport_state: Default::default(),
command_state: Default::default(), command_state: Default::default(),
numeric_prefix: Default::default(), numeric_prefix: Default::default(),
original_location: Default::default(), range_select: Default::default(),
original_sheet: Default::default(),
range_sheet: Default::default(),
start_range: Default::default(),
end_range: Default::default(),
dirty: Default::default(), dirty: Default::default(),
popup: Default::default(), popup: Default::default(),
} }
@ -175,29 +200,22 @@ impl<'ws> Workspace<'ws> {
pub fn selected_range_to_string(&self) -> String { pub fn selected_range_to_string(&self) -> String {
let state = &self.state; let state = &self.state;
let start = state if let Some((start, end)) = state.range_select.get_range() {
.start_range let a1 = format!("{}{}", start.to_range_part(), format!(":{}", end.to_range_part()));
.as_ref() if let Some(range_sheet) = state.range_select.sheet {
.map(|addr| addr.to_range_part()) if range_sheet != self.book.current_sheet {
.unwrap_or_else(|| String::new()); return format!(
let end = state "{}!{}",
.end_range self.book
.as_ref() .get_sheet_name_by_idx(range_sheet as usize)
.map(|addr| format!(":{}", addr.to_range_part())) .expect("No such sheet index"),
.unwrap_or_else(|| String::new()); a1
if let Some(range_sheet) = state.range_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"),
start,
end
);
} }
return a1;
} }
format!("{}:{}", start, end) return String::new()
} }
/// Move a row down in the current sheet. /// Move a row down in the current sheet.
@ -342,8 +360,10 @@ impl<'ws> Workspace<'ws> {
return Ok(None); return Ok(None);
} }
KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => { KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => {
self.text_area.set_yank_text(self.selected_range_to_string()); self.text_area
.set_yank_text(self.selected_range_to_string());
self.text_area.paste(); self.text_area.paste();
self.state.dirty = true;
return Ok(None); return Ok(None);
} }
KeyCode::Enter => self.exit_edit_mode(true)?, KeyCode::Enter => self.exit_edit_mode(true)?,
@ -437,7 +457,13 @@ impl<'ws> Workspace<'ws> {
if key.kind == KeyEventKind::Press { if key.kind == KeyEventKind::Press {
match key.code { match key.code {
KeyCode::Esc => { KeyCode::Esc => {
self.state.reset_n_prefix(); 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 => { KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => {
self.enter_dialog_mode(self.render_help_text()); self.enter_dialog_mode(self.render_help_text());
@ -451,46 +477,52 @@ impl<'ws> Workspace<'ws> {
ws.move_left()?; ws.move_left()?;
Ok(()) Ok(())
})?; })?;
self.maybe_update_range_end();
} }
KeyCode::Char('j') => { KeyCode::Char('j') => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.move_down()?; ws.move_down()?;
Ok(()) Ok(())
})?; })?;
self.maybe_update_range_end();
} }
KeyCode::Char('k') => { KeyCode::Char('k') => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.move_up()?; ws.move_up()?;
Ok(()) Ok(())
})?; })?;
self.maybe_update_range_end();
} }
KeyCode::Char('l') => { KeyCode::Char('l') => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.move_right()?; ws.move_right()?;
Ok(()) Ok(())
})?; })?;
self.maybe_update_range_end();
} }
KeyCode::Char(' ') => { KeyCode::Char(' ') | KeyCode::Enter => {
if self.state.start_range.is_none() { if self.state.range_select.start.is_none() {
self.state.start_range = Some(self.book.location.clone()); self.state.range_select.start = Some(self.book.location.clone());
} else { } else {
self.state.end_range = Some(self.book.location.clone()); self.state.range_select.end = Some(self.book.location.clone());
self.exit_range_select_mode()?; self.exit_range_select_mode()?;
} }
} }
KeyCode::Char('n') if key.modifiers == KeyModifiers::CONTROL => { KeyCode::Char('n') if key.modifiers == KeyModifiers::CONTROL => {
self.state.range_select.reset_range_selection();
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.book.select_next_sheet(); ws.book.select_next_sheet();
Ok(()) Ok(())
})?; })?;
self.state.range_sheet = Some(self.book.current_sheet); self.state.range_select.sheet = Some(self.book.current_sheet);
} }
KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => { KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => {
self.state.range_select.reset_range_selection();
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.book.select_prev_sheet(); ws.book.select_prev_sheet();
Ok(()) Ok(())
})?; })?;
self.state.range_sheet = Some(self.book.current_sheet); self.state.range_select.sheet = Some(self.book.current_sheet);
} }
_ => { _ => {
// moop // moop
@ -500,6 +532,12 @@ impl<'ws> Workspace<'ws> {
Ok(None) 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>> { fn handle_navigation_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
if key.kind == KeyEventKind::Press { if key.kind == KeyEventKind::Press {
match key.code { match key.code {
@ -638,10 +676,6 @@ impl<'ws> Workspace<'ws> {
Ok(()) Ok(())
} }
fn enter_navigation_mode(&mut self) {
self.state.modality_stack.push(Modality::Navigate);
}
fn enter_command_mode(&mut self) { fn enter_command_mode(&mut self) {
self.state.modality_stack.push(Modality::Command); self.state.modality_stack.push(Modality::Command);
self.state.command_state.truncate(); self.state.command_state.truncate();
@ -655,11 +689,11 @@ impl<'ws> Workspace<'ws> {
} }
fn enter_range_select_mode(&mut self) { fn enter_range_select_mode(&mut self) {
self.state.range_sheet = Some(self.book.current_sheet); self.state.range_select.sheet = Some(self.book.current_sheet);
self.state.original_sheet = Some(self.book.current_sheet); self.state.range_select.original_sheet = Some(self.book.current_sheet);
self.state.original_location = Some(self.book.location.clone()); self.state.range_select.original_location = Some(self.book.location.clone());
self.state.start_range = None; self.state.range_select.start = None;
self.state.end_range = None; self.state.range_select.end = None;
self.state.modality_stack.push(Modality::RangeSelect); self.state.modality_stack.push(Modality::RangeSelect);
} }
@ -690,19 +724,23 @@ impl<'ws> Workspace<'ws> {
fn exit_range_select_mode(&mut self) -> Result<()> { fn exit_range_select_mode(&mut self) -> Result<()> {
self.book.current_sheet = self self.book.current_sheet = self
.state .state
.range_select
.original_sheet .original_sheet
.clone() .clone()
.expect("Missing original sheet"); .expect("Missing original sheet");
self.book.location = self self.book.location = self
.state .state
.range_select
.original_location .original_location
.clone() .clone()
.expect("Missing original location after range copy"); .expect("Missing original location after range copy");
self.state.original_location = None; self.state.range_select.original_location = None;
self.state.pop_modality(); self.state.pop_modality();
if self.state.modality() == &Modality::CellEdit { if self.state.modality() == &Modality::CellEdit {
self.text_area.set_yank_text(self.selected_range_to_string()); self.text_area
.set_yank_text(self.selected_range_to_string());
self.text_area.paste(); self.text_area.paste();
self.state.dirty = true;
} }
Ok(()) Ok(())
} }
@ -714,9 +752,8 @@ impl<'ws> Workspace<'ws> {
if self.state.dirty && keep { if self.state.dirty && keep {
self.book.edit_current_cell(contents)?; self.book.edit_current_cell(contents)?;
self.book.evaluate(); 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.state.dirty = false;
self.state.pop_modality(); self.state.pop_modality();
Ok(()) Ok(())

View File

@ -44,7 +44,7 @@ impl<'ws> Workspace<'ws> {
Box::new(move |rect: Rect, buf: &mut Buffer, ws: &mut Self| { Box::new(move |rect: Rect, buf: &mut Buffer, ws: &mut Self| {
let sheet_name = ws.book.get_sheet_name().unwrap_or("Unknown"); let sheet_name = ws.book.get_sheet_name().unwrap_or("Unknown");
let table_block = Block::bordered().title_top(sheet_name); 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()) .with_selected(ws.book.location.clone())
.block(table_block); .block(table_block);
StatefulWidget::render(viewport, rect, buf, &mut ws.state.viewport_state); StatefulWidget::render(viewport, rect, buf, &mut ws.state.viewport_state);

View File

@ -1,15 +1,23 @@
use ironcalc::base::Model; use ironcalc::base::Model;
use crate::ui::AppState;
use super::{Address, Book, Viewport, ViewportState}; use super::{Address, Book, Viewport, ViewportState};
#[test] #[test]
fn test_viewport_get_visible_columns() { fn test_viewport_get_visible_columns() {
let mut state = ViewportState::default(); 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 default_size = book.get_col_size(1).expect("Failed to get column size");
let width = dbg!(dbg!(default_size) * 12 / 2); let width = dbg!(dbg!(default_size) * 12 / 2);
let viewport = Viewport::new(&book).with_selected(Address { row: 1, col: 17 }); let app_state = AppState::default();
let cols = viewport.get_visible_columns((width + 5) as u16, &mut state).expect("Failed to get visible columns"); 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!(5, cols.len());
assert_eq!(17, cols.last().expect("Failed to get last column").idx); assert_eq!(17, cols.last().expect("Failed to get last column").idx);
} }
@ -17,32 +25,50 @@ fn test_viewport_get_visible_columns() {
#[test] #[test]
fn test_viewport_get_visible_rows() { fn test_viewport_get_visible_rows() {
let mut state = dbg!(ViewportState::default()); 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 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)); let rows = dbg!(viewport.get_visible_rows(height as u16, &mut state));
assert_eq!(height - 1, rows.len()); 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")); assert_eq!(17, *rows.last().expect("Failed to get last row"));
} }
#[test] #[test]
fn test_viewport_visible_columns_after_length_change() { fn test_viewport_visible_columns_after_length_change() {
let mut state = ViewportState::default(); 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 default_size = book.get_col_size(1).expect("Failed to get column size");
let width = dbg!(dbg!(default_size) * 12 / 2); let width = dbg!(dbg!(default_size) * 12 / 2);
{ {
let viewport = Viewport::new(&book).with_selected(Address { row: 1, col: 17 }); let app_state = AppState::default();
let cols = viewport.get_visible_columns((width + 5) as u16, &mut state).expect("Failed to get visible columns"); 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!(5, cols.len());
assert_eq!(17, cols.last().expect("Failed to get last column").idx); 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 app_state = AppState::default();
let cols = viewport.get_visible_columns((width + 5) as u16, &mut state).expect("Failed to get visible columns"); 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.len());
assert_eq!(1, cols.last().expect("Failed to get last column").idx); assert_eq!(1, cols.last().expect("Failed to get last column").idx);
} }

View File

@ -7,7 +7,7 @@ use ratatui::{
widgets::{Block, Cell, Row, StatefulWidget, Table, Widget}, widgets::{Block, Cell, Row, StatefulWidget, Table, Widget},
}; };
use super::{Address, Book}; use super::{Address, Book, RangeSelection};
// TODO(zaphar): Move this to the book module. // TODO(zaphar): Move this to the book module.
// NOTE(zaphar): This is stolen from ironcalc but ironcalc doesn't expose it // 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. /// A renderable viewport over a book.
pub struct Viewport<'book> { pub struct Viewport<'ws> {
pub(crate) selected: Address, pub(crate) selected: Address,
book: &'book Book, book: &'ws Book,
block: Option<Block<'book>>, range_selection: &'ws RangeSelection,
block: Option<Block<'ws>>,
} }
pub(crate) const COLNAMES: [&'static str; 26] = [ 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", "T", "U", "V", "W", "X", "Y", "Z",
]; ];
impl<'book> Viewport<'book> { impl<'ws> Viewport<'ws> {
pub fn new(book: &'book Book) -> Self { pub fn new(book: &'ws Book, app_state: &'ws RangeSelection) -> Self {
Self { Self {
book, book,
range_selection: app_state,
selected: Default::default(), selected: Default::default(),
block: None, block: None,
} }
@ -127,7 +129,7 @@ impl<'book> Viewport<'book> {
return Ok(visible); 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.block = Some(block);
self self
} }
@ -158,7 +160,17 @@ impl<'book> Viewport<'book> {
.book .book
.get_cell_addr_rendered(&Address { row: ri, col: *ci }) .get_cell_addr_rendered(&Address { row: ri, col: *ci })
.unwrap(); .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) { match (self.book.location.row == ri, self.book.location.col == *ci) {
(true, true) => cell.fg(Color::White).bg(Color::Rgb(57, 61, 71)), (true, true) => cell.fg(Color::White).bg(Color::Rgb(57, 61, 71)),
_ => cell, _ => cell,
@ -197,7 +209,7 @@ impl<'book> Viewport<'book> {
} }
} }
impl<'book> StatefulWidget for Viewport<'book> { impl<'ws> StatefulWidget for Viewport<'ws> {
type State = ViewportState; type State = ViewportState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {

View File

@ -363,24 +363,24 @@ fn test_range_copy() {
ws.handle_input(construct_modified_key_event(KeyCode::Char('r'), KeyModifiers::CONTROL)) ws.handle_input(construct_modified_key_event(KeyCode::Char('r'), KeyModifiers::CONTROL))
.expect("Failed to handle 'Ctrl-r' key event"); .expect("Failed to handle 'Ctrl-r' key event");
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last()); assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last());
assert_eq!(Some(original_loc.clone()), ws.state.original_location); assert_eq!(Some(original_loc.clone()), ws.state.range_select.original_location);
assert!(ws.state.start_range.is_none()); assert!(ws.state.range_select.start.is_none());
assert!(ws.state.end_range.is_none()); assert!(ws.state.range_select.end.is_none());
ws.handle_input(construct_key_event(KeyCode::Char('l'))) ws.handle_input(construct_key_event(KeyCode::Char('l')))
.expect("Failed to handle 'l' key event"); .expect("Failed to handle 'l' key event");
ws.handle_input(construct_key_event(KeyCode::Char(' '))) ws.handle_input(construct_key_event(KeyCode::Char(' ')))
.expect("Failed to handle ' ' key event"); .expect("Failed to handle ' ' key event");
assert_eq!(Some(Address {row:1, col:2, }), ws.state.start_range); assert_eq!(Some(Address {row:1, col:2, }), ws.state.range_select.start);
ws.handle_input(construct_key_event(KeyCode::Char('j'))) ws.handle_input(construct_key_event(KeyCode::Char('j')))
.expect("Failed to handle 'j' key event"); .expect("Failed to handle 'j' key event");
ws.handle_input(construct_key_event(KeyCode::Char(' '))) ws.handle_input(construct_key_event(KeyCode::Char(' ')))
.expect("Failed to handle ' ' key event"); .expect("Failed to handle ' ' key event");
assert!(ws.state.original_location.is_none()); assert!(ws.state.range_select.original_location.is_none());
assert_eq!(Some(Address {row:1, col:2, }), ws.state.start_range); assert_eq!(Some(Address {row:1, col:2, }), ws.state.range_select.start);
assert_eq!(Some(Address {row:2, col:2, }), ws.state.end_range); assert_eq!(Some(Address {row:2, col:2, }), ws.state.range_select.end);
assert_eq!(original_loc, ws.book.location); assert_eq!(original_loc, ws.book.location);
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last()); assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
@ -391,24 +391,24 @@ fn test_range_copy() {
ws.handle_input(construct_modified_key_event(KeyCode::Char('r'), KeyModifiers::CONTROL)) ws.handle_input(construct_modified_key_event(KeyCode::Char('r'), KeyModifiers::CONTROL))
.expect("Failed to handle 'Ctrl-r' key event"); .expect("Failed to handle 'Ctrl-r' key event");
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last()); assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last());
assert_eq!(Some(original_loc_2.clone()), ws.state.original_location); assert_eq!(Some(original_loc_2.clone()), ws.state.range_select.original_location);
assert!(ws.state.start_range.is_none()); assert!(ws.state.range_select.start.is_none());
assert!(ws.state.end_range.is_none()); assert!(ws.state.range_select.end.is_none());
ws.handle_input(construct_key_event(KeyCode::Char('h'))) ws.handle_input(construct_key_event(KeyCode::Char('h')))
.expect("Failed to handle 'h' key event"); .expect("Failed to handle 'h' key event");
ws.handle_input(construct_key_event(KeyCode::Char(' '))) ws.handle_input(construct_key_event(KeyCode::Char(' ')))
.expect("Failed to handle ' ' key event"); .expect("Failed to handle ' ' key event");
assert_eq!(Some(Address {row:5, col:4, }), ws.state.start_range); assert_eq!(Some(Address {row:5, col:4, }), ws.state.range_select.start);
ws.handle_input(construct_key_event(KeyCode::Char('k'))) ws.handle_input(construct_key_event(KeyCode::Char('k')))
.expect("Failed to handle 'k' key event"); .expect("Failed to handle 'k' key event");
ws.handle_input(construct_key_event(KeyCode::Char(' '))) ws.handle_input(construct_key_event(KeyCode::Char(' ')))
.expect("Failed to handle ' ' key event"); .expect("Failed to handle ' ' key event");
assert!(ws.state.original_location.is_none()); assert!(ws.state.range_select.original_location.is_none());
assert_eq!(Some(Address {row:5, col:4, }), ws.state.start_range); assert_eq!(Some(Address {row:5, col:4, }), ws.state.range_select.start);
assert_eq!(Some(Address {row:4, col:4, }), ws.state.end_range); assert_eq!(Some(Address {row:4, col:4, }), ws.state.range_select.end);
assert_eq!(original_loc_2, ws.book.location); assert_eq!(original_loc_2, ws.book.location);
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last()); assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
} }