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[*]}"