Use viewport state to be smarter about moving our corner

This commit is contained in:
Jeremy Wall 2024-11-25 18:16:10 -05:00
parent 9aa6c98f6a
commit 28533fc718
4 changed files with 142 additions and 61 deletions

View File

@ -21,7 +21,7 @@ pub mod render;
mod test; mod test;
use cmd::Cmd; use cmd::Cmd;
use render::Viewport; use render::{viewport::ViewportState, Viewport};
#[derive(Default, Debug, PartialEq, Clone)] #[derive(Default, Debug, PartialEq, Clone)]
pub enum Modality { pub enum Modality {
@ -35,6 +35,7 @@ pub enum Modality {
#[derive(Debug)] #[derive(Debug)]
pub struct AppState<'ws> { pub struct AppState<'ws> {
pub modality_stack: Vec<Modality>, pub modality_stack: Vec<Modality>,
pub viewport_state: ViewportState,
pub table_state: TableState, pub table_state: TableState,
pub command_state: TextState<'ws>, pub command_state: TextState<'ws>,
dirty: bool, dirty: bool,
@ -46,6 +47,7 @@ impl<'ws> Default for AppState<'ws> {
AppState { AppState {
modality_stack: vec![Modality::default()], modality_stack: vec![Modality::default()],
table_state: Default::default(), table_state: Default::default(),
viewport_state: Default::default(),
command_state: Default::default(), command_state: Default::default(),
dirty: Default::default(), dirty: Default::default(),
popup: Default::default(), popup: Default::default(),
@ -124,8 +126,7 @@ impl<'ws> Workspace<'ws> {
/// Move a row down in the current sheet. /// Move a row down in the current sheet.
pub fn move_down(&mut self) -> Result<()> { pub fn move_down(&mut self) -> Result<()> {
let mut loc = self.book.location.clone(); let mut loc = self.book.location.clone();
let (row_count, _) = self.book.get_size()?; if loc.row < render::viewport::LAST_ROW {
if loc.row < row_count {
loc.row += 1; loc.row += 1;
self.book.move_to(&loc)?; self.book.move_to(&loc)?;
} }
@ -155,8 +156,7 @@ impl<'ws> Workspace<'ws> {
/// Move a column to the left in the current sheet. /// Move a column to the left in the current sheet.
pub fn move_right(&mut self) -> Result<()> { pub fn move_right(&mut self) -> Result<()> {
let mut loc = self.book.location.clone(); let mut loc = self.book.location.clone();
let (_, col_count) = self.book.get_size()?; if loc.col < render::viewport::LAST_COLUMN {
if loc.col < col_count {
loc.col += 1; loc.col += 1;
self.book.move_to(&loc)?; self.book.move_to(&loc)?;
} }
@ -468,8 +468,8 @@ impl<'ws> Workspace<'ws> {
let table_block = Block::bordered().title_top(sheet_name); let table_block = Block::bordered().title_top(sheet_name);
// TODO(zaphar): We should be smarter about calculating the location properly // TODO(zaphar): We should be smarter about calculating the location properly
// Might require viewport state to do properly // Might require viewport state to do properly
let viewport = Viewport::new(&ws.book).with_corner(ws.book.location.clone()).block(table_block); let viewport = Viewport::new(&ws.book).with_selected(ws.book.location.clone()).block(table_block);
viewport.render(rect, buf); StatefulWidget::render(viewport, rect, buf, &mut ws.state.viewport_state);
}), }),
]; ];

View File

@ -13,6 +13,9 @@ use super::*;
pub mod viewport; pub mod viewport;
pub use viewport::Viewport; pub use viewport::Viewport;
#[cfg(test)]
mod test;
impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> { impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer) fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer)
where where

15
src/ui/render/test.rs Normal file
View File

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

View File

@ -2,20 +2,23 @@ use std::cmp::min;
use anyhow::Result; use anyhow::Result;
use ratatui::{ use ratatui::{
layout::{Constraint, Flex}, buffer::Buffer,
layout::{Constraint, Flex, Rect},
style::{Color, Stylize}, style::{Color, Stylize},
text::Text, text::Text,
widgets::{Block, Cell, Row, Table, Widget}, widgets::{Block, Cell, Row, StatefulWidget, Table, Widget},
}; };
use super::{Address, Book}; 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. // publically.
pub(crate) const LAST_COLUMN: usize = 16_384; 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. /// A visible column to show in our Viewport.
#[derive(Clone, Debug)]
pub struct VisibleColumn { pub struct VisibleColumn {
pub idx: usize, pub idx: usize,
pub length: u16, 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. /// A renderable viewport over a book.
pub struct Viewport<'book> { pub struct Viewport<'book> {
pub(crate) corner: Address, pub(crate) selected: Address,
book: &'book Book, book: &'book Book,
block: Option<Block<'book>>, block: Option<Block<'book>>,
} }
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", "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", "T", "U", "V", "W", "X", "Y", "Z",
]; ];
@ -43,25 +60,54 @@ impl<'book> Viewport<'book> {
pub fn new(book: &'book Book) -> Self { pub fn new(book: &'book Book) -> Self {
Self { Self {
book, book,
corner: Default::default(), selected: Default::default(),
block: None, block: None,
} }
} }
pub fn with_corner(mut self, corner: Address) -> Self { pub fn with_selected(mut self, location: Address) -> Self {
self.corner = corner; self.selected = location;
self self
} }
fn get_visible_columns(&self, width: u16) -> Result<Vec<VisibleColumn>> { pub(crate) fn get_visible_columns(
&self,
width: u16,
state: &ViewportState,
) -> Result<Vec<VisibleColumn>> {
let mut visible = Vec::new(); let mut visible = Vec::new();
let mut length = 0; // TODO(zaphar): This should be a shared constant with our first column.
for idx in self.corner.col..=LAST_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 size = self.book.get_col_size(idx)? as u16;
let updated_length = length + size; let updated_length = length + size;
if updated_length <= width { let col = VisibleColumn { idx, length: size };
if updated_length < width {
length = updated_length; 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); return Ok(visible);
@ -72,47 +118,60 @@ impl<'book> Viewport<'book> {
self self
} }
fn to_table<'widget>(&self, width: u16, height: u16) -> Result<Table<'widget>> { pub(crate) fn to_table<'widget>(
let visible_columns = self.get_visible_columns(width)?; &self,
let max_row = min(self.corner.row + height as usize, LAST_COLUMN); width: u16,
let rows: Vec<Row> = (self.corner.row..=max_row) height: u16,
.into_iter() state: &mut ViewportState,
.map(|ri| { ) -> Result<Table<'widget>> {
let mut cells = vec![Cell::new(Text::from(ri.to_string()))]; let visible_columns = self.get_visible_columns(width, state)?;
cells.extend(visible_columns.iter().map(|VisibleColumn { idx: ci, length: _, }| { if let Some(vc) = visible_columns.first() {
// TODO(zaphar): Is this safe? state.prev_corner.col = vc.idx
let content = self }
.book let max_row = min(state.prev_corner.row + height as usize, LAST_COLUMN);
.get_cell_addr_rendered(&Address { row: ri, col: *ci }) let rows: Vec<Row> =
.unwrap(); (state.prev_corner.row..=max_row)
let cell = Cell::new(Text::raw(content)); .into_iter()
match (self.book.location.row == ri, self.book.location.col == *ci) { .map(|ri| {
(true, true) => cell.fg(Color::White).underlined(), let mut cells = vec![Cell::new(Text::from(ri.to_string()))];
_ => cell cells.extend(visible_columns.iter().map(
.bg(if ri % 2 == 0 { |VisibleColumn { idx: ci, length: _ }| {
Color::Rgb(57, 61, 71) // TODO(zaphar): Is this safe?
} else { let content = self
Color::Rgb(165, 169, 160) .book
}) .get_cell_addr_rendered(&Address { row: ri, col: *ci })
.fg(if ri % 2 == 0 { .unwrap();
Color::White let cell = Cell::new(Text::raw(content));
} else { match (self.book.location.row == ri, self.book.location.col == *ci) {
Color::Rgb(31, 32, 34) (true, true) => cell.fg(Color::White).underlined(),
}), _ => cell
} .bg(if ri % 2 == 0 {
.bold() Color::Rgb(57, 61, 71)
})); } else {
Row::new(cells) Color::Rgb(165, 169, 160)
}) })
.collect(); .fg(if ri % 2 == 0 {
let constraints: Vec<Constraint> = visible_columns.iter() Color::White
} else {
Color::Rgb(31, 32, 34)
}),
}
.bold()
},
));
Row::new(cells)
})
.collect();
let constraints: Vec<Constraint> = visible_columns
.iter()
.map(|vc| Constraint::from(vc)) .map(|vc| Constraint::from(vc))
.collect(); .collect();
let end_idx = visible_columns.last().unwrap().idx;
let mut header = Vec::with_capacity(constraints.len()); let mut header = Vec::with_capacity(constraints.len());
header.push(Cell::new("")); 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; 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 // TODO(zaphar): We should calculate the length from the length of the stringified version of the
// row indexes. // row indexes.
@ -125,12 +184,16 @@ impl<'book> Viewport<'book> {
} }
} }
impl<'book> Widget for Viewport<'book> { impl<'book> StatefulWidget for Viewport<'book> {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { type State = ViewportState;
let mut table = self.to_table(area.width, area.height).expect("Failed to turn viewport into a table.");
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 { if let Some(block) = self.block {
table = table.block(block); table = table.block(block);
} }
table.render(area, buf); Widget::render(table, area, buf);
} }
} }