tuice tuice tuice tuice

This commit is contained in:
ClementTsang 2021-12-12 20:29:12 -05:00
parent b6b0493333
commit 58f78731b3
18 changed files with 203 additions and 59 deletions

View File

@ -236,9 +236,7 @@ impl Application for AppState {
fn view(&mut self) -> Element<'static, Self::Message> { fn view(&mut self) -> Element<'static, Self::Message> {
use crate::tuice::TextTable; use crate::tuice::TextTable;
Element::from(Row::with_children(vec![Element::from(TextTable::new( Row::with_children(vec![Element::from(TextTable::new(vec!["A", "B", "C"]))]).into()
vec!["A", "B", "C"],
))]))
} }
fn destroy(&mut self) { fn destroy(&mut self) {

View File

@ -7,14 +7,14 @@ pub use widget::*;
use enum_dispatch::enum_dispatch; use enum_dispatch::enum_dispatch;
use tui::{layout::Rect, Frame}; use tui::{layout::Rect, Frame};
use super::{Bounds, Event, LayoutNode, Size, Status}; use super::{Bounds, DrawContext, Event, LayoutNode, Size, Status};
/// A component displays information and can be interacted with. /// A component displays information and can be interacted with.
#[allow(unused_variables)] #[allow(unused_variables)]
#[enum_dispatch] #[enum_dispatch]
pub trait TmpComponent<Message> { pub trait TmpComponent<Message> {
/// Draws the component. /// Draws the component.
fn draw<Backend>(&mut self, area: Rect, frame: &mut Frame<'_, Backend>) fn draw<Backend>(&mut self, context: DrawContext<'_>, frame: &mut Frame<'_, Backend>)
where where
Backend: tui::backend::Backend; Backend: tui::backend::Backend;

View File

@ -1,11 +1,11 @@
use tui::{backend::Backend, layout::Rect, Frame}; use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Event, Status, TmpComponent}; use crate::tuice::{DrawContext, Event, Status, TmpComponent};
pub struct Block {} pub struct Block {}
impl<Message> TmpComponent<Message> for Block { impl<Message> TmpComponent<Message> for Block {
fn draw<B>(&mut self, _area: Rect, _frame: &mut Frame<'_, B>) fn draw<B>(&mut self, _context: DrawContext<'_>, _frame: &mut Frame<'_, B>)
where where
B: Backend, B: Backend,
{ {

View File

@ -1,11 +1,11 @@
use tui::{backend::Backend, layout::Rect, Frame}; use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Event, Status, TmpComponent}; use crate::tuice::{DrawContext, Event, Status, TmpComponent};
pub struct Carousel {} pub struct Carousel {}
impl<Message> TmpComponent<Message> for Carousel { impl<Message> TmpComponent<Message> for Carousel {
fn draw<B>(&mut self, _area: Rect, _frame: &mut Frame<'_, B>) fn draw<B>(&mut self, _context: DrawContext<'_>, _frame: &mut Frame<'_, B>)
where where
B: Backend, B: Backend,
{ {

View File

@ -1,11 +1,11 @@
use tui::{backend::Backend, layout::Rect, Frame}; use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Event, Status, TmpComponent}; use crate::tuice::{DrawContext, Event, Status, TmpComponent};
pub struct Column {} pub struct Column {}
impl<Message> TmpComponent<Message> for Column { impl<Message> TmpComponent<Message> for Column {
fn draw<B>(&mut self, _area: Rect, _frame: &mut Frame<'_, B>) fn draw<B>(&mut self, _context: DrawContext<'_>, _frame: &mut Frame<'_, B>)
where where
B: Backend, B: Backend,
{ {

View File

@ -1,6 +1,6 @@
use tui::{backend::Backend, layout::Rect, Frame}; use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Bounds, Element, Event, LayoutNode, Size, Status, TmpComponent}; use crate::tuice::{Bounds, DrawContext, Element, Event, LayoutNode, Size, Status, TmpComponent};
/// A [`Container`] just contains a child, as well as being able to be sized. /// A [`Container`] just contains a child, as well as being able to be sized.
/// ///
@ -38,7 +38,7 @@ impl<'a, Message> Container<'a, Message> {
} }
impl<'a, Message> TmpComponent<Message> for Container<'a, Message> { impl<'a, Message> TmpComponent<Message> for Container<'a, Message> {
fn draw<B>(&mut self, area: Rect, _frame: &mut Frame<'_, B>) fn draw<B>(&mut self, context: DrawContext<'_>, _frame: &mut Frame<'_, B>)
where where
B: Backend, B: Backend,
{ {

View File

@ -1,6 +1,6 @@
use tui::{backend::Backend, layout::Rect, Frame}; use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Bounds, Element, Event, LayoutNode, Size, Status, TmpComponent}; use crate::tuice::{Bounds, DrawContext, Element, Event, LayoutNode, Size, Status, TmpComponent};
pub struct FlexElement<'a, Message> { pub struct FlexElement<'a, Message> {
/// Represents a ratio with other [`FlexElement`]s on how far to expand. /// Represents a ratio with other [`FlexElement`]s on how far to expand.
@ -35,11 +35,11 @@ impl<'a, Message> FlexElement<'a, Message> {
self self
} }
pub(crate) fn draw<B>(&mut self, area: Rect, frame: &mut Frame<'_, B>) pub(crate) fn draw<B>(&mut self, context: DrawContext<'_>, frame: &mut Frame<'_, B>)
where where
B: Backend, B: Backend,
{ {
self.element.draw(area, frame) self.element.draw(context, frame)
} }
pub(crate) fn on_event( pub(crate) fn on_event(

View File

@ -1,6 +1,9 @@
use itertools::izip;
use tui::{backend::Backend, layout::Rect, Frame}; use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Bounds, Event, FlexElement, LayoutNode, Size, Status, TmpComponent}; use crate::tuice::{
Bounds, DrawContext, Element, Event, FlexElement, LayoutNode, Size, Status, TmpComponent,
};
#[derive(Default)] #[derive(Default)]
pub struct Row<'a, Message> { pub struct Row<'a, Message> {
@ -18,23 +21,35 @@ impl<'a, Message> Row<'a, Message> {
} }
} }
pub fn with_child(mut self) -> Self { pub fn with_child<E>(mut self, child: E) -> Self
where
E: Into<Element<'a, Message>>,
{
self.children.push(FlexElement::with_no_flex(child.into()));
self self
} }
pub fn with_flex_child(mut self) -> Self { pub fn with_flex_child<E>(mut self, child: E, flex: u16) -> Self
where
E: Into<Element<'a, Message>>,
{
self.children
.push(FlexElement::with_flex(child.into(), flex));
self self
} }
} }
impl<'a, Message> TmpComponent<Message> for Row<'a, Message> { impl<'a, Message> TmpComponent<Message> for Row<'a, Message> {
fn draw<B>(&mut self, area: Rect, frame: &mut Frame<'_, B>) fn draw<B>(&mut self, context: DrawContext<'_>, frame: &mut Frame<'_, B>)
where where
B: Backend, B: Backend,
{ {
self.children.iter_mut().for_each(|child| { self.children
child.draw(area, frame); .iter_mut()
}) .zip(context.children())
.for_each(|(child, child_node)| {
child.draw(child_node, frame);
});
} }
fn on_event(&mut self, _area: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status { fn on_event(&mut self, _area: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
@ -43,17 +58,75 @@ impl<'a, Message> TmpComponent<Message> for Row<'a, Message> {
fn layout(&self, bounds: Bounds, node: &mut LayoutNode) -> Size { fn layout(&self, bounds: Bounds, node: &mut LayoutNode) -> Size {
let mut remaining_bounds = bounds; let mut remaining_bounds = bounds;
let mut children = vec![LayoutNode::default(); self.children.len()];
let mut inflexible_children_indexes = vec![];
let mut offsets = vec![];
let mut current_x = 0;
let mut current_y = 0;
let mut sizes = Vec::with_capacity(self.children.len());
let mut current_size = Size::default();
let child_nodes: Vec<LayoutNode> = self let mut get_child_size = |child: &FlexElement<'_, Message>,
.children child_node: &mut LayoutNode,
remaining_bounds: &mut Bounds| {
let size = child.layout(*remaining_bounds, child_node);
current_size += size;
remaining_bounds.shrink_size(size);
offsets.push((current_x, current_y));
current_x += size.width;
current_y += size.height;
size
};
// We handle inflexible children first, then distribute all remaining
// space to flexible children.
self.children
.iter() .iter()
.map(|child| { .zip(children.iter_mut())
let mut child_node = LayoutNode::default(); .enumerate()
let size = child.layout(remaining_bounds, &mut child_node); .for_each(|(index, (child, child_node))| {
child_node if child.flex == 0 && remaining_bounds.has_space() {
}) let size = get_child_size(child, child_node, &mut remaining_bounds);
.collect(); sizes.push(size);
} else {
inflexible_children_indexes.push(index);
sizes.push(Size::default());
}
});
todo!() inflexible_children_indexes.into_iter().for_each(|index| {
// The index accesses are safe by above definitions, so we can use unsafe operations.
// If you EVER make changes to above, ensure this invariant still holds!
let child = unsafe { self.children.get_unchecked(index) };
let child_node = unsafe { children.get_unchecked_mut(index) };
let size = unsafe { sizes.get_unchecked_mut(index) };
*size = get_child_size(child, child_node, &mut remaining_bounds);
});
// If there is still remaining space after, distribute the rest if
// appropriate (e.x. current_size is too small for the bounds).
if current_size.width < bounds.min_width {
// For now, we'll cheat and just set it to be equal.
current_size.width = bounds.min_width;
}
if current_size.height < bounds.min_height {
// For now, we'll cheat and just set it to be equal.
current_size.height = bounds.min_height;
}
// Now that we're done determining sizes, convert all children into the appropriate
// layout nodes. Remember - parents determine children, and so, we determine
// children here!
izip!(sizes, offsets, children.iter_mut()).for_each(
|(size, offset, child): (Size, (u16, u16), &mut LayoutNode)| {
let rect = Rect::new(offset.0, offset.1, size.width, size.height);
child.rect = rect;
},
);
node.children = children;
current_size
} }
} }

View File

@ -1,6 +1,6 @@
use tui::{backend::Backend, layout::Rect, Frame}; use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Event, Status, TmpComponent}; use crate::tuice::{DrawContext, Event, Status, TmpComponent};
/// A [`Component`] to handle keyboard shortcuts and assign actions to them. /// A [`Component`] to handle keyboard shortcuts and assign actions to them.
/// ///
@ -8,7 +8,7 @@ use crate::tuice::{Event, Status, TmpComponent};
pub struct Shortcut {} pub struct Shortcut {}
impl<Message> TmpComponent<Message> for Shortcut { impl<Message> TmpComponent<Message> for Shortcut {
fn draw<B>(&mut self, _area: Rect, _frame: &mut Frame<'_, B>) fn draw<B>(&mut self, _context: DrawContext<'_>, _frame: &mut Frame<'_, B>)
where where
B: Backend, B: Backend,
{ {

View File

@ -14,7 +14,7 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::{ use crate::{
constants::TABLE_GAP_HEIGHT_LIMIT, constants::TABLE_GAP_HEIGHT_LIMIT,
tuice::{Event, Status, TmpComponent}, tuice::{DrawContext, Event, Status, TmpComponent},
}; };
pub use self::table_column::{TextColumn, TextColumnConstraint}; pub use self::table_column::{TextColumn, TextColumnConstraint};
@ -166,12 +166,14 @@ impl<'a, Message> TextTable<'a, Message> {
} }
impl<'a, Message> TmpComponent<Message> for TextTable<'a, Message> { impl<'a, Message> TmpComponent<Message> for TextTable<'a, Message> {
fn draw<B>(&mut self, area: Rect, frame: &mut Frame<'_, B>) fn draw<B>(&mut self, context: DrawContext<'_>, frame: &mut Frame<'_, B>)
where where
B: Backend, B: Backend,
{ {
let rect = context.rect();
self.table_gap = if !self.show_gap self.table_gap = if !self.show_gap
|| (self.rows.len() + 2 > area.height.into() && area.height < TABLE_GAP_HEIGHT_LIMIT) || (self.rows.len() + 2 > rect.height.into() && rect.height < TABLE_GAP_HEIGHT_LIMIT)
{ {
0 0
} else { } else {
@ -179,8 +181,8 @@ impl<'a, Message> TmpComponent<Message> for TextTable<'a, Message> {
}; };
let table_extras = 1 + self.table_gap; let table_extras = 1 + self.table_gap;
let scrollable_height = area.height.saturating_sub(table_extras); let scrollable_height = rect.height.saturating_sub(table_extras);
self.update_column_widths(area); self.update_column_widths(rect);
// Calculate widths first, since we need them later. // Calculate widths first, since we need them later.
let widths = self let widths = self
@ -195,7 +197,7 @@ impl<'a, Message> TmpComponent<Message> for TextTable<'a, Message> {
// Note: `get_list_start` already ensures `start` is within the bounds of the number of items, so no need to check! // Note: `get_list_start` already ensures `start` is within the bounds of the number of items, so no need to check!
let start = self let start = self
.state .state
.display_start_index(area, scrollable_height as usize); .display_start_index(rect, scrollable_height as usize);
let end = min(self.state.num_items(), start + scrollable_height as usize); let end = min(self.state.num_items(), start + scrollable_height as usize);
self.rows[start..end].to_vec() self.rows[start..end].to_vec()
@ -214,7 +216,7 @@ impl<'a, Message> TmpComponent<Message> for TextTable<'a, Message> {
table = table.highlight_style(self.style_sheet.selected_text); table = table.highlight_style(self.style_sheet.selected_text);
} }
frame.render_stateful_widget(table.widths(&widths), area, self.state.tui_state()); frame.render_stateful_widget(table.widths(&widths), rect, self.state.tui_state());
} }
fn on_event(&mut self, area: Rect, event: Event, messages: &mut Vec<Message>) -> Status { fn on_event(&mut self, area: Rect, event: Event, messages: &mut Vec<Message>) -> Status {

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

@ -0,0 +1,31 @@
use tui::layout::Rect;
use super::LayoutNode;
pub struct DrawContext<'a> {
current_node: &'a LayoutNode,
current_offset: (u16, u16),
}
impl<'a> DrawContext<'_> {
pub(crate) fn new() {}
pub(crate) fn rect(&self) -> Rect {
self.current_node.rect
}
pub(crate) fn children(&self) -> impl Iterator<Item = DrawContext<'_>> {
let new_offset = (
self.current_offset.0 + self.current_node.rect.x,
self.current_offset.1 + self.current_node.rect.y,
);
self.current_node
.children
.iter()
.map(move |layout_node| DrawContext {
current_node: layout_node,
current_offset: new_offset,
})
}
}

View File

@ -2,8 +2,8 @@ use enum_dispatch::enum_dispatch;
use tui::{layout::Rect, Frame}; use tui::{layout::Rect, Frame};
use super::{ use super::{
Block, Bounds, Carousel, Column, Container, Event, LayoutNode, Row, Shortcut, Size, Status, Block, Bounds, Carousel, Column, Container, DrawContext, Event, LayoutNode, Row, Shortcut,
TextTable, TmpComponent, Size, Status, TextTable, TmpComponent,
}; };
/// An [`Element`] is an instantiated [`Component`]. /// An [`Element`] is an instantiated [`Component`].

View File

@ -1,3 +1,5 @@
use crate::tuice::Size;
/// [`Bounds`] represent minimal and maximal widths/height constraints while laying things out. /// [`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) /// These are sent from a parent component to a child to determine the [`Size`](super::Size)
@ -18,12 +20,23 @@ pub struct Bounds {
} }
impl Bounds { impl Bounds {
pub fn with_two_bounds(width: u16, height: u16) -> Self { /// Shrinks the current bounds by some amount.
Self { pub fn shrink(&mut self, width: u16, height: u16) {
min_width: width, self.max_width = self.max_width.saturating_sub(width);
min_height: height, self.max_height = self.max_height.saturating_sub(height);
max_width: width, }
max_height: height,
} /// Shrinks by a given [`Size`].
pub fn shrink_size(&mut self, size: Size) {
self.max_width = self.max_width.saturating_sub(size.width);
self.max_height = self.max_height.saturating_sub(size.height);
}
/// 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.max_width == 0
|| self.max_height == 0
} }
} }

View File

@ -3,7 +3,7 @@ use tui::layout::Rect;
use crate::tuice::{Bounds, Element, LayoutNode, TmpComponent}; use crate::tuice::{Bounds, Element, LayoutNode, TmpComponent};
pub fn build_layout_tree<Message>(area: Rect, root: &Element<'_, Message>) -> LayoutNode { pub fn build_layout_tree<Message>(area: Rect, root: &Element<'_, Message>) -> LayoutNode {
let mut root_layout_node = LayoutNode::from_area(area); let mut root_layout_node = LayoutNode::from_rect(area);
let bounds = Bounds { let bounds = Bounds {
min_width: 0, min_width: 0,
min_height: 0, min_height: 0,

View File

@ -1,15 +1,15 @@
use tui::layout::Rect; use tui::layout::Rect;
#[derive(Default)] #[derive(Clone, Default)]
pub struct LayoutNode { pub struct LayoutNode {
pub area: Rect, pub rect: Rect,
pub children: Vec<LayoutNode>, pub children: Vec<LayoutNode>,
} }
impl LayoutNode { impl LayoutNode {
pub fn from_area(area: Rect) -> Self { pub fn from_rect(rect: Rect) -> Self {
Self { Self {
area, rect,
children: vec![], children: vec![],
} }
} }

View File

@ -1,11 +1,34 @@
use std::ops::{Add, AddAssign};
/// A [`Size`] represents calculated widths and heights for a component. /// A [`Size`] represents calculated widths and heights for a component.
/// ///
/// A [`Size`] is sent from a child component back up to its parents after /// A [`Size`] is sent from a child component back up to its parents after
/// first being given a [`Bounds`](super::Bounds) from the parent. /// first being given a [`Bounds`](super::Bounds) from the parent.
#[derive(Clone, Copy, Default)]
pub struct Size { pub struct Size {
/// The given width. /// The width that the component has determined.
pub width: u16, pub width: u16,
/// The given height. /// The height that the component has determined.
pub height: u16, pub height: u16,
} }
impl Add for Size {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self {
width: self.width + rhs.width,
height: self.height + rhs.height,
}
}
}
impl AddAssign for Size {
fn add_assign(&mut self, rhs: Self) {
*self = Self {
width: self.width + rhs.width,
height: self.height + rhs.height,
}
}
}

View File

@ -17,3 +17,6 @@ pub use layout::*;
pub mod element; pub mod element;
pub use element::*; pub use element::*;
pub mod context;
pub use context::*;

View File

@ -24,8 +24,8 @@ pub(crate) fn launch<A: Application + 'static>(
RuntimeEvent::UserInterface(event) => { RuntimeEvent::UserInterface(event) => {
let mut messages = vec![]; let mut messages = vec![];
let bounds = Rect::default(); // TODO: TEMP let rect = Rect::default(); // FIXME: TEMP
match user_interface.on_event(bounds, event, &mut messages) { match user_interface.on_event(rect, event, &mut messages) {
Status::Captured => {} Status::Captured => {}
Status::Ignored => { Status::Ignored => {
application.global_event_handler(event, &mut messages); application.global_event_handler(event, &mut messages);
@ -38,6 +38,7 @@ pub(crate) fn launch<A: Application + 'static>(
} }
user_interface = application.view(); user_interface = application.view();
// FIXME: Draw!
} }
RuntimeEvent::Custom(message) => { RuntimeEvent::Custom(message) => {
application.update(message); application.update(message);