refactor: move basic mode over

Because writing your own layout system and management is just *so much
fun*. Totally.

-------------------------------------------------------------------

Moves the basic mode system over to the new drawing/widget system. In
the process, it has forced me to completely redo how we do layouts...
again. This is because basic mode has widgets that control their own
height - this means the height of the columns and rows that wrap it
are also affected by the widget's height.

The previous system, using a constraint tree and splitting draw Rects
via tui-rs' built-in constraint solver, did not support this concept
very well. It was not simple to propagate up the widths/heights
towards parents while also using tui-rs' built-in constraint solver.
In the end, it was easier to just rewrite it using another algorithm.

We now follow a process very similar to Flutter's layout system.
Relevant links to the Flutter docs are found in the code or below:

- https://flutter.dev/docs/development/ui/layout/constraints
- https://flutter.dev/docs/resources/inside-flutter#sublinear-layouts

The gist of it, however, is that we now instead a few new options for
any element in the layout tree. A node can either:

- Grow to fill remaining space
- Take up as much room as its children
- Be a specific length

Technically right now, it's not perfect, in that leaf nodes can be as
large as their children (which makes no sense), though in that case it
just treats it as an expand.
This commit is contained in:
ClementTsang 2021-08-31 17:19:05 -04:00
parent 204b4dc351
commit eddc9a16c7
34 changed files with 2573 additions and 1384 deletions

View File

