diff options
Diffstat (limited to 'src')
32 files changed, 2589 insertions, 926 deletions
@@ -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; } diff --git a/src/app/layout_manager.rs b/src/app/layout_manager.rs index 7105fa73..672fea19 100644 --- a/src/app/layout_manager.rs +++ b/src/app/layout_manager.rs @@ -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); - - 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 { + 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(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 child_sum: u32 = - children.iter().map(|child| child.ratio.unwrap_or(1)).sum(); - - 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 { + 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); + + for widget in col_child { + let widget_id = + arena.new_node(LayoutNode::Widget(WidgetLayout::default())); + col_id.append(widget_id, &mut arena); + + 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)), + } + } |