From ef4150725476942784f1ad4cacfb21c3bed17c54 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Sat, 18 Dec 2021 14:25:08 -0800 Subject: [PATCH] fix(console): wrap controls line when the terminal is too narrow (#231) Currently, the controls line for table views is never wrapped, even if it's too long to fit on a single line in the current terminal width. This isn't great...especially since PR #223 is adding more controls, and will make the line even longer. This branch allows wrapping the controls paragraph if it doesn't fit in the current area. The code for this was surprisingly complex: while `tui` supports wrapping text, layout area sizes are determined _before_ text wrapping. So, we can't just say "use as many lines as are necessary to fit this text in the current terminal width", we have to implement that logic ourselves, and pass the required number of lines to the `Layout::constraints` method. ## Screenshots On the main branch, we don't wrap. Here's a wide enough terminal: ![image](https://user-images.githubusercontent.com/2796466/146655512-59ee64b1-eeb6-418d-b0d0-e84e5b80fb87.png) ...and here's one that's too narrow: ![image](https://user-images.githubusercontent.com/2796466/146655518-760387ab-b1c4-45f2-8f1a-eaf59a16067a.png) After this change, when the terminal is wide enough, it looks the same: ![image](https://user-images.githubusercontent.com/2796466/146655537-2b1f7f16-0b1f-4a43-a6bb-d40b25569eef.png) ...but if the terminal is too narrow, it wraps: ![image](https://user-images.githubusercontent.com/2796466/146655545-130241ab-e483-40da-ba51-4a5dcf513cc5.png) It might be nice to change this code to only wrap on commas, so each ` = ` isn't broken...but that seems like a lot more work than just wrapping on whitespace, and the current thing is better than it was before! --- console/src/view/async_ops.rs | 9 ++--- console/src/view/resources.rs | 10 +++--- console/src/view/table.rs | 65 ++++++++++++++++++++++++++--------- console/src/view/tasks.rs | 18 +++++----- 4 files changed, 69 insertions(+), 33 deletions(-) diff --git a/console/src/view/async_ops.rs b/console/src/view/async_ops.rs index 3dce3b53c..6b18e5575 100644 --- a/console/src/view/async_ops.rs +++ b/console/src/view/async_ops.rs @@ -14,7 +14,7 @@ use tui::{ layout, style::{self, Color, Style}, text::Spans, - widgets::{Cell, Paragraph, Row, Table}, + widgets::{Cell, Row, Table}, }; #[derive(Debug, Default)] @@ -189,11 +189,12 @@ impl TableList for AsyncOpsTable { .direction(layout::Direction::Vertical) .margin(0); + let controls = table::Controls::for_area(&area, styles); let chunks = layout .constraints( [ - layout::Constraint::Length(1), - layout::Constraint::Min(area.height - 1), + layout::Constraint::Length(controls.height), + layout::Constraint::Max(area.height), ] .as_ref(), ) @@ -223,7 +224,7 @@ impl TableList for AsyncOpsTable { .highlight_style(Style::default().add_modifier(style::Modifier::BOLD)); frame.render_stateful_widget(table, async_ops_area, &mut table_list_state.table_state); - frame.render_widget(Paragraph::new(table::controls(styles)), controls_area); + frame.render_widget(controls.paragraph, controls_area); table_list_state .sorted_items diff --git a/console/src/view/resources.rs b/console/src/view/resources.rs index fde61cf25..670b26d08 100644 --- a/console/src/view/resources.rs +++ b/console/src/view/resources.rs @@ -14,7 +14,7 @@ use tui::{ layout, style::{self, Color, Style}, text::Spans, - widgets::{Cell, Paragraph, Row, Table}, + widgets::{Cell, Row, Table}, }; #[derive(Debug, Default)] @@ -152,6 +152,8 @@ impl TableList for ResourcesTable { table_list_state.len() ))]); + let controls = table::Controls::for_area(&area, styles); + let layout = layout::Layout::default() .direction(layout::Direction::Vertical) .margin(0); @@ -159,8 +161,8 @@ impl TableList for ResourcesTable { let chunks = layout .constraints( [ - layout::Constraint::Length(1), - layout::Constraint::Min(area.height - 1), + layout::Constraint::Length(controls.height), + layout::Constraint::Max(area.height), ] .as_ref(), ) @@ -189,7 +191,7 @@ impl TableList for ResourcesTable { .highlight_style(Style::default().add_modifier(style::Modifier::BOLD)); frame.render_stateful_widget(table, tasks_area, &mut table_list_state.table_state); - frame.render_widget(Paragraph::new(table::controls(styles)), controls_area); + frame.render_widget(controls.paragraph, controls_area); table_list_state .sorted_items diff --git a/console/src/view/table.rs b/console/src/view/table.rs index 157d05369..8b6d177b1 100644 --- a/console/src/view/table.rs +++ b/console/src/view/table.rs @@ -6,7 +6,7 @@ use std::convert::TryFrom; use tui::{ layout, text::{self, Span, Spans, Text}, - widgets::TableState, + widgets::{Paragraph, TableState, Wrap}, }; use std::cell::RefCell; @@ -42,6 +42,11 @@ pub(crate) struct TableListState { pub(crate) table_state: TableState, } +pub(crate) struct Controls { + pub(crate) paragraph: Paragraph<'static>, + pub(crate) height: u16, +} + impl TableListState { pub(in crate::view) fn len(&self) -> usize { self.sorted_items.len() @@ -159,22 +164,6 @@ impl TableListState { } } -pub(in crate::view) fn controls(styles: &view::Styles) -> Text { - tui::text::Text::from(Spans::from(vec![ - Span::raw("controls: "), - bold(styles.if_utf8("\u{2190}\u{2192}", "left, right")), - text::Span::raw(" = select column (sort), "), - bold(styles.if_utf8("\u{2191}\u{2193}", "up, down")), - text::Span::raw(" = scroll, "), - bold(styles.if_utf8("\u{21B5}", "enter")), - text::Span::raw(" = view details, "), - bold("i"), - text::Span::raw(" = invert sort (highest/lowest), "), - bold("q"), - text::Span::raw(" = quit"), - ])) -} - impl Default for TableListState where T: TableList, @@ -192,3 +181,45 @@ where } } } + +impl Controls { + pub(in crate::view) fn for_area(area: &layout::Rect, styles: &view::Styles) -> Self { + let text = Text::from(Spans::from(vec![ + Span::raw("controls: "), + bold(styles.if_utf8("\u{2190}\u{2192}", "left, right")), + text::Span::raw(" = select column (sort), "), + bold(styles.if_utf8("\u{2191}\u{2193}", "up, down")), + text::Span::raw(" = scroll, "), + bold(styles.if_utf8("\u{21B5}", "enter")), + text::Span::raw(" = view details, "), + bold("i"), + text::Span::raw(" = invert sort (highest/lowest), "), + bold("q"), + text::Span::raw(" = quit"), + ])); + + // how many lines do we need to display the controls? + let mut height = 1; + + // if the area is narrower than the width of the controls text, we need + // to wrap the text across multiple lines. + let width = text.width() as u16; + if area.width < width { + height = width / area.width; + + // if the text's width is not neatly divisible by the area's width + // (and it almost never will be), round up for the remaining text. + if width % area.width > 0 { + height += 1 + }; + } + + Self { + // TODO(eliza): it would be nice if we could wrap this on commas, + // specifically, rather than whitespace...but that seems like a + // bunch of additional work... + paragraph: Paragraph::new(text).wrap(Wrap { trim: true }), + height, + } + } +} diff --git a/console/src/view/tasks.rs b/console/src/view/tasks.rs index efe21adcf..909a213eb 100644 --- a/console/src/view/tasks.rs +++ b/console/src/view/tasks.rs @@ -13,7 +13,7 @@ use tui::{ layout, style::{self, Color, Style}, text::{Span, Spans, Text}, - widgets::{self, Cell, ListItem, Paragraph, Row, Table}, + widgets::{self, Cell, ListItem, Row, Table}, }; #[derive(Debug, Default)] @@ -200,31 +200,33 @@ impl TableList for TasksTable { .direction(layout::Direction::Vertical) .margin(0); + let controls = table::Controls::for_area(&area, styles); + let (controls_area, tasks_area, warnings_area) = if warnings.is_empty() { let chunks = layout .constraints( [ - layout::Constraint::Length(1), - layout::Constraint::Min(area.height - 1), + layout::Constraint::Length(controls.height), + layout::Constraint::Max(area.height), ] .as_ref(), ) .split(area); (chunks[0], chunks[1], None) } else { + let warnings_height = warnings.len() as u16 + 2; let chunks = layout .constraints( [ - layout::Constraint::Length(1), - layout::Constraint::Length(warnings.len() as u16 + 2), - layout::Constraint::Min(area.height - 1), + layout::Constraint::Length(controls.height), + layout::Constraint::Length(warnings_height), + layout::Constraint::Max(area.height), ] .as_ref(), ) .split(area); (chunks[0], chunks[2], Some(chunks[1])) }; - // Fill all remaining characters in the frame with the task's fields. // // Ideally we'd use Min(0), and it would fill the rest of the space. But that is broken @@ -254,7 +256,7 @@ impl TableList for TasksTable { .highlight_style(Style::default().add_modifier(style::Modifier::BOLD)); frame.render_stateful_widget(table, tasks_area, &mut table_list_state.table_state); - frame.render_widget(Paragraph::new(table::controls(styles)), controls_area); + frame.render_widget(controls.paragraph, controls_area); if let Some(area) = warnings_area { let block = styles