This commit is contained in:
ClementTsang 2021-12-28 23:46:55 -05:00
parent 3dbe788920
commit 65e36901c0
10 changed files with 132 additions and 474 deletions

View File

@ -29,7 +29,6 @@ use frozen_state::FrozenState;
use crate::{ use crate::{
canvas::Painter, canvas::Painter,
constants, constants,
data_conversion::ConvertedData,
tuine::{Application, Element, Status, ViewContext}, tuine::{Application, Element, Status, ViewContext},
units::data_units::DataUnit, units::data_units::DataUnit,
Pid, Pid,
@ -135,6 +134,7 @@ pub enum AppMessages {
to_kill: Vec<Pid>, to_kill: Vec<Pid>,
signal: Option<i32>, signal: Option<i32>,
}, },
Expand,
ToggleFreeze, ToggleFreeze,
Reset, Reset,
Clean, Clean,
@ -150,23 +150,16 @@ pub struct AppState {
frozen_state: FrozenState, frozen_state: FrozenState,
current_screen: CurrentScreen, current_screen: CurrentScreen,
pub painter: Painter, pub painter: Painter,
layout: WidgetLayoutNode,
terminator: Arc<AtomicBool>, terminator: Arc<AtomicBool>,
} }
impl AppState { impl AppState {
/// Creates a new [`AppState`]. /// Creates a new [`AppState`].
pub fn new( pub fn new(
app_config: AppConfig, filters: DataFilters, layout_tree_output: LayoutCreationOutput, app_config: AppConfig, filters: DataFilters, layout: WidgetLayoutNode,
painter: Painter, used_widgets: UsedWidgets, painter: Painter,
) -> Result<Self> { ) -> Result<Self> {
let LayoutCreationOutput {
layout_tree: _,
root: _,
widget_lookup_map,
selected: _,
used_widgets,
} = layout_tree_output;
Ok(Self { Ok(Self {
app_config, app_config,
filters, filters,
@ -177,7 +170,7 @@ impl AppState {
data_collection: Default::default(), data_collection: Default::default(),
frozen_state: Default::default(), frozen_state: Default::default(),
current_screen: Default::default(), current_screen: Default::default(),
layout,
terminator: Self::register_terminator()?, terminator: Self::register_terminator()?,
}) })
} }
@ -221,6 +214,10 @@ impl Application for AppState {
// FIXME: Handle process termination // FIXME: Handle process termination
true true
} }
AppMessages::Expand => {
// FIXME: Expand current widget
true
}
AppMessages::ToggleFreeze => { AppMessages::ToggleFreeze => {
self.frozen_state.toggle(&self.data_collection); self.frozen_state.toggle(&self.data_collection);
true true
@ -246,7 +243,27 @@ impl Application for AppState {
} }
fn view<'b>(&mut self, ctx: &mut ViewContext<'_>) -> Element<Self::Message> { fn view<'b>(&mut self, ctx: &mut ViewContext<'_>) -> Element<Self::Message> {
todo!() match self.current_screen {
CurrentScreen::Main => {
// The main screen.
todo!()
}
CurrentScreen::Expanded => {
// Displayed when a user "expands" a widget
todo!()
}
CurrentScreen::Help => {
// The help dialog.
todo!()
}
CurrentScreen::Delete => {
// The delete dialog.
todo!()
}
}
} }
fn destructor(&mut self) { fn destructor(&mut self) {

View File

@ -1,14 +1,7 @@
use crate::{ use crate::{
app::{ app::SelectableType,
BasicCpu, BasicMem, BasicNet, BatteryTable, Carousel, DiskTable, Empty, MemGraph, NetGraph,
OldNetGraph, ProcessManager, SelectableType, TempTable,
},
error::{BottomError, Result}, error::{BottomError, Result},
options::{ options::layout_options::{FinalWidget, LayoutRow, LayoutRowChild, LayoutRule},
layout_options::{LayoutRow, LayoutRowChild, LayoutRule},
ProcessDefaults,
},
tuine::{Element, Flex},
}; };
use indextree::{Arena, NodeId}; use indextree::{Arena, NodeId};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
@ -17,9 +10,7 @@ use tui::layout::Rect;
use crate::app::widgets::Widget; use crate::app::widgets::Widget;
use super::{ use super::{event::SelectionAction, OldBottomWidget, UsedWidgets};
event::SelectionAction, AppConfig, AppState, CpuGraph, OldBottomWidget, TimeGraph, UsedWidgets,
};
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum BottomWidgetType { pub enum BottomWidgetType {
@ -41,12 +32,6 @@ pub enum BottomWidgetType {
Carousel, Carousel,
} }
impl Default for BottomWidgetType {
fn default() -> Self {
BottomWidgetType::Empty
}
}
impl FromStr for BottomWidgetType { impl FromStr for BottomWidgetType {
type Err = BottomError; type Err = BottomError;
@ -122,16 +107,6 @@ pub struct RowLayout {
pub bound: Rect, pub bound: Rect,
} }
impl RowLayout {
fn new(parent_rule: LayoutRule) -> Self {
Self {
last_selected: None,
parent_rule,
bound: Rect::default(),
}
}
}
/// Represents a column in the layout tree. /// Represents a column in the layout tree.
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub struct ColLayout { pub struct ColLayout {
@ -140,16 +115,6 @@ pub struct ColLayout {
pub bound: Rect, pub bound: Rect,
} }
impl ColLayout {
fn new(parent_rule: LayoutRule) -> Self {
Self {
last_selected: None,
parent_rule,
bound: Rect::default(),
}
}
}
/// Represents a widget in the layout tree. /// Represents a widget in the layout tree.
#[derive(Debug, PartialEq, Eq, Clone, Default)] #[derive(Debug, PartialEq, Eq, Clone, Default)]
pub struct WidgetLayout { pub struct WidgetLayout {
@ -178,327 +143,125 @@ pub enum MovementDirection {
Down, Down,
} }
pub fn initialize_widget_layout<Message>( /// An intermediate representation of the widget layout.
layout_rows: &[LayoutRow], app: &AppState, pub enum WidgetLayoutNode {
) -> anyhow::Result<Element<Message>> { Row {
let mut root = Flex::column(); children: Vec<WidgetLayoutNode>,
parent_rule: LayoutRule,
for layout_row in layout_rows { },
let mut row = Flex::row(); Col {
if let Some(children) = &layout_row.child { children: Vec<WidgetLayoutNode>,
for child in children { parent_rule: LayoutRule,
match child { },
LayoutRowChild::Widget(widget) => {} Carousel {
LayoutRowChild::Carousel { children: Vec<BottomWidgetType>,
carousel_children, selected: bool,
default, },
} => {} Widget {
LayoutRowChild::LayoutCol { widget_type: BottomWidgetType,
ratio, selected: bool,
child: children, width_rule: LayoutRule,
} => for child in children {}, height_rule: LayoutRule,
} },
}
}
root = root.with_child(row);
}
Ok(root.into())
} }
/// A wrapper struct to simplify the output of [`create_layout_tree`]. /// Parses the layout in the config into an intermediate representation.
pub struct LayoutCreationOutput { pub fn parse_widget_layout(
pub layout_tree: Arena<LayoutNode>, layout_rows: &[LayoutRow],
pub root: NodeId, ) -> anyhow::Result<(WidgetLayoutNode, UsedWidgets)> {
pub widget_lookup_map: FxHashMap<NodeId, OldBottomWidget>, let mut root_children = Vec::with_capacity(layout_rows.len());
pub selected: NodeId,
pub used_widgets: UsedWidgets,
}
/// Creates a new [`Arena<LayoutNode>`] from the given config and returns it, along with the [`NodeId`] representing
/// the root of the newly created [`Arena`], a mapping from [`NodeId`]s to [`BottomWidget`]s, and optionally, a default
/// selected [`NodeId`].
// FIXME: [AFTER REFACTOR] 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: &[LayoutRow], process_defaults: ProcessDefaults, app_config_fields: &AppConfig,
) -> Result<LayoutCreationOutput> {
fn add_widget_to_map(
widget_lookup_map: &mut FxHashMap<NodeId, OldBottomWidget>, widget_type: BottomWidgetType,
widget_id: NodeId, process_defaults: &ProcessDefaults, app_config_fields: &AppConfig,
width: LayoutRule, height: LayoutRule,
) -> Result<()> {
match widget_type {
BottomWidgetType::Cpu => {
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).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)
.width(width)
.height(height)
.into(),
);
} else {
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, app_config_fields)
.width(width)
.height(height)
.basic_mode(app_config_fields.use_basic_mode)
.show_scroll_index(app_config_fields.show_table_scroll_position)
.into(),
);
}
BottomWidgetType::Temp => {
widget_lookup_map.insert(
widget_id,
TempTable::from_config(app_config_fields)
.set_temp_type(app_config_fields.temperature_type.clone())
.width(width)
.height(height)
.basic_mode(app_config_fields.use_basic_mode)
.show_scroll_index(app_config_fields.show_table_scroll_position)
.into(),
);
}
BottomWidgetType::Disk => {
widget_lookup_map.insert(
widget_id,
DiskTable::from_config(app_config_fields)
.width(width)
.height(height)
.basic_mode(app_config_fields.use_basic_mode)
.show_scroll_index(app_config_fields.show_table_scroll_position)
.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(),
);
}
_ => {}
}
Ok(())
}
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 selected widget
let mut used_widgets = UsedWidgets::default(); let mut used_widgets = UsedWidgets::default();
for row in rows { for layout_row in layout_rows {
let row_id = arena.new_node(LayoutNode::Row(RowLayout::new( if let Some(children) = &layout_row.child {
row.ratio let mut row_children = Vec::with_capacity(children.len());
.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 { for child in children {
match child { match child {
LayoutRowChild::Widget(widget) => { LayoutRowChild::Widget(widget) => {
let widget_id = arena.new_node(LayoutNode::Widget(WidgetLayout::default())); let FinalWidget {
row_id.append(widget_id, &mut arena); rule,
widget_type,
default,
} = widget;
if let Some(true) = widget.default { let widget_type = widget_type.parse::<BottomWidgetType>()?;
first_selected = Some(widget_id);
}
if first_widget_seen.is_none() {
first_widget_seen = Some(widget_id);
}
let widget_type = widget.widget_type.parse::<BottomWidgetType>()?;
used_widgets.add(&widget_type); used_widgets.add(&widget_type);
add_widget_to_map( row_children.push(WidgetLayoutNode::Widget {
&mut widget_lookup_map,
widget_type, widget_type,
widget_id, selected: default.unwrap_or(false),
&process_defaults, width_rule: rule.unwrap_or_default(),
app_config_fields, height_rule: LayoutRule::default(),
widget.rule.unwrap_or_default(), });
LayoutRule::default(),
)?;
} }
LayoutRowChild::Carousel { LayoutRowChild::Carousel {
carousel_children, carousel_children,
default, default,
} => { } => {
if !carousel_children.is_empty() { let mut car_children = Vec::with_capacity(carousel_children.len());
let mut child_ids = Vec::with_capacity(carousel_children.len()); for widget_type in carousel_children {
let carousel_widget_id = let widget_type = widget_type.parse::<BottomWidgetType>()?;
arena.new_node(LayoutNode::Widget(WidgetLayout::default())); used_widgets.add(&widget_type);
row_id.append(carousel_widget_id, &mut arena);
if let Some(true) = default { car_children.push(widget_type);
first_selected = Some(carousel_widget_id);
}
if first_widget_seen.is_none() {
first_widget_seen = Some(carousel_widget_id);
}
// Handle the rest of the children.
for child in carousel_children {
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| {
widget_lookup_map
.get(&child_id)
.map(|w| (child_id, w.get_pretty_name().into()))
})
.collect(),
)
.into(),
);
} }
row_children.push(WidgetLayoutNode::Carousel {
children: car_children,
selected: default.unwrap_or(false),
});
} }
LayoutRowChild::LayoutCol { LayoutRowChild::LayoutCol {
ratio, ratio,
child: col_child, child: children,
} => { } => {
let col_id = arena.new_node(LayoutNode::Col(ColLayout::new( let mut col_children = Vec::with_capacity(children.len());
ratio for widget in children {
.map(|ratio| LayoutRule::Expand { ratio }) let FinalWidget {
.unwrap_or(LayoutRule::Child), rule,
))); widget_type,
row_id.append(col_id, &mut arena); default,
} = widget;
for widget in col_child { let widget_type = widget_type.parse::<BottomWidgetType>()?;
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);
}
if first_widget_seen.is_none() {
first_widget_seen = Some(widget_id);
}
let widget_type = widget.widget_type.parse::<BottomWidgetType>()?;
used_widgets.add(&widget_type); used_widgets.add(&widget_type);
add_widget_to_map( col_children.push(WidgetLayoutNode::Widget {
&mut widget_lookup_map,
widget_type, widget_type,
widget_id, selected: default.unwrap_or(false),
&process_defaults, width_rule: LayoutRule::default(),
app_config_fields, height_rule: rule.unwrap_or_default(),
LayoutRule::default(), });
widget.rule.unwrap_or_default(),
)?;
} }
row_children.push(WidgetLayoutNode::Col {
children: col_children,
parent_rule: match ratio {
Some(ratio) => LayoutRule::Expand { ratio: *ratio },
None => LayoutRule::Child,
},
});
} }
} }
} }
let row = WidgetLayoutNode::Row {
children: row_children,
parent_rule: match layout_row.ratio {
Some(ratio) => LayoutRule::Expand { ratio },
None => LayoutRule::Child,
},
};
root_children.push(row);
} }
} }
let selected: NodeId; let root = WidgetLayoutNode::Col {
if let Some(first_selected) = first_selected { children: root_children,
selected = first_selected; parent_rule: LayoutRule::Expand { ratio: 1 },
} else if let Some(first_widget_seen) = first_widget_seen { };
selected = first_widget_seen;
} else {
return Err(BottomError::ConfigError(
"A layout cannot contain zero widgets!".to_string(),
));
}
correct_layout_last_selections(&mut arena, selected); Ok((root, used_widgets))
Ok(LayoutCreationOutput {
layout_tree: arena,
root: root_id,
widget_lookup_map,
selected,
used_widgets,
})
} }
/// We may have situations where we also have to make sure the correct layout indices are selected. /// We may have situations where we also have to make sure the correct layout indices are selected.

