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