From 9aa6c98f6a6bffa88270e91a760a542288eeb694 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Mon, 25 Nov 2024 15:52:10 -0500 Subject: [PATCH 1/9] wip: viewport abstraction for a table view --- src/ui/mod.rs | 15 ++- src/ui/{render.rs => render/mod.rs} | 7 ++ src/ui/render/viewport.rs | 136 ++++++++++++++++++++++++++++ tmp.bash | 9 ++ 4 files changed, 158 insertions(+), 9 deletions(-) rename src/ui/{render.rs => render/mod.rs} (94%) create mode 100644 src/ui/render/viewport.rs create mode 100644 tmp.bash diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 259e419..2f603e7 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -10,7 +10,7 @@ use ratatui::{ buffer::Buffer, layout::{Constraint, Flex, Layout, Rect}, style::{Modifier, Style}, - widgets::{Block, Table, TableState, Widget}, + widgets::{Block, Table, TableState, Widget, WidgetRef}, }; use tui_prompts::{State, Status, TextPrompt, TextState}; use tui_textarea::{CursorMove, TextArea}; @@ -21,6 +21,7 @@ pub mod render; mod test; use cmd::Cmd; +use render::Viewport; #[derive(Default, Debug, PartialEq, Clone)] pub enum Modality { @@ -459,20 +460,16 @@ impl<'ws> Workspace<'ws> { ) -> Vec<(Rect, Box)> { use ratatui::widgets::StatefulWidget; let mut cs = vec![Constraint::Fill(4), Constraint::Fill(30)]; - let Address { row, col } = self.book.location; let mut rs: Vec> = vec![ Box::new(|rect: Rect, buf: &mut Buffer, ws: &mut Self| ws.text_area.render(rect, buf)), Box::new(move |rect: Rect, buf: &mut Buffer, ws: &mut Self| { let sheet_name = ws.book.get_sheet_name().unwrap_or("Unknown"); // Table widget display let table_block = Block::bordered().title_top(sheet_name); - let table_inner: Table = TryFrom::try_from(&ws.book).expect(""); - let table = table_inner.block(table_block); - // https://docs.rs/ratatui/latest/ratatui/widgets/struct.TableState.html - // TODO(zaphar): Apparently scrolling by columns doesn't work? - ws.state.table_state.select_cell(Some((row, col))); - ws.state.table_state.select_column(Some(col)); - StatefulWidget::render(table, rect, buf, &mut ws.state.table_state); + // TODO(zaphar): We should be smarter about calculating the location properly + // Might require viewport state to do properly + let viewport = Viewport::new(&ws.book).with_corner(ws.book.location.clone()).block(table_block); + viewport.render(rect, buf); }), ]; diff --git a/src/ui/render.rs b/src/ui/render/mod.rs similarity index 94% rename from src/ui/render.rs rename to src/ui/render/mod.rs index 34cbdd2..92524bd 100644 --- a/src/ui/render.rs +++ b/src/ui/render/mod.rs @@ -10,6 +10,9 @@ use tui_popup::Popup; use super::*; +pub mod viewport; +pub use viewport::Viewport; + impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> { fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer) where @@ -55,6 +58,10 @@ const COLNAMES: [&'static str; 26] = [ "T", "U", "V", "W", "X", "Y", "Z", ]; +// TODO(jwall): A Viewport widget where we assume lengths for column +// sizes would probably help us to manage column scrolling. +// Could use a ViewportState and stateful rendering. + impl<'t, 'book: 't> TryFrom<&'book Book> for Table<'t> { fn try_from(value: &'book Book) -> std::result::Result { // TODO(zaphar): This is apparently expensive. Maybe we can cache it somehow? diff --git a/src/ui/render/viewport.rs b/src/ui/render/viewport.rs new file mode 100644 index 0000000..c0ec549 --- /dev/null +++ b/src/ui/render/viewport.rs @@ -0,0 +1,136 @@ +use std::cmp::min; + +use anyhow::Result; +use ratatui::{ + layout::{Constraint, Flex}, + style::{Color, Stylize}, + text::Text, + widgets::{Block, Cell, Row, Table, Widget}, +}; + +use super::{Address, Book}; + +// NOTE(jwall): This is stolen from ironcalc but ironcalc doesn't expose it +// publically. +pub(crate) const LAST_COLUMN: usize = 16_384; +//pub(crate) const LAST_ROW: usize = 1_048_576; + +/// A visible column to show in our Viewport. +pub struct VisibleColumn { + pub idx: usize, + pub length: u16, +} + +impl<'a> From<&'a VisibleColumn> for Constraint { + fn from(value: &'a VisibleColumn) -> Self { + Constraint::Length(value.length as u16) + } +} + +/// A renderable viewport over a book. +pub struct Viewport<'book> { + pub(crate) corner: Address, + book: &'book Book, + block: Option>, +} + +const COLNAMES: [&'static str; 26] = [ + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", + "T", "U", "V", "W", "X", "Y", "Z", +]; + +impl<'book> Viewport<'book> { + pub fn new(book: &'book Book) -> Self { + Self { + book, + corner: Default::default(), + block: None, + } + } + + pub fn with_corner(mut self, corner: Address) -> Self { + self.corner = corner; + self + } + + fn get_visible_columns(&self, width: u16) -> Result> { + let mut visible = Vec::new(); + let mut length = 0; + for idx in self.corner.col..=LAST_COLUMN { + let size = self.book.get_col_size(idx)? as u16; + let updated_length = length + size; + if updated_length <= width { + length = updated_length; + visible.push(VisibleColumn { idx, length: size }); + } + } + return Ok(visible); + } + + pub fn block(mut self, block: Block<'book>) -> Self { + self.block = Some(block); + self + } + + fn to_table<'widget>(&self, width: u16, height: u16) -> Result> { + let visible_columns = self.get_visible_columns(width)?; + let max_row = min(self.corner.row + height as usize, LAST_COLUMN); + let rows: Vec = (self.corner.row..=max_row) + .into_iter() + .map(|ri| { + let mut cells = vec![Cell::new(Text::from(ri.to_string()))]; + cells.extend(visible_columns.iter().map(|VisibleColumn { idx: ci, length: _, }| { + // TODO(zaphar): Is this safe? + let content = self + .book + .get_cell_addr_rendered(&Address { row: ri, col: *ci }) + .unwrap(); + let cell = Cell::new(Text::raw(content)); + match (self.book.location.row == ri, self.book.location.col == *ci) { + (true, true) => cell.fg(Color::White).underlined(), + _ => cell + .bg(if ri % 2 == 0 { + Color::Rgb(57, 61, 71) + } else { + Color::Rgb(165, 169, 160) + }) + .fg(if ri % 2 == 0 { + Color::White + } else { + Color::Rgb(31, 32, 34) + }), + } + .bold() + })); + Row::new(cells) + }) + .collect(); + let constraints: Vec = visible_columns.iter() + .map(|vc| Constraint::from(vc)) + .collect(); + let mut header = Vec::with_capacity(constraints.len()); + header.push(Cell::new("")); + header.extend((self.corner.col..constraints.len()).map(|i| { + let count = (i / 26) + 1; + Cell::new(COLNAMES[(i-1) % 26].repeat(count)) + })); + // TODO(zaphar): We should calculate the length from the length of the stringified version of the + // row indexes. + let mut col_constraints = vec![Constraint::Length(5)]; + col_constraints.extend(constraints.into_iter()); + Ok(Table::new(rows, col_constraints) + .header(Row::new(header).underlined()) + .column_spacing(1) + .flex(Flex::Start)) + } +} + +impl<'book> Widget for Viewport<'book> { + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { + let mut table = self.to_table(area.width, area.height).expect("Failed to turn viewport into a table."); + if let Some(block) = self.block { + table = table.block(block); + } + table.render(area, buf); + } +} diff --git a/tmp.bash b/tmp.bash new file mode 100644 index 0000000..11fd8f4 --- /dev/null +++ b/tmp.bash @@ -0,0 +1,9 @@ +set -ue +set -o pipefail +export BUILD_ARG_one="widget" +export BUILD_ARG_two="acme" +VALS=() +printenv | grep BUILD_ARG | while read line;do + VALS+=($line) +done +echo "${VALS[*]}" From 28533fc71857ad63348752e09cd12463ea665071 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Mon, 25 Nov 2024 18:16:10 -0500 Subject: [PATCH 2/9] Use viewport state to be smarter about moving our corner --- src/ui/mod.rs | 14 ++-- src/ui/render/mod.rs | 3 + src/ui/render/test.rs | 15 ++++ src/ui/render/viewport.rs | 171 ++++++++++++++++++++++++++------------ 4 files changed, 142 insertions(+), 61 deletions(-) create mode 100644 src/ui/render/test.rs diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 2f603e7..432ddd8 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -21,7 +21,7 @@ pub mod render; mod test; use cmd::Cmd; -use render::Viewport; +use render::{viewport::ViewportState, Viewport}; #[derive(Default, Debug, PartialEq, Clone)] pub enum Modality { @@ -35,6 +35,7 @@ pub enum Modality { #[derive(Debug)] pub struct AppState<'ws> { pub modality_stack: Vec, + pub viewport_state: ViewportState, pub table_state: TableState, pub command_state: TextState<'ws>, dirty: bool, @@ -46,6 +47,7 @@ impl<'ws> Default for AppState<'ws> { AppState { modality_stack: vec![Modality::default()], table_state: Default::default(), + viewport_state: Default::default(), command_state: Default::default(), dirty: Default::default(), popup: Default::default(), @@ -124,8 +126,7 @@ impl<'ws> Workspace<'ws> { /// Move a row down in the current sheet. pub fn move_down(&mut self) -> Result<()> { let mut loc = self.book.location.clone(); - let (row_count, _) = self.book.get_size()?; - if loc.row < row_count { + if loc.row < render::viewport::LAST_ROW { loc.row += 1; self.book.move_to(&loc)?; } @@ -155,8 +156,7 @@ impl<'ws> Workspace<'ws> { /// Move a column to the left in the current sheet. pub fn move_right(&mut self) -> Result<()> { let mut loc = self.book.location.clone(); - let (_, col_count) = self.book.get_size()?; - if loc.col < col_count { + if loc.col < render::viewport::LAST_COLUMN { loc.col += 1; self.book.move_to(&loc)?; } @@ -468,8 +468,8 @@ impl<'ws> Workspace<'ws> { let table_block = Block::bordered().title_top(sheet_name); // TODO(zaphar): We should be smarter about calculating the location properly // Might require viewport state to do properly - let viewport = Viewport::new(&ws.book).with_corner(ws.book.location.clone()).block(table_block); - viewport.render(rect, buf); + let viewport = Viewport::new(&ws.book).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/mod.rs b/src/ui/render/mod.rs index 92524bd..b669401 100644 --- a/src/ui/render/mod.rs +++ b/src/ui/render/mod.rs @@ -13,6 +13,9 @@ use super::*; pub mod viewport; pub use viewport::Viewport; +#[cfg(test)] +mod test; + impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> { fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer) where diff --git a/src/ui/render/test.rs b/src/ui/render/test.rs new file mode 100644 index 0000000..f13b0fc --- /dev/null +++ b/src/ui/render/test.rs @@ -0,0 +1,15 @@ +use ironcalc::base::Model; + +use super::{Address, Book, Viewport, ViewportState, COLNAMES}; + +#[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 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"); + assert_eq!(5, cols.len()); + assert_eq!(17, cols.last().expect("Failed to get last column").idx); +} diff --git a/src/ui/render/viewport.rs b/src/ui/render/viewport.rs index c0ec549..d04c645 100644 --- a/src/ui/render/viewport.rs +++ b/src/ui/render/viewport.rs @@ -2,20 +2,23 @@ use std::cmp::min; use anyhow::Result; use ratatui::{ - layout::{Constraint, Flex}, + buffer::Buffer, + layout::{Constraint, Flex, Rect}, style::{Color, Stylize}, text::Text, - widgets::{Block, Cell, Row, Table, Widget}, + widgets::{Block, Cell, Row, StatefulWidget, Table, Widget}, }; use super::{Address, Book}; -// NOTE(jwall): This is stolen from ironcalc but ironcalc doesn't expose it +// TODO(zaphar): Move this to the book module. +// NOTE(zaphar): This is stolen from ironcalc but ironcalc doesn't expose it // publically. pub(crate) const LAST_COLUMN: usize = 16_384; -//pub(crate) const LAST_ROW: usize = 1_048_576; +pub(crate) const LAST_ROW: usize = 1_048_576; /// A visible column to show in our Viewport. +#[derive(Clone, Debug)] pub struct VisibleColumn { pub idx: usize, pub length: u16, @@ -27,14 +30,28 @@ impl<'a> From<&'a VisibleColumn> for Constraint { } } +#[derive(Debug, Default)] +pub struct ViewportState { + prev_corner: Address, + +} + +impl ViewportState { + pub fn new(location: Address) -> Self { + Self { + prev_corner: location, + } + } +} + /// A renderable viewport over a book. pub struct Viewport<'book> { - pub(crate) corner: Address, + pub(crate) selected: Address, book: &'book Book, block: Option>, } -const COLNAMES: [&'static str; 26] = [ +pub(crate) const COLNAMES: [&'static str; 26] = [ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", ]; @@ -43,25 +60,54 @@ impl<'book> Viewport<'book> { pub fn new(book: &'book Book) -> Self { Self { book, - corner: Default::default(), + selected: Default::default(), block: None, } } - pub fn with_corner(mut self, corner: Address) -> Self { - self.corner = corner; + pub fn with_selected(mut self, location: Address) -> Self { + self.selected = location; self } - fn get_visible_columns(&self, width: u16) -> Result> { + pub(crate) fn get_visible_columns( + &self, + width: u16, + state: &ViewportState, + ) -> Result> { let mut visible = Vec::new(); - let mut length = 0; - for idx in self.corner.col..=LAST_COLUMN { + // TODO(zaphar): This should be a shared constant with our first column. + // We start out with a length of 5 already researved + let mut length = 5; + let start_idx = std::cmp::min(self.selected.col, state.prev_corner.col); + for idx in start_idx..=LAST_COLUMN { let size = self.book.get_col_size(idx)? as u16; let updated_length = length + size; - if updated_length <= width { + let col = VisibleColumn { idx, length: size }; + if updated_length < width { length = updated_length; - visible.push(VisibleColumn { idx, length: size }); + visible.push(col); + } else if self.selected.col >= col.idx { + // We need a sliding window now + if let Some(first) = visible.first() { + // subtract the first columns size. + length = length - first.length; + // remove the first column. + visible = visible.into_iter().skip(1).collect(); + } + // Add this col to the visible. + length += size; + visible.push(col); + // What if the length is still too long? + if length > width { + if let Some(first) = visible.first() { + // subtract the first columns size. + length = length - first.length; + } + visible = visible.into_iter().skip(1).collect(); + } + } else { + break; } } return Ok(visible); @@ -72,47 +118,60 @@ impl<'book> Viewport<'book> { self } - fn to_table<'widget>(&self, width: u16, height: u16) -> Result> { - let visible_columns = self.get_visible_columns(width)?; - let max_row = min(self.corner.row + height as usize, LAST_COLUMN); - let rows: Vec = (self.corner.row..=max_row) - .into_iter() - .map(|ri| { - let mut cells = vec![Cell::new(Text::from(ri.to_string()))]; - cells.extend(visible_columns.iter().map(|VisibleColumn { idx: ci, length: _, }| { - // TODO(zaphar): Is this safe? - let content = self - .book - .get_cell_addr_rendered(&Address { row: ri, col: *ci }) - .unwrap(); - let cell = Cell::new(Text::raw(content)); - match (self.book.location.row == ri, self.book.location.col == *ci) { - (true, true) => cell.fg(Color::White).underlined(), - _ => cell - .bg(if ri % 2 == 0 { - Color::Rgb(57, 61, 71) - } else { - Color::Rgb(165, 169, 160) - }) - .fg(if ri % 2 == 0 { - Color::White - } else { - Color::Rgb(31, 32, 34) - }), - } - .bold() - })); - Row::new(cells) - }) - .collect(); - let constraints: Vec = visible_columns.iter() + pub(crate) fn to_table<'widget>( + &self, + width: u16, + height: u16, + state: &mut ViewportState, + ) -> Result> { + let visible_columns = self.get_visible_columns(width, state)?; + if let Some(vc) = visible_columns.first() { + state.prev_corner.col = vc.idx + } + let max_row = min(state.prev_corner.row + height as usize, LAST_COLUMN); + let rows: Vec = + (state.prev_corner.row..=max_row) + .into_iter() + .map(|ri| { + let mut cells = vec![Cell::new(Text::from(ri.to_string()))]; + cells.extend(visible_columns.iter().map( + |VisibleColumn { idx: ci, length: _ }| { + // TODO(zaphar): Is this safe? + let content = self + .book + .get_cell_addr_rendered(&Address { row: ri, col: *ci }) + .unwrap(); + let cell = Cell::new(Text::raw(content)); + match (self.book.location.row == ri, self.book.location.col == *ci) { + (true, true) => cell.fg(Color::White).underlined(), + _ => cell + .bg(if ri % 2 == 0 { + Color::Rgb(57, 61, 71) + } else { + Color::Rgb(165, 169, 160) + }) + .fg(if ri % 2 == 0 { + Color::White + } else { + Color::Rgb(31, 32, 34) + }), + } + .bold() + }, + )); + Row::new(cells) + }) + .collect(); + let constraints: Vec = visible_columns + .iter() .map(|vc| Constraint::from(vc)) .collect(); + let end_idx = visible_columns.last().unwrap().idx; let mut header = Vec::with_capacity(constraints.len()); header.push(Cell::new("")); - header.extend((self.corner.col..constraints.len()).map(|i| { + header.extend((state.prev_corner.col..end_idx).map(|i| { let count = (i / 26) + 1; - Cell::new(COLNAMES[(i-1) % 26].repeat(count)) + Cell::new(COLNAMES[(i - 1) % 26].repeat(count)) })); // TODO(zaphar): We should calculate the length from the length of the stringified version of the // row indexes. @@ -125,12 +184,16 @@ impl<'book> Viewport<'book> { } } -impl<'book> Widget for Viewport<'book> { - fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { - let mut table = self.to_table(area.width, area.height).expect("Failed to turn viewport into a table."); +impl<'book> StatefulWidget for Viewport<'book> { + type State = ViewportState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + let mut table = self + .to_table(area.width, area.height, state) + .expect("Failed to turn viewport into a table."); if let Some(block) = self.block { table = table.block(block); } - table.render(area, buf); + Widget::render(table, area, buf); } } From a22b51cdfd7c4431500f09ec04a308cc0b06c87c Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Tue, 26 Nov 2024 15:52:46 -0500 Subject: [PATCH 3/9] chore: comment wording --- src/ui/render/viewport.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/render/viewport.rs b/src/ui/render/viewport.rs index d04c645..e06bfdf 100644 --- a/src/ui/render/viewport.rs +++ b/src/ui/render/viewport.rs @@ -77,7 +77,7 @@ impl<'book> Viewport<'book> { ) -> Result> { let mut visible = Vec::new(); // TODO(zaphar): This should be a shared constant with our first column. - // We start out with a length of 5 already researved + // We start out with a length of 5 already reserved let mut length = 5; let start_idx = std::cmp::min(self.selected.col, state.prev_corner.col); for idx in start_idx..=LAST_COLUMN { From c564ce452d978cf877663362dceb53baad7a758f Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Mon, 25 Nov 2024 22:09:57 -0500 Subject: [PATCH 4/9] wip: Row scrolling --- src/ui/mod.rs | 8 +---- src/ui/render/mod.rs | 70 ++------------------------------------- src/ui/render/test.rs | 14 +++++++- src/ui/render/viewport.rs | 44 ++++++++++++++++-------- 4 files changed, 46 insertions(+), 90 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 432ddd8..a67ec75 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -6,11 +6,10 @@ use crate::book::Book; use anyhow::Result; use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use ratatui::{ - self, buffer::Buffer, layout::{Constraint, Flex, Layout, Rect}, style::{Modifier, Style}, - widgets::{Block, Table, TableState, Widget, WidgetRef}, + widgets::{Block, Widget}, }; use tui_prompts::{State, Status, TextPrompt, TextState}; use tui_textarea::{CursorMove, TextArea}; @@ -36,7 +35,6 @@ pub enum Modality { pub struct AppState<'ws> { pub modality_stack: Vec, pub viewport_state: ViewportState, - pub table_state: TableState, pub command_state: TextState<'ws>, dirty: bool, popup: Vec, @@ -46,7 +44,6 @@ impl<'ws> Default for AppState<'ws> { fn default() -> Self { AppState { modality_stack: vec![Modality::default()], - table_state: Default::default(), viewport_state: Default::default(), command_state: Default::default(), dirty: Default::default(), @@ -464,10 +461,7 @@ impl<'ws> Workspace<'ws> { Box::new(|rect: Rect, buf: &mut Buffer, ws: &mut Self| ws.text_area.render(rect, buf)), Box::new(move |rect: Rect, buf: &mut Buffer, ws: &mut Self| { let sheet_name = ws.book.get_sheet_name().unwrap_or("Unknown"); - // Table widget display let table_block = Block::bordered().title_top(sheet_name); - // TODO(zaphar): We should be smarter about calculating the location properly - // Might require viewport state to do properly let viewport = Viewport::new(&ws.book).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/mod.rs b/src/ui/render/mod.rs index b669401..facf854 100644 --- a/src/ui/render/mod.rs +++ b/src/ui/render/mod.rs @@ -1,9 +1,8 @@ use ratatui::{ self, - layout::{Constraint, Flex, Rect}, - style::{Color, Stylize}, + layout::Rect, text::{Line, Text}, - widgets::{Block, Cell, Row, Table, Widget}, + widgets::{Block, Widget}, Frame, }; use tui_popup::Popup; @@ -56,71 +55,6 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> { } } -const COLNAMES: [&'static str; 26] = [ - "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", - "T", "U", "V", "W", "X", "Y", "Z", -]; - -// TODO(jwall): A Viewport widget where we assume lengths for column -// sizes would probably help us to manage column scrolling. -// Could use a ViewportState and stateful rendering. - -impl<'t, 'book: 't> TryFrom<&'book Book> for Table<'t> { - fn try_from(value: &'book Book) -> std::result::Result { - // TODO(zaphar): This is apparently expensive. Maybe we can cache it somehow? - // We should do the correct thing here if this fails - let (row_count, col_count) = value.get_size()?; - let rows: Vec = (1..=row_count) - .into_iter() - .map(|ri| { - let mut cells = vec![Cell::new(Text::from(ri.to_string()))]; - cells.extend((1..=col_count).into_iter().map(|ci| { - // TODO(zaphar): Is this safe? - let content = value - .get_cell_addr_rendered(&Address { row: ri, col: ci }) - .unwrap(); - let cell = Cell::new(Text::raw(content)); - match (value.location.row == ri, value.location.col == ci) { - (true, true) => cell.fg(Color::White).underlined(), - _ => cell - .bg(if ri % 2 == 0 { - Color::Rgb(57, 61, 71) - } else { - Color::Rgb(165, 169, 160) - }) - .fg(if ri % 2 == 0 { - Color::White - } else { - Color::Rgb(31, 32, 34) - }), - } - .bold() - })); - Row::new(cells) - }) - .collect(); - let mut constraints: Vec = Vec::new(); - constraints.push(Constraint::Max(5)); - for col_idx in 0..col_count { - let size = value.get_col_size(col_idx + 1)?; - constraints.push(Constraint::Length(size as u16)); - } - let mut header = Vec::with_capacity(col_count as usize); - - header.push(Cell::new("")); - header.extend((0..(col_count as usize)).map(|i| { - let count = (i / 26) + 1; - Cell::new(COLNAMES[i % 26].repeat(count)) - })); - Ok(Table::new(rows, constraints) - .header(Row::new(header).underlined()) - .column_spacing(1) - .flex(Flex::Start)) - } - - type Error = anyhow::Error; -} - pub fn draw(frame: &mut Frame, ws: &mut Workspace) { frame.render_widget(ws, frame.area()); } diff --git a/src/ui/render/test.rs b/src/ui/render/test.rs index f13b0fc..26dc987 100644 --- a/src/ui/render/test.rs +++ b/src/ui/render/test.rs @@ -1,6 +1,6 @@ use ironcalc::base::Model; -use super::{Address, Book, Viewport, ViewportState, COLNAMES}; +use super::{Address, Book, Viewport, ViewportState}; #[test] fn test_viewport_get_visible_columns() { @@ -13,3 +13,15 @@ fn test_viewport_get_visible_columns() { assert_eq!(5, cols.len()); assert_eq!(17, cols.last().expect("Failed to get last column").idx); } + +#[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 height = 6; + let viewport = Viewport::new(&book).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, *rows.last().expect("Failed to get last row")); +} diff --git a/src/ui/render/viewport.rs b/src/ui/render/viewport.rs index e06bfdf..2e7823d 100644 --- a/src/ui/render/viewport.rs +++ b/src/ui/render/viewport.rs @@ -1,5 +1,3 @@ -use std::cmp::min; - use anyhow::Result; use ratatui::{ buffer::Buffer, @@ -33,15 +31,6 @@ impl<'a> From<&'a VisibleColumn> for Constraint { #[derive(Debug, Default)] pub struct ViewportState { prev_corner: Address, - -} - -impl ViewportState { - pub fn new(location: Address) -> Self { - Self { - prev_corner: location, - } - } } /// A renderable viewport over a book. @@ -70,6 +59,30 @@ impl<'book> Viewport<'book> { self } + pub(crate) fn get_visible_rows(&self, height: u16, state: &ViewportState) -> Vec { + // TODO(jeremy): For now the row default height is 1. We'll have + // to adjust that if this changes. + let mut length = 1; + let start_row = std::cmp::min(self.selected.row, state.prev_corner.row); + let mut start = start_row; + let mut end = start_row; + for row_idx in start_row..=LAST_ROW { + let updated_length = length + 1; + if updated_length <= height { + length = updated_length; + end = row_idx; + } else if self.selected.row >= row_idx { + start = start + 1; + end = row_idx; + } else { + //dbg!(&start); + //dbg!(&end); + break; + } + } + return (start..=end).collect(); + } + pub(crate) fn get_visible_columns( &self, width: u16, @@ -125,12 +138,15 @@ impl<'book> Viewport<'book> { state: &mut ViewportState, ) -> Result> { let visible_columns = self.get_visible_columns(width, state)?; + let visible_rows = self.get_visible_rows(height, state); if let Some(vc) = visible_columns.first() { state.prev_corner.col = vc.idx } - let max_row = min(state.prev_corner.row + height as usize, LAST_COLUMN); + if let Some(vr) = visible_rows.first() { + state.prev_corner.row = *vr; + } let rows: Vec = - (state.prev_corner.row..=max_row) + visible_rows .into_iter() .map(|ri| { let mut cells = vec![Cell::new(Text::from(ri.to_string()))]; @@ -189,7 +205,7 @@ impl<'book> StatefulWidget for Viewport<'book> { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { let mut table = self - .to_table(area.width, area.height, state) + .to_table(area.width - 2, area.height - 2, state) .expect("Failed to turn viewport into a table."); if let Some(block) = self.block { table = table.block(block); From fd52a4058728df4ac8765d14aacacb9b80d002a5 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Tue, 26 Nov 2024 17:08:38 -0500 Subject: [PATCH 5/9] fix: column naming boundary condition --- src/ui/render/viewport.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/render/viewport.rs b/src/ui/render/viewport.rs index 2e7823d..c5e6988 100644 --- a/src/ui/render/viewport.rs +++ b/src/ui/render/viewport.rs @@ -186,7 +186,7 @@ impl<'book> Viewport<'book> { let mut header = Vec::with_capacity(constraints.len()); header.push(Cell::new("")); header.extend((state.prev_corner.col..end_idx).map(|i| { - let count = (i / 26) + 1; + let count = if i == 26 { 1 } else { (i / 26) + 1 }; Cell::new(COLNAMES[(i - 1) % 26].repeat(count)) })); // TODO(zaphar): We should calculate the length from the length of the stringified version of the @@ -204,6 +204,8 @@ impl<'book> StatefulWidget for Viewport<'book> { type State = ViewportState; fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + // The block surrounding this table adds 2 additional rows and columns + // to the available rect for rendering this table. let mut table = self .to_table(area.width - 2, area.height - 2, state) .expect("Failed to turn viewport into a table."); From eee260824aa544d4b1ec59b24284ec59ffcfaddb Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Tue, 26 Nov 2024 17:09:57 -0500 Subject: [PATCH 6/9] chore: cleanup portion of #5 --- src/book/mod.rs | 2 -- src/ui/mod.rs | 2 -- src/ui/render/viewport.rs | 4 +--- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/book/mod.rs b/src/book/mod.rs index f532252..742db65 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -23,8 +23,6 @@ pub struct Book { pub(crate) model: Model, pub current_sheet: u32, pub location: crate::ui::Address, - // TODO(zaphar): Because the ironcalc model is sparse we need to track our render size - // separately } impl Book { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index a67ec75..851528f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -162,8 +162,6 @@ impl<'ws> Workspace<'ws> { /// Handle input in our ui loop. pub fn handle_input(&mut self, evt: Event) -> Result> { - // TODO(jwall): We probably want to separate this out into - // a pure function so we can script various testing scenarios. if let Event::Key(key) = evt { let result = match self.state.modality() { Modality::Navigate => self.handle_navigation_input(key)?, diff --git a/src/ui/render/viewport.rs b/src/ui/render/viewport.rs index c5e6988..f5dc763 100644 --- a/src/ui/render/viewport.rs +++ b/src/ui/render/viewport.rs @@ -60,7 +60,7 @@ impl<'book> Viewport<'book> { } pub(crate) fn get_visible_rows(&self, height: u16, state: &ViewportState) -> Vec { - // TODO(jeremy): For now the row default height is 1. We'll have + // NOTE(jeremy): For now the row default height is 1. We'll have // to adjust that if this changes. let mut length = 1; let start_row = std::cmp::min(self.selected.row, state.prev_corner.row); @@ -189,8 +189,6 @@ impl<'book> Viewport<'book> { let count = if i == 26 { 1 } else { (i / 26) + 1 }; Cell::new(COLNAMES[(i - 1) % 26].repeat(count)) })); - // TODO(zaphar): We should calculate the length from the length of the stringified version of the - // row indexes. let mut col_constraints = vec![Constraint::Length(5)]; col_constraints.extend(constraints.into_iter()); Ok(Table::new(rows, col_constraints) From 665d872f3d104e4d70da7c69f8eef21f14f2860a Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 27 Nov 2024 19:35:23 -0500 Subject: [PATCH 7/9] chore: unit tests for column change calculations portion of #5 --- src/ui/render/test.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/ui/render/test.rs b/src/ui/render/test.rs index 26dc987..68a5006 100644 --- a/src/ui/render/test.rs +++ b/src/ui/render/test.rs @@ -25,3 +25,25 @@ fn test_viewport_get_visible_rows() { 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 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"); + 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"); + { + 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"); + assert_eq!(1, cols.len()); + assert_eq!(1, cols.last().expect("Failed to get last column").idx); + } +} From 6b944557588b66118fb860f38b55a98b1035d306 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 27 Nov 2024 19:35:23 -0500 Subject: [PATCH 8/9] wip: Don't skip around so much when changing the size portion of #5 --- src/book/mod.rs | 2 +- src/ui/render/viewport.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/book/mod.rs b/src/book/mod.rs index 742db65..a8b04a1 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -16,7 +16,7 @@ use crate::ui::Address; #[cfg(test)] mod test; -const COL_PIXELS: f64 = 10.0; +const COL_PIXELS: f64 = 5.0; /// A spreadsheet book with some internal state tracking. pub struct Book { diff --git a/src/ui/render/viewport.rs b/src/ui/render/viewport.rs index f5dc763..a81b2b8 100644 --- a/src/ui/render/viewport.rs +++ b/src/ui/render/viewport.rs @@ -106,6 +106,7 @@ impl<'book> Viewport<'book> { // subtract the first columns size. length = length - first.length; // remove the first column. + // TODO(jwall): This is a bit inefficient. Can we do better? visible = visible.into_iter().skip(1).collect(); } // Add this col to the visible. @@ -193,7 +194,7 @@ impl<'book> Viewport<'book> { col_constraints.extend(constraints.into_iter()); Ok(Table::new(rows, col_constraints) .header(Row::new(header).underlined()) - .column_spacing(1) + .column_spacing(0) .flex(Flex::Start)) } } From 6b6b452cfbe172c8a2cc70ac520116220c8546fb Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Thu, 28 Nov 2024 07:48:29 -0500 Subject: [PATCH 9/9] feat: better styling of heaader and rows portion of #5 --- src/ui/render/viewport.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/ui/render/viewport.rs b/src/ui/render/viewport.rs index a81b2b8..0c478c9 100644 --- a/src/ui/render/viewport.rs +++ b/src/ui/render/viewport.rs @@ -160,18 +160,8 @@ impl<'book> Viewport<'book> { .unwrap(); let cell = Cell::new(Text::raw(content)); match (self.book.location.row == ri, self.book.location.col == *ci) { - (true, true) => cell.fg(Color::White).underlined(), - _ => cell - .bg(if ri % 2 == 0 { - Color::Rgb(57, 61, 71) - } else { - Color::Rgb(165, 169, 160) - }) - .fg(if ri % 2 == 0 { - Color::White - } else { - Color::Rgb(31, 32, 34) - }), + (true, true) => cell.fg(Color::White).bg(Color::Rgb(57, 61, 71)), + _ => cell, } .bold() }, @@ -186,9 +176,16 @@ impl<'book> Viewport<'book> { let end_idx = visible_columns.last().unwrap().idx; let mut header = Vec::with_capacity(constraints.len()); header.push(Cell::new("")); - header.extend((state.prev_corner.col..end_idx).map(|i| { + header.extend((state.prev_corner.col..=end_idx).map(|i| { let count = if i == 26 { 1 } else { (i / 26) + 1 }; + let even = i % 2 == 0; Cell::new(COLNAMES[(i - 1) % 26].repeat(count)) + .bg(if even { + Color::Rgb(57, 61, 71) + } else { + Color::Rgb(165, 169, 160) + }) + .fg(if even { Color::White } else { Color::Black }).bold() })); let mut col_constraints = vec![Constraint::Length(5)]; col_constraints.extend(constraints.into_iter());