mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-23 05:19:48 -04:00
wip: viewport abstraction for a table view
This commit is contained in:
parent
a4a48ee5b9
commit
9aa6c98f6a
@ -10,7 +10,7 @@ use ratatui::{
|
|||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::{Constraint, Flex, Layout, Rect},
|
layout::{Constraint, Flex, Layout, Rect},
|
||||||
style::{Modifier, Style},
|
style::{Modifier, Style},
|
||||||
widgets::{Block, Table, TableState, Widget},
|
widgets::{Block, Table, TableState, Widget, WidgetRef},
|
||||||
};
|
};
|
||||||
use tui_prompts::{State, Status, TextPrompt, TextState};
|
use tui_prompts::{State, Status, TextPrompt, TextState};
|
||||||
use tui_textarea::{CursorMove, TextArea};
|
use tui_textarea::{CursorMove, TextArea};
|
||||||
@ -21,6 +21,7 @@ pub mod render;
|
|||||||
mod test;
|
mod test;
|
||||||
|
|
||||||
use cmd::Cmd;
|
use cmd::Cmd;
|
||||||
|
use render::Viewport;
|
||||||
|
|
||||||
#[derive(Default, Debug, PartialEq, Clone)]
|
#[derive(Default, Debug, PartialEq, Clone)]
|
||||||
pub enum Modality {
|
pub enum Modality {
|
||||||
@ -459,20 +460,16 @@ impl<'ws> Workspace<'ws> {
|
|||||||
) -> Vec<(Rect, Box<dyn Fn(Rect, &mut Buffer, &mut Self)>)> {
|
) -> Vec<(Rect, Box<dyn Fn(Rect, &mut Buffer, &mut Self)>)> {
|
||||||
use ratatui::widgets::StatefulWidget;
|
use ratatui::widgets::StatefulWidget;
|
||||||
let mut cs = vec![Constraint::Fill(4), Constraint::Fill(30)];
|
let mut cs = vec![Constraint::Fill(4), Constraint::Fill(30)];
|
||||||
let Address { row, col } = self.book.location;
|
|
||||||
let mut rs: Vec<Box<dyn Fn(Rect, &mut Buffer, &mut Self)>> = vec![
|
let mut rs: Vec<Box<dyn Fn(Rect, &mut Buffer, &mut Self)>> = vec![
|
||||||
Box::new(|rect: Rect, buf: &mut Buffer, ws: &mut Self| ws.text_area.render(rect, buf)),
|
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| {
|
Box::new(move |rect: Rect, buf: &mut Buffer, ws: &mut Self| {
|
||||||
let sheet_name = ws.book.get_sheet_name().unwrap_or("Unknown");
|
let sheet_name = ws.book.get_sheet_name().unwrap_or("Unknown");
|
||||||
// Table widget display
|
// Table widget display
|
||||||
let table_block = Block::bordered().title_top(sheet_name);
|
let table_block = Block::bordered().title_top(sheet_name);
|
||||||
let table_inner: Table = TryFrom::try_from(&ws.book).expect("");
|
// TODO(zaphar): We should be smarter about calculating the location properly
|
||||||
let table = table_inner.block(table_block);
|
// Might require viewport state to do properly
|
||||||
// https://docs.rs/ratatui/latest/ratatui/widgets/struct.TableState.html
|
let viewport = Viewport::new(&ws.book).with_corner(ws.book.location.clone()).block(table_block);
|
||||||
// TODO(zaphar): Apparently scrolling by columns doesn't work?
|
viewport.render(rect, buf);
|
||||||
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);
|
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -10,6 +10,9 @@ use tui_popup::Popup;
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
pub mod viewport;
|
||||||
|
pub use viewport::Viewport;
|
||||||
|
|
||||||
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
|
||||||
@ -55,6 +58,10 @@ const COLNAMES: [&'static str; 26] = [
|
|||||||
"T", "U", "V", "W", "X", "Y", "Z",
|
"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> {
|
impl<'t, 'book: 't> TryFrom<&'book Book> for Table<'t> {
|
||||||
fn try_from(value: &'book Book) -> std::result::Result<Self, Self::Error> {
|
fn try_from(value: &'book Book) -> std::result::Result<Self, Self::Error> {
|
||||||
// TODO(zaphar): This is apparently expensive. Maybe we can cache it somehow?
|
// TODO(zaphar): This is apparently expensive. Maybe we can cache it somehow?
|
136
src/ui/render/viewport.rs
Normal file
136
src/ui/render/viewport.rs
Normal file
@ -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<Block<'book>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Vec<VisibleColumn>> {
|
||||||
|
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<Table<'widget>> {
|
||||||
|
let visible_columns = self.get_visible_columns(width)?;
|
||||||
|
let max_row = min(self.corner.row + height as usize, LAST_COLUMN);
|
||||||
|
let rows: Vec<Row> = (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<Constraint> = 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user