diff --git a/src/ui/mod.rs b/src/ui/mod.rs index bbd1163..1e18d85 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -33,17 +33,40 @@ pub enum Modality { RangeSelect, } +#[derive(Debug, Default)] +pub struct RangeSelection { + pub original_location: Option
, + pub original_sheet: Option, + pub sheet: Option, + pub start: Option
, + pub end: Option
, +} + +impl RangeSelection { + pub fn get_range(&self) -> Option<(Address, Address)> { + if let (Some(start), Some(end)) = (&self.start, &self.end) { + return Some(( + Address { + row: std::cmp::min(start.row, end.row), + col: std::cmp::min(start.col, end.col), + }, + Address { + row: std::cmp::max(start.row, end.row), + col: std::cmp::max(start.col, end.col), + }, + )); + } + None + } +} + #[derive(Debug)] pub struct AppState<'ws> { pub modality_stack: Vec, pub viewport_state: ViewportState, pub command_state: TextState<'ws>, pub numeric_prefix: Vec, - pub original_location: Option
, - pub original_sheet: Option, - pub range_sheet: Option, - pub start_range: Option
, - pub end_range: Option
, + pub range_select: RangeSelection, dirty: bool, popup: Vec, } @@ -55,11 +78,7 @@ impl<'ws> Default for AppState<'ws> { viewport_state: Default::default(), command_state: Default::default(), numeric_prefix: Default::default(), - original_location: Default::default(), - original_sheet: Default::default(), - range_sheet: Default::default(), - start_range: Default::default(), - end_range: Default::default(), + range_select: Default::default(), dirty: Default::default(), popup: Default::default(), } @@ -175,29 +194,22 @@ impl<'ws> Workspace<'ws> { pub fn selected_range_to_string(&self) -> String { let state = &self.state; - let start = state - .start_range - .as_ref() - .map(|addr| addr.to_range_part()) - .unwrap_or_else(|| String::new()); - let end = state - .end_range - .as_ref() - .map(|addr| format!(":{}", addr.to_range_part())) - .unwrap_or_else(|| String::new()); - 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 - ); + 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; } - format!("{}:{}", start, end) + return String::new() } /// Move a row down in the current sheet. @@ -342,7 +354,8 @@ impl<'ws> Workspace<'ws> { 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 + .set_yank_text(self.selected_range_to_string()); self.text_area.paste(); self.state.dirty = true; return Ok(None); @@ -438,7 +451,13 @@ impl<'ws> Workspace<'ws> { if key.kind == KeyEventKind::Press { match key.code { 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 => { self.enter_dialog_mode(self.render_help_text()); @@ -452,46 +471,52 @@ impl<'ws> Workspace<'ws> { 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(' ') => { - if self.state.start_range.is_none() { - self.state.start_range = Some(self.book.location.clone()); + 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.end_range = Some(self.book.location.clone()); + self.state.range_select.end = Some(self.book.location.clone()); self.exit_range_select_mode()?; } } KeyCode::Char('n') if key.modifiers == KeyModifiers::CONTROL => { + // TODO(jwall): We should reset our range selections. self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { ws.book.select_next_sheet(); 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 => { + // TODO(jwall): We should reset our range selections. self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { ws.book.select_prev_sheet(); Ok(()) })?; - self.state.range_sheet = Some(self.book.current_sheet); + self.state.range_select.sheet = Some(self.book.current_sheet); } _ => { // moop @@ -501,6 +526,12 @@ impl<'ws> Workspace<'ws> { 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> { if key.kind == KeyEventKind::Press { match key.code { @@ -652,11 +683,11 @@ impl<'ws> Workspace<'ws> { } fn enter_range_select_mode(&mut self) { - self.state.range_sheet = Some(self.book.current_sheet); - self.state.original_sheet = Some(self.book.current_sheet); - self.state.original_location = Some(self.book.location.clone()); - self.state.start_range = None; - self.state.end_range = None; + 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); } @@ -687,18 +718,21 @@ impl<'ws> Workspace<'ws> { 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.original_location = None; + 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 + .set_yank_text(self.selected_range_to_string()); self.text_area.paste(); self.state.dirty = true; } @@ -712,9 +746,8 @@ 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.state.pop_modality(); Ok(()) diff --git a/src/ui/render/mod.rs b/src/ui/render/mod.rs index af68aa8..bedf8e6 100644 --- a/src/ui/render/mod.rs +++ b/src/ui/render/mod.rs @@ -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); diff --git a/src/ui/render/test.rs b/src/ui/render/test.rs index 68a5006..001e389 100644 --- a/src/ui/render/test.rs +++ b/src/ui/render/test.rs @@ -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); } diff --git a/src/ui/render/viewport.rs b/src/ui/render/viewport.rs index 38b7653..aaed5d3 100644 --- a/src/ui/render/viewport.rs +++ b/src/ui/render/viewport.rs @@ -7,7 +7,7 @@ use ratatui::{ widgets::{Block, Cell, Row, StatefulWidget, Table, Widget}, }; -use super::{Address, Book}; +use super::{Address, AppState, 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>, + book: &'ws Book, + range_selection: &'ws RangeSelection, + block: Option>, } 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) { diff --git a/src/ui/test.rs b/src/ui/test.rs index b5879b1..47ed76e 100644 --- a/src/ui/test.rs +++ b/src/ui/test.rs @@ -363,24 +363,24 @@ fn test_range_copy() { 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.original_location); - assert!(ws.state.start_range.is_none()); - assert!(ws.state.end_range.is_none()); + 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.start_range); + 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.original_location.is_none()); - assert_eq!(Some(Address {row:1, col:2, }), ws.state.start_range); - assert_eq!(Some(Address {row:2, col:2, }), ws.state.end_range); + 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()); @@ -391,24 +391,24 @@ fn test_range_copy() { 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.original_location); - assert!(ws.state.start_range.is_none()); - assert!(ws.state.end_range.is_none()); + 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.start_range); + 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.original_location.is_none()); - assert_eq!(Some(Address {row:5, col:4, }), ws.state.start_range); - assert_eq!(Some(Address {row:4, col:4, }), ws.state.end_range); + 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()); }