@ -282,8 +282,12 @@ impl AppState {
}
KeyCode::Char('q') => EventResult::Quit,
KeyCode::Char('e') => {
self.is_expanded = !self.is_expanded;
EventResult::Redraw
if self.app_config_fields.use_basic_mode {
EventResult::NoRedraw
} else {
self.is_expanded = !self.is_expanded;
EventResult::Redraw
}
}
KeyCode::Char('?') => {
self.help_dialog_state.is_showing_help = true;
@ -353,14 +357,29 @@ impl AppState {
} else {
for (id, widget) in self.widget_lookup_map.iter_mut() {
if widget.does_border_intersect_mouse(&event) {
let was_id_already_selected = self.selected_widget == *id;
self.selected_widget = *id;
let result = widget.handle_mouse_event(event);
let new_id;
match widget.selectable_type() {
SelectableType::Selectable => {
new_id = *id;
}
SelectableType::Unselectable => {
let result = widget.handle_mouse_event(event);
return self.convert_widget_event_result(result);
}
SelectableType::Redirect(redirected_id) => {
new_id = redirected_id;
}
}
let was_id_already_selected = self.selected_widget == new_id;
self.selected_widget = new_id;
if was_id_already_selected {
return self.convert_widget_event_result(result);
} else {
// If the aren't equal, *force* a redraw.
// If the weren't equal, *force* a redraw.
let _ = self.convert_widget_event_result(result);
return EventResult::Redraw;
}

View File

@ -1,12 +1,18 @@
use crate::{
app::{DiskTable, MemGraph, NetGraph, OldNetGraph, ProcessManager, TempTable},
app::{
BasicCpu, BasicMem, BasicNet, BatteryTable, Carousel, DiskTable, Empty, MemGraph, NetGraph,
OldNetGraph, ProcessManager, TempTable,
},
error::{BottomError, Result},
options::layout_options::{Row, RowChildren},
options::{
layout_options::{LayoutRule, Row, RowChildren},
ProcessDefaults,
},
};
use fxhash::FxHashMap;
use indextree::{Arena, NodeId};
use std::collections::BTreeMap;
use tui::layout::Constraint;
use std::{cmp::min, collections::BTreeMap};
use tui::layout::Rect;
use typed_builder::*;
use crate::app::widgets::Widget;
@ -910,6 +916,7 @@ pub enum BottomWidgetType {
BasicNet,
BasicTables,
Battery,
Carousel,
}
impl BottomWidgetType {
@ -958,6 +965,9 @@ impl std::str::FromStr for BottomWidgetType {
"disk" => Ok(BottomWidgetType::Disk),
"empty" => Ok(BottomWidgetType::Empty),
"battery" | "batt" => Ok(BottomWidgetType::Battery),
"bcpu" => Ok(BottomWidgetType::BasicCpu),
"bmem" => Ok(BottomWidgetType::BasicMem),
"bnet" => Ok(BottomWidgetType::BasicNet),
_ => Err(BottomError::ConfigError(format!(
"\"{}\" is an invalid widget name.
@ -987,43 +997,73 @@ Supported widget names:
// --- New stuff ---
/// Represents a row in the layout tree.
#[derive(PartialEq, Eq, Default)]
#[derive(PartialEq, Eq, Clone)]
pub struct RowLayout {
last_selected_index: usize,
pub constraints: Vec<Constraint>,
pub parent_rule: LayoutRule,
pub bound: Rect,
}
impl RowLayout {
fn new(parent_rule: LayoutRule) -> Self {
Self {
last_selected_index: 0,
parent_rule,
bound: Rect::default(),
}
}
}
/// Represents a column in the layout tree.
#[derive(PartialEq, Eq, Default)]
#[derive(PartialEq, Eq, Clone)]
pub struct ColLayout {
last_selected_index: usize,
pub constraints: Vec<Constraint>,
pub parent_rule: LayoutRule,
pub bound: Rect,
}
impl ColLayout {
fn new(parent_rule: LayoutRule) -> Self {
Self {
last_selected_index: 0,
parent_rule,
bound: Rect::default(),
}
}
}
/// Represents a widget in the layout tree.
#[derive(PartialEq, Eq, Clone, Default)]
pub struct WidgetLayout {
pub bound: Rect,
}
/// A [`LayoutNode`] represents a single node in the overall widget hierarchy. Each node is one of:
/// - [`LayoutNode::Row`] (a non-leaf that distributes its children horizontally)
/// - [`LayoutNode::Col`] (a non-leaf node that distributes its children vertically)
/// - [`LayoutNode::Widget`] (a leaf node that contains the ID of the widget it is associated with)
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Clone)]
pub enum LayoutNode {
/// A non-leaf that distributes its children horizontally
Row(RowLayout),
/// A non-leaf node that distributes its children vertically
Col(ColLayout),
/// A leaf node that contains the ID of the widget it is associated with
Widget,
Widget(WidgetLayout),
}
impl LayoutNode {
pub fn set_constraints(&mut self, constraints: Vec<Constraint>) {
fn set_bound(&mut self, bound: Rect) {
match self {
LayoutNode::Row(row) => {
row.constraints = constraints;
row.bound = bound;
}
LayoutNode::Col(col) => {
col.constraints = constraints;
col.bound = bound;
}
LayoutNode::Widget(widget) => {
widget.bound = bound;
}
LayoutNode::Widget => {}
}
}
}
@ -1050,86 +1090,140 @@ pub struct LayoutCreationOutput {
/// selected [`NodeId`].
// FIXME: This is currently jury-rigged "glue" just to work with the existing config system! We are NOT keeping it like this, it's too awful to keep like this!
pub fn create_layout_tree(
rows: &[Row], process_defaults: crate::options::ProcessDefaults,
app_config_fields: &AppConfigFields,
rows: &[Row], process_defaults: ProcessDefaults, app_config_fields: &AppConfigFields,
) -> Result<LayoutCreationOutput> {
fn add_widget_to_map(
widget_lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, widget_type: BottomWidgetType,
widget_id: NodeId, process_defaults: &crate::options::ProcessDefaults,
app_config_fields: &AppConfigFields,
widget_id: NodeId, process_defaults: &ProcessDefaults, app_config_fields: &AppConfigFields,
width: LayoutRule, height: LayoutRule,
) -> Result<()> {
match widget_type {
BottomWidgetType::Cpu => {
widget_lookup_map
.insert(widget_id, CpuGraph::from_config(app_config_fields).into());
widget_lookup_map.insert(
widget_id,
CpuGraph::from_config(app_config_fields)
.width(width)
.height(height)
.into(),
);
}
BottomWidgetType::Mem => {
let graph = TimeGraph::from_config(app_config_fields);
widget_lookup_map.insert(widget_id, MemGraph::new(graph).into());
widget_lookup_map.insert(
widget_id,
MemGraph::new(graph).width(width).height(height).into(),
);
}
BottomWidgetType::Net => {
if app_config_fields.use_old_network_legend {
widget_lookup_map.insert(
widget_id,
OldNetGraph::from_config(app_config_fields).into(),
OldNetGraph::from_config(app_config_fields)
.width(width)
.height(height)
.into(),
);
} else {
widget_lookup_map
.insert(widget_id, NetGraph::from_config(app_config_fields).into());
widget_lookup_map.insert(
widget_id,
NetGraph::from_config(app_config_fields)
.width(width)
.height(height)
.into(),
);
}
}
BottomWidgetType::Proc => {
widget_lookup_map.insert(widget_id, ProcessManager::new(process_defaults).into());
widget_lookup_map.insert(
widget_id,
ProcessManager::new(process_defaults)
.width(width)
.height(height)
.basic_mode(app_config_fields.use_basic_mode)
.into(),
);
}
BottomWidgetType::Temp => {
widget_lookup_map.insert(
widget_id,
TempTable::default()
.set_temp_type(app_config_fields.temperature_type.clone())
.width(width)
.height(height)
.basic_mode(app_config_fields.use_basic_mode)
.into(),
);
}
BottomWidgetType::Disk => {
widget_lookup_map.insert(widget_id, DiskTable::default().into());
widget_lookup_map.insert(
widget_id,
DiskTable::default()
.width(width)
.height(height)
.basic_mode(app_config_fields.use_basic_mode)
.into(),
);
}
BottomWidgetType::Battery => {
widget_lookup_map.insert(
widget_id,
BatteryTable::default()
.width(width)
.height(height)
.basic_mode(app_config_fields.use_basic_mode)
.into(),
);
}
BottomWidgetType::BasicCpu => {
widget_lookup_map.insert(
widget_id,
BasicCpu::from_config(app_config_fields).width(width).into(),
);
}
BottomWidgetType::BasicMem => {
widget_lookup_map.insert(widget_id, BasicMem::default().width(width).into());
}
BottomWidgetType::BasicNet => {
widget_lookup_map.insert(
widget_id,
BasicNet::from_config(app_config_fields).width(width).into(),
);
}
BottomWidgetType::Empty => {
widget_lookup_map.insert(
widget_id,
Empty::default().width(width).height(height).into(),
);
}
BottomWidgetType::Battery => {}
_ => {}
}
Ok(())
}
let mut layout_tree = Arena::new();
let root_id = layout_tree.new_node(LayoutNode::Col(ColLayout::default()));
let mut arena = Arena::new();
let root_id = arena.new_node(LayoutNode::Col(ColLayout::new(LayoutRule::Expand {
ratio: 1,
})));
let mut widget_lookup_map = FxHashMap::default();
let mut first_selected = None;
let mut first_widget_seen = None; // Backup
let mut first_widget_seen = None; // Backup selected widget
let mut used_widgets = UsedWidgets::default();
let row_sum: u32 = rows.iter().map(|row| row.ratio.unwrap_or(1)).sum();
let mut root_constraints = Vec::with_capacity(rows.len());
for row in rows {
root_constraints.push(Constraint::Ratio(row.ratio.unwrap_or(1), row_sum));
let layout_node = LayoutNode::Row(RowLayout::default());
let row_id = layout_tree.new_node(layout_node);
root_id.append(row_id, &mut layout_tree);
let row_id = arena.new_node(LayoutNode::Row(RowLayout::new(
row.ratio
.map(|ratio| LayoutRule::Expand { ratio })
.unwrap_or(LayoutRule::Child),
)));
root_id.append(row_id, &mut arena);
if let Some(cols) = &row.child {
let mut row_constraints = Vec::with_capacity(cols.len());
let col_sum: u32 = cols
.iter()
.map(|col| match col {
RowChildren::Widget(widget) => widget.ratio.unwrap_or(1),
RowChildren::Col { ratio, child: _ } => ratio.unwrap_or(1),
})
.sum();
for col in cols {
match col {
if let Some(children) = &row.child {
for child in children {
match child {
RowChildren::Widget(widget) => {
row_constraints.push(Constraint::Ratio(widget.ratio.unwrap_or(1), col_sum));
let widget_id = layout_tree.new_node(LayoutNode::Widget);
row_id.append(widget_id, &mut layout_tree);
let widget_id = arena.new_node(LayoutNode::Widget(WidgetLayout::default()));
row_id.append(widget_id, &mut arena);
if let Some(true) = widget.default {
first_selected = Some(widget_id);
@ -1148,28 +1242,108 @@ pub fn create_layout_tree(
widget_id,
&process_defaults,
app_config_fields,
widget.rule.unwrap_or_default(),
LayoutRule::default(),
)?;
}
RowChildren::Carousel {
carousel_children,
default,
} => {
if !carousel_children.is_empty() {
let mut child_ids = Vec::with_capacity(carousel_children.len());
let carousel_widget_id =
arena.new_node(LayoutNode::Widget(WidgetLayout::default()));
row_id.append(carousel_widget_id, &mut arena);
// Add the first widget as a default widget if needed.
{
let widget_id =
arena.new_node(LayoutNode::Widget(WidgetLayout::default()));
carousel_widget_id.append(widget_id, &mut arena);
let widget_type =
carousel_children[0].parse::<BottomWidgetType>()?;
used_widgets.add(&widget_type);
if let Some(true) = default {
first_selected = Some(widget_id);
}
if first_widget_seen.is_none() {
first_widget_seen = Some(widget_id);
}
add_widget_to_map(
&mut widget_lookup_map,
widget_type,
widget_id,
&process_defaults,
app_config_fields,
LayoutRule::default(),
LayoutRule::default(),
)?;
child_ids.push(widget_id);
}
// Handle the rest of the children.
for child in carousel_children[1..].iter() {
let widget_id =
arena.new_node(LayoutNode::Widget(WidgetLayout::default()));
carousel_widget_id.append(widget_id, &mut arena);
let widget_type = child.parse::<BottomWidgetType>()?;
used_widgets.add(&widget_type);
add_widget_to_map(
&mut widget_lookup_map,
widget_type,
widget_id,
&process_defaults,
app_config_fields,
LayoutRule::default(),
LayoutRule::default(),
)?;
child_ids.push(widget_id);
}
widget_lookup_map.insert(
carousel_widget_id,
Carousel::new(
child_ids
.into_iter()
.filter_map(|child_id| {
if let Some(w) = widget_lookup_map.get(&child_id) {
Some((child_id, w.get_pretty_name().into()))
} else {
None
}
})
.collect(),
)
.into(),
);
}
}
RowChildren::Col {
ratio,
child: children,
child: col_child,
} => {
row_constraints.push(Constraint::Ratio(ratio.unwrap_or(1), col_sum));
let col_node = LayoutNode::Col(ColLayout::default());
let col_id = layout_tree.new_node(col_node);
row_id.append(col_id, &mut layout_tree);
let col_id = arena.new_node(LayoutNode::Col(ColLayout::new(
ratio
.map(|ratio| LayoutRule::Expand { ratio })
.unwrap_or(LayoutRule::Child),
)));
row_id.append(col_id, &mut arena);
let child_sum: u32 =
children.iter().map(|child| child.ratio.unwrap_or(1)).sum();
for widget in col_child {
let widget_id =
arena.new_node(LayoutNode::Widget(WidgetLayout::default()));
col_id.append(widget_id, &mut arena);
let mut col_constraints = Vec::with_capacity(children.len());
for child in children {
col_constraints
.push(Constraint::Ratio(child.ratio.unwrap_or(1), child_sum));
let widget_id = layout_tree.new_node(LayoutNode::Widget);
col_id.append(widget_id, &mut layout_tree);
if let Some(true) = child.default {
if let Some(true) = widget.default {
first_selected = Some(widget_id);
}
@ -1177,7 +1351,7 @@ pub fn create_layout_tree(
first_widget_seen = Some(widget_id);
}
let widget_type = child.widget_type.parse::<BottomWidgetType>()?;
let widget_type = widget.widget_type.parse::<BottomWidgetType>()?;
used_widgets.add(&widget_type);
add_widget_to_map(
@ -1186,25 +1360,17 @@ pub fn create_layout_tree(
widget_id,
&process_defaults,
app_config_fields,
LayoutRule::default(),
widget.rule.unwrap_or_default(),
)?;
}
layout_tree[col_id]
.get_mut()
.set_constraints(col_constraints);
}
}
}
layout_tree[row_id]
.get_mut()
.set_constraints(row_constraints);
}
}
layout_tree[root_id]
.get_mut()
.set_constraints(root_constraints);
let selected: NodeId;
if let Some(first_selected) = first_selected {
selected = first_selected;
} else if let Some(first_widget_seen) = first_widget_seen {
@ -1216,7 +1382,7 @@ pub fn create_layout_tree(
}
Ok(LayoutCreationOutput {
layout_tree,
layout_tree: arena,
root: root_id,
widget_lookup_map,
selected,
@ -1264,7 +1430,7 @@ pub fn move_widget_selection(
.and_then(|(parent_id, parent_node)| match parent_node.get() {
LayoutNode::Row(_) => Some((parent_id, current_id)),
LayoutNode::Col(_) => find_first_row(layout_tree, parent_id),
LayoutNode::Widget => None,
LayoutNode::Widget(_) => None,
})
}
@ -1285,21 +1451,23 @@ pub fn move_widget_selection(
.and_then(|(parent_id, parent_node)| match parent_node.get() {
LayoutNode::Row(_) => find_first_col(layout_tree, parent_id),
LayoutNode::Col(_) => Some((parent_id, current_id)),
LayoutNode::Widget => None,
LayoutNode::Widget(_) => None,
})
}
/// Descends to a leaf.
/// Descends to a leaf node.
fn descend_to_leaf(layout_tree: &Arena<LayoutNode>, current_id: NodeId) -> NodeId {
if let Some(current_node) = layout_tree.get(current_id) {
match current_node.get() {
LayoutNode::Row(RowLayout {
last_selected_index,
constraints: _,
parent_rule: _,
bound: _,
})
| LayoutNode::Col(ColLayout {
last_selected_index,
constraints: _,
parent_rule: _,
bound: _,
}) => {
if let Some(next_child) =
current_id.children(layout_tree).nth(*last_selected_index)
@ -1309,8 +1477,9 @@ pub fn move_widget_selection(
current_id
}
}
LayoutNode::Widget => {
LayoutNode::Widget(_) => {
// Halt!
// TODO: How does this handle carousel?
current_id
}
}
@ -1470,3 +1639,317 @@ pub fn move_widget_selection(
}
}
}
/// Generates the bounds for each node in the `arena, taking into account per-leaf desires,
/// and finally storing the calculated bounds in the given `arena`.
///
/// Stored bounds are given in *relative* coordinates - they are relative to their parents.
/// That is, you may have a child widget "start" at (0, 0), but its parent is actually at x = 5,s
/// so the absolute coordinate of the child widget is actually (5, 0).
///
/// The algorithm is mostly based on the algorithm used by Flutter, adapted to work for
/// our use case. For more information, check out both:
///
/// - [How the constraint system works in Flutter](https://flutter.dev/docs/development/ui/layout/constraints)
/// - [How Flutter does sublinear layout](https://flutter.dev/docs/resources/inside-flutter#sublinear-layout)
pub fn generate_layout(
root: NodeId, arena: &mut Arena<LayoutNode>, area: Rect,
lookup_map: &FxHashMap<NodeId, TmpBottomWidget>,
) {
// TODO: [Layout] Add some caching/dirty mechanisms to reduce calls.
/// A [`Size`] is a set of widths and heights that a node in our layout wants to be.
#[derive(Default, Clone, Copy, Debug)]
struct Size {
width: u16,
height: u16,
}
/// A [`LayoutConstraint`] is just a set of maximal widths/heights.
#[derive(Clone, Copy, Debug)]
struct LayoutConstraints {
max_width: u16,
max_height: u16,
}
impl LayoutConstraints {
fn new(max_width: u16, max_height: u16) -> Self {
Self {
max_width,
max_height,
}
}
/// Shrinks the width of itself given another width.
fn shrink_width(&mut self, width: u16) {
self.max_width = self.max_width.saturating_sub(width);
}
/// Shrinks the height of itself given another height.
fn shrink_height(&mut self, height: u16) {
self.max_height = self.max_height.saturating_sub(height);
}
/// Returns a new [`LayoutConstraints`] with a new width given a ratio.
fn ratio_width(&self, numerator: u32, denominator: u32) -> Self {
Self {
max_width: (self.max_width as u32 * numerator / denominator) as u16,
max_height: self.max_height,
}
}
/// Returns a new [`LayoutConstraints`] with a new height given a ratio.
fn ratio_height(&self, numerator: u32, denominator: u32) -> Self {
Self {
max_width: self.max_width,
max_height: (self.max_height as u32 * numerator / denominator) as u16,
}
}
}
/// The internal recursive call to build a layout. Builds off of `arena` and stores bounds inside it.
fn layout(
node: NodeId, arena: &mut Arena<LayoutNode>,
lookup_map: &FxHashMap<NodeId, TmpBottomWidget>, mut constraints: LayoutConstraints,
) -> Size {
if let Some(layout_node) = arena.get(node).map(|n| n.get()) {
match layout_node {
LayoutNode::Row(row) => {
let children = node.children(arena).collect::<Vec<_>>();
let mut row_bounds = vec![Size::default(); children.len()];
if let LayoutRule::Length { length } = row.parent_rule {
constraints.max_height = length;
}
let (flexible_indices, inflexible_indices): (Vec<_>, Vec<_>) = children
.iter()
.enumerate()
.filter_map(|(itx, node)| {
if let Some(layout_node) = arena.get(*node).map(|n| n.get()) {
match layout_node {
LayoutNode::Row(RowLayout { parent_rule, .. })
| LayoutNode::Col(ColLayout { parent_rule, .. }) => {
match parent_rule {
LayoutRule::Expand { ratio } => {
Some((itx, true, *ratio))
}
LayoutRule::Child => Some((itx, false, 0)),
LayoutRule::Length { .. } => Some((itx, false, 0)),
}
}
LayoutNode::Widget(_) => {
if let Some(widget) = lookup_map.get(node) {
match widget.width() {
LayoutRule::Expand { ratio } => {
Some((itx, true, ratio))
}
LayoutRule::Child => Some((itx, false, 0)),
LayoutRule::Length { .. } => Some((itx, false, 0)),
}
} else {
None
}
}
}
} else {
None
}
})
.partition(|(_itx, is_flex, _ratio)| *is_flex);
// First handle non-flexible children.
for (index, _, _) in inflexible_indices {
// The unchecked get is safe, since the index is obtained by iterating through the children
// vector in the first place.
let child = unsafe { children.get_unchecked(index) };
let desired_size = layout(*child, arena, lookup_map, constraints);
constraints.shrink_width(desired_size.width);
// This won't panic, since the two vectors are the same length.
row_bounds[index] = desired_size;
}
// Handle flexible children now.
let denominator: u32 = flexible_indices.iter().map(|(_, _, ratio)| ratio).sum();
let original_constraints = constraints.clone();
let mut split_constraints = flexible_indices
.iter()
.map(|(_, _, numerator)| {
let constraint =
original_constraints.ratio_width(*numerator, denominator);
constraints.shrink_width(constraint.max_width);
constraint
})
.collect::<Vec<_>>();
(0..constraints.max_width)
.zip(&mut split_constraints)
.for_each(|(_, split_constraint)| {
split_constraint.max_width += 1;
});
for ((index, _, _), constraint) in
flexible_indices.into_iter().zip(split_constraints)
{
// The unchecked get is safe, since the index is obtained by iterating through the children
// vector in the first place.
let child = unsafe { children.get_unchecked(index) };
let desired_size = layout(*child, arena, lookup_map, constraint);
// This won't panic, since the two vectors are the same length.
row_bounds[index] = desired_size;
}
// Now let's turn each Size into a relative Rect!
let mut current_x = 0;
row_bounds.iter().zip(children).for_each(|(size, child)| {
let bound = Rect::new(current_x, 0, size.width, size.height);
current_x += size.width;
if let Some(node) = arena.get_mut(child) {
node.get_mut().set_bound(bound);
}
});
Size {
height: row_bounds.iter().map(|size| size.height).max().unwrap_or(0),
width: row_bounds.into_iter().map(|size| size.width).sum(),
}
}
LayoutNode::Col(col) => {
let children = node.children(arena).collect::<Vec<_>>();
let mut col_bounds = vec![Size::default(); children.len()];
if let LayoutRule::Length { length } = col.parent_rule {
constraints.max_width = length;
}
let (flexible_indices, inflexible_indices): (Vec<_>, Vec<_>) = children
.iter()
.enumerate()
.filter_map(|(itx, node)| {
if let Some(layout_node) = arena.get(*node).map(|n| n.get()) {
match layout_node {
LayoutNode::Row(RowLayout { parent_rule, .. })
| LayoutNode::Col(ColLayout { parent_rule, .. }) => {
match parent_rule {
LayoutRule::Expand { ratio } => {
Some((itx, true, *ratio))
}
LayoutRule::Child => Some((itx, false, 0)),
LayoutRule::Length { .. } => Some((itx, false, 0)),
}
}
LayoutNode::Widget(_) => {
if let Some(widget) = lookup_map.get(node) {
match widget.height() {
LayoutRule::Expand { ratio } => {
Some((itx, true, ratio))
}
LayoutRule::Child => Some((itx, false, 0)),
LayoutRule::Length { length: _ } => {
Some((itx, false, 0))
}
}
} else {
None
}
}
}
} else {
None
}
})
.partition(|(_itx, is_flex, _ratio)| *is_flex);
for (index, _, _) in inflexible_indices {
// The unchecked get is safe, since the index is obtained by iterating through the children
// vector in the first place.
let child = unsafe { children.get_unchecked(index) };
let desired_size = layout(*child, arena, lookup_map, constraints);
constraints.shrink_height(desired_size.height);
// This won't panic, since the two vectors are the same length.
col_bounds[index] = desired_size;
}
let denominator: u32 = flexible_indices.iter().map(|(_, _, ratio)| ratio).sum();
let original_constraints = constraints.clone();
let mut split_constraints = flexible_indices
.iter()
.map(|(_, _, numerator)| {
let new_constraint =
original_constraints.ratio_height(*numerator, denominator);
constraints.shrink_height(new_constraint.max_height);
new_constraint
})
.collect::<Vec<_>>();
(0..constraints.max_height)
.zip(&mut split_constraints)
.for_each(|(_, split_constraint)| {
split_constraint.max_height += 1;
});
for ((index, _, _), constraint) in
flexible_indices.into_iter().zip(split_constraints)
{
// The unchecked get is safe, since the index is obtained by iterating through the children
// vector in the first place.
let child = unsafe { children.get_unchecked(index) };
let desired_size = layout(*child, arena, lookup_map, constraint);
// This won't panic, since the two vectors are the same length.
col_bounds[index] = desired_size;
}
// Now let's turn each Size into a relative Rect!
let mut current_y = 0;
col_bounds.iter().zip(children).for_each(|(size, child)| {
let bound = Rect::new(0, current_y, size.width, size.height);
current_y += size.height;
if let Some(node) = arena.get_mut(child) {
node.get_mut().set_bound(bound);
}
});
Size {
width: col_bounds.iter().map(|size| size.width).max().unwrap_or(0),
height: col_bounds.into_iter().map(|size| size.height).sum(),
}
}
LayoutNode::Widget(_) => {
if let Some(widget) = lookup_map.get(&node) {
let width = match widget.width() {
LayoutRule::Expand { ratio: _ } => constraints.max_width,
LayoutRule::Length { length } => min(length, constraints.max_width),
LayoutRule::Child => constraints.max_width,
};
let height = match widget.height() {
LayoutRule::Expand { ratio: _ } => constraints.max_height,
LayoutRule::Length { length } => min(length, constraints.max_height),
LayoutRule::Child => constraints.max_height,
};
Size { width, height }
} else {
Size::default()
}
}
}
} else {
Size::default()
}
}
// And this is all you need to call, the layout function will do it all~
layout(
root,
arena,
lookup_map,
LayoutConstraints::new(area.width, area.height),
);
}

View File

@ -2,6 +2,7 @@ use std::time::Instant;
use crossterm::event::{KeyEvent, MouseEvent};
use enum_dispatch::enum_dispatch;
use indextree::NodeId;
use tui::{backend::Backend, layout::Rect, widgets::TableState, Frame};
use crate::{
@ -11,6 +12,7 @@ use crate::{
},
canvas::Painter,
constants,
options::layout_options::LayoutRule,
};
mod tui_widgets;
@ -18,26 +20,8 @@ mod tui_widgets;
pub mod base;
pub use base::*;
pub mod process;
pub use process::*;
pub mod net;
pub use net::*;
pub mod mem;
pub use mem::*;
pub mod cpu;
pub use cpu::*;
pub mod disk;
pub use disk::*;
pub mod battery;
pub use self::battery::*;
pub mod temp;
pub use temp::*;
pub mod bottom_widgets;
pub use bottom_widgets::*;
use super::data_farmer::DataCollection;
@ -131,15 +115,38 @@ pub trait Widget {
/// Returns a [`Widget`]'s "pretty" display name.
fn get_pretty_name(&self) -> &'static str;
/// Draws a [`Widget`]. Defaults to doing nothing.
/// Draws a [`Widget`]. The default implementation draws nothing.
fn draw<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
) {
// TODO: Remove the default implementation in the future!
}
/// How a [`Widget`] updates its internal displayed data. Defaults to doing nothing.
/// How a [`Widget`] updates its internal data that'll be displayed. Called after every data harvest.
/// The default implementation does nothing with the data.
fn update_data(&mut self, data_collection: &DataCollection) {}
/// Returns the desired width from the [`Widget`].
fn width(&self) -> LayoutRule;
/// Returns the desired height from the [`Widget`].
fn height(&self) -> LayoutRule;
/// Returns whether this [`Widget`] can be expanded. The default implementation returns `true`.
fn expandable(&self) -> bool {
true
}
/// Returns whether this [`Widget`] can be selected. The default implementation returns [`SelectableType::Selectable`].
fn selectable_type(&self) -> SelectableType {
SelectableType::Selectable
}
}
/// Whether a widget can be selected, not selected, or redirected upon selection.
pub enum SelectableType {
Selectable,
Unselectable,
Redirect(NodeId),
}
/// The "main" widgets that are used by bottom to display information!
@ -154,6 +161,11 @@ pub enum TmpBottomWidget {
OldNetGraph,
ProcessManager,
BatteryTable,
BasicCpu,
BasicMem,
BasicNet,
Carousel,
Empty,
}
// ----- Old stuff below -----

View File

@ -15,8 +15,5 @@ pub use scrollable::Scrollable;
pub mod text_input;
pub use text_input::TextInput;
pub mod carousel;
pub use carousel::Carousel;
pub mod sort_menu;
pub use sort_menu::SortMenu;

View File

@ -1,42 +0,0 @@
use indextree::NodeId;
use tui::layout::Rect;
use crate::app::Component;
/// A container that "holds"" multiple [`BottomWidget`]s through their [`NodeId`]s.
pub struct Carousel {
index: usize,
children: Vec<NodeId>,
bounds: Rect,
}
impl Carousel {
/// Creates a new [`Carousel`] with the specified children.
pub fn new(children: Vec<NodeId>) -> Self {
Self {
index: 0,
children,
bounds: Rect::default(),
}
}
/// Adds a new child to a [`Carousel`].
pub fn add_child(&mut self, child: NodeId) {
self.children.push(child);
}
/// Returns the currently selected [`NodeId`] if possible.
pub fn get_currently_selected(&self) -> Option<&NodeId> {
self.children.get(self.index)
}
}
impl Component for Carousel {
fn bounds(&self) -> tui::layout::Rect {
self.bounds
}
fn set_bounds(&mut self, new_bounds: tui::layout::Rect) {
self.bounds = new_bounds;
}
}

View File

@ -384,6 +384,9 @@ where
use tui::widgets::Row;
let inner_area = block.inner(block_area);
if inner_area.height < 2 {
return;
}
let table_gap = if !self.show_gap || inner_area.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {

View File

@ -0,0 +1,35 @@
pub mod process;
pub use process::*;
pub mod net;
pub use net::*;
pub mod mem;
pub use mem::*;
pub mod cpu;
pub use cpu::*;
pub mod disk;
pub use disk::*;
pub mod battery;
pub use self::battery::*;
pub mod temp;
pub use temp::*;
pub mod basic_cpu;
pub use basic_cpu::BasicCpu;
pub mod basic_mem;
pub use basic_mem::BasicMem;
pub mod basic_net;
pub use basic_net::BasicNet;
pub mod carousel;
pub use carousel::Carousel;
pub mod empty;
pub use empty::Empty;

View File

@ -0,0 +1,205 @@
use std::cmp::max;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
widgets::Block,
Frame,
};
use crate::{
app::{widgets::tui_widgets::PipeGauge, AppConfigFields, Component, DataCollection, Widget},
canvas::Painter,
constants::SIDE_BORDERS,
options::layout_options::LayoutRule,
};
const REQUIRED_COLUMNS: usize = 4;
#[derive(Debug)]
pub struct BasicCpu {
bounds: Rect,
display_data: Vec<(f64, String, String)>,
width: LayoutRule,
showing_avg: bool,
}
impl BasicCpu {
/// Creates a new [`BasicCpu`] given a [`AppConfigFields`].
pub fn from_config(app_config_fields: &AppConfigFields) -> Self {
Self {
bounds: Default::default(),
display_data: Default::default(),
width: Default::default(),
showing_avg: app_config_fields.show_average_cpu,
}
}
/// Sets the width.
pub fn width(mut self, width: LayoutRule) -> Self {
self.width = width;
self
}
}
impl Component for BasicCpu {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, new_bounds: Rect) {
self.bounds = new_bounds;
}
}
impl Widget for BasicCpu {
fn get_pretty_name(&self) -> &'static str {
"CPU"
}
fn draw<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
) {
const CONSTRAINTS: [Constraint; 2 * REQUIRED_COLUMNS - 1] = [
Constraint::Ratio(1, REQUIRED_COLUMNS as u32),
Constraint::Length(2),
Constraint::Ratio(1, REQUIRED_COLUMNS as u32),
Constraint::Length(2),
Constraint::Ratio(1, REQUIRED_COLUMNS as u32),
Constraint::Length(2),
Constraint::Ratio(1, REQUIRED_COLUMNS as u32),
];
let block = Block::default()
.borders(*SIDE_BORDERS)
.border_style(painter.colours.highlighted_border_style);
let inner_area = block.inner(area);
let split_area = Layout::default()
.direction(Direction::Horizontal)
.constraints(CONSTRAINTS)
.split(inner_area)
.into_iter()
.enumerate()
.filter_map(
|(index, rect)| {
if index % 2 == 0 {
Some(rect)
} else {
None
}
},
);
let display_data_len = self.display_data.len();
let length = display_data_len / REQUIRED_COLUMNS;
let largest_height = max(
1,
length
+ (if display_data_len % REQUIRED_COLUMNS == 0 {
0
} else {
1
}),
);
let mut leftover = display_data_len % REQUIRED_COLUMNS;
let column_heights = (0..REQUIRED_COLUMNS).map(|_| {
if leftover > 0 {
leftover -= 1;
length + 1
} else {
length
}
});
if selected {
f.render_widget(block, area);
}
let mut index_offset = 0;
split_area
.into_iter()
.zip(column_heights)
.for_each(|(area, height)| {
let column_areas = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1); largest_height])
.split(area);
let num_entries = if index_offset + height < display_data_len {
height
} else {
display_data_len - index_offset
};
let end = index_offset + num_entries;
self.display_data[index_offset..end]
.iter()
.zip(column_areas)
.enumerate()
.for_each(|(column_index, ((percent, label, usage_label), area))| {
let cpu_index = index_offset + column_index;
let style = if cpu_index == 0 {
painter.colours.avg_colour_style
} else {
let cpu_style_index = if self.showing_avg {
cpu_index - 1
} else {
cpu_index
};
painter.colours.cpu_colour_styles
[cpu_style_index % painter.colours.cpu_colour_styles.len()]
};
f.render_widget(
PipeGauge::default()
.ratio(*percent)
.style(style)
.gauge_style(style)
.start_label(label.clone())
.end_label(usage_label.clone()),
area,
);
});
index_offset = end;
});
}
fn update_data(&mut self, data_collection: &DataCollection) {
self.display_data = data_collection
.cpu_harvest
.iter()
.map(|data| {
(
data.cpu_usage / 100.0,
format!(
"{:3}",
data.cpu_count
.map(|c| c.to_string())
.unwrap_or(data.cpu_prefix.clone())
),
format!("{:3.0}%", data.cpu_usage.round()),
)
})
.collect::<Vec<_>>();
}
fn width(&self) -> LayoutRule {
self.width
}
fn height(&self) -> LayoutRule {
let display_data_len = self.display_data.len();
let length = max(
1,
(display_data_len / REQUIRED_COLUMNS) as u16
+ (if display_data_len % REQUIRED_COLUMNS == 0 {
0
} else {
1
}),
);
LayoutRule::Length { length }
}
}

View File

@ -0,0 +1,164 @@
use crossterm::event::{KeyCode, KeyEvent};
use tui::{
backend::Backend,
layout::{Constraint, Layout, Rect},
widgets::Block,
Frame,
};
use crate::{
app::{
event::WidgetEventResult, widgets::tui_widgets::PipeGauge, Component, DataCollection,
Widget,
},
canvas::Painter,
constants::SIDE_BORDERS,
data_conversion::{convert_mem_data_points, convert_mem_labels, convert_swap_data_points},
options::layout_options::LayoutRule,
};
#[derive(Debug)]
pub struct BasicMem {
bounds: Rect,
width: LayoutRule,
mem_data: (f64, String, String),
swap_data: Option<(f64, String, String)>,
use_percent: bool,
}
impl Default for BasicMem {
fn default() -> Self {
Self {
bounds: Default::default(),
width: Default::default(),
mem_data: (0.0, "0.0B/0.0B".to_string(), "0%".to_string()),
swap_data: None,
use_percent: false,
}
}
}
impl BasicMem {
/// Sets the width.
pub fn width(mut self, width: LayoutRule) -> Self {
self.width = width;
self
}
}
impl Component for BasicMem {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, new_bounds: Rect) {
self.bounds = new_bounds;
}
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
match event.code {
KeyCode::Char('%') if event.modifiers.is_empty() => {
self.use_percent = !self.use_percent;
WidgetEventResult::Redraw
}
_ => WidgetEventResult::NoRedraw,
}
}
}
impl Widget for BasicMem {
fn get_pretty_name(&self) -> &'static str {
"Memory"
}
fn draw<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
) {
let block = Block::default()
.borders(*SIDE_BORDERS)
.border_style(painter.colours.highlighted_border_style);
let inner_area = block.inner(area);
const CONSTRAINTS: [Constraint; 2] = [Constraint::Ratio(1, 2); 2];
let split_area = Layout::default()
.direction(tui::layout::Direction::Vertical)
.constraints(CONSTRAINTS)
.split(inner_area);
if selected {
f.render_widget(block, area);
}
let mut use_percentage =
self.use_percent || (split_area[0].width as usize) < self.mem_data.1.len() + 7;
if let Some(swap_data) = &self.swap_data {
use_percentage =
use_percentage || (split_area[1].width as usize) < swap_data.1.len() + 7;
f.render_widget(
PipeGauge::default()
.ratio(swap_data.0)
.style(painter.colours.swap_style)
.gauge_style(painter.colours.swap_style)
.start_label("SWP")
.end_label(if use_percentage {
swap_data.2.clone()
} else {
swap_data.1.clone()
}),
split_area[1],
);
}
f.render_widget(
PipeGauge::default()
.ratio(self.mem_data.0)
.style(painter.colours.ram_style)
.gauge_style(painter.colours.ram_style)
.start_label("RAM")
.end_label(if use_percentage {
self.mem_data.2.clone()
} else {
self.mem_data.1.clone()
}),
split_area[0],
);
}
fn update_data(&mut self, data_collection: &DataCollection) {
let (memory_labels, swap_labels) = convert_mem_labels(data_collection);
// TODO: [Data update optimization] Probably should just make another function altogether for basic mode.
self.mem_data = if let (Some(data), Some((_, fraction))) = (
convert_mem_data_points(data_collection, false).last(),
memory_labels,
) {
(
data.1 / 100.0,
fraction.trim().to_string(),
format!("{:3.0}%", data.1.round()),
)
} else {
(0.0, "0.0B/0.0B".to_string(), "0%".to_string())
};
self.swap_data = if let (Some(data), Some((_, fraction))) = (
convert_swap_data_points(data_collection, false).last(),
swap_labels,
) {
Some((
data.1 / 100.0,
fraction.trim().to_string(),
format!("{:3.0}%", data.1.round()),
))
} else {
None
};
}
fn width(&self) -> LayoutRule {
self.width
}
fn height(&self) -> LayoutRule {
LayoutRule::Length { length: 2 }
}
}

View File

@ -0,0 +1,134 @@
use tui::{
backend::Backend,
layout::{Constraint, Layout, Rect},
text::{Span, Spans},
widgets::{Block, Paragraph},
Frame,
};
use crate::{
app::{AppConfigFields, AxisScaling, Component, DataCollection, Widget},
canvas::Painter,
constants::SIDE_BORDERS,
data_conversion::convert_network_data_points,
options::layout_options::LayoutRule,
units::data_units::DataUnit,
};
#[derive(Debug)]
pub struct BasicNet {
bounds: Rect,
width: LayoutRule,
rx_display: String,
tx_display: String,
total_rx_display: String,
total_tx_display: String,
pub unit_type: DataUnit,
pub use_binary_prefix: bool,
}
impl BasicNet {
/// Creates a new [`BasicNet`] given a [`AppConfigFields`].
pub fn from_config(app_config_fields: &AppConfigFields) -> Self {
Self {
bounds: Default::default(),
width: Default::default(),
rx_display: "RX: 0b/s".to_string(),
tx_display: "TX: 0b/s".to_string(),
total_rx_display: "Total RX: 0B".to_string(),
total_tx_display: "Total TX: 0B".to_string(),
unit_type: app_config_fields.network_unit_type.clone(),
use_binary_prefix: app_config_fields.network_use_binary_prefix,
}
}
/// Sets the width.
pub fn width(mut self, width: LayoutRule) -> Self {
self.width = width;
self
}
}
impl Component for BasicNet {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, new_bounds: Rect) {
self.bounds = new_bounds;
}
}
impl Widget for BasicNet {
fn get_pretty_name(&self) -> &'static str {
"Network"
}
fn draw<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
) {
let block = Block::default()
.borders(*SIDE_BORDERS)
.border_style(painter.colours.highlighted_border_style);
let inner_area = block.inner(area);
const CONSTRAINTS: [Constraint; 2] = [Constraint::Ratio(1, 2); 2];
let split_area = Layout::default()
.direction(tui::layout::Direction::Horizontal)
.constraints(CONSTRAINTS)
.split(inner_area);
let texts = [
[
Spans::from(Span::styled(&self.rx_display, painter.colours.rx_style)),
Spans::from(Span::styled(&self.tx_display, painter.colours.tx_style)),
],
[
Spans::from(Span::styled(
&self.total_rx_display,
painter.colours.total_rx_style,
)),
Spans::from(Span::styled(
&self.total_tx_display,
painter.colours.total_tx_style,
)),
],
];
if selected {
f.render_widget(block, area);
}
IntoIterator::into_iter(texts)
.zip(split_area)
.for_each(|(text, area)| f.render_widget(Paragraph::new(text.to_vec()), area));
}
fn update_data(&mut self, data_collection: &DataCollection) {
let network_data = convert_network_data_points(
data_collection,
false, // TODO: I think the is_frozen here is also useless; see mem and cpu
true,
&AxisScaling::Linear,
&self.unit_type,
self.use_binary_prefix,
);
self.rx_display = format!("RX: {}", network_data.rx_display);
self.tx_display = format!("TX: {}", network_data.tx_display);
if let Some(total_rx_display) = network_data.total_rx_display {
self.total_rx_display = format!("Total RX: {}", total_rx_display);
}
if let Some(total_tx_display) = network_data.total_tx_display {
self.total_tx_display = format!("Total TX: {}", total_tx_display);
}
}
fn width(&self) -> LayoutRule {
self.width
}
fn height(&self) -> LayoutRule {
LayoutRule::Length { length: 2 }
}
}

View File

@ -1,14 +1,13 @@
use std::collections::HashMap;
use tui::layout::Rect;
use tui::{layout::Rect, widgets::Borders};
use crate::{
app::data_farmer::DataCollection,
app::{data_farmer::DataCollection, Component, Widget},
data_conversion::{convert_battery_harvest, ConvertedBatteryData},
options::layout_options::LayoutRule,
};
use super::{Component, Widget};
#[derive(Default)]
pub struct BatteryWidgetState {
pub currently_selected_battery_index: usize,
@ -36,30 +35,61 @@ impl BatteryState {
// TODO: Implement battery widget.
/// A table displaying battery information on a per-battery basis.
#[derive(Default)]
pub struct BatteryTable {
bounds: Rect,
selected_index: usize,
batteries: Vec<String>,
battery_data: Vec<ConvertedBatteryData>,
width: LayoutRule,
height: LayoutRule,
block_border: Borders,
}
impl Default for BatteryTable {
fn default() -> Self {
Self {
batteries: vec![],
bounds: Default::default(),
selected_index: 0,
battery_data: Default::default(),
width: LayoutRule::default(),
height: LayoutRule::default(),
block_border: Borders::ALL,
}
}
}
impl BatteryTable {
/// Creates a new [`BatteryTable`].
pub fn new(batteries: Vec<String>) -> Self {
Self {
batteries,
..Default::default()
}
/// Sets the width.
pub fn width(mut self, width: LayoutRule) -> Self {
self.width = width;
self
}
/// Sets the height.
pub fn height(mut self, height: LayoutRule) -> Self {
self.height = height;
self
}
/// Returns the index of the currently selected battery.
pub fn index(&self) -> usize {
self.selected_index
}
/// Returns a reference to the battery names.
pub fn batteries(&self) -> &[String] {
&self.batteries
}
/// Sets the block border style.
pub fn basic_mode(mut self, basic_mode: bool) -> Self {
if basic_mode {
self.block_border = *crate::constants::SIDE_BORDERS;
}
self
}
}
impl Component for BatteryTable {
@ -80,4 +110,12 @@ impl Widget for BatteryTable {
fn update_data(&mut self, data_collection: &DataCollection) {
self.battery_data = convert_battery_harvest(data_collection);
}
fn width(&self) -> LayoutRule {
self.width
}
fn height(&self) -> LayoutRule {
self.height
}
}

View File

@ -0,0 +1,209 @@
use std::borrow::Cow;
use crossterm::event::MouseEvent;
use indextree::NodeId;
use tui::{
backend::Backend,
layout::{Constraint, Layout, Rect},
text::{Span, Spans},
widgets::Paragraph,
Frame,
};
use crate::{
app::{
does_bound_intersect_coordinate, event::WidgetEventResult, Component, SelectableType,
Widget,
},
canvas::Painter,
options::layout_options::LayoutRule,
};
/// A container that "holds"" multiple [`BottomWidget`]s through their [`NodeId`]s.
#[derive(PartialEq, Eq)]
pub struct Carousel {
index: usize,
children: Vec<(NodeId, Cow<'static, str>)>,
bounds: Rect,
width: LayoutRule,
height: LayoutRule,
left_button_bounds: Rect,
right_button_bounds: Rect,
}
impl Carousel {
/// Creates a new [`Carousel`] with the specified children.
pub fn new(children: Vec<(NodeId, Cow<'static, str>)>) -> Self {
Self {
index: 0,
children,
bounds: Default::default(),
width: Default::default(),
height: Default::default(),
left_button_bounds: Default::default(),
right_button_bounds: Default::default(),
}
}
/// Sets the width.
pub fn width(mut self, width: LayoutRule) -> Self {
self.width = width;
self
}
/// Sets the height.
pub fn height(mut self, height: LayoutRule) -> Self {
self.height = height;
self
}
/// Adds a new child to a [`Carousel`].
pub fn add_child(&mut self, child: NodeId, name: Cow<'static, str>) {
self.children.push((child, name));
}
/// Returns the currently selected [`NodeId`] if possible.
pub fn get_currently_selected(&self) -> Option<NodeId> {
self.children.get(self.index).map(|i| i.0.clone())
}
fn get_next(&self) -> Option<&(NodeId, Cow<'static, str>)> {
self.children.get(if self.index + 1 == self.children.len() {
0
} else {
self.index + 1
})
}
fn get_prev(&self) -> Option<&(NodeId, Cow<'static, str>)> {
self.children.get(if self.index > 0 {
self.index - 1
} else {
self.children.len().saturating_sub(1)
})
}
fn increment_index(&mut self) {
if self.index + 1 == self.children.len() {
self.index = 0;
} else {
self.index += 1;
}
}
fn decrement_index(&mut self) {
if self.index > 0 {
self.index -= 1;
} else {
self.index = self.children.len().saturating_sub(1);
}
}
/// Draws the [`Carousel`] arrows, and returns back the remaining [`Rect`] to draw the child with.
pub fn draw_carousel<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect,
) -> Rect {
const CONSTRAINTS: [Constraint; 2] = [Constraint::Length(1), Constraint::Min(0)];
let split_area = Layout::default()
.constraints(CONSTRAINTS)
.direction(tui::layout::Direction::Vertical)
.split(area);
self.set_bounds(split_area[0]);
if let Some((_prev_id, prev_element_name)) = self.get_prev() {
let prev_arrow_text = Spans::from(Span::styled(
format!("{}", prev_element_name),
painter.colours.text_style,
));
self.left_button_bounds = Rect::new(
split_area[0].x,
split_area[0].y,
prev_arrow_text.width() as u16,
split_area[0].height,
);
f.render_widget(
Paragraph::new(vec![prev_arrow_text]).alignment(tui::layout::Alignment::Left),
split_area[0],
);
}
if let Some((_next_id, next_element_name)) = self.get_next() {
let next_arrow_text = Spans::from(Span::styled(
format!("{}", next_element_name),
painter.colours.text_style,
));
let width = next_arrow_text.width() as u16;
self.right_button_bounds = Rect::new(
split_area[0].right().saturating_sub(width + 1),
split_area[0].y,
width,
split_area[0].height,
);
f.render_widget(
Paragraph::new(vec![next_arrow_text]).alignment(tui::layout::Alignment::Right),
split_area[0],
);
}
split_area[1]
}
}
impl Component for Carousel {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, new_bounds: Rect) {
self.bounds = new_bounds;
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
match event.kind {
crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
let x = event.column;
let y = event.row;
if does_bound_intersect_coordinate(x, y, self.left_button_bounds) {
self.decrement_index();
WidgetEventResult::Redraw
} else if does_bound_intersect_coordinate(x, y, self.right_button_bounds) {
self.increment_index();
WidgetEventResult::Redraw
} else {
WidgetEventResult::NoRedraw
}
}
_ => WidgetEventResult::NoRedraw,
}
}
}
impl Widget for Carousel {
fn get_pretty_name(&self) -> &'static str {
"Carousel"
}
fn width(&self) -> LayoutRule {
self.width
}
fn height(&self) -> LayoutRule {
self.height
}
fn selectable_type(&self) -> SelectableType {
if let Some(node) = self.get_currently_selected() {
debug!("node: {:?}", node);
SelectableType::Redirect(node)
} else {
SelectableType::Unselectable
}
}
}

View File

@ -5,19 +5,20 @@ use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
widgets::{Block, Borders},
Frame,
};
use crate::{
app::{
event::WidgetEventResult, sort_text_table::SimpleSortableColumn, time_graph::TimeGraphData,
AppConfigFields, DataCollection,
AppConfigFields, AppScrollWidgetState, CanvasTableWidthState, Component, DataCollection,
TextTable, TimeGraph, Widget,
},
canvas::Painter,
data_conversion::{convert_cpu_data_points, ConvertedCpuData},
options::layout_options::LayoutRule,
};
use super::{AppScrollWidgetState, CanvasTableWidthState, Component, TextTable, TimeGraph, Widget};
pub struct CpuWidgetState {
pub current_display_time: u64,
pub is_legend_hidden: bool,
@ -87,6 +88,9 @@ pub struct CpuGraph {
display_data: Vec<ConvertedCpuData>,
load_avg_data: [f32; 3],
width: LayoutRule,
height: LayoutRule,
}
impl CpuGraph {
@ -113,8 +117,22 @@ impl CpuGraph {
selected: CpuGraphSelection::None,
display_data: Default::default(),
load_avg_data: [0.0; 3],
width: LayoutRule::default(),
height: LayoutRule::default(),
}
}
/// Sets the width.
pub fn width(mut self, width: LayoutRule) -> Self {
self.width = width;
self
}
/// Sets the height.
pub fn height(mut self, height: LayoutRule) -> Self {
self.height = height;
self
}
}
impl Component for CpuGraph {
@ -163,7 +181,7 @@ impl Widget for CpuGraph {
}
fn draw<B: Backend>(
&mut self, painter: &Painter, f: &mut tui::Frame<'_, B>, area: Rect, selected: bool,
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
) {
let constraints = match self.legend_position {
CpuGraphLegendPosition::Left => {
@ -304,4 +322,12 @@ impl Widget for CpuGraph {
convert_cpu_data_points(data_collection, &mut self.display_data, false); // TODO: Again, the "is_frozen" is probably useless
self.load_avg_data = data_collection.load_avg_harvest;
}
fn width(&self) -> LayoutRule {
self.width
}
fn height(&self) -> LayoutRule {
self.height
}
}

View File

@ -11,15 +11,12 @@ use tui::{
use crate::{
app::{
data_farmer::DataCollection, event::WidgetEventResult,
sort_text_table::SimpleSortableColumn,
sort_text_table::SimpleSortableColumn, text_table::TextTableData, AppScrollWidgetState,
CanvasTableWidthState, Component, TextTable, Widget,
},
canvas::Painter,
data_conversion::convert_disk_row,
};
use super::{
text_table::TextTableData, AppScrollWidgetState, CanvasTableWidthState, Component, TextTable,
Widget,
options::layout_options::LayoutRule,
};
pub struct DiskWidgetState {
@ -61,6 +58,10 @@ pub struct DiskTable {
bounds: Rect,
display_data: TextTableData,
width: LayoutRule,
height: LayoutRule,
block_border: Borders,
}
impl Default for DiskTable {
@ -79,10 +80,36 @@ impl Default for DiskTable {
table,
bounds: Rect::default(),
display_data: Default::default(),
width: LayoutRule::default(),
height: LayoutRule::default(),
block_border: Borders::ALL,
}
}
}
impl DiskTable {
/// Sets the width.
pub fn width(mut self, width: LayoutRule) -> Self {
self.width = width;
self
}
/// Sets the height.
pub fn height(mut self, height: LayoutRule) -> Self {
self.height = height;
self
}
/// Sets the block border style.
pub fn basic_mode(mut self, basic_mode: bool) -> Self {
if basic_mode {
self.block_border = *crate::constants::SIDE_BORDERS;
}
self
}
}
impl Component for DiskTable {
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
self.table.handle_key_event(event)
@ -103,7 +130,7 @@ impl Component for DiskTable {
impl Widget for DiskTable {
fn get_pretty_name(&self) -> &'static str {
"Disk"
"Disks"
}
fn draw<B: Backend>(
@ -115,7 +142,7 @@ impl Widget for DiskTable {
} else {
painter.colours.border_style
})
.borders(Borders::ALL);
.borders(self.block_border.clone());
self.table
.draw_tui_table(painter, f, &self.display_data, block, area, selected);
@ -124,4 +151,12 @@ impl Widget for DiskTable {
fn update_data(&mut self, data_collection: &DataCollection) {
self.display_data = convert_disk_row(data_collection);
}
fn width(&self) -> LayoutRule {
self.width
}
fn height(&self) -> LayoutRule {
self.height
}
}

View File

@ -0,0 +1,58 @@
use tui::layout::Rect;
use crate::{
app::{Component, Widget},
options::layout_options::LayoutRule,
};
pub struct Empty {
width: LayoutRule,
height: LayoutRule,
}
impl Default for Empty {
fn default() -> Self {
Self {
width: LayoutRule::default(),
height: LayoutRule::default(),
}
}
}
impl Empty {
/// Sets the width.
pub fn width(mut self, width: LayoutRule) -> Self {
self.width = width;
self
}
/// Sets the height.
pub fn height(mut self, height: LayoutRule) -> Self {
self.height = height;
self
}
}
impl Component for Empty {
fn bounds(&self) -> Rect {
// TODO: Maybe think of how to store this without making it available for clicking. Separate bounds out to the layout? Might
// need to keep the bounds calculations for some components, so maybe implement it specifically for them.
Rect::default()
}
fn set_bounds(&mut self, _new_bounds: Rect) {}
}
impl Widget for Empty {
fn get_pretty_name(&self) -> &'static str {
""
}
fn width(&self) -> LayoutRule {
self.width
}
fn height(&self) -> LayoutRule {
self.height
}
}

View File

@ -9,11 +9,11 @@ use tui::{
use crate::{
app::{event::WidgetEventResult, time_graph::TimeGraphData, DataCollection},
app::{Component, TimeGraph, Widget},
data_conversion::{convert_mem_data_points, convert_mem_labels, convert_swap_data_points},
options::layout_options::LayoutRule,
};
use super::{Component, TimeGraph, Widget};
pub struct MemWidgetState {
pub current_display_time: u64,
pub autohide_timer: Option<Instant>,
@ -60,6 +60,8 @@ pub struct MemGraph {
mem_data: Vec<(f64, f64)>,
swap_data: Vec<(f64, f64)>,
bounds: Rect,
width: LayoutRule,
height: LayoutRule,
}
impl MemGraph {
@ -72,8 +74,22 @@ impl MemGraph {
mem_data: Default::default(),
swap_data: Default::default(),
bounds: Rect::default(),
width: LayoutRule::default(),
height: LayoutRule::default(),
}
}
/// Sets the width.
pub fn width(mut self, width: LayoutRule) -> Self {
self.width = width;
self
}
/// Sets the height.
pub fn height(mut self, height: LayoutRule) -> Self {
self.height = height;
self
}
}
impl Component for MemGraph {
@ -152,4 +168,12 @@ impl Widget for MemGraph {
self.mem_labels = memory_labels;
self.swap_labels = swap_labels;
}
fn width(&self) -> LayoutRule {
self.width
}
fn height(&self) -> LayoutRule {
self.height
}
}

View File

@ -10,16 +10,15 @@ use tui::{
use crate::{
app::{
data_farmer::DataCollection, text_table::SimpleColumn, time_graph::TimeGraphData,
AppConfigFields, AxisScaling,
AppConfigFields, AxisScaling, Component, TextTable, TimeGraph, Widget,
},
canvas::Painter,
data_conversion::convert_network_data_points,
options::layout_options::LayoutRule,
units::data_units::DataUnit,
utils::gen_util::*,
};
use super::{Component, TextTable, TimeGraph, Widget};
pub struct NetWidgetState {
pub current_display_time: u64,
pub autohide_timer: Option<Instant>,
@ -436,6 +435,8 @@ pub struct NetGraph {
hide_legend: bool,
bounds: Rect,
width: LayoutRule,
height: LayoutRule,
}
impl NetGraph {
@ -457,6 +458,8 @@ impl NetGraph {
use_binary_prefix: app_config_fields.network_use_binary_prefix,
hide_legend: false,
bounds: Rect::default(),
width: LayoutRule::default(),
height: LayoutRule::default(),
}
}
@ -466,6 +469,18 @@ impl NetGraph {
self
}
/// Sets the width.
pub fn width(mut self, width: LayoutRule) -> Self {
self.width = width;
self
}
/// Sets the height.
pub fn height(mut self, height: LayoutRule) -> Self {
self.height = height;
self
}
/// Sets the draw cache for a [`NetGraph`].
pub fn set_draw_cache(&mut self) {
let current_time = -(self.graph.get_current_display_time() as f64);
@ -613,6 +628,14 @@ impl Widget for NetGraph {
self.total_tx_display = total_tx_display;
}
}
fn width(&self) -> LayoutRule {
self.width
}
fn height(&self) -> LayoutRule {
self.height
}
}
/// A widget denoting network usage via a graph and a separate, single row table. This is built on [`NetGraph`],
@ -621,6 +644,8 @@ pub struct OldNetGraph {
net_graph: NetGraph,
table: TextTable,
bounds: Rect,
width: LayoutRule,
height: LayoutRule,
}
impl OldNetGraph {
@ -636,8 +661,22 @@ impl OldNetGraph {
])
.unselectable(),
bounds: Rect::default(),
width: LayoutRule::default(),
height: LayoutRule::default(),
}
}
/// Sets the width.
pub fn width(mut self, width: LayoutRule) -> Self {
self.width = width;
self
}
/// Sets the height.
pub fn height(mut self, height: LayoutRule) -> Self {
self.height = height;
self
}
}
impl Component for OldNetGraph {
@ -732,4 +771,12 @@ impl Widget for OldNetGraph {
self.net_graph.total_tx_display = total_tx_display;
}
}
fn width(&self) -> LayoutRule {
self.width
}
fn height(&self) -> LayoutRule {
self.height
}
}

View File

@ -17,17 +17,18 @@ use crate::{
data_harvester::processes::ProcessHarvest,
event::{MultiKey, MultiKeyResult, ReturnSignal, WidgetEventResult},
query::*,
text_table::DesiredColumnWidth,
DataCollection,
},
canvas::Painter,
data_conversion::get_string_with_bytes,
data_harvester::processes::{self, ProcessSorting},
options::ProcessDefaults,
options::{layout_options::LayoutRule, ProcessDefaults},
utils::error::BottomError,
};
use ProcessSorting::*;
use super::{
use crate::app::{
does_bound_intersect_coordinate,
sort_text_table::{SimpleSortableColumn, SortStatus, SortableColumn},
text_table::TextTableData,
@ -800,11 +801,11 @@ impl SortableColumn for ProcessSortColumn {
self.sortable_column.default_descending()
}
fn sorting_status(&self) -> super::sort_text_table::SortStatus {
fn sorting_status(&self) -> SortStatus {
self.sortable_column.sorting_status()
}
fn set_sorting_status(&mut self, sorting_status: super::sort_text_table::SortStatus) {
fn set_sorting_status(&mut self, sorting_status: SortStatus) {
self.sortable_column.set_sorting_status(sorting_status)
}
@ -812,7 +813,7 @@ impl SortableColumn for ProcessSortColumn {
self.sortable_column.display_name()
}
fn get_desired_width(&self) -> &super::text_table::DesiredColumnWidth {
fn get_desired_width(&self) -> &DesiredColumnWidth {
self.sortable_column.get_desired_width()
}
@ -847,6 +848,11 @@ pub struct ProcessManager {
display_data: TextTableData,
process_filter: Option<Result<Query, BottomError>>,
block_border: Borders,
width: LayoutRule,
height: LayoutRule,
}
impl ProcessManager {
@ -880,12 +886,36 @@ impl ProcessManager {
search_modifiers: SearchModifiers::default(),
display_data: Default::default(),
process_filter: None,
block_border: Borders::ALL,
width: LayoutRule::default(),
height: LayoutRule::default(),
};
manager.set_tree_mode(process_defaults.is_tree);
manager
}
/// Sets the block border style.
pub fn basic_mode(mut self, basic_mode: bool) -> Self {
if basic_mode {
self.block_border = *crate::constants::SIDE_BORDERS;
}
self
}
/// Sets the width.
pub fn width(mut self, width: LayoutRule) -> Self {
self.width = width;
self
}
/// Sets the height.
pub fn height(mut self, height: LayoutRule) -> Self {
self.height = height;
self
}
fn set_tree_mode(&mut self, in_tree_mode: bool) {
self.in_tree_mode = in_tree_mode;
}
@ -1277,7 +1307,7 @@ impl Widget for ProcessManager {
} else {
painter.colours.border_style
})
.borders(Borders::ALL);
.borders(self.block_border.clone());
self.process_table.draw_tui_table(
painter,
@ -1488,4 +1518,12 @@ impl Widget for ProcessManager {
})
.collect::<Vec<_>>();
}
fn width(&self) -> LayoutRule {
self.width
}
fn height(&self) -> LayoutRule {
self.height
}
}

View File

@ -11,15 +11,12 @@ use tui::{
use crate::{
app::{
data_farmer::DataCollection, data_harvester::temperature::TemperatureType,
event::WidgetEventResult, sort_text_table::SimpleSortableColumn,
event::WidgetEventResult, sort_text_table::SimpleSortableColumn, text_table::TextTableData,
AppScrollWidgetState, CanvasTableWidthState, Component, TextTable, Widget,
},
canvas::Painter,
data_conversion::convert_temp_row,
};
use super::{
text_table::TextTableData, AppScrollWidgetState, CanvasTableWidthState, Component, TextTable,
Widget,
options::layout_options::LayoutRule,
};
pub struct TempWidgetState {
@ -61,6 +58,9 @@ pub struct TempTable {
bounds: Rect,
display_data: TextTableData,
temp_type: TemperatureType,
width: LayoutRule,
height: LayoutRule,
block_border: Borders,
}
impl Default for TempTable {
@ -76,6 +76,9 @@ impl Default for TempTable {
bounds: Rect::default(),
display_data: Default::default(),
temp_type: TemperatureType::default(),
width: LayoutRule::default(),
height: LayoutRule::default(),
block_border: Borders::ALL,
}
}
}
@ -86,6 +89,27 @@ impl TempTable {
self.temp_type = temp_type;
self
}
/// Sets the width.
pub fn width(mut self, width: LayoutRule) -> Self {
self.width = width;
self
}
/// Sets the height.
pub fn height(mut self, height: LayoutRule) -> Self {
self.height = height;
self
}
/// Sets the block border style.
pub fn basic_mode(mut self, basic_mode: bool) -> Self {
if basic_mode {
self.block_border = *crate::constants::SIDE_BORDERS;
}
self
}
}
impl Component for TempTable {
@ -120,7 +144,7 @@ impl Widget for TempTable {
} else {
painter.colours.border_style
})
.borders(Borders::ALL); // TODO: Also do the scrolling indicator!
.borders(self.block_border.clone()); // TODO: Also do the scrolling indicator!
self.table
.draw_tui_table(painter, f, &self.display_data, block, area, selected);
@ -129,4 +153,12 @@ impl Widget for TempTable {
fn update_data(&mut self, data_collection: &DataCollection) {
self.display_data = convert_temp_row(data_collection, &self.temp_type);
}
fn width(&self) -> LayoutRule {
self.width
}
fn height(&self) -> LayoutRule {
self.height
}
}

View File

@ -1,2 +1,5 @@
pub mod custom_legend_chart;
pub use custom_legend_chart::TimeChart;
pub mod pipe_gauge;
pub use pipe_gauge::PipeGauge;

View File

@ -0,0 +1,138 @@
use tui::{
buffer::Buffer,
layout::Rect,
style::Style,
text::Spans,
widgets::{Block, Widget},
};
/// A widget to measure something, using pipe characters ('|') as a unit.
#[derive(Debug, Clone)]
pub struct PipeGauge<'a> {
block: Option<Block<'a>>,
ratio: f64,
start_label: Option<Spans<'a>>,
end_label: Option<Spans<'a>>,
style: Style,
gauge_style: Style,
}
impl<'a> Default for PipeGauge<'a> {
fn default() -> Self {
Self {
block: None,
ratio: 0.0,
start_label: None,
end_label: None,
style: Style::default(),
gauge_style: Style::default(),
}
}
}
impl<'a> PipeGauge<'a> {
pub fn ratio(mut self, ratio: f64) -> Self {
if ratio < 0.0 {
self.ratio = 0.0;
} else if ratio > 1.0 {
self.ratio = 1.0;
} else {
self.ratio = ratio;
}
self
}
pub fn start_label<T>(mut self, start_label: T) -> Self
where
T: Into<Spans<'a>>,
{
self.start_label = Some(start_label.into());
self
}
pub fn end_label<T>(mut self, end_label: T) -> Self
where
T: Into<Spans<'a>>,
{
self.end_label = Some(end_label.into());
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn gauge_style(mut self, style: Style) -> Self {
self.gauge_style = style;
self
}
}
impl<'a> Widget for PipeGauge<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
let gauge_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
if gauge_area.height < 1 {
return;
}
let ratio = self.ratio;
let start_label = self
.start_label
.unwrap_or_else(move || Spans::from(format!("{:.0}%", ratio * 100.0)));
let (col, row) = buf.set_spans(
gauge_area.left(),
gauge_area.top(),
&start_label,
gauge_area.width,
);
let (col, row) = buf.set_spans(col, row, &Spans::from("["), gauge_area.width);
let start = col;
if start >= gauge_area.right() {
return;
}
let end_label = self.end_label.unwrap_or_default();
let gauge_end: u16 =
if (end_label.width() as u16) < (gauge_area.right().saturating_sub(start + 1)) {
let gauge_end = gauge_area
.right()
.saturating_sub(end_label.width() as u16 + 1);
{
let (col, row) = buf.set_spans(gauge_end, row, &end_label, gauge_area.width);
let _ = buf.set_spans(col, row, &Spans::from("]"), gauge_area.width);
}
gauge_end
} else {
let gauge_end = gauge_area.right().saturating_sub(1);
let _ = buf.set_spans(gauge_end, row, &Spans::from("]"), gauge_area.width);
gauge_end
};
let end = start + (f64::from(gauge_end.saturating_sub(start)) * self.ratio).floor() as u16;
for col in start..end {
buf.get_mut(col, row).set_symbol("|").set_style(Style {
fg: self.gauge_style.fg,
bg: None,
add_modifier: self.gauge_style.add_modifier,
sub_modifier: self.gauge_style.sub_modifier,
});
}
for col in end..gauge_end {
buf.get_mut(col, row).set_symbol(" ");
}
}
}

View File

@ -42,12 +42,7 @@ fn main() -> Result<()> {
let mut app = build_app(&matches, &mut config)?;
// Create painter and set colours.
let mut painter = canvas::Painter::init(
app.app_config_fields.table_gap,
app.app_config_fields.use_basic_mode,
&config,
get_color_scheme(&matches, &config)?,
)?;
let mut painter = canvas::Painter::init(&config, get_color_scheme(&matches, &config)?)?;
// Create termination mutex and cvar
#[allow(clippy::mutex_atomic)]

View File

@ -18,7 +18,7 @@ use dialogs::*;
use crate::{
app::{
self,
layout_manager::LayoutNode,
layout_manager::{generate_layout, ColLayout, LayoutNode, RowLayout},
text_table::TextTableData,
widgets::{Component, Widget},
TmpBottomWidget,
@ -97,20 +97,13 @@ impl FromStr for ColourScheme {
pub struct Painter {
pub colours: CanvasColours,
styled_help_text: Vec<Spans<'static>>,
is_mac_os: bool, // FIXME: This feels out of place...
table_height_offset: u16,
}
impl Painter {
pub fn init(
table_gap: u16, is_basic_mode: bool, config: &Config, colour_scheme: ColourScheme,
) -> anyhow::Result<Self> {
pub fn init(config: &Config, colour_scheme: ColourScheme) -> anyhow::Result<Self> {
let mut painter = Painter {
colours: CanvasColours::default(),
styled_help_text: Vec::default(),
is_mac_os: cfg!(target_os = "macos"),
table_height_offset: if is_basic_mode { 2 } else { 4 } + table_gap,
};
if let ColourScheme::Custom = colour_scheme {
@ -350,149 +343,110 @@ impl Painter {
current_widget.draw(self, f, draw_area, true);
}
} else {
/// A simple traversal through the `arena`.
/// A simple traversal through the `arena`, drawing all leaf elements.
fn traverse_and_draw_tree<B: Backend>(
node: NodeId, arena: &Arena<LayoutNode>, area: Rect, f: &mut Frame<'_, B>,
node: NodeId, arena: &Arena<LayoutNode>, f: &mut Frame<'_, B>,
lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, painter: &Painter,
canvas_data: &DisplayableData, selected_id: NodeId,
canvas_data: &DisplayableData, selected_id: NodeId, offset_x: u16,
offset_y: u16,
) {
if let Some(layout_node) = arena.get(node).map(|n| n.get()) {
match layout_node {
LayoutNode::Row(row) => {
let split_area = {
let initial_split = Layout::default()
.margin(0)
.direction(Direction::Horizontal)
.constraints(row.constraints.clone())
.split(area);
if initial_split.is_empty() {
vec![]
} else {
let mut checked_split =
Vec::with_capacity(initial_split.len());
let mut right_value = initial_split[0].right();
checked_split.push(initial_split[0]);
for rect in initial_split[1..].iter() {
if right_value == rect.left() {
right_value = rect.right();
checked_split.push(*rect);
} else {
let diff = rect.x.saturating_sub(right_value);
let new_rect = Rect::new(
right_value,
rect.y,
rect.width + diff,
rect.height,
);
right_value = new_rect.right();
checked_split.push(new_rect);
}
}
checked_split
}
};
for (child, child_area) in node.children(arena).zip(split_area) {
LayoutNode::Row(RowLayout { bound, .. })
| LayoutNode::Col(ColLayout { bound, .. }) => {
for child in node.children(arena) {
traverse_and_draw_tree(
child,
arena,
child_area,
f,
lookup_map,
painter,
canvas_data,
selected_id,
offset_x + bound.x,
offset_y + bound.y,
);
}
}
LayoutNode::Col(col) => {
let split_area = {
let initial_split = Layout::default()
.margin(0)
.direction(Direction::Vertical)
.constraints(col.constraints.clone())
.split(area);
LayoutNode::Widget(widget_layout) => {
let bound = widget_layout.bound;
let area = Rect::new(
bound.x + offset_x,
bound.y + offset_y,
bound.width,
bound.height,
);
if initial_split.is_empty() {
vec![]
} else {
let mut checked_split =
Vec::with_capacity(initial_split.len());
let mut bottom_value = initial_split[0].bottom();
checked_split.push(initial_split[0]);
for rect in initial_split[1..].iter() {
if bottom_value == rect.top() {
bottom_value = rect.bottom();
checked_split.push(*rect);
} else {
let diff = rect.y.saturating_sub(bottom_value);
let new_rect = Rect::new(
rect.x,
bottom_value,
rect.width,
rect.height + diff,
);
bottom_value = new_rect.bottom();
checked_split.push(new_rect);
}
}
checked_split
}
};
for (child, child_area) in node.children(arena).zip(split_area) {
traverse_and_draw_tree(
child,
arena,
child_area,
f,
lookup_map,
painter,
canvas_data,
selected_id,
);
}
}
LayoutNode::Widget => {
if let Some(widget) = lookup_map.get_mut(&node) {
widget.set_bounds(area);
widget.draw(painter, f, area, selected_id == node);
// debug!(
// "Original bound: {:?}, offset_x: {}, offset_y: {}, area: {:?}, widget: {}",
// bound,
// offset_x,
// offset_y,
// area,
// widget.get_pretty_name()
// );
if let TmpBottomWidget::Carousel(carousel) = widget {
let carousel: &mut crate::app::widgets::Carousel =
carousel.into();
let remaining_area =
carousel.draw_carousel(painter, f, area);
if let Some(to_draw_node) =
carousel.get_currently_selected()
{
if let Some(child_widget) =
lookup_map.get_mut(&to_draw_node)
{
debug!(
"Selected ID: {:?}, to_draw_node: {:?}",
selected_id, to_draw_node
);
child_widget.set_bounds(remaining_area);
child_widget.draw(
painter,
f,
remaining_area,
selected_id == to_draw_node,
);
}
}
} else {
widget.set_bounds(area);
widget.draw(painter, f, area, selected_id == node);
}
}
}
}
}
}
if let Some(frozen_draw_loc) = frozen_draw_loc {
self.draw_frozen_indicator(&mut f, frozen_draw_loc);
}
let root = &app_state.layout_tree_root;
let arena = &app_state.layout_tree;
let arena = &mut app_state.layout_tree;
let canvas_data = &app_state.canvas_data;
let selected_id = app_state.selected_widget;
generate_layout(*root, arena, draw_area, &app_state.widget_lookup_map);
let lookup_map = &mut app_state.widget_lookup_map;
traverse_and_draw_tree(
*root,
arena,
draw_area,
&arena,
f,
lookup_map,
self,
canvas_data,
selected_id,
0,
0,
);
}
})?;
app_state.is_force_redraw = false;
app_state.is_determining_widget_boundary = false;
Ok(())
}
}

View File

@ -1,19 +1,15 @@
pub mod basic_table_arrows;
pub mod battery_display;
pub mod cpu_basic;
pub mod cpu_graph;
pub mod mem_basic;
pub mod mem_graph;
pub mod network_basic;
pub mod network_graph;
pub mod process_table;
pub use basic_table_arrows::*;
pub use battery_display::*;
pub use cpu_basic::*;
pub use cpu_graph::*;
pub use mem_basic::*;
pub use mem_graph::*;
pub use network_basic::*;
pub use network_graph::*;
pub use process_table::*;

View File

@ -380,148 +380,148 @@ fn draw_cpu_legend<B: Backend>(
painter: &Painter, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect,
widget_id: u64,
) {
let recalculate_column_widths = app_state.should_get_widget_bounds();
if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&(widget_id - 1)) {
cpu_widget_state.is_legend_hidden = false;
let cpu_data: &mut [ConvertedCpuData] = &mut app_state.canvas_data.cpu_data;
let cpu_table_state = &mut cpu_widget_state.scroll_state.table_state;
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
let start_position = get_start_position(
usize::from(
(draw_loc.height + (1 - table_gap)).saturating_sub(painter.table_height_offset),
),
&cpu_widget_state.scroll_state.scroll_direction,
&mut cpu_widget_state.scroll_state.previous_scroll_position,
cpu_widget_state.scroll_state.current_scroll_position,
app_state.is_force_redraw,
);
cpu_table_state.select(Some(
cpu_widget_state
.scroll_state
.current_scroll_position
.saturating_sub(start_position),
));
// let recalculate_column_widths = app_state.should_get_widget_bounds();
// if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&(widget_id - 1)) {
// cpu_widget_state.is_legend_hidden = false;
// let cpu_data: &mut [ConvertedCpuData] = &mut app_state.canvas_data.cpu_data;
// let cpu_table_state = &mut cpu_widget_state.scroll_state.table_state;
// let is_on_widget = widget_id == app_state.current_widget.widget_id;
// let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
// 0
// } else {
// app_state.app_config_fields.table_gap
// };
// let start_position = get_start_position(
// usize::from(
// (draw_loc.height + (1 - table_gap)).saturating_sub(painter.table_height_offset),
// ),
// &cpu_widget_state.scroll_state.scroll_direction,
// &mut cpu_widget_state.scroll_state.previous_scroll_position,
// cpu_widget_state.scroll_state.current_scroll_position,
// app_state.is_force_redraw,
// );
// cpu_table_state.select(Some(
// cpu_widget_state
// .scroll_state
// .current_scroll_position
// .saturating_sub(start_position),
// ));
let sliced_cpu_data = &cpu_data[start_position..];
// let sliced_cpu_data = &cpu_data[start_position..];
let offset_scroll_index = cpu_widget_state
.scroll_state
.current_scroll_position
.saturating_sub(start_position);
let show_avg_cpu = app_state.app_config_fields.show_average_cpu;
// let offset_scroll_index = cpu_widget_state
// .scroll_state
// .current_scroll_position
// .saturating_sub(start_position);
// let show_avg_cpu = app_state.app_config_fields.show_average_cpu;
// Calculate widths
if recalculate_column_widths {
cpu_widget_state.table_width_state.desired_column_widths = vec![6, 4];
cpu_widget_state.table_width_state.calculated_column_widths = get_column_widths(
draw_loc.width,
&[None, None],
&(CPU_LEGEND_HEADER_LENS
.iter()
.map(|width| Some(*width))
.collect::<Vec<_>>()),
&[Some(0.5), Some(0.5)],
&(cpu_widget_state
.table_width_state
.desired_column_widths
.iter()
.map(|width| Some(*width))
.collect::<Vec<_>>()),
false,
);
}
// // Calculate widths
// if recalculate_column_widths {
// cpu_widget_state.table_width_state.desired_column_widths = vec![6, 4];
// cpu_widget_state.table_width_state.calculated_column_widths = get_column_widths(
// draw_loc.width,
// &[None, None],
// &(CPU_LEGEND_HEADER_LENS
// .iter()
// .map(|width| Some(*width))
// .collect::<Vec<_>>()),
// &[Some(0.5), Some(0.5)],
// &(cpu_widget_state
// .table_width_state
// .desired_column_widths
// .iter()
// .map(|width| Some(*width))
// .collect::<Vec<_>>()),
// false,
// );
// }
let dcw = &cpu_widget_state.table_width_state.desired_column_widths;
let ccw = &cpu_widget_state.table_width_state.calculated_column_widths;
let cpu_rows = sliced_cpu_data.iter().enumerate().map(|(itx, cpu)| {
let mut truncated_name =
if let (Some(desired_column_width), Some(calculated_column_width)) =
(dcw.get(0), ccw.get(0))
{
if *desired_column_width > *calculated_column_width {
Text::raw(&cpu.short_cpu_name)
} else {
Text::raw(&cpu.cpu_name)
}
} else {
Text::raw(&cpu.cpu_name)
};
// let dcw = &cpu_widget_state.table_width_state.desired_column_widths;
// let ccw = &cpu_widget_state.table_width_state.calculated_column_widths;
// let cpu_rows = sliced_cpu_data.iter().enumerate().map(|(itx, cpu)| {
// let mut truncated_name =
// if let (Some(desired_column_width), Some(calculated_column_width)) =
// (dcw.get(0), ccw.get(0))
// {
// if *desired_column_width > *calculated_column_width {
// Text::raw(&cpu.short_cpu_name)
// } else {
// Text::raw(&cpu.cpu_name)
// }
// } else {
// Text::raw(&cpu.cpu_name)
// };
let is_first_column_hidden = if let Some(calculated_column_width) = ccw.get(0) {
*calculated_column_width == 0
} else {
false
};
// let is_first_column_hidden = if let Some(calculated_column_width) = ccw.get(0) {
// *calculated_column_width == 0
// } else {
// false
// };
let truncated_legend = if is_first_column_hidden && cpu.legend_value.is_empty() {
// For the case where we only have room for one column, display "All" in the normally blank area.
Text::raw("All")
} else {
Text::raw(&cpu.legend_value)
};
// let truncated_legend = if is_first_column_hidden && cpu.legend_value.is_empty() {
// // For the case where we only have room for one column, display "All" in the normally blank area.
// Text::raw("All")
// } else {
// Text::raw(&cpu.legend_value)
// };
if !is_first_column_hidden
&& itx == offset_scroll_index
&& itx + start_position == ALL_POSITION
{
truncated_name.patch_style(painter.colours.currently_selected_text_style);
Row::new(vec![truncated_name, truncated_legend])
} else {
let cpu_string_row = vec![truncated_name, truncated_legend];
// if !is_first_column_hidden
// && itx == offset_scroll_index
// && itx + start_position == ALL_POSITION
// {
// truncated_name.patch_style(painter.colours.currently_selected_text_style);
// Row::new(vec![truncated_name, truncated_legend])
// } else {
// let cpu_string_row = vec![truncated_name, truncated_legend];
Row::new(cpu_string_row).style(if itx == offset_scroll_index {
painter.colours.currently_selected_text_style
} else if itx + start_position == ALL_POSITION {
painter.colours.all_colour_style
} else if show_avg_cpu {
if itx + start_position == AVG_POSITION {
painter.colours.avg_colour_style
} else {
painter.colours.cpu_colour_styles[(itx + start_position - AVG_POSITION - 1)
% painter.colours.cpu_colour_styles.len()]
}
} else {
painter.colours.cpu_colour_styles[(itx + start_position - ALL_POSITION - 1)
% painter.colours.cpu_colour_styles.len()]
})
}
});
// Row::new(cpu_string_row).style(if itx == offset_scroll_index {
// painter.colours.currently_selected_text_style
// } else if itx + start_position == ALL_POSITION {
// painter.colours.all_colour_style
// } else if show_avg_cpu {
// if itx + start_position == AVG_POSITION {
// painter.colours.avg_colour_style
// } else {
// painter.colours.cpu_colour_styles[(itx + start_position - AVG_POSITION - 1)
// % painter.colours.cpu_colour_styles.len()]
// }
// } else {
// painter.colours.cpu_colour_styles[(itx + start_position - ALL_POSITION - 1)
// % painter.colours.cpu_colour_styles.len()]
// })
// }
// });
// Note we don't set highlight_style, as it should always be shown for this widget.
let border_and_title_style = if is_on_widget {
painter.colours.highlighted_border_style
} else {
painter.colours.border_style
};
// // Note we don't set highlight_style, as it should always be shown for this widget.
// let border_and_title_style = if is_on_widget {
// painter.colours.highlighted_border_style
// } else {
// painter.colours.border_style
// };
// Draw
f.render_stateful_widget(
Table::new(cpu_rows)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(border_and_title_style),
)
.header(
Row::new(CPU_LEGEND_HEADER.to_vec())
.style(painter.colours.table_header_style)
.bottom_margin(table_gap),
)
.widths(
&(cpu_widget_state
.table_width_state
.calculated_column_widths
.iter()
.map(|calculated_width| Constraint::Length(*calculated_width as u16))
.collect::<Vec<_>>()),
),
draw_loc,
cpu_table_state,
);
}
// // Draw
// f.render_stateful_widget(
// Table::new(cpu_rows)
// .block(
// Block::default()
// .borders(Borders::ALL)
// .border_style(border_and_title_style),
// )
// .header(
// Row::new(CPU_LEGEND_HEADER.to_vec())
// .style(painter.colours.table_header_style)
// .bottom_margin(table_gap),
// )
// .widths(
// &(cpu_widget_state
// .table_width_state
// .calculated_column_widths
// .iter()
// .map(|calculated_width| Constraint::Length(*calculated_width as u16))
// .collect::<Vec<_>>()),
// ),
// draw_loc,
// cpu_table_state,
// );
// }
}

View File

@ -1,4 +1,3 @@
use crate::app;
use std::cmp::{max, min};
/// Return a (hard)-width vector for column widths.
@ -117,85 +116,79 @@ pub fn get_column_widths(
filtered_column_widths
}
/// FIXME: [command move] This is a greedy method of determining column widths. This is reserved for columns where we are okay with
/// shoving information as far right as required.
// pub fn greedy_get_column_widths() -> Vec<u16> {
// vec![]
// pub fn get_search_start_position(
// num_columns: usize, cursor_direction: &app::CursorDirection, cursor_bar: &mut usize,
// current_cursor_position: usize, is_force_redraw: bool,
// ) -> usize {
// if is_force_redraw {
// *cursor_bar = 0;
// }
// match cursor_direction {
// app::CursorDirection::Right => {
// if current_cursor_position < *cursor_bar + num_columns {
// // If, using previous_scrolled_position, we can see the element
// // (so within that and + num_rows) just reuse the current previously scrolled position
// *cursor_bar
// } else if current_cursor_position >= num_columns {
// // Else if the current position past the last element visible in the list, omit
// // until we can see that element
// *cursor_bar = current_cursor_position - num_columns;
// *cursor_bar
// } else {
// // Else, if it is not past the last element visible, do not omit anything
// 0
// }
// }
// app::CursorDirection::Left => {
// if current_cursor_position <= *cursor_bar {
// // If it's past the first element, then show from that element downwards
// *cursor_bar = current_cursor_position;
// } else if current_cursor_position >= *cursor_bar + num_columns {
// *cursor_bar = current_cursor_position - num_columns;
// }
// // Else, don't change what our start position is from whatever it is set to!
// *cursor_bar
// }
// }
// }
pub fn get_search_start_position(
num_columns: usize, cursor_direction: &app::CursorDirection, cursor_bar: &mut usize,
current_cursor_position: usize, is_force_redraw: bool,
) -> usize {
if is_force_redraw {
*cursor_bar = 0;
}
// pub fn get_start_position(
// num_rows: usize, scroll_direction: &app::ScrollDirection, scroll_position_bar: &mut usize,
// currently_selected_position: usize, is_force_redraw: bool,
// ) -> usize {
// if is_force_redraw {
// *scroll_position_bar = 0;
// }
match cursor_direction {
app::CursorDirection::Right => {
if current_cursor_position < *cursor_bar + num_columns {
// If, using previous_scrolled_position, we can see the element
// (so within that and + num_rows) just reuse the current previously scrolled position
*cursor_bar
} else if current_cursor_position >= num_columns {
// Else if the current position past the last element visible in the list, omit
// until we can see that element
*cursor_bar = current_cursor_position - num_columns;
*cursor_bar
} else {
// Else, if it is not past the last element visible, do not omit anything
0
}
}
app::CursorDirection::Left => {
if current_cursor_position <= *cursor_bar {
// If it's past the first element, then show from that element downwards
*cursor_bar = current_cursor_position;
} else if current_cursor_position >= *cursor_bar + num_columns {
*cursor_bar = current_cursor_position - num_columns;
}
// Else, don't change what our start position is from whatever it is set to!
*cursor_bar
}
}
}
pub fn get_start_position(
num_rows: usize, scroll_direction: &app::ScrollDirection, scroll_position_bar: &mut usize,
currently_selected_position: usize, is_force_redraw: bool,
) -> usize {
if is_force_redraw {
*scroll_position_bar = 0;
}
match scroll_direction {
app::ScrollDirection::Down => {
if currently_selected_position < *scroll_position_bar + num_rows {
// If, using previous_scrolled_position, we can see the element
// (so within that and + num_rows) just reuse the current previously scrolled position
*scroll_position_bar
} else if currently_selected_position >= num_rows {
// Else if the current position past the last element visible in the list, omit
// until we can see that element
*scroll_position_bar = currently_selected_position - num_rows;
*scroll_position_bar
} else {
// Else, if it is not past the last element visible, do not omit anything
0
}
}
app::ScrollDirection::Up => {
if currently_selected_position <= *scroll_position_bar {
// If it's past the first element, then show from that element downwards
*scroll_position_bar = currently_selected_position;
} else if currently_selected_position >= *scroll_position_bar + num_rows {
*scroll_position_bar = currently_selected_position - num_rows;
}
// Else, don't change what our start position is from whatever it is set to!
*scroll_position_bar
}
}
}
// match scroll_direction {
// app::ScrollDirection::Down => {
// if currently_selected_position < *scroll_position_bar + num_rows {
// // If, using previous_scrolled_position, we can see the element
// // (so within that and + num_rows) just reuse the current previously scrolled position
// *scroll_position_bar
// } else if currently_selected_position >= num_rows {
// // Else if the current position past the last element visible in the list, omit
// // until we can see that element
// *scroll_position_bar = currently_selected_position - num_rows;
// *scroll_position_bar
// } else {
// // Else, if it is not past the last element visible, do not omit anything
// 0
// }
// }
// app::ScrollDirection::Up => {
// if currently_selected_position <= *scroll_position_bar {
// // If it's past the first element, then show from that element downwards
// *scroll_position_bar = currently_selected_position;
// } else if currently_selected_position >= *scroll_position_bar + num_rows {
// *scroll_position_bar = currently_selected_position - num_rows;
// }
// // Else, don't change what our start position is from whatever it is set to!
// *scroll_position_bar
// }
// }
// }
/// Calculate how many bars are to be
/// drawn within basic mode's components.

View File

@ -383,11 +383,11 @@ pub static HELP_TEXT: Lazy<Vec<Vec<&'static str>>> = Lazy::new(|| {
// Default layouts
pub const DEFAULT_LAYOUT: &str = r##"
[[row]]
ratio=30
ratio=32
[[row.child]]
type="cpu"
[[row]]
ratio=40
ratio=36
[[row.child]]
ratio=4
type="mem"
@ -398,7 +398,7 @@ pub const DEFAULT_LAYOUT: &str = r##"
[[row.child.child]]
type="disk"
[[row]]
ratio=30
ratio=32
[[row.child]]
type="net"
[[row.child]]
@ -408,15 +408,15 @@ pub const DEFAULT_LAYOUT: &str = r##"
pub const DEFAULT_BATTERY_LAYOUT: &str = r##"
[[row]]
ratio=30
ratio=32
[[row.child]]
ratio=2
type="cpu"
type="cpu"
[[row.child]]
ratio=1
type="battery"
type="battery"
[[row]]
ratio=40
ratio=36
[[row.child]]
ratio=4
type="mem"
@ -427,7 +427,7 @@ pub const DEFAULT_BATTERY_LAYOUT: &str = r##"
[[row.child.child]]
type="disk"
[[row]]
ratio=30
ratio=32
[[row.child]]
type="net"
[[row.child]]
@ -437,6 +437,42 @@ pub const DEFAULT_BATTERY_LAYOUT: &str = r##"
pub const DEFAULT_BASIC_LAYOUT: &str = r##"
[[row]]
[[row.child]]
type = "bcpu"
[[row]]
[[row.child]]
type = "bmem"
[[row.child]]
type = "bnet"
[[row]]
[[row.child]]
[[row.child.child]]
length = 1
type = "empty"
[[row]]
[[row.child]]
default = true
carousel_children = ["proc", "temp", "disk"]
"##;
pub const DEFAULT_BASIC_BATTERY_LAYOUT: &str = r##"
[[row]]
[[row.child]]
type = "bcpu"
[[row]]
[[row.child]]
type = "bmem"
[[row.child]]
type = "bnet"
[[row]]
[[row.child]]
[[row.child.child]]
length = 1
type = "empty"
[[row]]
[[row.child]]
default = true
carousel_children = ["proc", "temp", "disk", "battery"]
"##;
// Config and flags

View File

@ -238,10 +238,18 @@ pub fn build_app(matches: &clap::ArgMatches<'static>, config: &mut Config) -> Re
network_use_binary_prefix,
};
let layout_tree_output = if get_use_basic_mode(matches, config) {
todo!()
} else if let Some(row) = &config.row {
let layout_tree_output = if let Some(row) = &config.row {
create_layout_tree(row, process_defaults, &app_config_fields)?
} else if get_use_basic_mode(matches, config) {
if get_use_battery(matches, config) {
let rows = toml::from_str::<Config>(DEFAULT_BASIC_BATTERY_LAYOUT)?
.row
.unwrap();
create_layout_tree(&rows, process_defaults, &app_config_fields)?
} else {
let rows = toml::from_str::<Config>(DEFAULT_BASIC_LAYOUT)?.row.unwrap();
create_layout_tree(&rows, process_defaults, &app_config_fields)?
}
} else if get_use_battery(matches, config) {
let rows = toml::from_str::<Config>(DEFAULT_BATTERY_LAYOUT)?
.row
@ -295,70 +303,70 @@ pub fn build_app(matches: &clap::ArgMatches<'static>, config: &mut Config) -> Re
))
}
pub fn get_widget_layout(
matches: &clap::ArgMatches<'static>, config: &Config,
) -> error::Result<(BottomLayout, u64, Option<BottomWidgetType>)> {
let left_legend = get_use_left_legend(matches, config);
let (default_widget_type, mut default_widget_count) =
get_default_widget_and_count(matches, config)?;
let mut default_widget_id = 1;
// pub fn get_widget_layout(
// matches: &clap::ArgMatches<'static>, config: &Config,
// ) -> error::Result<(BottomLayout, u64, Option<BottomWidgetType>)> {
// let left_legend = get_use_left_legend(matches, config);
// let (default_widget_type, mut default_widget_count) =
// get_default_widget_and_count(matches, config)?;
// let mut default_widget_id = 1;
let bottom_layout = if get_use_basic_mode(matches, config) {
default_widget_id = DEFAULT_WIDGET_ID;
// let bottom_layout = if get_use_basic_mode(matches, config) {
// default_widget_id = DEFAULT_WIDGET_ID;
BottomLayout::init_basic_default(get_use_battery(matches, config))
} else {
let ref_row: Vec<Row>; // Required to handle reference
let rows = match &config.row {
Some(r) => r,
None => {
// This cannot (like it really shouldn't) fail!
ref_row = toml::from_str::<Config>(if get_use_battery(matches, config) {
DEFAULT_BATTERY_LAYOUT
} else {
DEFAULT_LAYOUT
})?
.row
.unwrap();
&ref_row
}
};
// BottomLayout::init_basic_default(get_use_battery(matches, config))
// } else {
// let ref_row: Vec<Row>; // Required to handle reference
// let rows = match &config.row {
// Some(r) => r,
// None => {
// // This cannot (like it really shouldn't) fail!
// ref_row = toml::from_str::<Config>(if get_use_battery(matches, config) {
// DEFAULT_BATTERY_LAYOUT
// } else {
// DEFAULT_LAYOUT
// })?
// .row
// .unwrap();
// &ref_row
// }
// };
let mut iter_id = 0; // A lazy way of forcing unique IDs *shrugs*
let mut total_height_ratio = 0;
// let mut iter_id = 0; // A lazy way of forcing unique IDs *shrugs*
// let mut total_height_ratio = 0;
let mut ret_bottom_layout = BottomLayout {
rows: rows
.iter()
.map(|row| {
row.convert_row_to_bottom_row(
&mut iter_id,
&mut total_height_ratio,
&mut default_widget_id,
&default_widget_type,
&mut default_widget_count,
left_legend,
)
})
.collect::<error::Result<Vec<_>>>()?,
total_row_height_ratio: total_height_ratio,
};
// let mut ret_bottom_layout = BottomLayout {
// rows: rows
// .iter()
// .map(|row| {
// row.convert_row_to_bottom_row(
// &mut iter_id,
// &mut total_height_ratio,
// &mut default_widget_id,
// &default_widget_type,
// &mut default_widget_count,
// left_legend,
// )
// })
// .collect::<error::Result<Vec<_>>>()?,
// total_row_height_ratio: total_height_ratio,
// };
// Confirm that we have at least ONE widget left - if not, error out!
if iter_id > 0 {
ret_bottom_layout.get_movement_mappings();
// debug!("Bottom layout: {:#?}", ret_bottom_layout);
// // Confirm that we have at least ONE widget left - if not, error out!
// if iter_id > 0 {
// ret_bottom_layout.get_movement_mappings();
// // debug!("Bottom layout: {:#?}", ret_bottom_layout);
ret_bottom_layout
} else {
return Err(error::BottomError::ConfigError(
"please have at least one widget under the '[[row]]' section.".to_string(),
));
}
};
// ret_bottom_layout
// } else {
// return Err(error::BottomError::ConfigError(
// "please have at least one widget under the '[[row]]' section.".to_string(),
// ));
// }
// };
Ok((bottom_layout, default_widget_id, default_widget_type))
}
// Ok((bottom_layout, default_widget_id, default_widget_type))
// }
fn get_update_rate_in_milliseconds(
matches: &clap::ArgMatches<'static>, config: &Config,

View File

@ -1,5 +1,3 @@
use crate::app::layout_manager::*;
use crate::error::Result;
use serde::{Deserialize, Serialize};
/// Represents a row. This has a length of some sort (optional) and a vector
@ -7,327 +5,331 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Deserialize, Debug, Serialize)]
#[serde(rename = "row")]
pub struct Row {
pub ratio: Option<u32>,
pub child: Option<Vec<RowChildren>>,
pub ratio: Option<u32>,
}
impl Row {
pub fn convert_row_to_bottom_row(
&self, iter_id: &mut u64, total_height_ratio: &mut u32, default_widget_id: &mut u64,
default_widget_type: &Option<BottomWidgetType>, default_widget_count: &mut u64,
left_legend: bool,
) -> Result<OldBottomRow> {
// TODO: In the future we want to also add percentages.
// But for MVP, we aren't going to bother.
let row_ratio = self.ratio.unwrap_or(1);
let mut children = Vec::new();
// impl Row {
// pub fn convert_row_to_bottom_row(
// &self, iter_id: &mut u64, total_height_ratio: &mut u32, default_widget_id: &mut u64,
// default_widget_type: &Option<BottomWidgetType>, default_widget_count: &mut u64,
// left_legend: bool,
// ) -> Result<OldBottomRow> {
// // TODO: In the future we want to also add percentages.
// // But for MVP, we aren't going to bother.
// let row_ratio = self.ratio.unwrap_or(1);
// let mut children = Vec::new();
*total_height_ratio += row_ratio;
// *total_height_ratio += row_ratio;
let mut total_col_ratio = 0;
if let Some(row_children) = &self.child {
for row_child in row_children {
match row_child {
RowChildren::Widget(widget) => {
*iter_id += 1;
let width_ratio = widget.ratio.unwrap_or(1);
total_col_ratio += width_ratio;
let widget_type = widget.widget_type.parse::<BottomWidgetType>()?;
// let mut total_col_ratio = 0;
// if let Some(row_children) = &self.child {
// for row_child in row_children {
// match row_child {
// RowChildren::Widget(widget) => {
// *iter_id += 1;
// let width_ratio = widget.ratio.unwrap_or(1);
// total_col_ratio += width_ratio;
// let widget_type = widget.widget_type.parse::<BottomWidgetType>()?;
if let Some(default_widget_type_val) = default_widget_type {
if *default_widget_type_val == widget_type && *default_widget_count > 0
{
*default_widget_count -= 1;
if *default_widget_count == 0 {
*default_widget_id = *iter_id;
}
}
} else {
// Check default flag
if let Some(default_widget_flag) = widget.default {
if default_widget_flag {
*default_widget_id = *iter_id;
}
}
}
// if let Some(default_widget_type_val) = default_widget_type {
// if *default_widget_type_val == widget_type && *default_widget_count > 0
// {
// *default_widget_count -= 1;
// if *default_widget_count == 0 {
// *default_widget_id = *iter_id;
// }
// }
// } else {
// // Check default flag
// if let Some(default_widget_flag) = widget.default {
// if default_widget_flag {
// *default_widget_id = *iter_id;
// }
// }
// }
children.push(match widget_type {
BottomWidgetType::Cpu => {
let cpu_id = *iter_id;
*iter_id += 1;
OldBottomCol::builder()
.col_width_ratio(width_ratio)
.children(if left_legend {
vec![BottomColRow::builder()
.total_widget_ratio(20)
.children(vec![
BottomWidget::builder()
.width_ratio(3)
.widget_type(BottomWidgetType::CpuLegend)
.widget_id(*iter_id)
.canvas_handle_width(true)
.parent_reflector(Some((
WidgetDirection::Right,
1,
)))
.build(),
BottomWidget::builder()
.width_ratio(17)
.widget_type(BottomWidgetType::Cpu)
.widget_id(cpu_id)
.flex_grow(true)
.build(),
])
.build()]
} else {
vec![BottomColRow::builder()
.total_widget_ratio(20)
.children(vec![
BottomWidget::builder()
.width_ratio(17)
.widget_type(BottomWidgetType::Cpu)
.widget_id(cpu_id)
.flex_grow(true)
.build(),
BottomWidget::builder()
.width_ratio(3)
.widget_type(BottomWidgetType::CpuLegend)
.widget_id(*iter_id)
.canvas_handle_width(true)
.parent_reflector(Some((
WidgetDirection::Left,
1,
)))
.build(),
])
.build()]
})
.build()
}
BottomWidgetType::Proc => {
let proc_id = *iter_id;
let proc_search_id = *iter_id + 1;
*iter_id += 2;
OldBottomCol::builder()
.total_col_row_ratio(2)
.col_width_ratio(width_ratio)
.children(vec![
BottomColRow::builder()
.children(vec![
BottomWidget::builder()
.widget_type(BottomWidgetType::ProcSort)
.widget_id(*iter_id)
.canvas_handle_width(true)
.parent_reflector(Some((
WidgetDirection::Right,
2,
)))
.width_ratio(1)
.build(),
BottomWidget::builder()
.widget_type(BottomWidgetType::Proc)
.widget_id(proc_id)
.width_ratio(2)
.build(),
])
.total_widget_ratio(3)
.flex_grow(true)
.build(),
BottomColRow::builder()
.children(vec![BottomWidget::builder()
.widget_type(BottomWidgetType::ProcSearch)
.widget_id(proc_search_id)
.parent_reflector(Some((WidgetDirection::Up, 1)))
.build()])
.canvas_handle_height(true)
.build(),
])
.build()
}
_ => OldBottomCol::builder()
.col_width_ratio(width_ratio)
.children(vec![BottomColRow::builder()
.children(vec![BottomWidget::builder()
.widget_type(widget_type)
.widget_id(*iter_id)
.build()])
.build()])
.build(),
});
}
RowChildren::Col { ratio, child } => {
let col_width_ratio = ratio.unwrap_or(1);
total_col_ratio += col_width_ratio;
let mut total_col_row_ratio = 0;
let mut contains_proc = false;
// children.push(match widget_type {
// BottomWidgetType::Cpu => {
// let cpu_id = *iter_id;
// *iter_id += 1;
// OldBottomCol::builder()
// .col_width_ratio(width_ratio)
// .children(if left_legend {
// vec![BottomColRow::builder()
// .total_widget_ratio(20)
// .children(vec![
// BottomWidget::builder()
// .width_ratio(3)
// .widget_type(BottomWidgetType::CpuLegend)
// .widget_id(*iter_id)
// .canvas_handle_width(true)
// .parent_reflector(Some((
// WidgetDirection::Right,
// 1,
// )))
// .build(),
// BottomWidget::builder()
// .width_ratio(17)
// .widget_type(BottomWidgetType::Cpu)
// .widget_id(cpu_id)
// .flex_grow(true)
// .build(),
// ])
// .build()]
// } else {
// vec![BottomColRow::builder()
// .total_widget_ratio(20)
// .children(vec![
// BottomWidget::builder()
// .width_ratio(17)
// .widget_type(BottomWidgetType::Cpu)
// .widget_id(cpu_id)
// .flex_grow(true)
// .build(),
// BottomWidget::builder()
// .width_ratio(3)
// .widget_type(BottomWidgetType::CpuLegend)
// .widget_id(*iter_id)
// .canvas_handle_width(true)
// .parent_reflector(Some((
// WidgetDirection::Left,
// 1,
// )))
// .build(),
// ])
// .build()]
// })
// .build()
// }
// BottomWidgetType::Proc => {
// let proc_id = *iter_id;
// let proc_search_id = *iter_id + 1;
// *iter_id += 2;
// OldBottomCol::builder()
// .total_col_row_ratio(2)
// .col_width_ratio(width_ratio)
// .children(vec![
// BottomColRow::builder()
// .children(vec![
// BottomWidget::builder()
// .widget_type(BottomWidgetType::ProcSort)
// .widget_id(*iter_id)
// .canvas_handle_width(true)
// .parent_reflector(Some((
// WidgetDirection::Right,
// 2,
// )))
// .width_ratio(1)
// .build(),
// BottomWidget::builder()
// .widget_type(BottomWidgetType::Proc)
// .widget_id(proc_id)
// .width_ratio(2)
// .build(),
// ])
// .total_widget_ratio(3)
// .flex_grow(true)
// .build(),
// BottomColRow::builder()
// .children(vec![BottomWidget::builder()
// .widget_type(BottomWidgetType::ProcSearch)
// .widget_id(proc_search_id)
// .parent_reflector(Some((WidgetDirection::Up, 1)))
// .build()])
// .canvas_handle_height(true)
// .build(),
// ])
// .build()
// }
// _ => OldBottomCol::builder()
// .col_width_ratio(width_ratio)
// .children(vec![BottomColRow::builder()
// .children(vec![BottomWidget::builder()
// .widget_type(widget_type)
// .widget_id(*iter_id)
// .build()])
// .build()])
// .build(),
// });
// }
// RowChildren::Col { ratio, child } => {
// let col_width_ratio = ratio.unwrap_or(1);
// total_col_ratio += col_width_ratio;
// let mut total_col_row_ratio = 0;
// let mut contains_proc = false;
let mut col_row_children: Vec<BottomColRow> = Vec::new();
// let mut col_row_children: Vec<BottomColRow> = Vec::new();
for widget in child {
let widget_type = widget.widget_type.parse::<BottomWidgetType>()?;
*iter_id += 1;
let col_row_height_ratio = widget.ratio.unwrap_or(1);
total_col_row_ratio += col_row_height_ratio;
// for widget in child {
// let widget_type = widget.widget_type.parse::<BottomWidgetType>()?;
// *iter_id += 1;
// let col_row_height_ratio = widget.ratio.unwrap_or(1);
// total_col_row_ratio += col_row_height_ratio;
if let Some(default_widget_type_val) = default_widget_type {
if *default_widget_type_val == widget_type
&& *default_widget_count > 0
{
*default_widget_count -= 1;
if *default_widget_count == 0 {
*default_widget_id = *iter_id;
}
}
} else {
// Check default flag
if let Some(default_widget_flag) = widget.default {
if default_widget_flag {
*default_widget_id = *iter_id;
}
}
}
// if let Some(default_widget_type_val) = default_widget_type {
// if *default_widget_type_val == widget_type
// && *default_widget_count > 0
// {
// *default_widget_count -= 1;
// if *default_widget_count == 0 {
// *default_widget_id = *iter_id;
// }
// }
// } else {
// // Check default flag
// if let Some(default_widget_flag) = widget.default {
// if default_widget_flag {
// *default_widget_id = *iter_id;
// }
// }
// }
match widget_type {
BottomWidgetType::Cpu => {
let cpu_id = *iter_id;
*iter_id += 1;
if left_legend {
col_row_children.push(
BottomColRow::builder()
.col_row_height_ratio(col_row_height_ratio)
.total_widget_ratio(20)
.children(vec![
BottomWidget::builder()
.width_ratio(3)
.widget_type(BottomWidgetType::CpuLegend)
.widget_id(*iter_id)
.canvas_handle_width(true)
.parent_reflector(Some((
WidgetDirection::Right,
1,
)))
.build(),
BottomWidget::builder()
.width_ratio(17)
.widget_type(BottomWidgetType::Cpu)
.widget_id(cpu_id)
.flex_grow(true)
.build(),
])
.build(),
);
} else {
col_row_children.push(
BottomColRow::builder()
.col_row_height_ratio(col_row_height_ratio)
.total_widget_ratio(20)
.children(vec![
BottomWidget::builder()
.width_ratio(17)
.widget_type(BottomWidgetType::Cpu)
.widget_id(cpu_id)
.flex_grow(true)
.build(),
BottomWidget::builder()
.width_ratio(3)
.widget_type(BottomWidgetType::CpuLegend)
.widget_id(*iter_id)
.canvas_handle_width(true)
.parent_reflector(Some((
WidgetDirection::Left,
1,
)))
.build(),
])
.build(),
);
}
}
BottomWidgetType::Proc => {
contains_proc = true;
let proc_id = *iter_id;
let proc_search_id = *iter_id + 1;
*iter_id += 2;
col_row_children.push(
BottomColRow::builder()
.children(vec![
BottomWidget::builder()
.widget_type(BottomWidgetType::ProcSort)
.widget_id(*iter_id)
.canvas_handle_width(true)
.parent_reflector(Some((
WidgetDirection::Right,
2,
)))
.width_ratio(1)
.build(),
BottomWidget::builder()
.widget_type(BottomWidgetType::Proc)
.widget_id(proc_id)
.width_ratio(2)
.build(),
])
.col_row_height_ratio(col_row_height_ratio)
.total_widget_ratio(3)
.build(),
);
col_row_children.push(
BottomColRow::builder()
.col_row_height_ratio(col_row_height_ratio)
.children(vec![BottomWidget::builder()
.widget_type(BottomWidgetType::ProcSearch)
.widget_id(proc_search_id)
.parent_reflector(Some((WidgetDirection::Up, 1)))
.build()])
.canvas_handle_height(true)
.build(),
);
}
_ => col_row_children.push(
BottomColRow::builder()
.col_row_height_ratio(col_row_height_ratio)
.children(vec![BottomWidget::builder()
.widget_type(widget_type)
.widget_id(*iter_id)
.build()])
.build(),
),
}
}
// match widget_type {
// BottomWidgetType::Cpu => {
// let cpu_id = *iter_id;
// *iter_id += 1;
// if left_legend {
// col_row_children.push(
// BottomColRow::builder()
// .col_row_height_ratio(col_row_height_ratio)
// .total_widget_ratio(20)
// .children(vec![
// BottomWidget::builder()
// .width_ratio(3)
// .widget_type(BottomWidgetType::CpuLegend)
// .widget_id(*iter_id)
// .canvas_handle_width(true)
// .parent_reflector(Some((
// WidgetDirection::Right,
// 1,
// )))
// .build(),
// BottomWidget::builder()
// .width_ratio(17)
// .widget_type(BottomWidgetType::Cpu)
// .widget_id(cpu_id)
// .flex_grow(true)
// .build(),
// ])
// .build(),
// );
// } else {
// col_row_children.push(
// BottomColRow::builder()
// .col_row_height_ratio(col_row_height_ratio)
// .total_widget_ratio(20)
// .children(vec![
// BottomWidget::builder()
// .width_ratio(17)
// .widget_type(BottomWidgetType::Cpu)
// .widget_id(cpu_id)
// .flex_grow(true)
// .build(),
// BottomWidget::builder()
// .width_ratio(3)
// .widget_type(BottomWidgetType::CpuLegend)
// .widget_id(*iter_id)
// .canvas_handle_width(true)
// .parent_reflector(Some((
// WidgetDirection::Left,
// 1,
// )))
// .build(),
// ])
// .build(),
// );
// }
// }
// BottomWidgetType::Proc => {
// contains_proc = true;
// let proc_id = *iter_id;
// let proc_search_id = *iter_id + 1;
// *iter_id += 2;
// col_row_children.push(
// BottomColRow::builder()
// .children(vec![
// BottomWidget::builder()
// .widget_type(BottomWidgetType::ProcSort)
// .widget_id(*iter_id)
// .canvas_handle_width(true)
// .parent_reflector(Some((
// WidgetDirection::Right,
// 2,
// )))
// .width_ratio(1)
// .build(),
// BottomWidget::builder()
// .widget_type(BottomWidgetType::Proc)
// .widget_id(proc_id)
// .width_ratio(2)
// .build(),
// ])
// .col_row_height_ratio(col_row_height_ratio)
// .total_widget_ratio(3)
// .build(),
// );
// col_row_children.push(
// BottomColRow::builder()
// .col_row_height_ratio(col_row_height_ratio)
// .children(vec![BottomWidget::builder()
// .widget_type(BottomWidgetType::ProcSearch)
// .widget_id(proc_search_id)
// .parent_reflector(Some((WidgetDirection::Up, 1)))
// .build()])
// .canvas_handle_height(true)
// .build(),
// );
// }
// _ => col_row_children.push(
// BottomColRow::builder()
// .col_row_height_ratio(col_row_height_ratio)
// .children(vec![BottomWidget::builder()
// .widget_type(widget_type)
// .widget_id(*iter_id)
// .build()])
// .build(),
// ),
// }
// }
if contains_proc {
// Must adjust ratios to work with proc
total_col_row_ratio *= 2;
for child in &mut col_row_children {
// Multiply all non-proc or proc-search ratios by 2
if !child.children.is_empty() {
match child.children[0].widget_type {
BottomWidgetType::ProcSearch => {}
_ => child.col_row_height_ratio *= 2,
}
}
}
}
// if contains_proc {
// // Must adjust ratios to work with proc
// total_col_row_ratio *= 2;
// for child in &mut col_row_children {
// // Multiply all non-proc or proc-search ratios by 2
// if !child.children.is_empty() {
// match child.children[0].widget_type {
// BottomWidgetType::ProcSearch => {}
// _ => child.col_row_height_ratio *= 2,
// }
// }
// }
// }
children.push(
OldBottomCol::builder()
.total_col_row_ratio(total_col_row_ratio)
.col_width_ratio(col_width_ratio)
.children(col_row_children)
.build(),
);
}
}
}
}
// children.push(
// OldBottomCol::builder()
// .total_col_row_ratio(total_col_row_ratio)
// .col_width_ratio(col_width_ratio)
// .children(col_row_children)
// .build(),
// );
// }
// RowChildren::Carousel {
// carousel_children: _,
// default: _,
// } => {}
// }
// }
// }
Ok(OldBottomRow::builder()
.total_col_ratio(total_col_ratio)
.row_height_ratio(row_ratio)
.children(children)
.build())
}
}
// Ok(OldBottomRow::builder()
// .total_col_ratio(total_col_ratio)
// .row_height_ratio(row_ratio)
// .children(children)
// .build())
// }
// }
/// Represents a child of a Row - either a Col (column) or a FinalWidget.
///
@ -338,6 +340,11 @@ impl Row {
#[serde(untagged)]
pub enum RowChildren {
Widget(FinalWidget),
/// The first one in the list is the "default" selected widget.
Carousel {
carousel_children: Vec<String>,
default: Option<bool>,
},
Col {
ratio: Option<u32>,
child: Vec<FinalWidget>,
@ -347,8 +354,24 @@ pub enum RowChildren {
/// Represents a widget.
#[derive(Clone, Deserialize, Debug, Serialize)]
pub struct FinalWidget {
pub ratio: Option<u32>,
#[serde(flatten)]
pub rule: Option<LayoutRule>,
#[serde(rename = "type")]
pub widget_type: String,
pub default: Option<bool>,
}
/// A "rule" denoting how this component is to be laid out.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(untagged)]
pub enum LayoutRule {
Child,
Expand { ratio: u32 },
Length { length: u16 },
}
impl Default for LayoutRule {
fn default() -> Self {
LayoutRule::Expand { ratio: 1 }
}
}

View File

@ -1,469 +1,3 @@
//! Mocks layout management, so we can check if we broke anything.
use bottom::app::layout_manager::{BottomLayout, BottomWidgetType};
use bottom::constants::{DEFAULT_BATTERY_LAYOUT, DEFAULT_LAYOUT, DEFAULT_WIDGET_ID};
use bottom::options::{layout_options::Row, Config};
use bottom::utils::error;
const PROC_LAYOUT: &str = r##"
[[row]]
[[row.child]]
type="proc"
[[row]]
[[row.child]]
type="proc"
[[row.child]]
type="proc"
[[row]]
[[row.child]]
type="proc"
[[row.child]]
type="proc"
"##;
fn test_create_layout(
rows: &[Row], default_widget_id: u64, default_widget_type: Option<BottomWidgetType>,
default_widget_count: u64, left_legend: bool,
) -> BottomLayout {
let mut iter_id = 0; // A lazy way of forcing unique IDs *shrugs*
let mut total_height_ratio = 0;
let mut default_widget_count = default_widget_count;
let mut default_widget_id = default_widget_id;
let mut ret_bottom_layout = BottomLayout {
rows: rows
.iter()
.map(|row| {
row.convert_row_to_bottom_row(
&mut iter_id,
&mut total_height_ratio,
&mut default_widget_id,
&default_widget_type,
&mut default_widget_count,
left_legend,
)
})
.collect::<error::Result<Vec<_>>>()
.unwrap(),
total_row_height_ratio: total_height_ratio,
};
ret_bottom_layout.get_movement_mappings();
ret_bottom_layout
}
#[test]
/// Tests the default setup.
fn test_default_movement() {
let rows = toml::from_str::<Config>(DEFAULT_LAYOUT)
.unwrap()
.row
.unwrap();
let ret_bottom_layout = test_create_layout(&rows, DEFAULT_WIDGET_ID, None, 1, false);
// Simple tests for the top CPU widget
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[0].down_neighbour,
Some(3)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[0].right_neighbour,
Some(2)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[0].left_neighbour,
None
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[0].up_neighbour,
None
);
// Test CPU legend
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[1].down_neighbour,
Some(4)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[1].right_neighbour,
None
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[1].left_neighbour,
Some(1)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[1].up_neighbour,
None
);
// Test memory->temp, temp->disk, disk->memory mappings
assert_eq!(
ret_bottom_layout.rows[1].children[0].children[0].children[0].right_neighbour,
Some(4)
);
assert_eq!(
ret_bottom_layout.rows[1].children[1].children[0].children[0].down_neighbour,
Some(5)
);
assert_eq!(
ret_bottom_layout.rows[1].children[1].children[1].children[0].left_neighbour,
Some(3)
);
// Test disk -> processes, processes -> process sort, process sort -> network
assert_eq!(
ret_bottom_layout.rows[1].children[1].children[1].children[0].down_neighbour,
Some(7)
);
assert_eq!(
ret_bottom_layout.rows[2].children[1].children[0].children[1].left_neighbour,
Some(9)
);
assert_eq!(
ret_bottom_layout.rows[2].children[1].children[0].children[0].left_neighbour,
Some(6)
);
}
#[test]
/// Tests battery movement in the default setup.
fn test_default_battery_movement() {
let rows = toml::from_str::<Config>(DEFAULT_BATTERY_LAYOUT)
.unwrap()
.row
.unwrap();
let ret_bottom_layout = test_create_layout(&rows, DEFAULT_WIDGET_ID, None, 1, false);
// Simple tests for the top CPU widget
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[0].down_neighbour,
Some(4)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[0].right_neighbour,
Some(2)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[0].left_neighbour,
None
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[0].up_neighbour,
None
);
// Test CPU legend
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[1].down_neighbour,
Some(5)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[1].right_neighbour,
Some(3)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[1].left_neighbour,
Some(1)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[1].up_neighbour,
None
);
}
#[test]
/// Tests using left_legend.
fn test_left_legend() {
let rows = toml::from_str::<Config>(DEFAULT_LAYOUT)
.unwrap()
.row
.unwrap();
let ret_bottom_layout = test_create_layout(&rows, DEFAULT_WIDGET_ID, None, 1, true);
// Legend
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[0].down_neighbour,
Some(3)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[0].right_neighbour,
Some(1)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[0].left_neighbour,
None
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[0].up_neighbour,
None
);
// Widget
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[1].down_neighbour,
Some(3)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[1].right_neighbour,
None
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[1].left_neighbour,
Some(2)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[1].up_neighbour,
None
);
}
#[test]
/// Tests explicit default widget.
fn test_default_widget_in_layout() {
let proc_layout = r##"
[[row]]
[[row.child]]
type="proc"
[[row]]
[[row.child]]
type="proc"
[[row.child]]
type="proc"
[[row]]
[[row.child]]
type="proc"
default=true
[[row.child]]
type="proc"
"##;
let rows = toml::from_str::<Config>(proc_layout).unwrap().row.unwrap();
let mut iter_id = 0; // A lazy way of forcing unique IDs *shrugs*
let mut total_height_ratio = 0;
let mut default_widget_count = 1;
let mut default_widget_id = DEFAULT_WIDGET_ID;
let default_widget_type = None;
let left_legend = false;
let mut ret_bottom_layout = BottomLayout {
rows: rows
.iter()
.map(|row| {
row.convert_row_to_bottom_row(
&mut iter_id,
&mut total_height_ratio,
&mut default_widget_id,
&default_widget_type,
&mut default_widget_count,
left_legend,
)
})
.collect::<error::Result<Vec<_>>>()
.unwrap(),
total_row_height_ratio: total_height_ratio,
};
ret_bottom_layout.get_movement_mappings();
assert_eq!(default_widget_id, 10);
}
#[test]
/// Tests default widget by setting type and count.
fn test_default_widget_by_option() {
let rows = toml::from_str::<Config>(PROC_LAYOUT).unwrap().row.unwrap();
let mut iter_id = 0; // A lazy way of forcing unique IDs *shrugs*
let mut total_height_ratio = 0;
let mut default_widget_count = 3;
let mut default_widget_id = DEFAULT_WIDGET_ID;
let default_widget_type = Some(BottomWidgetType::Proc);
let left_legend = false;
let mut ret_bottom_layout = BottomLayout {
rows: rows
.iter()
.map(|row| {
row.convert_row_to_bottom_row(
&mut iter_id,
&mut total_height_ratio,
&mut default_widget_id,
&default_widget_type,
&mut default_widget_count,
left_legend,
)
})
.collect::<error::Result<Vec<_>>>()
.unwrap(),
total_row_height_ratio: total_height_ratio,
};
ret_bottom_layout.get_movement_mappings();
assert_eq!(default_widget_id, 7);
}
#[test]
fn test_proc_custom_layout() {
let rows = toml::from_str::<Config>(PROC_LAYOUT).unwrap().row.unwrap();
let ret_bottom_layout = test_create_layout(&rows, DEFAULT_WIDGET_ID, None, 1, false);
// First proc widget
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[1].down_neighbour,
Some(2)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[1].left_neighbour,
Some(3)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[1].right_neighbour,
None
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[1].up_neighbour,
None
);
// Its search
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[1].children[0].down_neighbour,
Some(4)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[1].children[0].left_neighbour,
None
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[1].children[0].right_neighbour,
None
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[1].children[0].up_neighbour,
Some(1)
);
// Its sort
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[0].down_neighbour,
Some(2)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[0].left_neighbour,
None
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[0].right_neighbour,
Some(1)
);
assert_eq!(
ret_bottom_layout.rows[0].children[0].children[0].children[0].up_neighbour,
None
);
// Let us now test the second row's first widget...
assert_eq!(
ret_bottom_layout.rows[1].children[0].children[0].children[1].down_neighbour,
Some(5)
);
assert_eq!(
ret_bottom_layout.rows[1].children[0].children[0].children[1].left_neighbour,
Some(6)
);
assert_eq!(
ret_bottom_layout.rows[1].children[0].children[0].children[1].right_neighbour,
Some(9)
);
assert_eq!(
ret_bottom_layout.rows[1].children[0].children[0].children[1].up_neighbour,
Some(2)
);
// Sort
assert_eq!(
ret_bottom_layout.rows[1].children[0].children[0].children[0].down_neighbour,
Some(5)
);
assert_eq!(
ret_bottom_layout.rows[1].children[0].children[0].children[0].left_neighbour,
None
);
assert_eq!(
ret_bottom_layout.rows[1].children[0].children[0].children[0].right_neighbour,
Some(4)
);
assert_eq!(
ret_bottom_layout.rows[1].children[0].children[0].children[0].up_neighbour,
Some(2)
);
// Search
assert_eq!(
ret_bottom_layout.rows[1].children[0].children[1].children[0].down_neighbour,
Some(10)
);
assert_eq!(
ret_bottom_layout.rows[1].children[0].children[1].children[0].left_neighbour,
None
);
assert_eq!(
ret_bottom_layout.rows[1].children[0].children[1].children[0].right_neighbour,
Some(8)
);
assert_eq!(
ret_bottom_layout.rows[1].children[0].children[1].children[0].up_neighbour,
Some(4)
);
// Third row, second
assert_eq!(
ret_bottom_layout.rows[2].children[1].children[0].children[1].down_neighbour,
Some(14)
);
assert_eq!(
ret_bottom_layout.rows[2].children[1].children[0].children[1].left_neighbour,
Some(15)
);
assert_eq!(
ret_bottom_layout.rows[2].children[1].children[0].children[1].right_neighbour,
None
);
assert_eq!(
ret_bottom_layout.rows[2].children[1].children[0].children[1].up_neighbour,
Some(8)
);
// Sort
assert_eq!(
ret_bottom_layout.rows[2].children[1].children[0].children[0].down_neighbour,
Some(14)
);
assert_eq!(
ret_bottom_layout.rows[2].children[1].children[0].children[0].left_neighbour,
Some(10)
);
assert_eq!(
ret_bottom_layout.rows[2].children[1].children[0].children[0].right_neighbour,
Some(13)
);
assert_eq!(
ret_bottom_layout.rows[2].children[1].children[0].children[0].up_neighbour,
Some(8)
);
// Search
assert_eq!(
ret_bottom_layout.rows[2].children[1].children[1].children[0].down_neighbour,
None
);
assert_eq!(
ret_bottom_layout.rows[2].children[1].children[1].children[0].left_neighbour,
Some(11)
);
assert_eq!(
ret_bottom_layout.rows[2].children[1].children[1].children[0].right_neighbour,
None
);
assert_eq!(
ret_bottom_layout.rows[2].children[1].children[1].children[0].up_neighbour,
Some(13)
);
}
// TODO: Redo testing.

View File

@ -1,9 +1 @@
// TODO: Test basic mode
// #[test]
// fn test_basic_mode() {
// let ret_bottom_layout = BottomLayout::init_basic_default(false);
// }
// TODO: Test moving around with procs and their hidden children.
// TODO: Test moving around with cpus if they get hidden.
// TODO: Redo testing