Layout done

This commit is contained in:
ClementTsang 2021-12-29 02:32:47 -05:00
parent 65e36901c0
commit ac23654c1c
12 changed files with 199 additions and 85 deletions

View File

@ -29,6 +29,7 @@ use frozen_state::FrozenState;
use crate::{
canvas::Painter,
constants,
data_conversion::ConvertedData,
tuine::{Application, Element, Status, ViewContext},
units::data_units::DataUnit,
Pid,
@ -150,14 +151,14 @@ pub struct AppState {
frozen_state: FrozenState,
current_screen: CurrentScreen,
pub painter: Painter,
layout: WidgetLayoutNode,
main_layout: WidgetLayoutRoot,
terminator: Arc<AtomicBool>,
}
impl AppState {
/// Creates a new [`AppState`].
pub fn new(
app_config: AppConfig, filters: DataFilters, layout: WidgetLayoutNode,
app_config: AppConfig, filters: DataFilters, main_layout: WidgetLayoutRoot,
used_widgets: UsedWidgets, painter: Painter,
) -> Result<Self> {
Ok(Self {
@ -170,7 +171,7 @@ impl AppState {
data_collection: Default::default(),
frozen_state: Default::default(),
current_screen: Default::default(),
layout,
main_layout,
terminator: Self::register_terminator()?,
})
}
@ -247,10 +248,20 @@ impl Application for AppState {
CurrentScreen::Main => {
// The main screen.
todo!()
let data_source = match &self.frozen_state {
FrozenState::NotFrozen => &self.data_collection,
FrozenState::Frozen(frozen_data) => frozen_data,
};
let mut data = ConvertedData::new(data_source);
let layout = &self.main_layout;
layout.build(ctx, self, &mut data)
}
CurrentScreen::Expanded => {
// Displayed when a user "expands" a widget
// FIXME: Handle frozen
todo!()
}
CurrentScreen::Help => {

View File

@ -1,8 +1,11 @@
use crate::{
app::SelectableType,
data_conversion::ConvertedData,
error::{BottomError, Result},
options::layout_options::{FinalWidget, LayoutRow, LayoutRowChild, LayoutRule},
tuine::*,
};
use anyhow::anyhow;
use indextree::{Arena, NodeId};
use rustc_hash::FxHashMap;
use std::str::FromStr;
@ -10,26 +13,21 @@ use tui::layout::Rect;
use crate::app::widgets::Widget;
use super::{event::SelectionAction, OldBottomWidget, UsedWidgets};
use super::{event::SelectionAction, AppState, OldBottomWidget, UsedWidgets};
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum BottomWidgetType {
Empty,
Cpu,
CpuLegend,
Mem,
Net,
Proc,
ProcSearch,
ProcSort,
Temp,
Disk,
BasicCpu,
BasicMem,
BasicNet,
BasicTables,
Battery,
Carousel,
}
impl FromStr for BottomWidgetType {
@ -143,15 +141,37 @@ pub enum MovementDirection {
Down,
}
/// The root of the widget layout intermediate representation. A wrapper around a `Vec` of [`WidgetLayoutNode`];
/// it ALWAYS represents a column.
#[derive(Debug, Clone)]
pub struct WidgetLayoutRoot {
pub children: Vec<WidgetLayoutNode>,
}
impl WidgetLayoutRoot {
pub fn build<Message>(
&self, ctx: &mut ViewContext<'_>, app_state: &AppState, data: &mut ConvertedData<'_>,
) -> Element<Message> {
Flex::column_with_children(
self.children
.iter()
.map(|child| child.build(ctx, app_state, data))
.collect(),
)
.into()
}
}
/// An intermediate representation of the widget layout.
#[derive(Debug, Clone)]
pub enum WidgetLayoutNode {
Row {
children: Vec<WidgetLayoutNode>,
parent_rule: LayoutRule,
parent_ratio: u16,
},
Col {
children: Vec<WidgetLayoutNode>,
parent_rule: LayoutRule,
parent_ratio: u16,
},
Carousel {
children: Vec<BottomWidgetType>,
@ -160,15 +180,126 @@ pub enum WidgetLayoutNode {
Widget {
widget_type: BottomWidgetType,
selected: bool,
width_rule: LayoutRule,
height_rule: LayoutRule,
rule: LayoutRule,
},
}
impl WidgetLayoutNode {
fn new_row<Message>(
ctx: &mut ViewContext<'_>, app_state: &AppState, data: &mut ConvertedData<'_>,
children: &[WidgetLayoutNode], parent_ratio: u16,
) -> FlexElement<Message> {
FlexElement::with_flex(
Flex::row_with_children(
children
.iter()
.map(|child| child.build(ctx, app_state, data))
.collect(),
),
parent_ratio,
)
}
fn new_col<Message>(
ctx: &mut ViewContext<'_>, app_state: &AppState, data: &mut ConvertedData<'_>,
children: &[WidgetLayoutNode], parent_ratio: u16,
) -> FlexElement<Message> {
FlexElement::with_flex(
Flex::column_with_children(
children
.iter()
.map(|child| child.build(ctx, app_state, data))
.collect(),
),
parent_ratio,
)
}
fn new_carousel<Message>(
ctx: &mut ViewContext<'_>, app_state: &AppState, data: &mut ConvertedData<'_>,
children: &[BottomWidgetType], selected: bool,
) -> FlexElement<Message> {
// FIXME: Carousel!
FlexElement::new(Flex::row_with_children(
children
.iter()
.map(|child| {
FlexElement::with_no_flex(Self::make_element(
ctx, app_state, data, child, false,
))
})
.collect(),
))
}
fn make_element<Message>(
ctx: &mut ViewContext<'_>, app_state: &AppState, data: &mut ConvertedData<'_>,
widget_type: &BottomWidgetType, selected: bool,
) -> Element<Message> {
let painter = &app_state.painter;
let config = &app_state.app_config;
match widget_type {
BottomWidgetType::Empty => Empty::default().into(),
BottomWidgetType::Cpu => CpuGraph::build(ctx, painter, config, data).into(),
BottomWidgetType::Mem => MemGraph::build(ctx, painter, config, data).into(),
BottomWidgetType::Net => NetGraph::build(ctx, painter, config, data).into(),
BottomWidgetType::Proc => ProcessTable::build(ctx, painter, config, data).into(),
BottomWidgetType::Temp => TempTable::build(ctx, painter, config, data).into(),
BottomWidgetType::Disk => DiskTable::build(ctx, painter, config, data).into(),
BottomWidgetType::BasicCpu => CpuSimple::build(ctx, painter, config, data).into(),
BottomWidgetType::BasicMem => MemSimple::build(ctx, painter, config, data).into(),
BottomWidgetType::BasicNet => NetSimple::build(ctx, painter, config, data).into(),
BottomWidgetType::Battery => BatteryTable::build(ctx, painter, config, data).into(),
}
}
fn wrap_element<Message>(element: Element<Message>, rule: &LayoutRule) -> FlexElement<Message> {
match rule {
LayoutRule::Expand { ratio } => FlexElement::with_flex(element, *ratio),
LayoutRule::Length { width, height } => {
if width.is_some() || height.is_some() {
FlexElement::with_no_flex(
Container::with_child(element).width(*width).height(*height),
)
} else {
FlexElement::with_flex(element, 1)
}
}
}
}
pub fn build<Message>(
&self, ctx: &mut ViewContext<'_>, app_state: &AppState, data: &mut ConvertedData<'_>,
) -> FlexElement<Message> {
match self {
WidgetLayoutNode::Row {
children,
parent_ratio,
} => Self::new_row(ctx, app_state, data, children, *parent_ratio),
WidgetLayoutNode::Col {
children,
parent_ratio,
} => Self::new_col(ctx, app_state, data, children, *parent_ratio),
WidgetLayoutNode::Carousel { children, selected } => {
Self::new_carousel(ctx, app_state, data, children, *selected)
}
WidgetLayoutNode::Widget {
widget_type,
selected,
rule,
} => WidgetLayoutNode::wrap_element(
Self::make_element(ctx, app_state, data, widget_type, *selected),
rule,
),
}
}
}
/// Parses the layout in the config into an intermediate representation.
pub fn parse_widget_layout(
layout_rows: &[LayoutRow],
) -> anyhow::Result<(WidgetLayoutNode, UsedWidgets)> {
) -> anyhow::Result<(WidgetLayoutRoot, UsedWidgets)> {
let mut root_children = Vec::with_capacity(layout_rows.len());
let mut used_widgets = UsedWidgets::default();
@ -191,8 +322,7 @@ pub fn parse_widget_layout(
row_children.push(WidgetLayoutNode::Widget {
widget_type,
selected: default.unwrap_or(false),
width_rule: rule.unwrap_or_default(),
height_rule: LayoutRule::default(),
rule: rule.unwrap_or_default(),
});
}
LayoutRowChild::Carousel {
@ -229,17 +359,13 @@ pub fn parse_widget_layout(
col_children.push(WidgetLayoutNode::Widget {
widget_type,
selected: default.unwrap_or(false),
width_rule: LayoutRule::default(),
height_rule: rule.unwrap_or_default(),
rule: 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,
},
parent_ratio: ratio.unwrap_or(1),
});
}
}
@ -247,44 +373,19 @@ pub fn parse_widget_layout(
let row = WidgetLayoutNode::Row {
children: row_children,
parent_rule: match layout_row.ratio {
Some(ratio) => LayoutRule::Expand { ratio },
None => LayoutRule::Child,
},
parent_ratio: layout_row.ratio.unwrap_or(1),
};
root_children.push(row);
}
}
let root = WidgetLayoutNode::Col {
children: root_children,
parent_rule: LayoutRule::Expand { ratio: 1 },
};
Ok((root, used_widgets))
}
/// We may have situations where we also have to make sure the correct layout indices are selected.
/// For example, when we select a widget by clicking, we want to update the layout so that it's as if a user
/// manually moved to it via keybinds.
///
/// We can do this by just going through the ancestors, starting from the widget itself.
pub fn correct_layout_last_selections(arena: &mut Arena<LayoutNode>, selected: NodeId) {
let mut selected_ancestors = selected.ancestors(arena).collect::<Vec<_>>();
let prev_node = selected_ancestors.pop();
if let Some(mut prev_node) = prev_node {
for node in selected_ancestors {
if let Some(layout_node) = arena.get_mut(node).map(|n| n.get_mut()) {
match layout_node {
LayoutNode::Row(RowLayout { last_selected, .. })
| LayoutNode::Col(ColLayout { last_selected, .. }) => {
*last_selected = Some(prev_node);
}
LayoutNode::Widget(_) => {}
}
}
prev_node = node;
}
if root_children.is_empty() {
Err(anyhow!("The layout cannot be empty!"))
} else {
let root = WidgetLayoutRoot {
children: root_children,
};
Ok((root, used_widgets))
}
}

View File

@ -200,6 +200,6 @@ impl Widget for BasicCpu {
}),
);
LayoutRule::Length { length }
todo!()
}
}

View File

@ -160,6 +160,6 @@ impl Widget for BasicMem {
}
fn height(&self) -> LayoutRule {
LayoutRule::Length { length: 2 }
todo!()
}
}

View File

@ -129,6 +129,6 @@ impl Widget for BasicNet {
}
fn height(&self) -> LayoutRule {
LayoutRule::Length { length: 2 }
todo!()
}
}

View File

@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
#[serde(rename = "row")]
pub struct LayoutRow {
pub child: Option<Vec<LayoutRowChild>>,
pub ratio: Option<u32>,
pub ratio: Option<u16>,
}
/// Represents a child of a Row - either a Col (column) or a FinalWidget.
@ -24,7 +24,7 @@ pub enum LayoutRowChild {
default: Option<bool>,
},
LayoutCol {
ratio: Option<u32>,
ratio: Option<u16>,
child: Vec<FinalWidget>,
},
}
@ -43,16 +43,15 @@ pub struct FinalWidget {
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(untagged)]
pub enum LayoutRule {
/// Let the child decide how big to make the current node.
Child,
/// Expand to whatever space is left; the `ratio` determines how much
/// space to take if there is more than one [`LayoutRule::Expand`] component.
Expand { ratio: u16 },
/// Expand to whatever space is left; the `ratio` determines how
/// much space to take if there are more than one
/// [`LayoutRule::Expand`] component.
Expand { ratio: u32 },
/// Take up exactly `length` space if possible.
Length { length: u16 },
/// Take up an exact amount of space, if possible.
Length {
width: Option<u16>,
height: Option<u16>,
},
}
impl Default for LayoutRule {

View File

@ -21,6 +21,7 @@ pub trait Application: Sized {
/// always returning false.
fn is_terminated(&self) -> bool;
/// Creates the user interface.
fn view<'b>(&mut self, ctx: &mut ViewContext<'_>) -> Element<Self::Message>;
/// To run upon stopping the application.

View File

@ -13,6 +13,7 @@ pub struct FlexElement<Message> {
}
impl<Message> FlexElement<Message> {
/// Creates a new [`FlexElement`] with a flex of 1.
pub fn new<I: Into<Element<Message>>>(element: I) -> Self {
Self {
flex: 1,

View File

@ -156,7 +156,7 @@ impl<Message> TmpComponent<Message> for Flex<Message> {
flexible_children_indexes.into_iter().for_each(|index| {
// The index accesses are assumed to be safe by above definitions.
// This means that we can use the unsafe operations below.
// This means that we can use the unsafe operations below to avoid bounds checks.
//
// NB: If you **EVER** make changes in this function, ensure these assumptions
// still hold!

View File

@ -4,7 +4,7 @@ use crate::tuine::Size;
///
/// These are sent from a parent component to a child to determine the [`Size`](super::Size)
/// of a child, which is passed back up to the parent.
#[derive(Clone, Copy, Default)]
#[derive(Clone, Copy, Debug, Default)]
pub struct Bounds {
/// The minimal width available.
pub min_width: u16,
@ -34,8 +34,8 @@ impl Bounds {
/// Returns whether there is any space left in this bound for laying out things.
pub fn has_space(&self) -> bool {
self.min_width > self.max_width
|| self.min_height > self.max_height
self.min_width >= self.max_width
|| self.min_height >= self.max_height
|| self.max_width == 0
|| self.max_height == 0
}

View File

@ -4,7 +4,7 @@ use std::ops::{Add, AddAssign};
///
/// A [`Size`] is sent from a child component back up to its parents after
/// first being given a [`Bounds`](super::Bounds) from the parent.
#[derive(Clone, Copy, Default)]
#[derive(Clone, Copy, Debug, Default)]
pub struct Size {
/// The width that the component has determined.
pub width: u16,

View File

@ -28,11 +28,11 @@ where
A: Application + 'static,
B: Backend,
{
let mut app_data = AppData::default(); // FIXME: This needs to be cleared periodically, DO!
let mut app_data = AppData::default(); // FIXME: This needs to be cleared periodically!
let mut layout: LayoutNode = LayoutNode::default();
let mut user_interface = {
let mut ui = new_user_interface(&mut application, &mut app_data);
let mut ui = create_user_interface(&mut application, &mut app_data);
draw(&mut ui, terminal, &mut app_data, &mut layout)?;
ui
};
@ -48,13 +48,17 @@ where
&mut layout,
event,
) {
user_interface = new_user_interface(&mut application, &mut app_data);
if application.is_terminated() {
break;
}
user_interface = create_user_interface(&mut application, &mut app_data);
draw(&mut user_interface, terminal, &mut app_data, &mut layout)?;
}
}
RuntimeEvent::Custom(message) => {
if application.update(message) {
user_interface = new_user_interface(&mut application, &mut app_data);
user_interface = create_user_interface(&mut application, &mut app_data);
draw(&mut user_interface, terminal, &mut app_data, &mut layout)?;
}
}
@ -62,7 +66,7 @@ where
width: _,
height: _,
} => {
user_interface = new_user_interface(&mut application, &mut app_data);
user_interface = create_user_interface(&mut application, &mut app_data);
// FIXME: Also nuke any cache and the like...
draw(&mut user_interface, terminal, &mut app_data, &mut layout)?;
}
@ -91,10 +95,7 @@ where
let event_handled =
match user_interface.on_event(&mut state_ctx, &draw_ctx, event, &mut messages) {
Status::Captured => {
// TODO: What to do on capture?
Status::Captured
}
Status::Captured => Status::Captured,
Status::Ignored => application.global_event_handler(event, &mut messages),
};
@ -113,7 +114,7 @@ where
}
/// Creates a new [`Element`] representing the root of the user interface.
fn new_user_interface<A>(application: &mut A, app_data: &mut AppData) -> Element<A::Message>
fn create_user_interface<A>(application: &mut A, app_data: &mut AppData) -> Element<A::Message>
where
A: Application + 'static,
{