View File

@ -119,7 +119,6 @@ pub struct SimpleSortableColumn {
original_name: Cow<'static, str>, original_name: Cow<'static, str>,
pub shortcut: Option<(KeyEvent, String)>, pub shortcut: Option<(KeyEvent, String)>,
pub default_descending: bool, pub default_descending: bool,
x_bounds: Option<(u16, u16)>,
pub internal: SimpleColumn, pub internal: SimpleColumn,
@ -138,7 +137,6 @@ impl SimpleSortableColumn {
original_name, original_name,
shortcut, shortcut,
default_descending, default_descending,
x_bounds: None,
internal: SimpleColumn::new(full_name, desired_width), internal: SimpleColumn::new(full_name, desired_width),
sorting_status: SortStatus::NotSorting, sorting_status: SortStatus::NotSorting,
} }

View File

@ -225,7 +225,7 @@ pub const HELP_CONTENTS_TEXT: [&str; 8] = [
]; ];
pub const GENERAL_HELP_TITLE: &str = "General"; pub const GENERAL_HELP_TITLE: &str = "General";
pub const GENERAL_HELP_TEXT: [[&str; 2]; 21] = [ pub const GENERAL_HELP_TEXT: [[&str; 2]; 23] = [
["q, Ctrl-c", "Quit"], ["q, Ctrl-c", "Quit"],
[ [
"Esc", "Esc",
@ -251,6 +251,8 @@ pub const GENERAL_HELP_TEXT: [[&str; 2]; 21] = [
["+", "Zoom in on chart (decrease time range)"], ["+", "Zoom in on chart (decrease time range)"],
["-", "Zoom out on chart (increase time range)"], ["-", "Zoom out on chart (increase time range)"],
["=", "Reset zoom"], ["=", "Reset zoom"],
["Page Up", "Move up one page in a table"],
["Page Down", "Move down one page in a table"],
[ [
"Mouse scroll", "Mouse scroll",
"Scroll through the tables or zoom in/out of charts by scrolling up/down", "Scroll through the tables or zoom in/out of charts by scrolling up/down",

View File

@ -241,7 +241,7 @@ pub fn build_app(matches: &clap::ArgMatches<'static>, config: &mut Config) -> Re
&rows &rows
}; };
let layout_tree_output = create_layout_tree(row_ref, process_defaults, &app_config_fields)?; let (layout, used_widgets) = parse_widget_layout(row_ref)?;
let disk_filter = let disk_filter =
get_ignore_list(&config.disk_filter).context("Update 'disk_filter' in your config file")?; get_ignore_list(&config.disk_filter).context("Update 'disk_filter' in your config file")?;
@ -259,7 +259,13 @@ pub fn build_app(matches: &clap::ArgMatches<'static>, config: &mut Config) -> Re
}; };
let painter = Painter::init(&config, get_color_scheme(&matches, &config)?)?; let painter = Painter::init(&config, get_color_scheme(&matches, &config)?)?;
AppState::new(app_config_fields, data_filter, layout_tree_output, painter) AppState::new(
app_config_fields,
data_filter,
layout,
used_widgets,
painter,
)
} }
fn get_update_rate_in_milliseconds( fn get_update_rate_in_milliseconds(

View File

@ -1,6 +1,3 @@
use anyhow::{anyhow, Result};
use enum_dispatch::enum_dispatch;
pub mod simple_table; pub mod simple_table;
pub use simple_table::*; pub use simple_table::*;

View File

@ -1,10 +1,9 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tui::style::Style; use tui::style::Style;
use crate::tuine::{ use crate::tuine::{
self, block, self, block,
text_table::{self, DataRow, SortType, TextTableProps}, text_table::{self, DataRow, SortType, TextTableProps},
Block, Event, Shortcut, StatefulComponent, Status, TextTable, TmpComponent, ViewContext, Block, Shortcut, StatefulComponent, TextTable, TmpComponent, ViewContext,
}; };
/// A set of styles for a [`SimpleTable`]. /// A set of styles for a [`SimpleTable`].

View File

@ -3,8 +3,8 @@ use crate::{
canvas::Painter, canvas::Painter,
data_conversion::ConvertedData, data_conversion::ConvertedData,
tuine::{ tuine::{
Bounds, DataRow, DrawContext, LayoutNode, SimpleTable, Size, StateContext, Status, Bounds, DrawContext, LayoutNode, SimpleTable, Size, StateContext, Status, TmpComponent,
TmpComponent, ViewContext, ViewContext,
}, },
}; };

View File

@ -1,121 +0,0 @@
use tui::{
layout::Rect,
text::Span,
widgets::{Block, Borders},
};
use crate::canvas::Painter;
/// A factory pattern builder for a tui [`Block`].
pub struct BlockBuilder {
borders: Borders,
selected: bool,
show_esc: bool,
name: &'static str,
hide_title: bool,
extra_text: Option<String>,
}
impl BlockBuilder {
/// Creates a new [`BlockBuilder`] with the name of block.
pub fn new(name: &'static str) -> Self {
Self {
borders: Borders::ALL,
selected: false,
show_esc: false,
name,
hide_title: false,
extra_text: None,
}
}
/// Indicates that this block is currently selected, and should be drawn as such.
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
/// Indicates that this block should show esc, and should be drawn as such.
pub fn show_esc(mut self, show_esc: bool) -> Self {
self.show_esc = show_esc;
self
}
/// Indicates that this block has some extra text beyond the name.
pub fn extra_text(mut self, extra_text: Option<String>) -> Self {
self.extra_text = extra_text;
self
}
/// Determines the borders of the built [`Block`].
pub fn borders(mut self, borders: Borders) -> Self {
self.borders = borders;
self
}
/// Forcibly hides the title of the built [`Block`].
pub fn hide_title(mut self, hide_title: bool) -> Self {
self.hide_title = hide_title;
self
}
/// Converts the [`BlockBuilder`] into an actual [`Block`].
pub fn build(self, painter: &Painter, area: Rect) -> Block<'static> {
let has_title = !self.hide_title
&& (self.borders.contains(Borders::TOP) || self.borders.contains(Borders::BOTTOM));
let border_style = if self.selected {
painter.colours.highlighted_border_style
} else {
painter.colours.border_style
};
let block = Block::default()
.border_style(border_style)
.borders(self.borders);
let inner_width = block.inner(area).width as usize;
if has_title {
let name = Span::styled(
format!(" {} ", self.name),
painter.colours.widget_title_style,
);
let mut title_len = name.width();
let mut title = vec![name, Span::from(""), Span::from(""), Span::from("")];
if self.show_esc {
const EXPAND_TEXT: &str = " Esc to go back ";
const EXPAND_TEXT_LEN: usize = EXPAND_TEXT.len();
let expand_span = Span::styled(EXPAND_TEXT, border_style);
if title_len + EXPAND_TEXT_LEN <= inner_width {
title_len += EXPAND_TEXT_LEN;
title[3] = expand_span;
}
}
if let Some(extra_text) = self.extra_text {
let extra_span = Span::styled(
format!("{} ", extra_text),
painter.colours.widget_title_style,
);
let width = extra_span.width();
if title_len + width <= inner_width {
title_len += width;
title[1] = extra_span;
}
}
if self.show_esc {
let difference = inner_width.saturating_sub(title_len);
title[2] = Span::styled("".repeat(difference), border_style);
}
block.title(title)
} else {
block
}
}
}

View File

@ -3,6 +3,3 @@ pub use custom_legend_chart::TimeChart;
pub mod pipe_gauge; pub mod pipe_gauge;
pub use pipe_gauge::PipeGauge; pub use pipe_gauge::PipeGauge;
pub mod block_builder;
pub use block_builder::BlockBuilder;