Skip to content

Commit

Permalink
Merge pull request #1 from achristmascarl/horizontal-only-scrollable-…
Browse files Browse the repository at this point in the history
…table

Horizontal only scrollable table
  • Loading branch information
achristmascarl authored Jul 12, 2024
2 parents 2704146 + d11f822 commit 6064e30
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 273 deletions.
30 changes: 20 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ lazy_static = "1.4.0"
libc = "0.2.148"
log = "0.4.20"
pretty_assertions = "1.4.0"
ratatui = { version = "0.26.0", features = ["serde", "macros", "unstable-widget-ref"] }
ratatui = { version = "0.27.0", features = ["serde", "macros", "unstable-widget-ref"] }
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.107"
signal-hook = "0.3.17"
Expand Down
2 changes: 1 addition & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ impl App {
if action != Action::Tick && action != Action::Render {
log::debug!("{action:?}");
}
let mut action_consumed = false;
let action_consumed = false;
match &action {
Action::Tick => {
self.last_tick_key_events.drain(..);
Expand Down
2 changes: 1 addition & 1 deletion src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
pub mod data;
pub mod editor;
pub mod menu;
pub mod scrollable;
pub mod scroll_table;

/// `Component` is a trait that represents a visual and interactive element of the user interface.
/// Implementors of this trait can be registered with the main application loop and will be able to receive events,
Expand Down
20 changes: 6 additions & 14 deletions src/components/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{
action::Action,
app::{App, AppState},
components::{
scrollable::{ScrollDirection, Scrollable},
scroll_table::{ScrollDirection, ScrollTable},
Component,
},
config::{Config, KeyBindings},
Expand Down Expand Up @@ -43,7 +43,7 @@ impl<'a, T> DataComponent<'a> for T where T: Component + SettableDataTable<'a>
pub struct Data<'a> {
command_tx: Option<UnboundedSender<Action>>,
config: Config,
scrollable: Scrollable<'a>,
scrollable: ScrollTable<'a>,
data_state: DataState,
state: Arc<Mutex<AppState>>,
}
Expand All @@ -53,7 +53,7 @@ impl<'a> Data<'a> {
Data {
command_tx: None,
config: Config::default(),
scrollable: Scrollable::default(),
scrollable: ScrollTable::default(),
data_state: DataState::Blank,
state,
}
Expand All @@ -75,11 +75,7 @@ impl<'a> SettableDataTable<'a> for Data<'a> {
let value_rows = rows.iter().map(|r| Row::new(row_to_vec(r)).bottom_margin(1)).collect::<Vec<Row>>();
let buf_table =
Table::default().rows(value_rows).header(header_row).style(Style::default()).column_spacing(1);
self.scrollable.child(
Box::new(buf_table),
16_u16.saturating_mul(headers.len() as u16),
4_u16.saturating_mul(rows.len() as u16),
);
self.scrollable.set_table(Box::new(buf_table), 36_u16.saturating_mul(headers.len() as u16), rows.len());
self.data_state = DataState::HasResults;
}
},
Expand Down Expand Up @@ -130,11 +126,8 @@ impl<'a> Component for Data<'a> {
}

fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::Query(query) => {
self.scrollable.reset_scroll();
},
_ => {},
if let Action::Query(query) = action {
self.scrollable.reset_scroll();
}
Ok(None)
}
Expand All @@ -158,7 +151,6 @@ impl<'a> Component for Data<'a> {
},
DataState::HasResults => {
if !state.table_buf_logged {
self.scrollable.log();
state.table_buf_logged = true;
}
self.scrollable.block(block);
Expand Down
192 changes: 192 additions & 0 deletions src/components/scroll_table.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
use std::{borrow::BorrowMut, cell::RefCell};

use color_eyre::eyre::Result;
use ratatui::{
buffer::Cell,
prelude::*,
widgets::{
Block, ScrollDirection as RatatuiScrollDir, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidgetRef,
Table, TableState, WidgetRef,
},
};
use symbols::scrollbar;

use super::Component;

pub enum ScrollDirection {
Left,
Right,
Up,
Down,
}

#[derive(Debug, Clone, Default)]
pub struct ScrollTable<'a> {
viewport_buffer: Buffer,
table: Table<'a>,
parent_area: Rect,
block: Option<Block<'a>>,
requested_width: u16,
max_height: u16,
x_offset: u16,
y_offset: usize,
max_x_offset: u16,
max_y_offset: usize,
}

impl<'a> ScrollTable<'a> {
pub fn new() -> Self {
Self {
viewport_buffer: Buffer::empty(Rect::new(0, 0, 0, 0)),
table: Table::default(),
parent_area: Rect::new(0, 0, 0, 0),
block: None,
requested_width: 0,
max_height: 0,
x_offset: 0,
y_offset: 0,
max_x_offset: 0,
max_y_offset: 0,
}
}

pub fn set_table(&mut self, table: Box<Table<'a>>, requested_width: u16, row_count: usize) -> &mut Self {
let max_height = u16::MAX.saturating_div(requested_width);
self.table = *table;
self.requested_width = requested_width;
self.max_height = max_height;
self.max_y_offset = row_count.saturating_sub(1);
self
}

pub fn block(&mut self, block: Block<'a>) -> &mut Self {
self.block = Some(block);
self
}

pub fn scroll(&mut self, direction: ScrollDirection) -> &mut Self {
match direction {
ScrollDirection::Left => self.x_offset = self.x_offset.saturating_sub(1),
ScrollDirection::Right => self.x_offset = Ord::min(self.x_offset.saturating_add(1), self.max_x_offset),
ScrollDirection::Up => self.y_offset = self.y_offset.saturating_sub(1),
ScrollDirection::Down => self.y_offset = Ord::min(self.y_offset.saturating_add(1), self.max_y_offset),
}
self
}

pub fn reset_scroll(&mut self) -> &mut Self {
self.x_offset = 0;
self.y_offset = 0;
self
}

fn widget(&'a self) -> Renderer<'a> {
Renderer::new(self, self.y_offset)
}
}

impl<'a> Component for ScrollTable<'a> {
fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> {
self.parent_area = area;
self.max_x_offset = get_max_x_offset(self.requested_width, &self.parent_area, &self.block);
let max_x_offset = self.max_x_offset;
let x_offset = self.x_offset;
f.render_widget(self.widget(), area);
let vertical_scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight).symbols(scrollbar::VERTICAL);
let mut vertical_scrollbar_state = ScrollbarState::new(self.max_y_offset).position(self.y_offset);
let horizontal_scrollbar =
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).symbols(scrollbar::HORIZONTAL).thumb_symbol("▀");
let mut horizontal_scrollbar_state = ScrollbarState::new(max_x_offset as usize).position(x_offset as usize);
match (self.max_x_offset, self.max_y_offset) {
(0, 0) => {},
(0, y) => {
f.render_stateful_widget(
vertical_scrollbar,
area.inner(Margin { vertical: 1, horizontal: 0 }),
&mut vertical_scrollbar_state,
);
},
(x, 0) => {
f.render_stateful_widget(
horizontal_scrollbar,
area.inner(Margin { vertical: 0, horizontal: 1 }),
&mut horizontal_scrollbar_state,
);
},
(x, y) => {
f.render_stateful_widget(
vertical_scrollbar,
area.inner(Margin { vertical: 1, horizontal: 0 }),
&mut vertical_scrollbar_state,
);
f.render_stateful_widget(
horizontal_scrollbar,
area.inner(Margin { vertical: 0, horizontal: 1 }),
&mut horizontal_scrollbar_state,
);
},
};
Ok(())
}
}

