summaryrefslogtreecommitdiffstats
path: root/src/app/widgets
diff options
context:
space:
mode:
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]);
+
+ 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 {
+