summaryrefslogtreecommitdiffstats
path: root/src/canvas
diff options
context:
space:
mode:
Diffstat (limited to 'src/canvas')
-rw-r--r--src/canvas/components.rs8
-rw-r--r--src/canvas/components/data_table.rs249
-rw-r--r--src/canvas/components/data_table/column.rs280
-rw-r--r--src/canvas/components/data_table/data_type.rs28
-rw-r--r--src/canvas/components/data_table/draw.rs281
-rw-r--r--src/canvas/components/data_table/props.rs21
-rw-r--r--src/canvas/components/data_table/sortable.rs544
-rw-r--r--src/canvas/components/data_table/state.rs88
-rw-r--r--src/canvas/components/data_table/styling.rs26
-rw-r--r--src/canvas/components/time_graph.rs273
-rw-r--r--src/canvas/components/tui_widget.rs4
-rw-r--r--src/canvas/components/tui_widget/pipe_gauge.rs224
-rw-r--r--src/canvas/components/tui_widget/time_chart.rs1348
-rw-r--r--src/canvas/components/tui_widget/time_chart/canvas.rs666
-rw-r--r--src/canvas/components/tui_widget/time_chart/points.rs214
-rw-r--r--src/canvas/components/widget_carousel.rs (renamed from src/canvas/widgets/basic_table_arrows.rs)8
-rw-r--r--src/canvas/dialogs/dd_dialog.rs9
-rw-r--r--src/canvas/dialogs/help_dialog.rs48
-rw-r--r--src/canvas/styling.rs (renamed from src/canvas/canvas_styling.rs)25
-rw-r--r--src/canvas/styling/colour_utils.rs (renamed from src/canvas/canvas_styling/colour_utils.rs)32
-rw-r--r--src/canvas/widgets.rs1
-rw-r--r--src/canvas/widgets/battery_display.rs22
-rw-r--r--src/canvas/widgets/cpu_basic.rs16
-rw-r--r--src/canvas/widgets/cpu_graph.rs29
-rw-r--r--src/canvas/widgets/disk_table.rs14
-rw-r--r--src/canvas/widgets/mem_basic.rs9
-rw-r--r--src/canvas/widgets/mem_graph.rs13
-rw-r--r--src/canvas/widgets/network_basic.rs5
-rw-r--r--src/canvas/widgets/network_graph.rs63
-rw-r--r--src/canvas/widgets/process_table.rs23
-rw-r--r--src/canvas/widgets/temperature_table.rs12
31 files changed, 4451 insertions, 132 deletions
diff --git a/src/canvas/components.rs b/src/canvas/components.rs
new file mode 100644
index 00000000..05a63112
--- /dev/null
+++ b/src/canvas/components.rs
@@ -0,0 +1,8 @@
+//! Lower-level components used throughout bottom.
+
+pub mod data_table;
+pub mod time_graph;
+mod tui_widget;
+pub mod widget_carousel;
+
+pub use tui_widget::*;
diff --git a/src/canvas/components/data_table.rs b/src/canvas/components/data_table.rs
new file mode 100644
index 00000000..f4e26f0c
--- /dev/null
+++ b/src/canvas/components/data_table.rs
@@ -0,0 +1,249 @@
+pub mod column;
+pub mod data_type;
+pub mod draw;
+pub mod props;
+pub mod sortable;
+pub mod state;
+pub mod styling;
+
+use std::{convert::TryInto, marker::PhantomData};
+
+pub use column::*;
+pub use data_type::*;
+pub use draw::*;
+pub use props::DataTableProps;
+pub use sortable::*;
+pub use state::{DataTableState, ScrollDirection};
+pub use styling::*;
+
+use crate::utils::general::ClampExt;
+
+/// A [`DataTable`] is a component that displays data in a tabular form.
+///
+/// Note that [`DataTable`] takes a generic type `S`, bounded by [`SortType`]. This controls whether this table
+/// expects sorted data or not, with two expected types:
+///
+/// - [`Unsortable`]: The default if otherwise not specified. This table does not expect sorted data.
+/// - [`Sortable`]: This table expects sorted data, and there are helper functions to
+/// facilitate things like sorting based on a selected column, shortcut column selection support, mouse column
+/// selection support, etc.
+pub struct DataTable<DataType, Header, S = Unsortable, C = Column<Header>> {
+ pub columns: Vec<C>,
+ pub state: DataTableState,
+ pub props: DataTableProps,
+ pub styling: DataTableStyling,
+ data: Vec<DataType>,
+ sort_type: S,
+ first_draw: bool,
+ first_index: Option<usize>,
+ _pd: PhantomData<(DataType, S, Header)>,
+}
+
+impl<DataType: DataToCell<H>, H: ColumnHeader> DataTable<DataType, H, Unsortable, Column<H>> {
+ pub fn new<C: Into<Vec<Column<H>>>>(
+ columns: C, props: DataTableProps, styling: DataTableStyling,
+ ) -> Self {
+ Self {
+ columns: columns.into(),
+ state: DataTableState::default(),
+ props,
+ styling,
+ data: vec![],
+ sort_type: Unsortable,
+ first_draw: true,
+ first_index: None,
+ _pd: PhantomData,
+ }
+ }
+}
+
+impl<DataType: DataToCell<H>, H: ColumnHeader, S: SortType, C: DataTableColumn<H>>
+ DataTable<DataType, H, S, C>
+{
+ /// Sets the default value selected on first initialization, if possible.
+ pub fn first_draw_index(mut self, first_index: usize) -> Self {
+ self.first_index = Some(first_index);
+ self
+ }
+
+ /// Sets the scroll position to the first value.
+ pub fn to_first(&mut self) {
+ self.state.current_index = 0;
+ self.state.scroll_direction = ScrollDirection::Up;
+ }
+
+ /// Sets the scroll position to the last value.
+ pub fn to_last(&mut self) {
+ self.state.current_index = self.data.len().saturating_sub(1);
+ self.state.scroll_direction = ScrollDirection::Down;
+ }
+
+ /// Updates the scroll position to be valid for the number of entries.
+ pub fn set_data(&mut self, data: Vec<DataType>) {
+ self.data = data;
+ let max_pos = self.data.len().saturating_sub(1);
+ if self.state.current_index > max_pos {
+ self.state.current_index = max_pos;
+ self.state.display_start_index = 0;
+ self.state.scroll_direction = ScrollDirection::Down;
+ }
+ }
+
+ /// Increments the scroll position if possible by a positive/negative offset. If there is a
+ /// valid change, this function will also return the new position wrapped in an [`Option`].
+ pub fn increment_position(&mut self, change: i64) -> Option<usize> {
+ let max_index = self.data.len();
+ let current_index = self.state.current_index;
+
+ if change == 0
+ || (change > 0 && current_index == max_index)
+ || (change < 0 && current_index == 0)
+ {
+ return None;
+ }
+
+ let csp: Result<i64, _> = self.state.current_index.try_into();
+ if let Ok(csp) = csp {
+ let proposed: Result<usize, _> = (csp + change).try_into();
+ if let Ok(proposed) = proposed {
+ if proposed < self.data.len() {
+ self.state.current_index = proposed;
+ self.state.scroll_direction = if change < 0 {
+ ScrollDirection::Up
+ } else {
+ ScrollDirection::Down
+ };
+
+ return Some(self.state.current_index);
+ }
+ }
+ }
+
+ None
+ }
+
+ /// Updates the scroll position to a selected index.
+ #[allow(clippy::comparison_chain)]
+ pub fn set_position(&mut self, new_index: usize) {
+ let new_index = new_index.clamp_upper(self.data.len().saturating_sub(1));
+ if self.state.current_index < new_index {
+ self.state.scroll_direction = ScrollDirection::Down;
+ } else if self.state.current_index > new_index {
+ self.state.scroll_direction = ScrollDirection::Up;
+ }
+ self.state.current_index = new_index;
+ }
+
+ /// Returns the current scroll index.
+ pub fn current_index(&self) -> usize {
+ self.state.current_index
+ }
+
+ /// Optionally returns the currently selected item, if there is one.
+ pub fn current_item(&self) -> Option<&DataType> {
+ self.data.get(self.state.current_index)
+ }
+
+ /// Returns ratatui's internal selection.
+ pub fn ratatui_selected(&self) -> Option<usize> {
+ self.state.table_state.selected()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use std::{borrow::Cow, num::NonZeroU16};
+
+ use super::*;
+
+ #[derive(Clone, PartialEq, Eq, Debug)]
+ struct TestType {
+ index: usize,
+ }
+
+ impl DataToCell<&'static str> for TestType {
+ fn to_cell(
+ &self, _column: &&'static str, _calculated_width: NonZeroU16,
+ ) -> Option<Cow<'static, str>> {
+ None
+ }
+
+ fn column_widths<C: DataTableColumn<&'static str>>(
+ _data: &[Self], _columns: &[C],
+ ) -> Vec<u16>
+ where
+ Self: Sized,
+ {
+ vec![]
+ }
+ }
+
+ #[test]
+ fn test_data_table_operations() {
+ let columns = [Column::hard("a", 10), Column::hard("b", 10)];
+ let props = DataTableProps {
+ title: Some("test".into()),
+ table_gap: 1,
+ left_to_right: false,
+ is_basic: false,
+ show_table_scroll_position: true,
+ show_current_entry_when_unfocused: false,
+ };
+ let styling = DataTableStyling::default();
+
+ let mut table = DataTable::new(columns, props, styling);
+ table.set_data((0..=4).map(|index| TestType { index }).collect::<Vec<_>>());
+
+ table.to_last();
+ assert_eq!(table.current_index(), 4);
+ assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
+
+ table.to_first();
+ assert_eq!(table.current_index(), 0);
+ assert_eq!(table.state.scroll_direction, ScrollDirection::Up);
+
+ table.set_position(4);
+ assert_eq!(table.current_index(), 4);
+ assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
+
+ table.set_position(100);
+ assert_eq!(table.current_index(), 4);
+ assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
+ assert_eq!(table.current_item(), Some(&TestType { index: 4 }));
+
+ table.increment_position(-1);
+ assert_eq!(table.current_index(), 3);
+ assert_eq!(table.state.scroll_direction, ScrollDirection::Up);
+ assert_eq!(table.current_item(), Some(&TestType { index: 3 }));
+
+ table.increment_position(-3);
+ assert_eq!(table.current_index(), 0);
+ assert_eq!(table.state.scroll_direction, ScrollDirection::Up);
+ assert_eq!(table.current_item(), Some(&TestType { index: 0 }));
+
+ table.increment_position(-3);
+ assert_eq!(table.current_index(), 0);
+ assert_eq!(table.state.scroll_direction, ScrollDirection::Up);
+ assert_eq!(table.current_item(), Some(&TestType { index: 0 }));
+
+ table.increment_position(1);
+ assert_eq!(table.current_index(), 1);
+ assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
+ assert_eq!(table.current_item(), Some(&TestType { index: 1 }));
+
+ table.increment_position(3);
+ assert_eq!(table.current_index(), 4);
+ assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
+ assert_eq!(table.current_item(), Some(&TestType { index: 4 }));
+
+ table.increment_position(10);
+ assert_eq!(table.current_index(), 4);
+ assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
+ assert_eq!(table.current_item(), Some(&TestType { index: 4 }));
+
+ table.set_data((0..=2).map(|index| TestType { index }).collect::<Vec<_>>());
+ assert_eq!(table.current_index(), 2);
+ assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
+ assert_eq!(table.current_item(), Some(&TestType { index: 2 }));
+ }
+}
diff --git a/src/canvas/components/data_table/column.rs b/src/canvas/components/data_table/column.rs
new file mode 100644
index 00000000..7bbfcbfa
--- /dev/null
+++ b/src/canvas/components/data_table/column.rs
@@ -0,0 +1,280 @@
+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 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,
+
+ /// The max width, as a percentage of the total width available. If [`None`],
+ /// then it can grow as desired.
+ max_percentage: Option<f32>,
+ },
+
+ /// A width of this type is either as long as specified, or does not appear at all.
+ Hard(u16),
+
+ /// A width of this type always resizes to the column header's text width.
+ FollowHeader,
+}
+
+pub trait ColumnHeader {
+ /// The "text" version of the column header.
+ fn text(&self) -> Cow<'static, str>;
+
+ /// The version displayed when drawing the table. Defaults to [`ColumnHeader::text`].
+ #[inline(always)]
+ fn header(&self) -> Cow<'static, str> {
+ self.text()
+ }
+}
+
+impl ColumnHeader for &'static str {
+ fn text(&self) -> Cow<'static, str> {
+ Cow::Borrowed(self)
+ }
+}
+
+impl ColumnHeader for String {
+ fn text(&self) -> Cow<'static, str> {
+ Cow::Owned(self.clone())
+ }
+}
+
+pub trait DataTableColumn<H: ColumnHeader> {
+ fn inner(&self) -> &H;
+
+ fn inner_mut(&mut self) -> &mut H;
+
+ fn bounds(&self) -> ColumnWidthBounds;
+
+ fn bounds_mut(&mut self) -> &mut ColumnWidthBounds;
+
+ fn is_hidden(&self) -> bool;
+
+ fn set_is_hidden(&mut self, is_hidden: bool);
+
+ /// The actually displayed "header".
+ fn header(&self) -> Cow<'static, str>;
+
+ /// The header length, along with any required additional lengths for things like arrows.
+ /// Defaults to getting the length of [`DataTableColumn::header`].
+ fn header_len(&self) -> usize {
+ self.header().len()
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct Column<H> {
+ /// The inner column header.
+ inner: H,
+
+ /// A restriction on this column's width.
+ bounds: ColumnWidthBounds,
+
+ /// Marks that this column is currently "hidden", and should *always* be skipped.
+ is_hidden: bool,
+}
+
+impl<H: ColumnHeader> DataTableColumn<H> for Column<H> {
+ #[inline]
+ fn inner(&self) -> &H {
+ &self.inner
+ }
+
+ #[inline]
+ fn inner_mut(&mut self) -> &mut H {
+ &mut self.inner
+ }
+
+ #[inline]
+ fn bounds(&self) -> ColumnWidthBounds {
+ self.bounds
+ }
+
+ #[inline]
+ fn bounds_mut(&mut self) -> &mut ColumnWidthBounds {
+ &mut self.bounds
+ }
+
+ #[inline]
+ fn is_hidden(&self) -> bool {
+ self.is_hidden
+ }
+
+ #[inline]
+ fn set_is_hidden(&mut self, is_hidden: bool) {
+ self.is_hidden = is_hidden;
+ }
+
+ fn header(&self) -> Cow<'static, str> {
+ self.inner.text()
+ }
+}
+
+impl<H: ColumnHeader> Column<H> {
+ pub const fn new(inner: H) -> Self {
+ Self {
+ inner,
+ bounds: ColumnWidthBounds::FollowHeader,
+ is_hidden: false,
+ }
+ }
+
+ pub const fn hard(inner: H, width: u16) -> Self {
+ Self {
+ inner,
+ bounds: ColumnWidthBounds::Hard(width),
+ is_hidden: false,
+ }
+ }
+
+ pub const fn soft(inner: H, max_percentage: Option<f32>) -> Self {
+ Self {
+ inner,
+ bounds: ColumnWidthBounds::Soft {
+ desired: 0,
+ max_percentage,
+ },
+ is_hidden: false,
+ }
+ }
+}
+
+pub trait CalculateColumnWidths<H> {
+ /// Calculates widths for the columns of this table, given the current width when called.
+ ///
+ /// * `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<NonZeroU16>;
+}
+
+impl<H, C> CalculateColumnWidths<H> for [C]
+where
+ H: ColumnHeader,
+ C: DataTableColumn<H>,
+{
+ 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![];
+ let columns = if left_to_right {
+ Either::Left(self.iter())
+ } else {
+ Either::Right(self.iter().rev())
+ };
+
+ for column in columns {
+ if column.is_hidden() {
+ continue;
+ }
+
+ match &column.bounds() {
+ ColumnWidthBounds::Soft {
+ desired,
+ max_percentage,
+ } => {
+ let min_width = column.header_len() as u16;
+ if min_width > total_width_left {
+ break;
+ }
+
+ let soft_limit = max(
+ if let Some(max_percentage) = max_percentage {
+ ((*max_percentage * f32::from(total_width)).ceil()) as u16
+ } else {
+ *desired
+ },
+ min_width,
+ );
+ let space_taken = min(min(soft_limit, *desired), total_width_left);
+
+ if stop_allocating_space(space_taken, total_width_left) {
+ break;
+ } 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 stop_allocating_space(min_width, total_width_left) {
+ break;
+ } 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 stop_allocating_space(min_width, total_width_left) {
+ break;
+ } 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 !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() {
+ if num_dist == 0 {
+ break;
+ }
+
+ 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;
+ }
+ }
+
+ calculated_widths
+ }
+}
diff --git a/src/canvas/components/data_table/data_type.rs b/src/canvas/components/data_table/data_type.rs
new file mode 100644
index 00000000..ebac3c07
--- /dev/null
+++ b/src/canvas/components/data_table/data_type.rs
@@ -0,0 +1,28 @@
+use std::{borrow::Cow, num::NonZeroU16};
+
+use tui::widgets::Row;
+
+use super::{ColumnHeader, DataTableColumn};
+use crate::canvas::Painter;
+
+pub trait DataToCell<H>
+where
+ H: ColumnHeader,
+{
+ /// Given data, a column, and its corresponding width, return the string in the cell that will
+ /// be displayed in the [`DataTable`](super::DataTable).
+ fn to_cell(&self, column: &H, calculated_width: NonZeroU16) -> Option<Cow<'static, str>>;
+
+ /// Apply styling to the generated [`Row`] of cells.
+ ///
+ /// The default implementation just returns the `row` that is passed in.
+ #[inline(always)]
+ fn style_row<'a>(&self, row: Row<'a>, _painter: &Painter) -> Row<'a> {
+ row
+ }
+
+ /// Returns the desired column widths in light of having seen data.
+ fn column_widths<C: DataTableColumn<H>>(data: &[Self], columns: &[C]) -> Vec<u16>
+ where
+ Self: Sized;
+}
diff --git a/src/canvas/components/data_table/draw.rs b/src/canvas/components/data_table/draw.rs
new file mode 100644
index 00000000..46304f83
--- /dev/null
+++ b/src/canvas/components/data_table/draw.rs
@@ -0,0 +1,281 @@
+use std::{
+ cmp::{max, min},
+ iter::once,
+};
+
+use concat_string::concat_string;
+use tui::{
+ layout::{Constraint, Direction, Layout, Rect},
+ text::{Line, Span, Text},
+ widgets::{Block, Borders, Row, Table},
+ Frame,
+};
+use unicode_segmentation::UnicodeSegmentation;
+
+use super::{
+ CalculateColumnWidths, ColumnHeader, ColumnWidthBounds, DataTable, DataTableColumn, DataToCell,
+ SortType,
+};
+use crate::{
+ app::layout_manager::BottomWidget,
+ canvas::Painter,
+ constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT},
+ utils::strings::truncate_to_text,
+};
+
+pub enum SelectionState {
+ NotSelected,
+ Selected,
+ Expanded,
+}
+
+impl SelectionState {
+ pub fn new(is_expanded: bool, is_on_widget: bool) -> Self {
+ if is_expanded {
+ SelectionState::Expanded
+ } else if is_on_widget {
+ SelectionState::Selected
+ } else {
+ SelectionState::NotSelected
+ }
+ }
+}
+
+/// A [`DrawInfo`] is information required on each draw call.
+pub struct DrawInfo {
+ pub loc: Rect,
+ pub force_redraw: bool,
+ pub recalculate_column_widths: bool,
+ pub selection_state: SelectionState,
+}
+
+impl DrawInfo {
+ pub fn is_on_widget(&self) -> bool {
+ matches!(self.selection_state, SelectionState::Selected)
+ || matches!(self.selection_state, SelectionState::Expanded)
+ }
+
+ pub fn is_expanded(&self) -> bool {
+ matches!(self.selection_state, SelectionState::Expanded)
+ }
+}
+
+impl<DataType, H, S, C> DataTable<DataType, H, S, C>
+where
+ DataType: DataToCell<H>,
+ H: ColumnHeader,
+ S: SortType,
+ C: DataTableColumn<H>,
+{
+ fn block<'a>(&self, draw_info: &'a DrawInfo, data_len: usize) -> Block<'a> {
+ let border_style = match draw_info.selection_state {
+ SelectionState::NotSelected => self.styling.border_style,
+ SelectionState::Selected | SelectionState::Expanded => {
+ self.styling.highlighted_border_style
+ }
+ };
+
+ if !self.props.is_basic {
+ let block = Block::default()
+ .borders(Borders::ALL)
+ .border_style(border_style);
+
+ if let Some(title) = self.generate_title(draw_info, data_len) {
+ block.title(title)
+ } else {
+ block
+ }
+ } else if draw_info.is_on_widget() {
+ // Implies it is basic mode but selected.
+ Block::default()
+ .borders(SIDE_BORDERS)
+ .border_style(border_style)
+ } else {
+ Block::default().borders(Borders::NONE)
+ }
+ }
+
+ /// Generates a title, given the available space.
+ pub fn generate_title<'a>(
+ &self, draw_info: &'a DrawInfo, total_items: usize,
+ ) -> Option<Line<'a>> {
+ self.props.title.as_ref().map(|title| {
+ let current_index = self.state.current_index.saturating_add(1);
+ let draw_loc = draw_info.loc;
+ let title_style = self.styling.title_style;
+ let border_style = if draw_info.is_on_widget() {
+ self.styling.highlighted_border_style
+ } else {
+ self.styling.border_style
+ };
+
+ let title = if self.props.show_table_scroll_position {
+ let pos = current_index.to_string();
+ let tot = total_items.to_string();
+ let title_string = concat_string!(title, "(", pos, " of ", tot, ") ");
+
+ if title_string.len() + 2 <= draw_loc.width.into() {
+ title_string
+ } else {
+ title.to_string()
+ }
+ } else {
+ title.to_string()
+ };
+
+ if draw_info.is_expanded() {
+ let title_base = concat_string!(title, "── Esc to go back ");
+ let lines = "─".repeat(usize::from(draw_loc.width).saturating_sub(
+ UnicodeSegmentation::graphemes(title_base.as_str(), true).count() + 2,
+ ));
+ let esc = concat_string!("─", lines, "─ Esc to go back ");
+ Line::from(vec![
+ Span::styled(title, title_style),
+ Span::styled(esc, border_style),
+ ])
+ } else {
+ Line::from(Span::styled(title, title_style))
+ }
+ })
+ }
+
+ pub fn draw(
+ &mut self, f: &mut Frame<'_>, draw_info: &DrawInfo, widget: Option<&mut BottomWidget>,
+ painter: &Painter,
+ ) {
+ let draw_horizontal = !self.props.is_basic || draw_info.is_on_widget();
+ let draw_loc = draw_info.loc;
+ let margined_draw_loc = Layout::default()
+ .constraints([Constraint::Percentage(100)])
+ .horizontal_margin(u16::from(!draw_horizontal))
+ .direction(Direction::Horizontal)
+ .split(draw_loc)[0];
+
+ let block = self.block(draw_info, self.data.len());
+
+ let (inner_width, inner_height) = {
+ let inner_rect = block.inner(margined_draw_loc);
+ self.state.inner_rect = inner_rect;
+ (inner_rect.width, inner_rect.height)
+ };
+
+ if inner_width == 0 || inner_height == 0 {
+ f.render_widget(block, margined_draw_loc);
+ } else {
+ // Calculate widths
+ if draw_info.recalculate_column_widths {
+ let col_widths = DataType::column_widths(&self.data, &self.columns);
+
+ self.columns
+ .iter_mut()
+ .zip(&col_widths)
+ .for_each(|(column, &width)| {
+ let header_len = column.header_len() as u16;
+ if let ColumnWidthBounds::Soft {
+ desired,
+ max_percentage: _,
+ } = &mut column.bounds_mut()
+ {
+ *desired = max(header_len, width);
+ }
+ });
+
+ self.state.calculated_widths = self
+ .columns
+ .calculate_column_widths(inner_width, self.props.left_to_right);
+
+ // Update draw loc in widget map
+ if let Some(widget) = widget {
+ widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
+ widget.bottom_right_corner =
+ Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
+ }
+ }
+
+ let show_header = inner_height > 1;
+ let header_height = u16::from(show_header);
+ let table_gap = if !show_header || draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
+ 0
+ } else {
+ self.props.table_gap
+ };
+
+ if !self.data.is_empty() || !self.first_draw {
+ if self.first_draw {
+