deps: bump ratatui to 0.26 (#1406)

* deps: bump ratatui to 0.26

* adjust process width

* a few nonzero optimizations

* add a todo

* update comments to be less confusing about time chart
This commit is contained in:
Clement Tsang 2024-02-03 19:59:12 -05:00 committed by GitHub
parent 8d84b688b0
commit b6660610d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1469 additions and 487 deletions

35
Cargo.lock generated
View file

@ -224,6 +224,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "castaway"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc"
dependencies = [
"rustversion",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.83" version = "1.0.83"
@ -312,6 +321,19 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "compact_str"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"ryu",
"static_assertions",
]
[[package]] [[package]]
name = "concat-string" name = "concat-string"
version = "1.0.1" version = "1.0.1"
@ -990,12 +1012,13 @@ dependencies = [
[[package]] [[package]]
name = "ratatui" name = "ratatui"
version = "0.25.0" version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5659e52e4ba6e07b2dad9f1158f578ef84a73762625ddb51536019f34d180eb" checksum = "154b85ef15a5d1719bcaa193c3c81fe645cd120c156874cd660fe49fd21d1373"
dependencies = [ dependencies = [
"bitflags 2.4.1", "bitflags 2.4.1",
"cassowary", "cassowary",
"compact_str",
"crossterm", "crossterm",
"indoc", "indoc",
"itertools 0.12.0", "itertools 0.12.0",
@ -1309,18 +1332,18 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "strum" name = "strum"
version = "0.25.0" version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f"
dependencies = [ dependencies = [
"strum_macros", "strum_macros",
] ]
[[package]] [[package]]
name = "strum_macros" name = "strum_macros"
version = "0.25.3" version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",

View file

@ -99,7 +99,7 @@ sysinfo = "=0.30.5"
thiserror = "1.0.56" thiserror = "1.0.56"
time = { version = "0.3.30", features = ["formatting", "macros"] } time = { version = "0.3.30", features = ["formatting", "macros"] }
toml_edit = { version = "0.21.0", features = ["serde"] } toml_edit = { version = "0.21.0", features = ["serde"] }
tui = { version = "0.25.0", package = "ratatui" } tui = { version = "0.26.0", package = "ratatui" }
unicode-segmentation = "1.10.1" unicode-segmentation = "1.10.1"
unicode-width = "0.1.11" unicode-width = "0.1.11"

View file

@ -2586,7 +2586,7 @@ impl App {
.get_widget_state(self.current_widget.widget_id) .get_widget_state(self.current_widget.widget_id)
{ {
if let Some(visual_index) = if let Some(visual_index) =
proc_widget_state.table.tui_selected() proc_widget_state.table.ratatui_selected()
{ {
let is_tree_mode = matches!( let is_tree_mode = matches!(
proc_widget_state.mode, proc_widget_state.mode,
@ -2614,7 +2614,7 @@ impl App {
.get_widget_state(self.current_widget.widget_id - 2) .get_widget_state(self.current_widget.widget_id - 2)
{ {
if let Some(visual_index) = if let Some(visual_index) =
proc_widget_state.sort_table.tui_selected() proc_widget_state.sort_table.ratatui_selected()
{ {
self.change_process_sort_position( self.change_process_sort_position(
offset_clicked_entry as i64 - visual_index as i64, offset_clicked_entry as i64 - visual_index as i64,
@ -2629,7 +2629,7 @@ impl App {
.get_widget_state(self.current_widget.widget_id - 1) .get_widget_state(self.current_widget.widget_id - 1)
{ {
if let Some(visual_index) = if let Some(visual_index) =
cpu_widget_state.table.tui_selected() cpu_widget_state.table.ratatui_selected()
{ {
self.change_cpu_legend_position( self.change_cpu_legend_position(
offset_clicked_entry as i64 - visual_index as i64, offset_clicked_entry as i64 - visual_index as i64,
@ -2644,7 +2644,7 @@ impl App {
.get_widget_state(self.current_widget.widget_id) .get_widget_state(self.current_widget.widget_id)
{ {
if let Some(visual_index) = if let Some(visual_index) =
temp_widget_state.table.tui_selected() temp_widget_state.table.ratatui_selected()
{ {
self.change_temp_position( self.change_temp_position(
offset_clicked_entry as i64 - visual_index as i64, offset_clicked_entry as i64 - visual_index as i64,
@ -2659,7 +2659,7 @@ impl App {
.get_widget_state(self.current_widget.widget_id) .get_widget_state(self.current_widget.widget_id)
{ {
if let Some(visual_index) = if let Some(visual_index) =
disk_widget_state.table.tui_selected() disk_widget_state.table.ratatui_selected()
{ {
self.change_disk_position( self.change_disk_position(
offset_clicked_entry as i64 - visual_index as i64, offset_clicked_entry as i64 - visual_index as i64,

View file

@ -72,7 +72,8 @@ pub struct Painter {
/// The constraints of a widget relative to its parent. /// The constraints of a widget relative to its parent.
/// ///
/// This is used over ratatui's internal representation due to https://github.com/ClementTsang/bottom/issues/896. /// This is used over ratatui's internal representation due to
/// <https://github.com/ClementTsang/bottom/issues/896>.
pub enum LayoutConstraint { pub enum LayoutConstraint {
CanvasHandled, CanvasHandled,
Grow, Grow,
@ -498,6 +499,8 @@ impl Painter {
} }
if self.derived_widget_draw_locs.is_empty() || app_state.is_force_redraw { if self.derived_widget_draw_locs.is_empty() || app_state.is_force_redraw {
// TODO: Can I remove this? Does ratatui's layout constraints work properly for fixing
// https://github.com/ClementTsang/bottom/issues/896 now?
fn get_constraints( fn get_constraints(
direction: Direction, constraints: &[LayoutConstraint], area: Rect, direction: Direction, constraints: &[LayoutConstraint], area: Rect,
) -> Vec<Rect> { ) -> Vec<Rect> {

View file

@ -144,14 +144,16 @@ impl<DataType: DataToCell<H>, H: ColumnHeader, S: SortType, C: DataTableColumn<H
self.data.get(self.state.current_index) self.data.get(self.state.current_index)
} }
/// Returns tui-rs' internal selection. /// Returns ratatui's internal selection.
pub fn tui_selected(&self) -> Option<usize> { pub fn ratatui_selected(&self) -> Option<usize> {
self.state.table_state.selected() self.state.table_state.selected()
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::num::NonZeroU16;
use super::*; use super::*;
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
@ -161,7 +163,7 @@ mod test {
impl DataToCell<&'static str> for TestType { impl DataToCell<&'static str> for TestType {
fn to_cell( fn to_cell(
&self, _column: &&'static str, _calculated_width: u16, &self, _column: &&'static str, _calculated_width: NonZeroU16,
) -> Option<tui::text::Text<'_>> { ) -> Option<tui::text::Text<'_>> {
None None
} }

View file

@ -1,12 +1,13 @@
use std::{ use std::{
borrow::Cow, borrow::Cow,
cmp::{max, min}, cmp::{max, min},
num::NonZeroU16,
}; };
/// A bound on the width of a column. /// A bound on the width of a column.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum ColumnWidthBounds { pub enum ColumnWidthBounds {
/// A width of this type is either as long as `min`, but can otherwise shrink and grow up to a point. /// A width of this type is as long as `desired`, but can otherwise shrink and grow up to a point.
Soft { Soft {
/// The desired, calculated width. Take this if possible as the base starting width. /// The desired, calculated width. Take this if possible as the base starting width.
desired: u16, desired: u16,
@ -151,7 +152,7 @@ pub trait CalculateColumnWidths<H> {
/// ///
/// * `total_width` is the total width on the canvas that the columns can try and work with. /// * `total_width` is the total width on the canvas that the columns can try and work with.
/// * `left_to_right` is whether to size from left-to-right (`true`) or right-to-left (`false`). /// * `left_to_right` is whether to size from left-to-right (`true`) or right-to-left (`false`).
fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec<u16>; fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec<NonZeroU16>;
} }
impl<H, C> CalculateColumnWidths<H> for [C] impl<H, C> CalculateColumnWidths<H> for [C]
@ -159,19 +160,25 @@ where
H: ColumnHeader, H: ColumnHeader,
C: DataTableColumn<H>, C: DataTableColumn<H>,
{ {
fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec<u16> { fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec<NonZeroU16> {
use itertools::Either; use itertools::Either;
const COLUMN_SPACING: u16 = 1;
#[inline]
fn stop_allocating_space(desired: u16, available: u16) -> bool {
desired > available || desired == 0
}
let mut total_width_left = total_width; let mut total_width_left = total_width;
let mut calculated_widths = vec![0; self.len()]; let mut calculated_widths = vec![];
let columns = if left_to_right { let columns = if left_to_right {
Either::Left(self.iter().zip(calculated_widths.iter_mut())) Either::Left(self.iter())
} else { } else {
Either::Right(self.iter().zip(calculated_widths.iter_mut()).rev()) Either::Right(self.iter().rev())
}; };
let mut num_columns = 0; for column in columns {
for (column, calculated_width) in columns {
if column.is_hidden() { if column.is_hidden() {
continue; continue;
} }
@ -196,41 +203,60 @@ where
); );
let space_taken = min(min(soft_limit, *desired), total_width_left); let space_taken = min(min(soft_limit, *desired), total_width_left);
if min_width > space_taken || min_width == 0 { if stop_allocating_space(space_taken, total_width_left) {
break; break;
} else if space_taken > 0 { } else {
total_width_left = total_width_left.saturating_sub(space_taken + 1); total_width_left =
*calculated_width = space_taken; total_width_left.saturating_sub(space_taken + COLUMN_SPACING);
num_columns += 1;
// SAFETY: This is safe as we call `stop_allocating_space` which checks that
// the value pushed is greater than zero.
unsafe {
calculated_widths.push(NonZeroU16::new_unchecked(space_taken));
}
} }
} }
ColumnWidthBounds::Hard(width) => { ColumnWidthBounds::Hard(width) => {
let min_width = *width; let min_width = *width;
if min_width > total_width_left || min_width == 0 { if stop_allocating_space(min_width, total_width_left) {
break; break;
} else if min_width > 0 { } else {
total_width_left = total_width_left.saturating_sub(min_width + 1); total_width_left =
*calculated_width = min_width; total_width_left.saturating_sub(min_width + COLUMN_SPACING);
num_columns += 1;
// SAFETY: This is safe as we call `stop_allocating_space` which checks that
// the value pushed is greater than zero.
unsafe {
calculated_widths.push(NonZeroU16::new_unchecked(min_width));
}
} }
} }
ColumnWidthBounds::FollowHeader => { ColumnWidthBounds::FollowHeader => {
let min_width = column.header_len() as u16; let min_width = column.header_len() as u16;
if min_width > total_width_left || min_width == 0 { if stop_allocating_space(min_width, total_width_left) {
break; break;
} else if min_width > 0 { } else {
total_width_left = total_width_left.saturating_sub(min_width + 1); total_width_left =
*calculated_width = min_width; total_width_left.saturating_sub(min_width + COLUMN_SPACING);
num_columns += 1;
// SAFETY: This is safe as we call `stop_allocating_space` which checks that
// the value pushed is greater than zero.
unsafe {
calculated_widths.push(NonZeroU16::new_unchecked(min_width));
}
} }
} }
} }
} }
if num_columns > 0 { if !calculated_widths.is_empty() {
// Redistribute remaining. if !left_to_right {
let mut num_dist = num_columns; calculated_widths.reverse();
let amount_per_slot = total_width_left / num_dist; }
// Redistribute remaining space.
let mut num_dist = calculated_widths.len() as u16;
let amount_per_slot = total_width_left / num_dist; // Safe from DBZ by above empty check.
total_width_left %= num_dist; total_width_left %= num_dist;
for width in calculated_widths.iter_mut() { for width in calculated_widths.iter_mut() {
@ -238,16 +264,14 @@ where
break; break;
} }
if *width > 0 { if total_width_left > 0 {
if total_width_left > 0 { *width = width.saturating_add(amount_per_slot + 1);
*width += amount_per_slot + 1; total_width_left -= 1;
total_width_left -= 1; } else {
} else { *width = width.saturating_add(amount_per_slot);
*width += amount_per_slot;
}
num_dist -= 1;
} }
num_dist -= 1;
} }
} }

View file

@ -1,3 +1,5 @@
use std::num::NonZeroU16;
use tui::{text::Text, widgets::Row}; use tui::{text::Text, widgets::Row};
use super::{ColumnHeader, DataTableColumn}; use super::{ColumnHeader, DataTableColumn};
@ -8,7 +10,7 @@ where
H: ColumnHeader, H: ColumnHeader,
{ {
/// Given data, a column, and its corresponding width, return what should be displayed in the [`DataTable`](super::DataTable). /// Given data, a column, and its corresponding width, return what should be displayed in the [`DataTable`](super::DataTable).
fn to_cell(&self, column: &H, calculated_width: u16) -> Option<Text<'_>>; fn to_cell(&self, column: &H, calculated_width: NonZeroU16) -> Option<Text<'_>>;
/// Apply styling to the generated [`Row`] of cells. /// Apply styling to the generated [`Row`] of cells.
/// ///

View file

@ -249,18 +249,7 @@ where
}; };
let mut table = Table::new( let mut table = Table::new(
rows, rows,
&(self self.state.calculated_widths.iter().map(|nzu| nzu.get()),
.state
.calculated_widths
.iter()
.filter_map(|&width| {
if width == 0 {
None
} else {
Some(Constraint::Length(width))
}
})
.collect::<Vec<_>>()),
) )
.block(block) .block(block)
.highlight_style(highlight_style) .highlight_style(highlight_style)

View file

@ -1,4 +1,4 @@
use std::{borrow::Cow, marker::PhantomData}; use std::{borrow::Cow, marker::PhantomData, num::NonZeroU16};
use concat_string::concat_string; use concat_string::concat_string;
use itertools::Itertools; use itertools::Itertools;
@ -52,18 +52,17 @@ pub struct Sortable {
/// and therefore only [`Unsortable`] and [`Sortable`] can implement it. /// and therefore only [`Unsortable`] and [`Sortable`] can implement it.
pub trait SortType: private::Sealed { pub trait SortType: private::Sealed {
/// Constructs the table header. /// Constructs the table header.
fn build_header<H, C>(&self, columns: &[C], widths: &[u16]) -> Row<'_> fn build_header<H, C>(&self, columns: &[C], widths: &[NonZeroU16]) -> Row<'_>
where where
H: ColumnHeader, H: ColumnHeader,
C: DataTableColumn<H>, C: DataTableColumn<H>,
{ {
Row::new(columns.iter().zip(widths).filter_map(|(c, &width)| { Row::new(
if width == 0 { columns
None .iter()
} else { .zip(widths)
Some(truncate_to_text(&c.header(), width)) .map(|(c, &width)| truncate_to_text(&c.header(), width.get())),
} )
}))
} }
} }
@ -79,7 +78,7 @@ mod private {
impl SortType for Unsortable {} impl SortType for Unsortable {}
impl SortType for Sortable { impl SortType for Sortable {
fn build_header<H, C>(&self, columns: &[C], widths: &[u16]) -> Row<'_> fn build_header<H, C>(&self, columns: &[C], widths: &[NonZeroU16]) -> Row<'_>
where where
H: ColumnHeader, H: ColumnHeader,
C: DataTableColumn<H>, C: DataTableColumn<H>,
@ -92,17 +91,17 @@ impl SortType for Sortable {
.iter() .iter()
.zip(widths) .zip(widths)
.enumerate() .enumerate()
.filter_map(|(index, (c, &width))| { .map(|(index, (c, &width))| {
if width == 0 { if index == self.sort_index {
None
} else if index == self.sort_index {
let arrow = match self.order { let arrow = match self.order {
SortOrder::Ascending => UP_ARROW, SortOrder::Ascending => UP_ARROW,
SortOrder::Descending => DOWN_ARROW, SortOrder::Descending => DOWN_ARROW,
}; };
Some(truncate_to_text(&concat_string!(c.header(), arrow), width)) // TODO: I think I can get away with removing the truncate_to_text call since
// I almost always bind to at least the header size...
truncate_to_text(&concat_string!(c.header(), arrow), width.get())
} else { } else {
Some(truncate_to_text(&c.header(), width)) truncate_to_text(&c.header(), width.get())
} }
}), }),
) )
@ -331,7 +330,7 @@ where
.iter() .iter()
.map(|width| { .map(|width| {
let entry_start = start; let entry_start = start;
start += width + 1; // +1 for the gap b/w cols. start += width.get() + 1; // +1 for the gap b/w cols.
entry_start entry_start
}) })
@ -361,7 +360,7 @@ mod test {
impl DataToCell<ColumnType> for TestType { impl DataToCell<ColumnType> for TestType {
fn to_cell( fn to_cell(
&self, _column: &ColumnType, _calculated_width: u16, &self, _column: &ColumnType, _calculated_width: NonZeroU16,
) -> Option<tui::text::Text<'_>> { ) -> Option<tui::text::Text<'_>> {
None None
} }

View file

@ -1,3 +1,5 @@
use std::num::NonZeroU16;
use tui::{layout::Rect, widgets::TableState}; use tui::{layout::Rect, widgets::TableState};
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
@ -21,11 +23,11 @@ pub struct DataTableState {
/// The direction of the last attempted scroll. /// The direction of the last attempted scroll.
pub scroll_direction: ScrollDirection, pub scroll_direction: ScrollDirection,
/// tui-rs' internal table state. /// ratatui's internal table state.
pub table_state: TableState, pub table_state: TableState,
/// The calculated widths. /// The calculated widths.
pub calculated_widths: Vec<u16>, pub calculated_widths: Vec<NonZeroU16>,
/// The current inner [`Rect`]. /// The current inner [`Rect`].
pub inner_rect: Rect, pub inner_rect: Rect,

View file

@ -51,8 +51,8 @@ pub struct TimeGraph<'a> {
/// Any legend constraints. /// Any legend constraints.
pub legend_constraints: Option<(Constraint, Constraint)>, pub legend_constraints: Option<(Constraint, Constraint)>,
/// The marker type. Unlike tui-rs' native charts, we assume /// The marker type. Unlike ratatui's native charts, we assume
/// only a single type of market. /// only a single type of marker.
pub marker: Marker, pub marker: Marker,
} }

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,18 @@
//! Vendored from <https://github.com/fdehau/tui-rs/blob/fafad6c96109610825aad89c4bba5253e01101ed/src/widgets/canvas/mod.rs>. //! Vendored from <https://github.com/fdehau/tui-rs/blob/fafad6c96109610825aad89c4bba5253e01101ed/src/widgets/canvas/mod.rs>
//! Main difference is in the Braille rendering, which can now effectively be done in a single layer without the effects //! and <https://github.com/ratatui-org/ratatui/blob/c8dd87918d44fff6d4c3c78e1fc821a3275db1ae/src/widgets/canvas.rs>.
//! of doing it all in a single layer via the normal tui-rs crate. This means you can do it all in a single pass, with //!
//! just one string alloc and no resets. //! The main thing this is pulled in for is overriding how `BrailleGrid`'s draw logic works, as changing it is
//! needed in order to draw all datasets in only one layer back in [`super::TimeChart::render`]. More specifically,
//! the current implementation in ratatui `|=`s all the cells together if they overlap, but since we are smashing
//! all the layers together which may have different colours, we instead just _replace_ whatever was in that cell
//! with the newer colour + character.
//!
//! See <https://github.com/ClementTsang/bottom/pull/918> and <https://github.com/ClementTsang/bottom/pull/937> for the
//! original motivation.
use std::fmt::Debug; use std::{fmt::Debug, iter::zip};
use itertools::Itertools;
use tui::{ use tui::{
buffer::Buffer, buffer::Buffer,
layout::Rect, layout::Rect,
@ -128,7 +136,7 @@ pub struct Label<'a> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Layer { struct Layer {
string: String, string: String,
colors: Vec<Color>, colors: Vec<(Color, Color)>,
} }
trait Grid: Debug { trait Grid: Debug {
@ -179,7 +187,7 @@ impl Grid for BrailleGrid {
fn save(&self) -> Layer { fn save(&self) -> Layer {
Layer { Layer {
string: String::from_utf16(&self.cells).unwrap(), string: String::from_utf16(&self.cells).unwrap(),
colors: self.colors.clone(), colors: self.colors.iter().map(|c| (*c, Color::Reset)).collect(),
} }
} }
@ -247,7 +255,7 @@ impl Grid for CharGrid {
fn save(&self) -> Layer { fn save(&self) -> Layer {
Layer { Layer {
string: self.cells.iter().collect(), string: self.cells.iter().collect(),
colors: self.colors.clone(), colors: self.colors.iter().map(|c| (*c, Color::Reset)).collect(),
} }
} }
@ -277,6 +285,113 @@ pub struct Painter<'a, 'b> {
resolution: (f64, f64), resolution: (f64, f64),
} }
/// The HalfBlockGrid is a grid made up of cells each containing a half block character.
///
/// In terminals, each character is usually twice as tall as it is wide. Unicode has a couple of
/// vertical half block characters, the upper half block '▀' and lower half block '▄' which take up
/// half the height of a normal character but the full width. Together with an empty space ' ' and a
/// full block '█', we can effectively double the resolution of a single cell. In addition, because
/// each character can have a foreground and background color, we can control the color of the upper
/// and lower half of each cell. This allows us to draw shapes with a resolution of 1x2 "pixels" per
/// cell.
///
/// This allows for more flexibility than the BrailleGrid which only supports a single
/// foreground color for each 2x4 dots cell, and the CharGrid which only supports a single
/// character for each cell.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
struct HalfBlockGrid {
/// width of the grid in number of terminal columns
width: u16,
/// height of the grid in number of terminal rows
height: u16,
/// represents a single color for each "pixel" arranged in column, row order
pixels: Vec<Vec<Color>>,
}
impl HalfBlockGrid {
/// Create a new [`HalfBlockGrid`] with the given width and height measured in terminal columns
/// and rows respectively.
fn new(width: u16, height: u16) -> HalfBlockGrid {
HalfBlockGrid {
width,
height,
pixels: vec![vec![Color::Reset; width as usize]; height as usize * 2],
}
}
}
impl Grid for HalfBlockGrid {
fn width(&self) -> u16 {
self.width
}
fn height(&self) -> u16 {
self.height
}
fn resolution(&self) -> (f64, f64) {
(f64::from(self.width), f64::from(self.height) * 2.0)
}
fn save(&self) -> Layer {
// Given that we store the pixels in a grid, and that we want to use 2 pixels arranged
// vertically to form a single terminal cell, which can be either empty, upper half block,
// lower half block or full block, we need examine the pixels in vertical pairs to decide
// what character to print in each cell. So these are the 4 states we use to represent each
// cell:
//
// 1. upper: reset, lower: reset => ' ' fg: reset / bg: reset
// 2. upper: reset, lower: color => '▄' fg: lower color / bg: reset
// 3. upper: color, lower: reset => '▀' fg: upper color / bg: reset
// 4. upper: color, lower: color => '▀' fg: upper color / bg: lower color
//
// Note that because the foreground reset color (i.e. default foreground color) is usually
// not the same as the background reset color (i.e. default background color), we need to
// swap around the colors for that state (2 reset/color).
//
// When the upper and lower colors are the same, we could continue to use an upper half
// block, but we choose to use a full block instead. This allows us to write unit tests that
// treat the cell as a single character instead of two half block characters.
// Note we implement this slightly differently to what is done in ratatui's repo,
// since their version doesn't seem to compile for me...
// TODO: Whenever I add this as a valid marker, make sure this works fine with the overriden
// time_chart drawing-layer-thing.
// Join the upper and lower rows, and emit a tuple vector of strings to print, and their colours.
let (string, colors) = self
.pixels
.iter()
.tuples()
.flat_map(|(upper_row, lower_row)| zip(upper_row, lower_row))
.map(|(upper, lower)| match (upper, lower) {
(Color::Reset, Color::Reset) => (' ', (Color::Reset, Color::Reset)),
(Color::Reset, &lower) => (symbols::half_block::LOWER, (Color::Reset, lower)),
(&upper, Color::Reset) => (symbols::half_block::UPPER, (upper, Color::Reset)),
(&upper, &lower) => {
let c = if lower == upper {
symbols::half_block::FULL
} else {
symbols::half_block::UPPER
};
(c, (upper, lower))
}
})
.unzip();
Layer { string, colors }
}
fn reset(&mut self) {
self.pixels.fill(vec![Color::Reset; self.width as usize]);
}
fn paint(&mut self, x: usize, y: usize, color: Color) {
self.pixels[y][x] = color;
}
}
impl<'a, 'b> Painter<'a, 'b> { impl<'a, 'b> Painter<'a, 'b> {
/// Convert the (x, y) coordinates to location of a point on the grid /// Convert the (x, y) coordinates to location of a point on the grid
/// ///
@ -366,7 +481,7 @@ impl<'a> Context<'a> {
symbols::Marker::Block => Box::new(CharGrid::new(width, height, '█')), symbols::Marker::Block => Box::new(CharGrid::new(width, height, '█')),
symbols::Marker::Bar => Box::new(CharGrid::new(width, height, '▄')), symbols::Marker::Bar => Box::new(CharGrid::new(width, height, '▄')),
symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)), symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)),
symbols::Marker::HalfBlock => Box::new(CharGrid::new(width, height, '▀')), symbols::Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),
}; };
Context { Context {
x_bounds, x_bounds,
@ -507,7 +622,7 @@ where
// Paint whatever is in the ctx. // Paint whatever is in the ctx.
let layer = ctx.grid.save(); let layer = ctx.grid.save();
for (i, (ch, color)) in layer for (i, (ch, (fg, bg))) in layer
.string .string
.chars() .chars()
.zip(layer.colors.into_iter()) .zip(layer.colors.into_iter())
@ -517,7 +632,8 @@ where
let (x, y) = (i % width, i / width); let (x, y) = (i % width, i / width);
buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top()) buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top())
.set_char(ch) .set_char(ch)
.set_fg(color); .set_fg(fg)
.set_bg(bg);
} }
} }

View file

@ -0,0 +1,215 @@
use tui::{
style::Color,
widgets::{
canvas::{Line as CanvasLine, Points},
GraphType,
},
};
use crate::utils::general::partial_ordering;
use super::{Context, Dataset, Point, TimeChart};
impl TimeChart<'_> {
pub(crate) fn draw_points(&self, ctx: &mut Context<'_>) {
// Idea is to:
// - Go over all datasets, determine *where* a point will be drawn.
// - Last point wins for what gets drawn.
// - We set _all_ points for all datasets before actually rendering.
//
// By doing this, it's a bit more efficient from my experience than looping
// over each dataset and rendering a new layer each time.
//
// See <https://github.com/ClementTsang/bottom/pull/918> and <https://github.com/ClementTsang/bottom/pull/937>
// for the original motivation.
//
// We also additionally do some interpolation logic because we may get caught missing some points
// when drawing, but we generally want to avoid jarring gaps between the edges when there's
// a point that is off screen and so a line isn't drawn (right edge generally won't have this issue
// issue but it can happen in some cases).
for dataset in &self.datasets {
let color = dataset.style.fg.unwrap_or(Color::Reset);
let start_bound = self.x_axis.bounds[0];
let end_bound = self.x_axis.bounds[1];
let (start_index, interpolate_start) = get_start(dataset, start_bound);
let (end_index, interpolate_end) = get_end(dataset, end_bound);
let data_slice = &dataset.data[start_index..end_index];
if let Some(interpolate_start) = interpolate_start {
if let (Some(older_point), Some(newer_point)) = (
dataset.data.get(interpolate_start),
dataset.data.get(interpolate_start + 1),
) {
let interpolated_point = (
self.x_axis.bounds[0],
interpolate_point(older_point, newer_point, self.x_axis.bounds[0]),
);
if let GraphType::Line = dataset.graph_type {
ctx.draw(&CanvasLine {
x1: interpolated_point.0,
y1: interpolated_point.1,
x2: newer_point.0,
y2: newer_point.1,
color,
});
} else {
ctx.draw(&Points {
coords: &[interpolated_point],
color,
});
}
}
}
if let GraphType::Line = dataset.graph_type {
for data in data_slice.windows(2) {
ctx.draw(&CanvasLine {
x1: data[0].0,
y1: data[0].1,
x2: data[1].0,
y2: data[1].1,
color,
});
}
} else {
ctx.draw(&Points {
coords: data_slice,
color,
});
}
if let Some(interpolate_end) = interpolate_end {
if let (Some(older_point), Some(newer_point)) = (
dataset.data.get(interpolate_end - 1),
dataset.data.get(interpolate_end),
) {
let interpolated_point = (
self.x_axis.bounds[1],
interpolate_point(older_point, newer_point, self.x_axis.bounds[1]),
);
if let GraphType::Line = dataset.graph_type {
ctx.draw(&CanvasLine {
x1: older_point.0,
y1: older_point.1,
x2: interpolated_point.0,
y2: interpolated_point.1,
color,
});
} else {
ctx.draw(&Points {
coords: &[interpolated_point],
color,
});
}
}
}
}
}
}
/// Returns the start index and potential interpolation index given the start time and the dataset.
fn get_start(dataset: &Dataset<'_>, start_bound: f64) -> (usize, Option<usize>) {
match dataset
.data
.binary_search_by(|(x, _y)| partial_ordering(x, &start_bound))
{
Ok(index) => (index, None),
Err(index) => (index, index.checked_sub(1)),
}
}
/// Returns the end position and potential interpolation index given the end time and the dataset.
fn get_end(dataset: &Dataset<'_>, end_bound: f64) -> (usize, Option<usize>) {
match dataset
.data
.binary_search_by(|(x, _y)| partial_ordering(x, &end_bound))
{
// In the success case, this means we found an index. Add one since we want to include this index and we
// expect to use the returned index as part of a (m..n) range.
Ok(index) => (index.saturating_add(1), None),
// In the fail case, this means we did not find an index, and the returned index is where one would *insert*
// the location. This index is where one would insert to fit inside the dataset - and since this is an end
// bound, index is, in a sense, already "+1" for our range later.
Err(index) => (index, {
let sum = index.checked_add(1);
match sum {
Some(s) if s < dataset.data.len() => sum,
_ => None,
}
}),
}
}
/// Returns the y-axis value for a given `x`, given two points to draw a line between.
fn interpolate_point(older_point: &Point, newer_point: &Point, x: f64) -> f64 {
let delta_x = newer_point.0 - older_point.0;
let delta_y = newer_point.1 - older_point.1;
let slope = delta_y / delta_x;
(older_point.1 + (x - older_point.0) * slope).max(0.0)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn time_chart_test_interpolation() {
let data = [(-3.0, 8.0), (-1.0, 6.0), (0.0, 5.0)];
assert_eq!(interpolate_point(&data[1], &data[2], 0.0), 5.0);
assert_eq!(interpolate_point(&data[1], &data[2], -0.25), 5.25);
assert_eq!(interpolate_point(&data[1], &data[2], -0.5), 5.5);
assert_eq!(interpolate_point(&data[0], &data[1], -1.0), 6.0);
assert_eq!(interpolate_point(&data[0], &data[1], -1.5), 6.5);
assert_eq!(interpolate_point(&data[0], &data[1], -2.0), 7.0);
assert_eq!(interpolate_point(&data[0], &data[1], -2.5), 7.5);
assert_eq!(interpolate_point(&data[0], &data[1], -3.0), 8.0);
}
#[test]
fn time_chart_empty_dataset() {
let data = [];
let dataset = Dataset::default().data(&data);
assert_eq!(get_start(&dataset, -100.0), (0, None));
assert_eq!(get_start(&dataset, -3.0), (0, None));
assert_eq!(get_end(&dataset, 0.0), (0, None));
assert_eq!(get_end(&dataset, 100.0), (0, None));
}
#[test]
fn time_chart_test_data_trimming() {
let data = [
(-3.0, 8.0),
(-2.5, 15.0),
(-2.0, 9.0),
(-1.0, 6.0),
(0.0, 5.0),
];
let dataset = Dataset::default().data(&data);
// Test start point cases (miss and hit)
assert_eq!(get_start(&dataset, -100.0), (0, None));
assert_eq!(get_start(&dataset, -3.0), (0, None));
assert_eq!(get_start(&dataset, -2.8), (1, Some(0)));
assert_eq!(get_start(&dataset, -2.5), (1, None));
assert_eq!(get_start(&dataset, -2.4), (2, Some(1)));
// Test end point cases (miss and hit)
assert_eq!(get_end(&dataset, -2.5), (2, None));
assert_eq!(get_end(&dataset, -2.4), (2, Some(3)));
assert_eq!(get_end(&dataset, -1.4), (3, Some(4)));
assert_eq!(get_end(&dataset, -1.0), (4, None));
assert_eq!(get_end(&dataset, 0.0), (5, None));
assert_eq!(get_end(&dataset, 1.0), (5, None));
assert_eq!(get_end(&dataset, 100.0), (5, None));
}
}

View file

@ -345,8 +345,8 @@ fn adjust_network_data_point(
// So for example, let's say I use 390 Mb/s. If I drew 4 segments, it would be 97.5, 195, 292.5, 390, and // So for example, let's say I use 390 Mb/s. If I drew 4 segments, it would be 97.5, 195, 292.5, 390, and
// probably something like 438.75? // probably something like 438.75?
// //
// So, how do we do this in tui-rs? Well, if we are using intervals that tie in perfectly to the max // So, how do we do this in ratatui? Well, if we are using intervals that tie in perfectly to the max
// value we want... then it's actually not that hard. Since tui-rs accepts a vector as labels and will // value we want... then it's actually not that hard. Since ratatui accepts a vector as labels and will
// properly space them all out... we just work with that and space it out properly. // properly space them all out... we just work with that and space it out properly.
// //
// Dynamic chart idea based off of FreeNAS's chart design. // Dynamic chart idea based off of FreeNAS's chart design.

View file

@ -1,6 +1,9 @@
use std::{cmp::Ordering, num::NonZeroUsize}; use std::{cmp::Ordering, num::NonZeroUsize};
use tui::text::{Line, Span, Text}; use tui::{
style::Style,
text::{Line, Span, Text},
};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
@ -64,6 +67,8 @@ pub fn get_decimal_prefix(quantity: u64, unit: &str) -> (f64, String) {
pub fn truncate_to_text<'a, U: Into<usize>>(content: &str, width: U) -> Text<'a> { pub fn truncate_to_text<'a, U: Into<usize>>(content: &str, width: U) -> Text<'a> {
Text { Text {
lines: vec![Line::from(vec![Span::raw(truncate_str(content, width))])], lines: vec![Line::from(vec![Span::raw(truncate_str(content, width))])],
style: Style::default(),
alignment: None,
} }
} }

View file

@ -1,4 +1,4 @@
use std::{borrow::Cow, time::Instant}; use std::{borrow::Cow, num::NonZeroU16, time::Instant};
use concat_string::concat_string; use concat_string::concat_string;
use tui::{style::Style, text::Text, widgets::Row}; use tui::{style::Style, text::Text, widgets::Row};
@ -81,9 +81,11 @@ impl CpuWidgetTableData {
} }
impl DataToCell<CpuWidgetColumn> for CpuWidgetTableData { impl DataToCell<CpuWidgetColumn> for CpuWidgetTableData {
fn to_cell(&self, column: &CpuWidgetColumn, calculated_width: u16) -> Option<Text<'_>> { fn to_cell(&self, column: &CpuWidgetColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
const CPU_TRUNCATE_BREAKPOINT: u16 = 5; const CPU_TRUNCATE_BREAKPOINT: u16 = 5;
let calculated_width = calculated_width.get();
// This is a bit of a hack, but apparently we can avoid having to do any fancy checks // This is a bit of a hack, but apparently we can avoid having to do any fancy checks
// of showing the "All" on a specific column if the other is hidden by just always // of showing the "All" on a specific column if the other is hidden by just always
// showing it on the CPU (first) column - if there isn't room for it, it will just collapse // showing it on the CPU (first) column - if there isn't room for it, it will just collapse

View file

@ -1,4 +1,4 @@
use std::{borrow::Cow, cmp::max}; use std::{borrow::Cow, cmp::max, num::NonZeroU16};
use kstring::KString; use kstring::KString;
use tui::text::Text; use tui::text::Text;
@ -128,11 +128,8 @@ impl ColumnHeader for DiskWidgetColumn {
} }
impl DataToCell<DiskWidgetColumn> for DiskWidgetData { impl DataToCell<DiskWidgetColumn> for DiskWidgetData {
fn to_cell(&self, column: &DiskWidgetColumn, calculated_width: u16) -> Option<Text<'_>> { fn to_cell(&self, column: &DiskWidgetColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
if calculated_width == 0 { let calculated_width = calculated_width.get();
return None;
}
let text = match column { let text = match column {
DiskWidgetColumn::Disk => truncate_to_text(&self.name, calculated_width), DiskWidgetColumn::Disk => truncate_to_text(&self.name, calculated_width),
DiskWidgetColumn::Mount => truncate_to_text(&self.mount_point, calculated_width), DiskWidgetColumn::Mount => truncate_to_text(&self.mount_point, calculated_width),

View file

@ -89,7 +89,7 @@ fn make_column(column: ProcColumn) -> SortColumn<ProcColumn> {
TotalRead => SortColumn::hard(TotalRead, 8).default_descending(), TotalRead => SortColumn::hard(TotalRead, 8).default_descending(),
TotalWrite => SortColumn::hard(TotalWrite, 8).default_descending(), TotalWrite => SortColumn::hard(TotalWrite, 8).default_descending(),
User => SortColumn::soft(User, Some(0.05)), User => SortColumn::soft(User, Some(0.05)),
State => SortColumn::hard(State, 7), State => SortColumn::hard(State, 9),
Time => SortColumn::new(Time), Time => SortColumn::new(Time),
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
GpuMem => SortColumn::new(GpuMem).default_descending(), GpuMem => SortColumn::new(GpuMem).default_descending(),

View file

@ -1,6 +1,7 @@
use std::{ use std::{
cmp::{max, Ordering}, cmp::{max, Ordering},
fmt::Display, fmt::Display,
num::NonZeroU16,
time::Duration, time::Duration,
}; };
@ -299,10 +300,8 @@ impl ProcWidgetData {
} }
impl DataToCell<ProcColumn> for ProcWidgetData { impl DataToCell<ProcColumn> for ProcWidgetData {
fn to_cell(&self, column: &ProcColumn, calculated_width: u16) -> Option<Text<'_>> { fn to_cell(&self, column: &ProcColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
if calculated_width == 0 { let calculated_width = calculated_width.get();
return None;
}
// TODO: Optimize the string allocations here... // TODO: Optimize the string allocations here...
// TODO: Also maybe just pull in the to_string call but add a variable for the differences. // TODO: Also maybe just pull in the to_string call but add a variable for the differences.

View file

@ -1,4 +1,4 @@
use std::borrow::Cow; use std::{borrow::Cow, num::NonZeroU16};
use tui::text::Text; use tui::text::Text;
@ -16,12 +16,8 @@ impl ColumnHeader for SortTableColumn {
} }
impl DataToCell<SortTableColumn> for &'static str { impl DataToCell<SortTableColumn> for &'static str {
fn to_cell(&self, _column: &SortTableColumn, calculated_width: u16) -> Option<Text<'_>> { fn to_cell(&self, _column: &SortTableColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
if calculated_width == 0 { Some(truncate_to_text(self, calculated_width.get()))
return None;
}
Some(truncate_to_text(self, calculated_width))
} }
fn column_widths<C: DataTableColumn<SortTableColumn>>(data: &[Self], _columns: &[C]) -> Vec<u16> fn column_widths<C: DataTableColumn<SortTableColumn>>(data: &[Self], _columns: &[C]) -> Vec<u16>
@ -33,12 +29,8 @@ impl DataToCell<SortTableColumn> for &'static str {
} }
impl DataToCell<SortTableColumn> for Cow<'static, str> { impl DataToCell<SortTableColumn> for Cow<'static, str> {
fn to_cell(&self, _column: &SortTableColumn, calculated_width: u16) -> Option<Text<'_>> { fn to_cell(&self, _column: &SortTableColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
if calculated_width == 0 { Some(truncate_to_text(self, calculated_width.get()))
return None;
}
Some(truncate_to_text(self, calculated_width))
} }
fn column_widths<C: DataTableColumn<SortTableColumn>>(data: &[Self], _columns: &[C]) -> Vec<u16> fn column_widths<C: DataTableColumn<SortTableColumn>>(data: &[Self], _columns: &[C]) -> Vec<u16>

View file

@ -1,4 +1,4 @@
use std::{borrow::Cow, cmp::max}; use std::{borrow::Cow, cmp::max, num::NonZeroU16};
use concat_string::concat_string; use concat_string::concat_string;
use kstring::KString; use kstring::KString;
@ -55,14 +55,10 @@ impl TempWidgetData {
} }
impl DataToCell<TempWidgetColumn> for TempWidgetData { impl DataToCell<TempWidgetColumn> for TempWidgetData {
fn to_cell(&self, column: &TempWidgetColumn, calculated_width: u16) -> Option<Text<'_>> { fn to_cell(&self, column: &TempWidgetColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
if calculated_width == 0 {
return None;
}
Some(match column { Some(match column {
TempWidgetColumn::Sensor => truncate_to_text(&self.sensor, calculated_width), TempWidgetColumn::Sensor => truncate_to_text(&self.sensor, calculated_width.get()),
TempWidgetColumn::Temp => truncate_to_text(&self.temperature(), calculated_width), TempWidgetColumn::Temp => truncate_to_text(&self.temperature(), calculated_width.get()),
}) })
} }