Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refac(console): generalize controls widget #427

Merged
merged 8 commits into from
Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 3 additions & 21 deletions tokio-console/src/view/async_ops.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub(crate) use crate::view::table::view_controls;
use crate::{
state::{
async_ops::{AsyncOp, SortBy},
Expand All @@ -6,7 +7,7 @@ use crate::{
},
view::{
self, bold,
table::{self, TableList, TableListState},
table::{TableList, TableListState},
DUR_LEN, DUR_TABLE_PRECISION,
},
};
Expand Down Expand Up @@ -193,24 +194,6 @@ impl TableList<9> for AsyncOpsTable {
table_list_state.len()
))]);

let layout = layout::Layout::default()
.direction(layout::Direction::Vertical)
.margin(0);

let controls = table::Controls::for_area(&area, styles);
let chunks = layout
.constraints(
[
layout::Constraint::Length(controls.height),
layout::Constraint::Max(area.height),
]
.as_ref(),
)
.split(area);

let controls_area = chunks[0];
let async_ops_area = chunks[1];

let attributes_width = layout::Constraint::Percentage(100);
let widths = &[
id_width.constraint(),
Expand All @@ -231,8 +214,7 @@ impl TableList<9> for AsyncOpsTable {
.highlight_symbol(view::TABLE_HIGHLIGHT_SYMBOL)
.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(controls.paragraph, controls_area);
frame.render_stateful_widget(table, area, &mut table_list_state.table_state);

table_list_state
.sorted_items
Expand Down
145 changes: 145 additions & 0 deletions tokio-console/src/view/controls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use crate::view::{self, bold};

use ratatui::{
layout,
text::{Span, Spans, Text},
widgets::{Paragraph, Widget},
};

/// A list of controls which are available in all views.
const UNIVERSAL_CONTROLS: &[ControlDisplay] = &[
ControlDisplay {
action: "toggle pause",
keys: &[KeyDisplay {
base: "space",
utf8: None,
}],
},
ControlDisplay {
action: "quit",
keys: &[KeyDisplay {
base: "q",
utf8: None,
}],
},
];

/// Construct a widget to display the controls available to the user in the
/// current view.
pub(crate) struct Controls {
paragraph: Paragraph<'static>,
height: u16,
}

impl Controls {
pub(in crate::view) fn new(
view_controls: &'static [ControlDisplay],
area: &layout::Rect,
styles: &view::Styles,
) -> Self {
let mut spans_controls = Vec::with_capacity(view_controls.len() + UNIVERSAL_CONTROLS.len());
spans_controls.extend(view_controls.iter().map(|c| c.to_spans(styles)));
spans_controls.extend(UNIVERSAL_CONTROLS.iter().map(|c| c.to_spans(styles)));

let mut lines = vec![Spans::from(vec![Span::from("controls: ")])];
let mut current_line = lines.last_mut().expect("This vector is never empty");
let separator = Span::from(", ");

let controls_count: usize = spans_controls.len();
for (idx, spans) in spans_controls.into_iter().enumerate() {
hds marked this conversation as resolved.
Show resolved Hide resolved
// If this is the first item on this line - or first item on the
// first line, then always include it - even if it goes beyond the
// line width, not much we can do anyway.
if idx == 0 || current_line.width() == 0 {
current_line.0.extend(spans.0);
continue;
}

// Include the width of our separator in the current item if we
// aren't placing the last item. This is the separator after the
// new element.
let needed_trailing_separator_width = if idx == controls_count + 1 {
separator.width()
} else {
0
};

let total_width = current_line.width()
+ separator.width()
+ spans.width()
+ needed_trailing_separator_width;

// If the current item fits on this line, append it.
// Otherwise, append only the separator - we accounted for its
// width in the previous loop iteration - and then create a new
// line for the current item.
if total_width <= area.width as usize {
current_line.0.push(separator.clone());
current_line.0.extend(spans.0);
} else {
hds marked this conversation as resolved.
Show resolved Hide resolved
current_line.0.push(separator.clone());
lines.push(spans);
current_line = lines.last_mut().expect("This vector is never empty");
}
}

let height = lines.len() as u16;
let text = Text::from(lines);

Self {
paragraph: Paragraph::new(text),
height,
}
}

pub(crate) fn height(&self) -> u16 {
self.height
}

pub(crate) fn into_widget(self) -> impl Widget {
self.paragraph
}
}

/// Construct span to display a control.
///
/// A control is made up of an action and one or more keys that will trigger
/// that action.
#[derive(Clone)]
pub(crate) struct ControlDisplay {
pub(crate) action: &'static str,
pub(crate) keys: &'static [KeyDisplay],
}

/// A key or keys which will be displayed to the user as part of spans
/// constructed by `ControlDisplay`.
///
/// The `base` description of the key should be ASCII only, more advanced
/// descriptions can be supplied for that key in the `utf8` field. This
/// allows the application to pick the best one to display at runtime
/// based on the termainal being used.
#[derive(Clone)]
pub(crate) struct KeyDisplay {
pub(crate) base: &'static str,
pub(crate) utf8: Option<&'static str>,
}

impl ControlDisplay {
pub(crate) fn to_spans(&self, styles: &view::Styles) -> Spans<'static> {
let mut spans = Vec::new();

spans.push(Span::from(self.action));
spans.push(Span::from(" = "));
for (idx, key_display) in self.keys.iter().enumerate() {
if idx > 0 {
spans.push(Span::from(" or "))
}
spans.push(bold(match key_display.utf8 {
Some(utf8) => styles.if_utf8(utf8, key_display.base),
None => key_display.base,
}));
}

Spans::from(spans)
}
}
1 change: 1 addition & 0 deletions tokio-console/src/view/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use ratatui::{
use std::{borrow::Cow, cmp};

mod async_ops;
mod controls;
mod durations;
mod mini_histogram;
mod percentiles;
Expand Down
37 changes: 24 additions & 13 deletions tokio-console/src/view/resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ use crate::{
state::State,
view::{
self,
async_ops::{AsyncOpsTable, AsyncOpsTableCtx},
bold, TableListState,
async_ops::{self, AsyncOpsTable, AsyncOpsTableCtx},
bold,
controls::{ControlDisplay, Controls, KeyDisplay},
TableListState,
},
};
use once_cell::sync::OnceCell;
use ratatui::{
layout::{self, Layout},
text::{Span, Spans, Text},
widgets::{Block, Paragraph},
widgets::Paragraph,
};
use std::{cell::RefCell, rc::Rc};

Expand Down Expand Up @@ -42,14 +45,15 @@ impl ResourceView {
state: &mut State,
) {
let resource = &*self.resource.borrow();
let controls = Controls::new(view_controls(), &area, styles);

let (controls_area, stats_area, async_ops_area) = {
let chunks = Layout::default()
.direction(layout::Direction::Vertical)
.constraints(
[
// controls
layout::Constraint::Length(1),
layout::Constraint::Length(controls.height()),
// resource stats
layout::Constraint::Length(8),
// async ops
Expand All @@ -72,14 +76,6 @@ impl ResourceView {
)
.split(stats_area);

let controls = Spans::from(vec![
Span::raw("controls: "),
bold(styles.if_utf8("\u{238B} esc", "esc")),
Span::raw(" = return to task list, "),
bold("q"),
Span::raw(" = quit"),
]);

let overview = vec![
Spans::from(vec![bold("ID: "), Span::raw(resource.id_str())]),
Spans::from(vec![bold("Parent ID: "), Span::raw(resource.parent())]),
Expand Down Expand Up @@ -107,7 +103,7 @@ impl ResourceView {
Paragraph::new(overview).block(styles.border_block().title("Resource"));
let fields_widget = Paragraph::new(fields).block(styles.border_block().title("Attributes"));

frame.render_widget(Block::default().title(controls), controls_area);
frame.render_widget(controls.into_widget(), controls_area);
frame.render_widget(resource_widget, stats_area[0]);
frame.render_widget(fields_widget, stats_area[1]);
let ctx = AsyncOpsTableCtx {
Expand All @@ -119,3 +115,18 @@ impl ResourceView {
self.initial_render = false;
}
}

fn view_controls() -> &'static [ControlDisplay] {
static VIEW_CONTROLS: OnceCell<Vec<ControlDisplay>> = OnceCell::new();

VIEW_CONTROLS.get_or_init(|| {
let resource_controls = &[ControlDisplay {
action: "return to task list",
keys: &[KeyDisplay {
base: "esc",
utf8: Some("\u{238B} esc"),
}],
}];
[resource_controls, async_ops::view_controls()].concat()
})
}
9 changes: 5 additions & 4 deletions tokio-console/src/view/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use crate::{
},
view::{
self, bold,
table::{self, TableList, TableListState},
controls::Controls,
table::{view_controls, TableList, TableListState},
DUR_LEN, DUR_TABLE_PRECISION,
},
};
Expand Down Expand Up @@ -163,7 +164,7 @@ impl TableList<9> for ResourcesTable {
table_list_state.len()
))]);

let controls = table::Controls::for_area(&area, styles);
let controls = Controls::new(view_controls(), &area, styles);

let layout = layout::Layout::default()
.direction(layout::Direction::Vertical)
Expand All @@ -172,7 +173,7 @@ impl TableList<9> for ResourcesTable {
let chunks = layout
.constraints(
[
layout::Constraint::Length(controls.height),
layout::Constraint::Length(controls.height()),
layout::Constraint::Max(area.height),
]
.as_ref(),
Expand Down Expand Up @@ -202,7 +203,7 @@ impl TableList<9> 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(controls.paragraph, controls_area);
frame.render_widget(controls.into_widget(), controls_area);

table_list_state
.sorted_items
Expand Down
Loading