wip: bits of polish

* show what you are selecting in range mode
* Fix error with cell edit not updating after Enter
This commit is contained in:
Jeremy Wall 2024-12-05 17:27:06 -05:00
parent 5f6f45141c
commit 61a6e93515
5 changed files with 155 additions and 84 deletions

View File

@ -33,17 +33,40 @@ pub enum Modality {
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
}
}
#[derive(Debug)]
pub struct AppState<'ws> {
pub modality_stack: Vec<Modality>,
pub viewport_state: ViewportState,
pub command_state: TextState<'ws>,
pub numeric_prefix: Vec<char>,
pub original_location: Option<Address>,
pub original_sheet: Option<u32>,
pub range_sheet: Option<u32>,
pub start_range: Option<Address>,
pub end_range: Option<Address>,
pub range_select: RangeSelection,
dirty: bool,
popup: Vec<String>,
}
@ -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<Option<ExitCode>> {
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(())

View File

@ -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);

View File

@ -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);
}

View File

@ -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<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) {

View File

@ -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());
}