mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-22 21:09:48 -04:00
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:
parent
5f6f45141c
commit
61a6e93515
129
src/ui/mod.rs
129
src/ui/mod.rs
@ -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(())
|
||||
|
@ -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);
|
||||
|
@ -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, 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) {
|
||||
|
@ -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());
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user