This commit is contained in:
ClementTsang 2021-12-11 17:58:18 -05:00
parent f1ec2fd70f
commit bf81a389b8
19 changed files with 294 additions and 78 deletions

View File

@ -43,8 +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 are more than one
/// [`LayoutRule::Expand`] component.
Expand { ratio: u32 },
/// Take up exactly `length` space if possible.
Length { length: u16 },
}

View File

@ -1,9 +1,12 @@
pub mod base;
pub use base::*;
pub mod widget;
pub use widget::*;
use tui::{layout::Rect, Frame};
use super::{Event, Status};
use super::{Bounds, Context, Event, LayoutNode, Size, Status};
/// A component displays information and can be interacted with.
#[allow(unused_variables)]
@ -11,14 +14,23 @@ pub trait Component<Message, Backend>
where
Backend: tui::backend::Backend,
{
/// Handles an [`Event`]. Defaults to just ignoring the event.
fn on_event(&mut self, bounds: Rect, event: Event, messages: &mut Vec<Message>) -> Status {
/// Draws the component.
fn draw(&mut self, area: Rect, context: &Context, frame: &mut Frame<'_, Backend>);
/// How a component should react to an [`Event`].
///
/// Defaults to just ignoring the event.
fn on_event(&mut self, area: Rect, event: Event, messages: &mut Vec<Message>) -> Status {
Status::Ignored
}
/// Returns the desired layout of the component. Defaults to returning
fn layout(&self) {}
/// Draws the component.
fn draw(&mut self, bounds: Rect, frame: &mut Frame<'_, Backend>);
/// How a component should size itself and its children, given some [`Bounds`].
///
/// Defaults to returning a [`Size`] that fills up the bounds given.
fn layout(&self, bounds: Bounds) -> Size {
Size {
width: bounds.max_width,
height: bounds.max_height,
}
}
}

View File

@ -1,6 +1,6 @@
use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Component, Event, Status};
use crate::tuice::{Component, Context, Event, Status};
pub struct Block {}
@ -8,11 +8,11 @@ impl<Message, B> Component<Message, B> for Block
where
B: Backend,
{
fn draw(&mut self, _bounds: Rect, _frame: &mut Frame<'_, B>) {
fn draw(&mut self, _area: Rect, _context: &Context, _frame: &mut Frame<'_, B>) {
todo!()
}
fn on_event(&mut self, _bounds: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
fn on_event(&mut self, _area: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
Status::Ignored
}
}

View File

@ -1,6 +1,6 @@
use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Component, Event, Status};
use crate::tuice::{Component, Context, Event, Status};
pub struct Carousel {}
@ -8,11 +8,11 @@ impl<Message, B> Component<Message, B> for Carousel
where
B: Backend,
{
fn draw(&mut self, _bounds: Rect, _frame: &mut Frame<'_, B>) {
fn draw(&mut self, _area: Rect, _context: &Context, _frame: &mut Frame<'_, B>) {
todo!()
}
fn on_event(&mut self, _bounds: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
fn on_event(&mut self, _area: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
Status::Ignored
}
}

View File

@ -1,6 +1,6 @@
use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Component, Event, Status};
use crate::tuice::{Component, Context, Event, Status};
pub struct Column {}
@ -8,11 +8,11 @@ impl<Message, B> Component<Message, B> for Column
where
B: Backend,
{
fn draw(&mut self, _bounds: Rect, _frame: &mut Frame<'_, B>) {
fn draw(&mut self, _area: Rect, _context: &Context, _frame: &mut Frame<'_, B>) {
todo!()
}
fn on_event(&mut self, _bounds: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
fn on_event(&mut self, _area: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
Status::Ignored
}
}

View File

@ -0,0 +1,68 @@
use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Bounds, Component, Context, Event, LayoutNode, Length, Size, Status};
pub struct Container<'a, Message, B>
where
B: Backend,
{
width: Length,
height: Length,
child: Box<dyn Component<Message, B> + 'a>,
}
impl<'a, Message, B> Container<'a, Message, B>
where
B: Backend,
{
pub fn new(child: Box<dyn Component<Message, B> + 'a>) -> Self {
Self {
width: Length::Flex,
height: Length::Flex,
child,
}
}
}
impl<'a, Message, B> Component<Message, B> for Container<'a, Message, B>
where
B: Backend,
{
fn draw(&mut self, area: Rect, _context: &Context, _frame: &mut Frame<'_, B>) {
todo!()
}
fn on_event(&mut self, _area: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
todo!()
}
fn layout(&self, bounds: Bounds) -> Size {
let width = match self.width {
Length::Flex => {
todo!()
}
Length::FlexRatio(ratio) => {
todo!()
}
Length::Fixed(length) => length.clamp(bounds.min_width, bounds.max_width),
Length::Child => {
todo!()
}
};
let height = match self.height {
Length::Flex => {
todo!()
}
Length::FlexRatio(ratio) => {
todo!()
}
Length::Fixed(length) => length.clamp(bounds.min_height, bounds.max_height),
Length::Child => {
todo!()
}
};
Size { height, width }
}
}

View File

@ -15,3 +15,9 @@ pub use block::Block;
pub mod carousel;
pub use carousel::Carousel;
pub mod sized_box;
pub use sized_box::SizedBox;
pub mod container;
pub use container::Container;

View File

@ -1,6 +1,6 @@
use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Component, Event, Status};
use crate::tuice::{Bounds, Component, Context, Event, Size, Status};
#[derive(Default)]
pub struct Row<'a, Message, B>
@ -29,14 +29,14 @@ impl<'a, Message, B> Component<Message, B> for Row<'a, Message, B>
where
B: Backend,
{
fn draw(&mut self, bounds: Rect, frame: &mut Frame<'_, B>) {
fn draw(&mut self, area: Rect, context: &Context, frame: &mut Frame<'_, B>) {
self.children.iter_mut().for_each(|child| {
// TODO: This is just temp! We need layout!
child.draw(bounds, frame);
child.draw(area, context, frame);
})
}
fn on_event(&mut self, _bounds: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
fn on_event(&mut self, _area: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
Status::Ignored
}
}

View File

@ -1,6 +1,6 @@
use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Component, Event, Status};
use crate::tuice::{Component, Context, Event, Status};
/// A [`Component`] to handle keyboard shortcuts and assign actions to them.
///
@ -11,11 +11,11 @@ impl<Message, B> Component<Message, B> for Shortcut
where
B: Backend,
{
fn draw(&mut self, _bounds: Rect, _frame: &mut Frame<'_, B>) {
fn draw(&mut self, _area: Rect, _context: &Context, _frame: &mut Frame<'_, B>) {
todo!()
}
fn on_event(&mut self, _bounds: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
fn on_event(&mut self, _area: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
Status::Ignored
}
}

View File

@ -0,0 +1,59 @@
use tui::backend::Backend;
use crate::tuice::{Component, Length};
pub struct SizedBox<'a, Message, B>
where
B: Backend,
{
width: Length,
height: Length,
child: Box<dyn Component<Message, B> + 'a>,
}
impl<'a, Message, B> SizedBox<'a, Message, B>
where
B: Backend,
{
/// Creates a new [`SizedBox`] for a child component
/// with a [`Length::Flex`] width and height.
pub fn new(child: Box<dyn Component<Message, B> + 'a>) -> Self {
Self {
width: Length::Flex,
height: Length::Flex,
child,
}
}
/// Creates a new [`SizedBox`] for a child component
/// with a [`Length::Flex`] height.
pub fn with_width(child: Box<dyn Component<Message, B> + 'a>, width: Length) -> Self {
Self {
width,
height: Length::Flex,
child,
}
}
/// Creates a new [`SizedBox`] for a child component
/// with a [`Length::Flex`] width.
pub fn with_height(child: Box<dyn Component<Message, B> + 'a>, height: Length) -> Self {
Self {
width: Length::Flex,
height,
child,
}
}
/// Sets the width of the [`SizedBox`].
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`SizedBox`].
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
}

View File

@ -14,7 +14,7 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::{
constants::TABLE_GAP_HEIGHT_LIMIT,
tuice::{Component, Event, Status},
tuice::{Component, Context, Event, Status},
};
pub use self::table_column::{TextColumn, TextColumnConstraint};
@ -179,7 +179,55 @@ impl<'a, Message, B> Component<Message, B> for TextTable<'a, Message>
where
B: Backend,
{
fn on_event(&mut self, bounds: Rect, event: Event, messages: &mut Vec<Message>) -> Status {
fn draw(&mut self, area: Rect, context: &Context, frame: &mut Frame<'_, B>) {
self.table_gap = if !self.show_gap
|| (self.rows.len() + 2 > area.height.into() && area.height < TABLE_GAP_HEIGHT_LIMIT)
{
0
} else {
1
};
let table_extras = 1 + self.table_gap;
let scrollable_height = area.height.saturating_sub(table_extras);
self.update_column_widths(area);
// Calculate widths first, since we need them later.
let widths = self
.column_widths
.iter()
.map(|column| Constraint::Length(*column))
.collect::<Vec<_>>();
// Then calculate rows. We truncate the amount of data read based on height,
// as well as truncating some entries based on available width.
let data_slice = {
// Note: `get_list_start` already ensures `start` is within the bounds of the number of items, so no need to check!
let start = self
.state
.display_start_index(area, scrollable_height as usize);
let end = min(self.state.num_items(), start + scrollable_height as usize);
self.rows[start..end].to_vec()
};
// Now build up our headers...
let header = Row::new(self.columns.iter().map(|column| column.name.clone()))
.style(self.style_sheet.table_header)
.bottom_margin(self.table_gap);
let mut table = Table::new(data_slice)
.header(header)
.style(self.style_sheet.text);
if self.show_selected_entry {
table = table.highlight_style(self.style_sheet.selected_text);
}
frame.render_stateful_widget(table.widths(&widths), area, self.state.tui_state());
}
fn on_event(&mut self, area: Rect, event: Event, messages: &mut Vec<Message>) -> Status {
use crate::tuice::MouseBoundIntersect;
use crossterm::event::{MouseButton, MouseEventKind};
@ -194,10 +242,10 @@ where
}
}
Event::Mouse(mouse_event) => {
if mouse_event.does_mouse_intersect_bounds(bounds) {
if mouse_event.does_mouse_intersect_bounds(area) {
match mouse_event.kind {
MouseEventKind::Down(MouseButton::Left) => {
let y = mouse_event.row - bounds.top();
let y = mouse_event.row - area.top();
if self.sortable && y == 0 {
todo!()
@ -230,55 +278,6 @@ where
}
}
}
fn draw(&mut self, bounds: Rect, frame: &mut Frame<'_, B>) {
self.table_gap = if !self.show_gap
|| (self.rows.len() + 2 > bounds.height.into()
&& bounds.height < TABLE_GAP_HEIGHT_LIMIT)
{
0
} else {
1
};
let table_extras = 1 + self.table_gap;
let scrollable_height = bounds.height.saturating_sub(table_extras);
self.update_column_widths(bounds);
// Calculate widths first, since we need them later.
let widths = self
.column_widths
.iter()
.map(|column| Constraint::Length(*column))
.collect::<Vec<_>>();
// Then calculate rows. We truncate the amount of data read based on height,
// as well as truncating some entries based on available width.
let data_slice = {
// Note: `get_list_start` already ensures `start` is within the bounds of the number of items, so no need to check!
let start = self
.state
.display_start_index(bounds, scrollable_height as usize);
let end = min(self.state.num_items(), start + scrollable_height as usize);
self.rows[start..end].to_vec()
};
// Now build up our headers...
let header = Row::new(self.columns.iter().map(|column| column.name.clone()))
.style(self.style_sheet.table_header)
.bottom_margin(self.table_gap);
let mut table = Table::new(data_slice)
.header(header)
.style(self.style_sheet.text);
if self.show_selected_entry {
table = table.highlight_style(self.style_sheet.selected_text);
}
frame.render_stateful_widget(table.widths(&widths), bounds, self.state.tui_state());
}
}
#[cfg(test)]

View File

6
src/tuice/context.rs Normal file
View File

@ -0,0 +1,6 @@
use crate::app::layout_manager::LayoutNode;
/// Internal management for drawing and the like.
pub struct Context {
layout_root: LayoutNode,
}

View File

@ -0,0 +1,17 @@
/// [`Bounds`] represent minimal and maximal widths/height constraints while laying things out.
///
/// 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.
pub struct Bounds {
/// The minimal width available.
pub min_width: u16,
/// The minimal height available.
pub min_height: u16,
/// The maximal width available.
pub max_width: u16,
/// The maximal height available.
pub max_height: u16,
}

View File

@ -0,0 +1,12 @@
use tui::layout::Rect;
/// A node for the layout tree.
pub enum LayoutNode {
Leaf {
area: Rect,
},
Branch {
area: Rect,
children: Vec<LayoutNode>,
},
}

View File

@ -1,4 +1,4 @@
/// Which strategy to use while laying out widgets.
/// Which strategy to use while laying out things.
pub enum Length {
/// Fill in remaining space. Equivalent to `Length::FlexRatio(1)`.
Flex,
@ -8,4 +8,7 @@ pub enum Length {
/// Fill in a fixed amount of space.
Fixed(u16),
/// Let the child determine how large to make the component.
Child,
}

View File

@ -1,2 +1,15 @@
pub mod length;
pub use length::Length;
pub mod bounds;
pub use bounds::Bounds;
pub mod size;
pub use size::Size;
pub mod layout_node;
pub use layout_node::LayoutNode;
pub fn build_layout() -> LayoutNode {
todo!()
}

11
src/tuice/layout/size.rs Normal file
View File

@ -0,0 +1,11 @@
/// A [`Size`] represents calculated widths and heights for a component.
///
/// A [`Size`] is sent from a child component back up to its parents after
/// first being given a [`Bounds`](super::Bounds) from the parent.
pub struct Size {
/// The given width.
pub width: u16,
/// The given height.
pub height: u16,
}

View File

@ -14,3 +14,6 @@ pub use runtime::RuntimeEvent;
pub mod layout;
pub use layout::*;
pub mod context;
pub use context::*;