mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-04-08 17:05:59 +02:00
Layout done
This commit is contained in:
parent
65e36901c0
commit
ac23654c1c
19
src/app.rs
19
src/app.rs
@ -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 => {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -200,6 +200,6 @@ impl Widget for BasicCpu {
|
||||
}),
|
||||
);
|
||||
|
||||
LayoutRule::Length { length }
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
@ -160,6 +160,6 @@ impl Widget for BasicMem {
|
||||
}
|
||||
|
||||
fn height(&self) -> LayoutRule {
|
||||
LayoutRule::Length { length: 2 }
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
@ -129,6 +129,6 @@ impl Widget for BasicNet {
|
||||
}
|
||||
|
||||
fn height(&self) -> LayoutRule {
|
||||
LayoutRule::Length { length: 2 }
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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!
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user