fn get_max_x_offset(requested_width: u16, parent_area: &Rect, parent_block: &Option<Block>) -> u16 {
let render_area = parent_block.inner_if_some(*parent_area);
if render_area.is_empty() {
return 0_u16;
}
let parent_width = render_area.width;
requested_width.saturating_sub(parent_width)
}

// based on scrolling approach from tui-textarea:
// https://github.com/rhysd/tui-textarea/blob/main/src/widget.rs
pub struct Renderer<'a>(&'a ScrollTable<'a>, TableState);

impl<'a> Renderer<'a> {
pub fn new(scrollable: &'a ScrollTable<'a>, y_offset: usize) -> Self {
Self(scrollable, TableState::default().with_offset(y_offset))
}

pub fn offset(&self) -> usize {
self.1.offset()
}
}

impl<'a> Widget for Renderer<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let scrollable = self.0;
let table = &scrollable.table;
let mut table_state = self.1;
scrollable.block.render_ref(area, buf);
let render_area = scrollable.block.inner_if_some(area);
if render_area.is_empty() {
return;
}
let area = render_area.intersection(buf.area);
let mut content_buf = Buffer::empty(Rect::new(
0,
0,
scrollable.requested_width,
std::cmp::min(scrollable.max_height, render_area.height),
));
ratatui::widgets::StatefulWidgetRef::render_ref(table, content_buf.area, &mut content_buf, &mut table_state);
let content_width = content_buf.area.width;
let content_height = content_buf.area.height;
let max_x = Ord::min(area.x.saturating_add(area.width), area.x.saturating_add(content_width));
let max_y = Ord::min(area.y.saturating_add(area.height), area.y.saturating_add(content_height));
for y in area.y..max_y {
let content_y = y - area.y;
let row = get_row(&content_buf.content, content_y, content_width);
for x in area.x..max_x {
let content_x = x + scrollable.x_offset - area.x;
let cell = &row[content_x as usize];
buf.get_mut(x, y).set_symbol(cell.symbol()).set_fg(cell.fg).set_bg(cell.bg).set_skip(cell.skip);
}
}
}
}

fn get_row(content: &[Cell], row: u16, width: u16) -> Vec<Cell> {
content[((row * width) as usize)..(((row + 1) * width) as usize)].to_vec()
}
Loading

0 comments on commit 6064e30

Please sign in to comment.