mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-23 05:19:48 -04:00
commit
79a119ea93
@ -16,15 +16,13 @@ use crate::ui::Address;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test;
|
mod test;
|
||||||
|
|
||||||
const COL_PIXELS: f64 = 10.0;
|
const COL_PIXELS: f64 = 5.0;
|
||||||
|
|
||||||
/// A spreadsheet book with some internal state tracking.
|
/// A spreadsheet book with some internal state tracking.
|
||||||
pub struct Book {
|
pub struct Book {
|
||||||
pub(crate) model: Model,
|
pub(crate) model: Model,
|
||||||
pub current_sheet: u32,
|
pub current_sheet: u32,
|
||||||
pub location: crate::ui::Address,
|
pub location: crate::ui::Address,
|
||||||
// TODO(zaphar): Because the ironcalc model is sparse we need to track our render size
|
|
||||||
// separately
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Book {
|
impl Book {
|
||||||
|
@ -6,11 +6,10 @@ use crate::book::Book;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
self,
|
|
||||||
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, Widget},
|
||||||
};
|
};
|
||||||
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 +20,7 @@ pub mod render;
|
|||||||
mod test;
|
mod test;
|
||||||
|
|
||||||
use cmd::Cmd;
|
use cmd::Cmd;
|
||||||
|
use render::{viewport::ViewportState, Viewport};
|
||||||
|
|
||||||
#[derive(Default, Debug, PartialEq, Clone)]
|
#[derive(Default, Debug, PartialEq, Clone)]
|
||||||
pub enum Modality {
|
pub enum Modality {
|
||||||
@ -34,7 +34,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 table_state: TableState,
|
pub viewport_state: ViewportState,
|
||||||
pub command_state: TextState<'ws>,
|
pub command_state: TextState<'ws>,
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
popup: Vec<String>,
|
popup: Vec<String>,
|
||||||
@ -44,7 +44,7 @@ impl<'ws> Default for AppState<'ws> {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
AppState {
|
AppState {
|
||||||
modality_stack: vec![Modality::default()],
|
modality_stack: vec![Modality::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(),
|
||||||
@ -123,8 +123,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)?;
|
||||||
}
|
}
|
||||||
@ -154,8 +153,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)?;
|
||||||
}
|
}
|
||||||
@ -164,8 +162,6 @@ impl<'ws> Workspace<'ws> {
|
|||||||
|
|
||||||
/// Handle input in our ui loop.
|
/// Handle input in our ui loop.
|
||||||
pub fn handle_input(&mut self, evt: Event) -> Result<Option<ExitCode>> {
|
pub fn handle_input(&mut self, evt: Event) -> Result<Option<ExitCode>> {
|
||||||
// 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 {
|
if let Event::Key(key) = evt {
|
||||||
let result = match self.state.modality() {
|
let result = match self.state.modality() {
|
||||||
Modality::Navigate => self.handle_navigation_input(key)?,
|
Modality::Navigate => self.handle_navigation_input(key)?,
|
||||||
@ -459,20 +455,13 @@ 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
|
|
||||||
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("");
|
let viewport = Viewport::new(&ws.book).with_selected(ws.book.location.clone()).block(table_block);
|
||||||
let table = table_inner.block(table_block);
|
StatefulWidget::render(viewport, rect, buf, &mut ws.state.viewport_state);
|
||||||
// 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);
|
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
116
src/ui/render.rs
116
src/ui/render.rs
@ -1,116 +0,0 @@
|
|||||||
use ratatui::{
|
|
||||||
self,
|
|
||||||
layout::{Constraint, Flex, Rect},
|
|
||||||
style::{Color, Stylize},
|
|
||||||
text::{Line, Text},
|
|
||||||
widgets::{Block, Cell, Row, Table, Widget},
|
|
||||||
Frame,
|
|
||||||
};
|
|
||||||
use tui_popup::Popup;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
|
|
||||||
fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer)
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
let outer_block = Block::bordered()
|
|
||||||
.title(Line::from(
|
|
||||||
self.name
|
|
||||||
.file_name()
|
|
||||||
.map(|p| p.to_string_lossy().to_string())
|
|
||||||
.unwrap_or_else(|| String::from("Unknown")),
|
|
||||||
))
|
|
||||||
.title_bottom(match self.state.modality() {
|
|
||||||
Modality::Navigate => "navigate",
|
|
||||||
Modality::CellEdit => "edit",
|
|
||||||
Modality::Command => "command",
|
|
||||||
Modality::Dialog => "",
|
|
||||||
})
|
|
||||||
.title_bottom(
|
|
||||||
Line::from(format!(
|
|
||||||
"{},{}",
|
|
||||||
self.book.location.row, self.book.location.col
|
|
||||||
))
|
|
||||||
.right_aligned(),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (rect, f) in self.get_render_parts(area.clone()) {
|
|
||||||
f(rect, buf, self);
|
|
||||||
}
|
|
||||||
|
|
||||||
outer_block.render(area, buf);
|
|
||||||
|
|
||||||
if self.state.modality() == &Modality::Dialog {
|
|
||||||
let lines = Text::from_iter(self.state.popup.iter().cloned());
|
|
||||||
let popup = Popup::new(lines);
|
|
||||||
popup.render(area, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<'t, 'book: 't> TryFrom<&'book Book> for Table<'t> {
|
|
||||||
fn try_from(value: &'book Book) -> std::result::Result<Self, Self::Error> {
|
|
||||||
// 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<Row> = (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<Constraint> = 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());
|
|
||||||
}
|
|
60
src/ui/render/mod.rs
Normal file
60
src/ui/render/mod.rs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
use ratatui::{
|
||||||
|
self,
|
||||||
|
layout::Rect,
|
||||||
|
text::{Line, Text},
|
||||||
|
widgets::{Block, Widget},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
use tui_popup::Popup;
|
||||||
|
|
||||||
|
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
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let outer_block = Block::bordered()
|
||||||
|
.title(Line::from(
|
||||||
|
self.name
|
||||||
|
.file_name()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| String::from("Unknown")),
|
||||||
|
))
|
||||||
|
.title_bottom(match self.state.modality() {
|
||||||
|
Modality::Navigate => "navigate",
|
||||||
|
Modality::CellEdit => "edit",
|
||||||
|
Modality::Command => "command",
|
||||||
|
Modality::Dialog => "",
|
||||||
|
})
|
||||||
|
.title_bottom(
|
||||||
|
Line::from(format!(
|
||||||
|
"{},{}",
|
||||||
|
self.book.location.row, self.book.location.col
|
||||||
|
))
|
||||||
|
.right_aligned(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (rect, f) in self.get_render_parts(area.clone()) {
|
||||||
|
f(rect, buf, self);
|
||||||
|
}
|
||||||
|
|
||||||
|
outer_block.render(area, buf);
|
||||||
|
|
||||||
|
if self.state.modality() == &Modality::Dialog {
|
||||||
|
let lines = Text::from_iter(self.state.popup.iter().cloned());
|
||||||
|
let popup = Popup::new(lines);
|
||||||
|
popup.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(frame: &mut Frame, ws: &mut Workspace) {
|
||||||
|
frame.render_widget(ws, frame.area());
|
||||||
|
}
|
49
src/ui/render/test.rs
Normal file
49
src/ui/render/test.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
use ironcalc::base::Model;
|
||||||
|
|
||||||
|
use super::{Address, Book, Viewport, ViewportState};
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
}
|
213
src/ui/render/viewport.rs
Normal file
213
src/ui/render/viewport.rs
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Constraint, Flex, Rect},
|
||||||
|
style::{Color, Stylize},
|
||||||
|
text::Text,
|
||||||
|
widgets::{Block, Cell, Row, StatefulWidget, Table, Widget},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{Address, Book};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
/// A visible column to show in our Viewport.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct ViewportState {
|
||||||
|
prev_corner: Address,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A renderable viewport over a book.
|
||||||
|
pub struct Viewport<'book> {
|
||||||
|
pub(crate) selected: Address,
|
||||||
|
book: &'book Book,
|
||||||
|
block: Option<Block<'book>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
];
|
||||||
|
|
||||||
|
impl<'book> Viewport<'book> {
|
||||||
|
pub fn new(book: &'book Book) -> Self {
|
||||||
|
Self {
|
||||||
|
book,
|
||||||
|
selected: Default::default(),
|
||||||
|
block: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_selected(mut self, location: Address) -> Self {
|
||||||
|
self.selected = location;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_visible_rows(&self, height: u16, state: &ViewportState) -> Vec<usize> {
|
||||||
|
// 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);
|
||||||
|
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,
|
||||||
|
state: &ViewportState,
|
||||||
|
) -> Result<Vec<VisibleColumn>> {
|
||||||
|
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 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 {
|
||||||
|
let size = self.book.get_col_size(idx)? as u16;
|
||||||
|
let updated_length = length + size;
|
||||||
|
let col = VisibleColumn { idx, length: size };
|
||||||
|
if updated_length < width {
|
||||||
|
length = updated_length;
|
||||||
|
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.
|
||||||
|
// TODO(jwall): This is a bit inefficient. Can we do better?
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn block(mut self, block: Block<'book>) -> Self {
|
||||||
|
self.block = Some(block);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn to_table<'widget>(
|
||||||
|
&self,
|
||||||
|
width: u16,
|
||||||
|
height: u16,
|
||||||
|
state: &mut ViewportState,
|
||||||
|
) -> Result<Table<'widget>> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if let Some(vr) = visible_rows.first() {
|
||||||
|
state.prev_corner.row = *vr;
|
||||||
|
}
|
||||||
|
let rows: Vec<Row> =
|
||||||
|
visible_rows
|
||||||
|
.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).bg(Color::Rgb(57, 61, 71)),
|
||||||
|
_ => cell,
|
||||||
|
}
|
||||||
|
.bold()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
Row::new(cells)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let constraints: Vec<Constraint> = 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((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());
|
||||||
|
Ok(Table::new(rows, col_constraints)
|
||||||
|
.header(Row::new(header).underlined())
|
||||||
|
.column_spacing(0)
|
||||||
|
.flex(Flex::Start))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
|
if let Some(block) = self.block {
|
||||||
|
table = table.block(block);
|
||||||
|
}
|
||||||
|
Widget::render(table, area, buf);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user