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

View File

@ -99,7 +99,7 @@ sysinfo = "=0.30.5"
thiserror = "1.0.56"
time = { version = "0.3.30", features = ["formatting", "macros"] }
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-width = "0.1.11"

View File

@ -2586,7 +2586,7 @@ impl App {
.get_widget_state(self.current_widget.widget_id)
{
if let Some(visual_index) =
proc_widget_state.table.tui_selected()
proc_widget_state.table.ratatui_selected()
{
let is_tree_mode = matches!(
proc_widget_state.mode,
@ -2614,7 +2614,7 @@ impl App {
.get_widget_state(self.current_widget.widget_id - 2)
{
if let Some(visual_index) =
proc_widget_state.sort_table.tui_selected()
proc_widget_state.sort_table.ratatui_selected()
{
self.change_process_sort_position(
offset_clicked_entry as i64 - visual_index as i64,
@ -2629,7 +2629,7 @@ impl App {
.get_widget_state(self.current_widget.widget_id - 1)
{
if let Some(visual_index) =
cpu_widget_state.table.tui_selected()
cpu_widget_state.table.ratatui_selected()
{
self.change_cpu_legend_position(
offset_clicked_entry as i64 - visual_index as i64,
@ -2644,7 +2644,7 @@ impl App {
.get_widget_state(self.current_widget.widget_id)
{
if let Some(visual_index) =
temp_widget_state.table.tui_selected()
temp_widget_state.table.ratatui_selected()
{
self.change_temp_position(
offset_clicked_entry as i64 - visual_index as i64,
@ -2659,7 +2659,7 @@ impl App {
.get_widget_state(self.current_widget.widget_id)
{
if let Some(visual_index) =
disk_widget_state.table.tui_selected()
disk_widget_state.table.ratatui_selected()
{
self.change_disk_position(
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.
///
/// 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 {
CanvasHandled,
Grow,
@ -498,6 +499,8 @@ impl Painter {
}
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(
direction: Direction, constraints: &[LayoutConstraint], area: 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)
}
/// Returns tui-rs' internal selection.
pub fn tui_selected(&self) -> Option<usize> {
/// Returns ratatui's internal selection.
pub fn ratatui_selected(&self) -> Option<usize> {
self.state.table_state.selected()
}
}
#[cfg(test)]
mod test {
use std::num::NonZeroU16;
use super::*;
#[derive(Clone, PartialEq, Eq, Debug)]
@ -161,7 +163,7 @@ mod test {
impl DataToCell<&'static str> for TestType {
fn to_cell(
&self, _column: &&'static str, _calculated_width: u16,
&self, _column: &&'static str, _calculated_width: NonZeroU16,
) -> Option<tui::text::Text<'_>> {
None
}

View File

@ -1,12 +1,13 @@
use std::{
borrow::Cow,
cmp::{max, min},
num::NonZeroU16,
};
/// A bound on the width of a column.
#[derive(Clone, Copy, Debug)]
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 {
/// The desired, calculated width. Take this if possible as the base starting width.
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.
/// * `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]
@ -159,19 +160,25 @@ where
H: ColumnHeader,
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;
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 calculated_widths = vec![0; self.len()];
let mut calculated_widths = vec![];
let columns = if left_to_right {
Either::Left(self.iter().zip(calculated_widths.iter_mut()))
Either::Left(self.iter())
} else {
Either::Right(self.iter().zip(calculated_widths.iter_mut()).rev())
Either::Right(self.iter().rev())
};
let mut num_columns = 0;
for (column, calculated_width) in columns {
for column in columns {
if column.is_hidden() {
continue;
}
@ -196,41 +203,60 @@ where
);
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;
} else if space_taken > 0 {
total_width_left = total_width_left.saturating_sub(space_taken + 1);
*calculated_width = space_taken;
num_columns += 1;
} else {
total_width_left =
total_width_left.saturating_sub(space_taken + COLUMN_SPACING);
// 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) => {
let min_width = *width;
if min_width > total_width_left || min_width == 0 {
if stop_allocating_space(min_width, total_width_left) {
break;
} else if min_width > 0 {
total_width_left = total_width_left.saturating_sub(min_width + 1);
*calculated_width = min_width;
num_columns += 1;
} else {
total_width_left =
total_width_left.saturating_sub(min_width + COLUMN_SPACING);
// 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 => {
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;
} else if min_width > 0 {
total_width_left = total_width_left.saturating_sub(min_width + 1);
*calculated_width = min_width;
num_columns += 1;
} else {
total_width_left =
total_width_left.saturating_sub(min_width + COLUMN_SPACING);
// 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 {
// Redistribute remaining.
let mut num_dist = num_columns;
let amount_per_slot = total_width_left / num_dist;
if !calculated_widths.is_empty() {
if !left_to_right {
calculated_widths.reverse();
}
// 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;
for width in calculated_widths.iter_mut() {
@ -238,16 +264,14 @@ where
break;
}
if *width > 0 {
if total_width_left > 0 {
*width += amount_per_slot + 1;
total_width_left -= 1;
} else {
*width += amount_per_slot;
}
num_dist -= 1;
if total_width_left > 0 {
*width = width.saturating_add(amount_per_slot + 1);
total_width_left -= 1;
} else {
*width = width.saturating_add(amount_per_slot);
}
num_dist -= 1;
}
}

View File

@ -1,3 +1,5 @@
use std::num::NonZeroU16;
use tui::{text::Text, widgets::Row};
use super::{ColumnHeader, DataTableColumn};
@ -8,7 +10,7 @@ where
H: ColumnHeader,
{
/// 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.
///

View File

@ -249,18 +249,7 @@ where
};
let mut table = Table::new(
rows,
&(self
.state
.calculated_widths
.iter()
.filter_map(|&width| {
if width == 0 {
None
} else {
Some(Constraint::Length(width))
}
})
.collect::<Vec<_>>()),
self.state.calculated_widths.iter().map(|nzu| nzu.get()),
)
.block(block)
.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 itertools::Itertools;
@ -52,18 +52,17 @@ pub struct Sortable {
/// and therefore only [`Unsortable`] and [`Sortable`] can implement it.
pub trait SortType: private::Sealed {
/// 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
H: ColumnHeader,
C: DataTableColumn<H>,
{
Row::new(columns.iter().zip(widths).filter_map(|(c, &width)| {
if width == 0 {
None
} else {
Some(truncate_to_text(&c.header(), width))
}
}))
Row::new(
columns
.iter()
.zip(widths)
.map(|(c, &width)| truncate_to_text(&c.header(), width.get())),
)
}
}
@ -79,7 +78,7 @@ mod private {
impl SortType for Unsortable {}
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
H: ColumnHeader,
C: DataTableColumn<H>,
@ -92,17 +91,17 @@ impl SortType for Sortable {
.iter()
.zip(widths)
.enumerate()
.filter_map(|(index, (c, &width))| {
if width == 0 {
None
} else if index == self.sort_index {
.map(|(index, (c, &width))| {
if index == self.sort_index {
let arrow = match self.order {
SortOrder::Ascending => UP_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 {
Some(truncate_to_text(&c.header(), width))
truncate_to_text(&c.header(), width.get())
}
}),
)
@ -331,7 +330,7 @@ where
.iter()
.map(|width| {
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
})
@ -361,7 +360,7 @@ mod test {
impl DataToCell<ColumnType> for TestType {
fn to_cell(
&self, _column: &ColumnType, _calculated_width: u16,
&self, _column: &ColumnType, _calculated_width: NonZeroU16,
) -> Option<tui::text::Text<'_>> {
None
}

View File

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

View File

@ -51,8 +51,8 @@ pub struct TimeGraph<'a> {
/// Any legend constraints.
pub legend_constraints: Option<(Constraint, Constraint)>,
/// The marker type. Unlike tui-rs' native charts, we assume
/// only a single type of market.
/// The marker type. Unlike ratatui's native charts, we assume
/// only a single type of 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>.
//! Main difference is in the Braille rendering, which can now effectively be done in a single layer without the effects
//! 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.
//! Vendored from <https://github.com/fdehau/tui-rs/blob/fafad6c96109610825aad89c4bba5253e01101ed/src/widgets/canvas/mod.rs>
//! and <https://github.com/ratatui-org/ratatui/blob/c8dd87918d44fff6d4c3c78e1fc821a3275db1ae/src/widgets/canvas.rs>.
//!
//! 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::{
buffer::Buffer,
layout::Rect,
@ -128,7 +136,7 @@ pub struct Label<'a> {
#[derive(Debug, Clone)]
struct Layer {
string: String,
colors: Vec<Color>,
colors: Vec<(Color, Color)>,
}
trait Grid: Debug {
@ -179,7 +187,7 @@ impl Grid for BrailleGrid {
fn save(&self) -> Layer {
Layer {
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 {
Layer {
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),
}
/// 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> {
/// 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::Bar => Box::new(CharGrid::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 {
x_bounds,
@ -507,7 +622,7 @@ where
// Paint whatever is in the ctx.
let layer = ctx.grid.save();
for (i, (ch, color)) in layer
for (i, (ch, (fg, bg))) in layer
.string
.chars()
.zip(layer.colors.into_iter())
@ -517,7 +632,8 @@ where
let (x, y) = (i % width, i / width);
buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top())
.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
// 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
// value we want... then it's actually not that hard. Since tui-rs accepts a vector as labels and will
// 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 ratatui accepts a vector as labels and will
// 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.

View File

@ -1,6 +1,9 @@
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_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> {
Text {
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 tui::{style::Style, text::Text, widgets::Row};
@ -81,9 +81,11 @@ impl 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;
let calculated_width = calculated_width.get();
// 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
// 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 tui::text::Text;
@ -128,11 +128,8 @@ impl ColumnHeader for DiskWidgetColumn {
}
impl DataToCell<DiskWidgetColumn> for DiskWidgetData {
fn to_cell(&self, column: &DiskWidgetColumn, calculated_width: u16) -> Option<Text<'_>> {
if calculated_width == 0 {
return None;
}
fn to_cell(&self, column: &DiskWidgetColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
let calculated_width = calculated_width.get();
let text = match column {
DiskWidgetColumn::Disk => truncate_to_text(&self.name, 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(),
TotalWrite => SortColumn::hard(TotalWrite, 8).default_descending(),
User => SortColumn::soft(User, Some(0.05)),
State => SortColumn::hard(State, 7),
State => SortColumn::hard(State, 9),
Time => SortColumn::new(Time),
#[cfg(feature = "gpu")]
GpuMem => SortColumn::new(GpuMem).default_descending(),

View File

@ -1,6 +1,7 @@
use std::{
cmp::{max, Ordering},
fmt::Display,
num::NonZeroU16,
time::Duration,
};
@ -299,10 +300,8 @@ impl ProcWidgetData {
}
impl DataToCell<ProcColumn> for ProcWidgetData {
fn to_cell(&self, column: &ProcColumn, calculated_width: u16) -> Option<Text<'_>> {
if calculated_width == 0 {
return None;
}
fn to_cell(&self, column: &ProcColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
let calculated_width = calculated_width.get();
// TODO: Optimize the string allocations here...
// 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;
@ -16,12 +16,8 @@ impl ColumnHeader for SortTableColumn {
}
impl DataToCell<SortTableColumn> for &'static str {
fn to_cell(&self, _column: &SortTableColumn, calculated_width: u16) -> Option<Text<'_>> {
if calculated_width == 0 {
return None;
}
Some(truncate_to_text(self, calculated_width))
fn to_cell(&self, _column: &SortTableColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
Some(truncate_to_text(self, calculated_width.get()))
}
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> {
fn to_cell(&self, _column: &SortTableColumn, calculated_width: u16) -> Option<Text<'_>> {
if calculated_width == 0 {
return None;
}
Some(truncate_to_text(self, calculated_width))
fn to_cell(&self, _column: &SortTableColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
Some(truncate_to_text(self, calculated_width.get()))
}
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 kstring::KString;
@ -55,14 +55,10 @@ impl TempWidgetData {
}
impl DataToCell<TempWidgetColumn> for TempWidgetData {
fn to_cell(&self, column: &TempWidgetColumn, calculated_width: u16) -> Option<Text<'_>> {
if calculated_width == 0 {
return None;
}
fn to_cell(&self, column: &TempWidgetColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
Some(match column {
TempWidgetColumn::Sensor => truncate_to_text(&self.sensor, calculated_width),
TempWidgetColumn::Temp => truncate_to_text(&self.temperature(), calculated_width),
TempWidgetColumn::Sensor => truncate_to_text(&self.sensor, calculated_width.get()),
TempWidgetColumn::Temp => truncate_to_text(&self.temperature(), calculated_width.get()),
})
}