summaryrefslogtreecommitdiffstats
path: root/src/app/widgets
diff options
context:
space:
mode:
authorClementTsang <cjhtsang@uwaterloo.ca>2021-08-31 17:19:05 -0400
committerClementTsang <cjhtsang@uwaterloo.ca>2021-09-05 19:09:11 -0400
commiteddc9a16c7ac1be90144e8a247b13d6fd96238cf (patch)
tree8d197acfebf39a02cd8cc4abd0770f4508cbae6f /src/app/widgets
parent204b4dc3510b70618741f82a1e2df6c17c6dadda (diff)
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.
Diffstat (limited to 'src/app/widgets')
-rw-r--r--src/app/widgets/base.rs3
-rw-r--r--src/app/widgets/base/carousel.rs42
-rw-r--r--src/app/widgets/base/text_table.rs3
-rw-r--r--src/app/widgets/basic_cpu.rs0
-rw-r--r--src/app/widgets/basic_mem.rs0
-rw-r--r--src/app/widgets/basic_net.rs0
-rw-r--r--src/app/widgets/bottom_widgets.rs35
-rw-r--r--src/app/widgets/bottom_widgets/basic_cpu.rs205
-rw-r--r--src/app/widgets/bottom_widgets/basic_mem.rs164
-rw-r--r--src/app/widgets/bottom_widgets/basic_net.rs134
-rw-r--r--src/app/widgets/bottom_widgets/battery.rs (renamed from src/app/widgets/battery.rs)58
-rw-r--r--src/app/widgets/bottom_widgets/carousel.rs209
-rw-r--r--src/app/widgets/bottom_widgets/cpu.rs (renamed from src/app/widgets/cpu.rs)34
-rw-r--r--src/app/widgets/bottom_widgets/disk.rs (renamed from src/app/widgets/disk.rs)51
-rw-r--r--src/app/widgets/bottom_widgets/empty.rs58
-rw-r--r--src/app/widgets/bottom_widgets/mem.rs (renamed from src/app/widgets/mem.rs)28
-rw-r--r--src/app/widgets/bottom_widgets/net.rs (renamed from src/app/widgets/net.rs)53
-rw-r--r--src/app/widgets/bottom_widgets/process.rs (renamed from src/app/widgets/process.rs)50
-rw-r--r--src/app/widgets/bottom_widgets/temp.rs (renamed from src/app/widgets/temp.rs)46
-rw-r--r--src/app/widgets/tui_widgets.rs3
-rw-r--r--src/app/widgets/tui_widgets/pipe_gauge.rs138
21 files changed, 1229 insertions, 85 deletions
diff --git a/src/app/widgets/base.rs b/src/app/widgets/base.rs
index c5bf80cf..3e63e3e6 100644
--- a/src/app/widgets/base.rs
+++ b/src/app/widgets/base.rs
@@ -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;
diff --git a/src/app/widgets/base/carousel.rs b/src/app/widgets/base/carousel.rs
deleted file mode 100644
index 4454a847..00000000
--- a/src/app/widgets/base/carousel.rs
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/app/widgets/base/text_table.rs b/src/app/widgets/base/text_table.rs
index 03111566..6cf460f9 100644
--- a/src/app/widgets/base/text_table.rs
+++ b/src/app/widgets/base/text_table.rs
@@ -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 {
diff --git a/src/app/widgets/basic_cpu.rs b/src/app/widgets/basic_cpu.rs
deleted file mode 100644
index e69de29b..00000000
--- a/src/app/widgets/basic_cpu.rs
+++ /dev/null
diff --git a/src/app/widgets/basic_mem.rs b/src/app/widgets/basic_mem.rs
deleted file mode 100644
index e69de29b..00000000
--- a/src/app/widgets/basic_mem.rs
+++ /dev/null
diff --git a/src/app/widgets/basic_net.rs b/src/app/widgets/basic_net.rs
deleted file mode 100644
index e69de29b..00000000
--- a/src/app/widgets/basic_net.rs
+++ /dev/null
diff --git a/src/app/widgets/bottom_widgets.rs b/src/app/widgets/bottom_widgets.rs
new file mode 100644
index 00000000..fbc7c78c
--- /dev/null
+++ b/src/app/widgets/bottom_widgets.rs
@@ -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;
diff --git a/src/app/widgets/bottom_widgets/basic_cpu.rs b/src/app/widgets/bottom_widgets/basic_cpu.rs
new file mode 100644
index 00000000..5b862367
--- /dev/null
+++ b/src/app/widgets/bottom_widgets/basic_cpu.rs
@@ -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 }
+ }
+}
diff --git a/src/app/widgets/bottom_widgets/basic_mem.rs b/src/app/widgets/bottom_widgets/basic_mem.rs
new file mode 100644
index 00000000..7fbd1fd0
--- /dev/null
+++ b/src/app/widgets/bottom_widgets/basic_mem.rs
@@ -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 }
+ }
+}
diff --git a/src/app/widgets/bottom_widgets/basic_net.rs b/src/app/widgets/bottom_widgets/basic_net.rs
new file mode 100644
index 00000000..60b0c44b
--- /dev/null
+++ b/src/app/widgets/bottom_widgets/basic_net.rs
@@ -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 }
+ }
+}
diff --git a/src/app/widgets/battery.rs b/src/app/widgets/bottom_widgets/battery.rs
index e12e2fe6..2680a8d7 100644
--- a/src/app/widgets/battery.rs
+++ b/src/app/widgets/bottom_widgets/battery.rs
@@ -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 BatteryTable {
- /// Creates a new [`BatteryTable`].
- pub fn new(batteries: Vec<String>) -> Self {
+impl Default for BatteryTable {
+ fn default() -> Self {
Self {
- batteries,
- ..Default::default()
+ batteries: vec![],
+ bounds: Default::default(),
+ selected_index: 0,
+ battery_data: Default::default(),
+ width: LayoutRule::default(),
+ height: LayoutRule::default(),
+ block_border: Borders::ALL,
}
}
+}
+
+impl BatteryTable {
+ /// 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
+ }
}
diff --git a/src/app/widgets/bottom_widgets/carousel.rs b/src/app/widgets/bottom_widgets/carousel.rs
new file mode 100644
index 00000000..0f91c212
--- /dev/null
+++ b/src/app/widgets/bottom_widgets/carousel.rs
@@ -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]);
+
+