mirror of
https://github.com/ClementTsang/bottom
synced 2024-11-25 13:40:20 +00:00
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:
parent
8d84b688b0
commit
b6660610d0
22 changed files with 1469 additions and 487 deletions
35
Cargo.lock
generated
35
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
10
src/app.rs
10
src/app.rs
|
@ -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,
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
///
|
///
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
215
src/canvas/components/tui_widget/time_chart/points.rs
Normal file
215
src/canvas/components/tui_widget/time_chart/points.rs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue