summaryrefslogtreecommitdiffstats
path: root/src/components/text_table/state.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/text_table/state.rs')
-rw-r--r--src/components/text_table/state.rs678
1 files changed, 0 insertions, 678 deletions
diff --git a/src/components/text_table/state.rs b/src/components/text_table/state.rs
deleted file mode 100644
index ef3163ac..00000000
--- a/src/components/text_table/state.rs
+++ /dev/null
@@ -1,678 +0,0 @@
-use std::{borrow::Cow, convert::TryInto, ops::Range};
-
-use itertools::Itertools;
-use tui::{layout::Rect, widgets::TableState};
-
-use crate::app::ScrollDirection;
-
-/// A bound on the width of a column.
-#[derive(Clone, Copy, Debug)]
-pub enum WidthBounds {
- /// A width of this type is either as long as `min`, but can otherwise shrink and grow up to a point.
- Soft {
- /// The minimum amount before giving up and hiding.
- min_width: u16,
-
- /// 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),
-
- /// Always uses the width of the [`CellContent`].
- CellWidth,
-}
-
-impl WidthBounds {
- pub const fn soft_from_str(name: &'static str, max_percentage: Option<f32>) -> WidthBounds {
- let len = name.len() as u16;
- WidthBounds::Soft {
- min_width: len,
- desired: len,
- max_percentage,
- }
- }
-
- pub const fn soft_from_str_with_alt(
- name: &'static str, alt: &'static str, max_percentage: Option<f32>,
- ) -> WidthBounds {
- WidthBounds::Soft {
- min_width: alt.len() as u16,
- desired: name.len() as u16,
- max_percentage,
- }
- }
-}
-
-/// A [`CellContent`] contains text information for display in a table.
-#[derive(Clone, Debug)]
-pub enum CellContent {
- Simple(Cow<'static, str>),
- HasAlt {
- alt: Cow<'static, str>,
- main: Cow<'static, str>,
- },
-}
-
-impl CellContent {
- /// Creates a new [`CellContent`].
- pub fn new<I>(name: I, alt: Option<I>) -> Self
- where
- I: Into<Cow<'static, str>>,
- {
- if let Some(alt) = alt {
- CellContent::HasAlt {
- alt: alt.into(),
- main: name.into(),
- }
- } else {
- CellContent::Simple(name.into())
- }
- }
-
- /// Returns the length of the [`CellContent`]. Note that for a [`CellContent::HasAlt`], it will return
- /// the length of the "main" field.
- pub fn len(&self) -> usize {
- match self {
- CellContent::Simple(s) => s.len(),
- CellContent::HasAlt { alt: _, main: long } => long.len(),
- }
- }
-
- /// Whether the [`CellContent`]'s text is empty.
- pub fn is_empty(&self) -> bool {
- self.len() == 0
- }
-
- pub fn main_text(&self) -> &Cow<'static, str> {
- match self {
- CellContent::Simple(main) => main,
- CellContent::HasAlt { alt: _, main } => main,
- }
- }
-}
-
-pub trait TableComponentHeader {
- fn header_text(&self) -> &CellContent;
-}
-
-impl TableComponentHeader for CellContent {
- fn header_text(&self) -> &CellContent {
- self
- }
-}
-impl From<Cow<'static, str>> for CellContent {
- fn from(c: Cow<'static, str>) -> Self {
- CellContent::Simple(c)
- }
-}
-
-impl From<&'static str> for CellContent {
- fn from(s: &'static str) -> Self {
- CellContent::Simple(s.into())
- }
-}
-
-impl From<String> for CellContent {
- fn from(s: String) -> Self {
- CellContent::Simple(s.into())
- }
-}
-
-pub struct TableComponentColumn<H: TableComponentHeader> {
- /// The header of the column.
- pub header: H,
-
- /// A restriction on this column's width, if desired.
- pub width_bounds: WidthBounds,
-
- /// The calculated width of the column.
- pub calculated_width: u16,
-
- /// Marks that this column is currently "hidden", and should *always* be skipped.
- pub is_hidden: bool,
-}
-
-impl<H: TableComponentHeader> TableComponentColumn<H> {
- pub fn new_custom(header: H, width_bounds: WidthBounds) -> Self {
- Self {
- header,
- width_bounds,
- calculated_width: 0,
- is_hidden: false,
- }
- }
-
- pub fn new(header: H) -> Self {
- Self {
- header,
- width_bounds: WidthBounds::CellWidth,
- calculated_width: 0,
- is_hidden: false,
- }
- }
-
- pub fn new_hard(header: H, width: u16) -> Self {
- Self {
- header,
- width_bounds: WidthBounds::Hard(width),
- calculated_width: 0,
- is_hidden: false,
- }
- }
-
- pub fn new_soft(header: H, max_percentage: Option<f32>) -> Self {
- let min_width = header.header_text().len() as u16;
- Self {
- header,
- width_bounds: WidthBounds::Soft {
- min_width,
- desired: min_width,
- max_percentage,
- },
- calculated_width: 0,
- is_hidden: false,
- }
- }
-
- pub fn is_zero_width(&self) -> bool {
- self.calculated_width == 0
- }
-
- pub fn is_skipped(&self) -> bool {
- self.is_zero_width() || self.is_hidden
- }
-}
-
-#[derive(Debug, Clone, Copy, Eq, PartialEq)]
-pub enum SortOrder {
- Ascending,
- Descending,
-}
-
-impl SortOrder {
- pub fn is_descending(&self) -> bool {
- matches!(self, SortOrder::Descending)
- }
-}
-
-/// Represents the current table's sorting state.
-#[derive(Debug)]
-pub enum SortState {
- Unsortable,
- Sortable(SortableState),
-}
-
-#[derive(Debug)]
-pub struct SortableState {
- /// The "x locations" of the headers.
- visual_mappings: Vec<Range<u16>>,
-
- /// The "y location" of the header row. Since all headers share the same y-location we just set it once here.
- y_loc: u16,
-
- /// This is a bit of a lazy hack to handle this for now - ideally the entire [`SortableState`]
- /// is instead handled by a separate table struct that also can access the columns and their default sort orderings.
- default_sort_orderings: Vec<SortOrder>,
-
- /// The currently selected sort index.
- pub current_index: usize,
-
- /// The current sorting order.
- pub order: SortOrder,
-}
-
-impl SortableState {
- /// Creates a new [`SortableState`].
- pub fn new(
- default_index: usize, default_order: SortOrder, default_sort_orderings: Vec<SortOrder>,
- ) -> Self {
- Self {
- visual_mappings: Default::default(),
- y_loc: 0,
- default_sort_orderings,
- current_index: default_index,
- order: default_order,
- }
- }
-
- /// Toggles the current sort order.
- pub fn toggle_order(&mut self) {
- self.order = match self.order {
- SortOrder::Ascending => SortOrder::Descending,
- SortOrder::Descending => SortOrder::Ascending,
- }
- }
-
- /// Updates the visual index.
- ///
- /// This function will create a *sorted* range list - in debug mode,
- /// the program will assert this, but it will not do so in release mode!
- pub fn update_visual_index(&mut self, draw_loc: Rect, row_widths: &[u16]) {
- let mut start = draw_loc.x;
- let visual_index = row_widths
- .iter()
- .map(|width| {
- let range_start = start;
- let range_end = start + width + 1; // +1 for the gap b/w cols.
- start = range_end;
- range_start..range_end
- })
- .collect_vec();
-
- debug_assert!(visual_index.iter().all(|a| { a.start <= a.end }));
-
- debug_assert!(visual_index
- .iter()
- .tuple_windows()
- .all(|(a, b)| { b.start >= a.end }));
-
- self.visual_mappings = visual_index;
- self.y_loc = draw_loc.y;
- }
-
- /// Given some `x` and `y`, if possible, select the corresponding column or toggle the column if already selected,
- /// and otherwise do nothing.
- ///
- /// If there was some update, the corresponding column type will be returned. If nothing happens, [`None`] is
- /// returned.
- pub fn try_select_location(&mut self, x: u16, y: u16) -> Option<usize> {
- if self.y_loc == y {
- if let Some(index) = self.get_range(x) {
- self.update_sort_index(index);
- Some(self.current_index)
- } else {
- None
- }
- } else {
- None
- }
- }
-
- /// Updates the sort index, and sets the sort order as appropriate.
- ///
- /// If the index is different from the previous one, it will move to the new index and set the sort order
- /// to the prescribed default sort order.
- ///
- /// If the index is the same as the previous one, it will simply toggle the current sort order.
- pub fn update_sort_index(&mut self, index: usize) {
- if self.current_index == index {
- self.toggle_order();
- } else {
- self.current_index = index;
- self.order = self.default_sort_orderings[index];
- }
- }
-
- /// Given a `needle` coordinate, select the corresponding index and value.
- fn get_range(&self, needle: u16) -> Option<usize> {
- match self
- .visual_mappings
- .binary_search_by_key(&needle, |range| range.start)
- {
- Ok(index) => Some(index),
- Err(index) => index.checked_sub(1),
- }
- .and_then(|index| {
- if needle < self.visual_mappings[index].end {
- Some(index)
- } else {
- None
- }
- })
- }
-}
-
-/// [`TableComponentState`] deals with fields for a scrollable's current state.
-pub struct TableComponentState<H: TableComponentHeader = CellContent> {
- pub current_scroll_position: usize,
- pub scroll_bar: usize,
- pub scroll_direction: ScrollDirection,
- pub table_state: TableState,
- pub columns: Vec<TableComponentColumn<H>>,
- pub sort_state: SortState,
-}
-
-impl<H: TableComponentHeader> TableComponentState<H> {
- pub fn new(columns: Vec<TableComponentColumn<H>>) -> Self {
- Self {
- current_scroll_position: 0,
- scroll_bar: 0,
- scroll_direction: ScrollDirection::Down,
- table_state: Default::default(),
- columns,
- sort_state: SortState::Unsortable,
- }
- }
-
- pub fn sort_state(mut self, sort_state: SortState) -> Self {
- self.sort_state = sort_state;
- self
- }
-
- /// Calculates widths for the columns for this table.
- ///
- /// * `total_width` is the, well, total width available.
- /// * `left_to_right` is a boolean whether to go from left to right if true, or right to left if
- /// false.
- ///
- /// **NOTE:** Trailing 0's may break tui-rs, remember to filter them out later!
- pub fn calculate_column_widths(&mut self, total_width: u16, left_to_right: bool) {
- use itertools::Either;
- use std::cmp::{max, min};
-
- let mut total_width_left = total_width;
-
- let columns = if left_to_right {
- Either::Left(self.columns.iter_mut())
- } else {
- Either::Right(self.columns.iter_mut().rev())
- };
-
- let arrow_offset = match self.sort_state {
- SortState::Unsortable => 0,
- SortState::Sortable { .. } => 1,
- };
-
- let mut num_columns = 0;
- let mut skip_iter = false;
- for column in columns {
- column.calculated_width = 0;
-
- if column.is_hidden || skip_iter {
- continue;
- }
-
- match &column.width_bounds {
- WidthBounds::Soft {
- min_width,
- desired,
- max_percentage,
- } => {
- let min_width = *min_width + arrow_offset;
- if min_width > total_width_left {
- skip_iter = true;
- continue;
- }
-
- let soft_limit = max(
- if let Some(max_percentage) = max_percentage {
- // TODO: Rust doesn't have an `into()` or `try_into()` for floats to integers.
- ((*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 min_width > space_taken || min_width == 0 {
- skip_iter = true;
- } else if space_taken > 0 {
- total_width_left = total_width_left.saturating_sub(space_taken + 1);
- column.calculated_width = space_taken;
- num_columns += 1;
- }
- }
- WidthBounds::CellWidth => {
- let width = column.header.header_text().len() as u16;
- let min_width = width + arrow_offset;
-
- if min_width > total_width_left || min_width == 0 {
- skip_iter = true;
- } else if min_width > 0 {
- total_width_left = total_width_left.saturating_sub(min_width + 1);
- column.calculated_width = min_width;
- num_columns += 1;
- }
- }
- WidthBounds::Hard(width) => {
- let min_width = *width + arrow_offset;
-
- if min_width > total_width_left || min_width == 0 {
- skip_iter = true;
- } else if min_width > 0 {
- total_width_left = total_width_left.saturating_sub(min_width + 1);
- column.calculated_width = min_width;
- num_columns += 1;
- }
- }
- }
- }
-
- if num_columns > 0 {
- // Redistribute remaining.
- let mut num_dist = num_columns;
- let amount_per_slot = total_width_left / num_dist;
- total_width_left %= num_dist;
-
- for column in self.columns.iter_mut() {
- if num_dist == 0 {
- break;
- }
-
- if column.calculated_width > 0 {
- if total_width_left > 0 {
- column.calculated_width += amount_per_slot + 1;
- total_width_left -= 1;
- } else {
- column.calculated_width += amount_per_slot;
- }
-
- num_dist -= 1;
- }
- }
- }
- }
-
- /// Updates the position if possible, and if there is a valid change, returns the new position.
- pub fn update_position(&mut self, change: i64, num_entries: usize) -> Option<usize> {
- let min_index = 0;
- let max_index = num_entries.saturating_sub(1);
-
- if change == 0
- || (change > 0 && self.current_scroll_position == max_index)
- || (change < 0 && self.current_scroll_position == min_index)
- {
- return None;
- }
-
- let csp: Result<i64, _> = self.current_scroll_position.try_into();
- if let Ok(csp) = csp {
- self.current_scroll_position =
- (csp + change).clamp(min_index as i64, max_index as i64) as usize;
-
- if change < 0 {
- self.scroll_direction = ScrollDirection::Up;
- } else {
- self.scroll_direction = ScrollDirection::Down;
- }
-
- Some(self.current_scroll_position)
- } else {
- None
- }
- }
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
-
- #[test]
- fn test_scroll_update_position() {
- #[track_caller]
- fn check_scroll_update(
- scroll: &mut TableComponentState, change: i64, max: usize, ret: Option<usize>,
- new_position: usize,
- ) {
- assert_eq!(scroll.update_position(change, max), ret);
- assert_eq!(scroll.current_scroll_position, new_position);
- }
-
- let mut scroll = TableComponentState {
- current_scroll_position: 5,
- scroll_bar: 0,
- scroll_direction: ScrollDirection::Down,
- table_state: Default::default(),
- columns: vec![],
- sort_state: SortState::Unsortable,
- };
- let s = &mut scroll;
-
- // Update by 0. Should not change.
- check_scroll_update(s, 0, 15, None, 5);
-
- // Update by 5. Should increment to index 10.
- check_scroll_update(s, 5, 15, Some(10), 10);
-
- // Update by 5. Should clamp to max possible scroll index 14.
- check_scroll_update(s, 5, 15, Some(14), 14);
-
- // Update by 1. Should do nothing (already at max index 14).
- check_scroll_update(s, 1, 15, None, 14);
-
- // Update by -15. Should clamp to index 0.
- check_scroll_update(s, -15, 15, Some(0), 0);
-
- // Update by -1. Should do nothing (already at min index 0).
- check_scroll_update(s, -15, 15, None, 0);
-
- // Update by 0. Should do nothing.
- check_scroll_update(s, 0, 15, None, 0);
-
- // Update by 15. Should clamp to 14.
- check_scroll_update(s, 15, 15, Some(14), 14);
-
- // Update by 15 but with a larger bound. Should clamp to 15.
- check_scroll_update(s, 15, 16, Some(15), 15);
- }
-
- #[test]
- fn test_table_width_calculation() {
- #[track_caller]
- fn test_calculation(state: &mut TableComponentState, width: u16, expected: Vec<u16>) {
- state.calculate_column_widths(width, true);
- assert_eq!(
- state
- .columns
- .iter()
- .filter_map(|c| if c.calculated_width == 0 {
- None
- } else {
- Some(c.calculated_width)
- })
- .collect::<Vec<_>>(),
- expected
- )
- }
-
- let mut state = TableComponentState::new(vec![
- TableComponentColumn::new(CellContent::from("a")),
- TableComponentColumn::new_custom(
- "a".into(),
- WidthBounds::Soft {
- min_width: 1,
- desired: 10,
- max_percentage: Some(0.125),
- },
- ),
- TableComponentColumn::new_custom(
- "a".into(),
- WidthBounds::Soft {
- min_width: 2,
- desired: 10,
- max_percentage: Some(0.5),
- },
- ),
- ]);
-
- test_calculation(&mut state, 0, vec![]);
- test_calculation(&mut state, 1, vec![1]);
- test_calculation(&mut state, 2, vec![1]);
- test_calculation(&mut state, 3, vec![1, 1]);
- test_calculation(&mut state, 4, vec![1, 1]);
- test_calculation(&mut state, 5, vec![2, 1]);
- test_calculation(&mut state, 6, vec![1, 1, 2]);
- test_calculation(&mut state, 7, vec![1, 1, 3]);
- test_calculation(&mut state, 8, vec![1, 1, 4]);
- test_calculation(&mut state, 14, vec![2, 2, 7]);
- test_calculation(&mut state, 20, vec![2, 4, 11]);
- test_calculation(&mut state, 100, vec![27, 35, 35]);
-
- state.sort_state = SortState::Sortable(SortableState::new(1, SortOrder::Ascending, vec![]));
-
- test_calculation(&mut state, 0, vec![]);
- test_calculation(&mut state, 1, vec![]);
- test_calculation(&mut state, 2, vec![2]);
- test_calculation(&mut state, 3, vec![2]);
- test_calculation(&mut state, 4, vec![3]);
- test_calculation(&mut state, 5, vec![2, 2]);
- test_calculation(&mut state, 6, vec![2, 2]);
- test_calculation(&mut state, 7, vec![3, 2]);
- test_calculation(&mut state, 8, vec![3, 3]);
- test_calculation(&mut state, 14, vec![2, 2, 7]);
- test_calculation(&mut state, 20, vec![3, 4, 10]);
- test_calculation(&mut state, 100, vec![27, 35, 35]);
- }
-
- #[test]
- fn test_visual_index_selection() {
- let mut state = SortableState::new(
- 0,
- SortOrder::Ascending,
- vec![SortOrder::Ascending, SortOrder::Descending],
- );
-
- const X_OFFSET: u16 = 10;
- const Y_OFFSET: u16 = 15;
- state.update_visual_index(Rect::new(X_OFFSET, Y_OFFSET, 20, 15), &[4, 14]);
-
- #[track_caller]
- fn test_selection(
- state: &mut SortableState, from_x_offset: u16, from_y_offset: u16,
- result: (Option<usize>, SortOrder),
- ) {
- assert_eq!(
- state.try_select_location(X_OFFSET + from_x_offset, Y_OFFSET + from_y_offset),
- result.0
- );
- assert_eq!(state.order, result.1);
- }
-
- use SortOrder::*;
-
- // Clicking on these don't do anything, so don't show any change.
- test_selection(&mut state, 5, 1, (None, Ascending));
- test_selection(&mut state, 21, 0, (None, Ascending));
-
- // Clicking on the first column should toggle it as it is already selected.
- test_selection(&mut state, 3, 0, (Some(0), Descending));
-
- // Clicking on the first column should toggle it again as it is already selected.
- test_selection(&mut state, 4, 0, (Some(0), Ascending));
-
- // Clicking on second column should select and switch to the descending ordering as that is its default.
- test_selection(&mut state, 5, 0, (Some(1), Descending));
-
- // Clicking on second column should toggle it.
- test_selection(&mut state, 19, 0, (Some(1), Ascending));
-
- // Overshoot, should not do anything.
- test_selection(&mut state, 20, 0, (None, Ascending));
-
- // Further overshoot, should not do anything.
- test_selection(&mut state, 25, 0, (None, Ascending));
-
- // Go back to first column, should be ascending to match default for index 0.
- test_selection(&mut state, 3, 0, (Some(0), Ascending));
-
- // Click on first column should then go to descending as it is already selected and ascending.
- test_selection(&mut state, 3, 0, (Some(0), Descending));
- }
-}