From ef44e2cf328c75565ca110b566f0ab1b6432ff6e Mon Sep 17 00:00:00 2001 From: ClementTsang Date: Sun, 30 Jan 2022 21:33:20 -0500 Subject: tmp commit --- Cargo.lock | 7 + Cargo.toml | 2 +- src/tuine/component/base/block.rs | 421 ++++++++++++++++++++++++----- src/tuine/component/base/flex/mod.rs | 6 +- src/tuine/component/base/padding.rs | 16 ++ src/tuine/component/base/shortcut.rs | 6 +- src/tuine/component/base/text_table/mod.rs | 4 +- src/tuine/component/base/time_graph.rs | 25 -- src/tuine/component/mod.rs | 3 +- src/tuine/context/context.rs | 35 +++ src/tuine/context/mod.rs | 3 + src/tuine/key.rs | 16 ++ src/tuine/layout/size.rs | 2 +- src/tuine/mod.rs | 10 +- tests/invalid_config_tests.rs | 2 +- 15 files changed, 449 insertions(+), 109 deletions(-) create mode 100644 src/tuine/context/context.rs diff --git a/Cargo.lock b/Cargo.lock index af64176f..ba3f7c2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -247,6 +247,7 @@ dependencies = [ "float-ord", "futures", "futures-timer", + "gapbuffer", "heim", "indextree", "itertools", @@ -708,6 +709,12 @@ dependencies = [ "slab", ] +[[package]] +name = "gapbuffer" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e929b3ff01e4accdce7f5596a044890b5052ab7418ba8ce9ce5865d26ae4417" + [[package]] name = "getrandom" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 7cf45e5b..25aeb90d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ path = "src/bin/main.rs" doc = false [lib] -test = false doctest = false doc = false @@ -43,6 +42,7 @@ enum_dispatch = "0.3.7" float-ord = "0.3.2" futures = "0.3.14" futures-timer = "3.0.2" # TODO: Remove? +gapbuffer = "0.1.1" indextree = "4.3.1" # TODO: Remove? itertools = "0.10.0" once_cell = "1.5.2" diff --git a/src/tuine/component/base/block.rs b/src/tuine/component/base/block.rs index 99776ffe..743a39d7 100644 --- a/src/tuine/component/base/block.rs +++ b/src/tuine/component/base/block.rs @@ -12,6 +12,13 @@ pub struct StyleSheet { pub border: Style, } +struct BorderOffsets { + left: u16, + right: u16, + top: u16, + bottom: u16, +} + /// A [`Block`] is a widget that draws a border around a child [`Component`], as well as optional /// titles. pub struct Block @@ -51,49 +58,30 @@ where self } - fn inner_rect(&self, original: Rect) -> Rect { - let mut inner = original; - - if self.borders.intersects(Borders::LEFT) { - inner.x = inner.x.saturating_add(1).min(inner.right()); - inner.width = inner.width.saturating_sub(1); - } - if self.borders.intersects(Borders::TOP) - || self.left_text.is_some() - || self.right_text.is_some() - { - inner.y = inner.y.saturating_add(1).min(inner.bottom()); - inner.height = inner.height.saturating_sub(1); - } - if self.borders.intersects(Borders::RIGHT) { - inner.width = inner.width.saturating_sub(1); - } - if self.borders.intersects(Borders::BOTTOM) { - inner.height = inner.height.saturating_sub(1); - } - inner + pub fn borders(mut self, borders: Borders) -> Self { + self.borders = borders; + self } - fn outer_size(&self, original: Size) -> Size { - let mut outer = original; - - if self.borders.intersects(Borders::LEFT) { - outer.width = outer.width.saturating_add(1); - } - if self.borders.intersects(Borders::TOP) - || self.left_text.is_some() - || self.right_text.is_some() - { - outer.height = outer.height.saturating_add(1); - } - if self.borders.intersects(Borders::RIGHT) { - outer.width = outer.width.saturating_add(1); - } - if self.borders.intersects(Borders::BOTTOM) { - outer.height = outer.height.saturating_add(1); + fn border_offsets(&self) -> BorderOffsets { + fn border_val(has_val: bool) -> u16 { + if has_val { + 1 + } else { + 0 + } } - outer + BorderOffsets { + left: border_val(self.borders.intersects(Borders::LEFT)), + right: border_val(self.borders.intersects(Borders::RIGHT)), + top: border_val( + self.borders.intersects(Borders::TOP) + || self.left_text.is_some() + || self.right_text.is_some(), + ), + bottom: border_val(self.borders.intersects(Borders::BOTTOM)), + } } } @@ -108,17 +96,17 @@ where B: Backend, { let rect = draw_ctx.global_rect(); - - frame.render_widget( - tui::widgets::Block::default() - .borders(self.borders) - .border_style(self.style_sheet.border), - rect, - ); - - if let Some(child) = &mut self.child { - if let Some(child_draw_ctx) = draw_ctx.children().next() { - child.draw(state_ctx, &child_draw_ctx, frame) + if rect.area() > 0 { + frame.render_widget( + tui::widgets::Block::default() + .borders(self.borders) + .border_style(self.style_sheet.border), + rect, + ); + if let Some(child) = &mut self.child { + if let Some(child_draw_ctx) = draw_ctx.children().next() { + child.draw(state_ctx, &child_draw_ctx, frame) + } } } } @@ -136,29 +124,45 @@ where Status::Ignored } - fn layout(&self, bounds: Bounds, node: &mut LayoutNode) -> crate::tuine::Size { + fn layout(&self, bounds: Bounds, node: &mut LayoutNode) -> Size { if let Some(child) = &self.child { - // Reduce bounds based on borders - let inner_rect = self.inner_rect(Rect::new(0, 0, bounds.max_width, bounds.max_height)); - let child_bounds = Bounds { - min_width: bounds.min_width, - min_height: bounds.min_height, - max_width: inner_rect.width, - max_height: inner_rect.height, - }; - - let mut child_node = LayoutNode::default(); - let child_size = child.layout(child_bounds, &mut child_node); - - child_node.rect = Rect::new( - inner_rect.x, - inner_rect.y, - child_size.width, - child_size.height, - ); - node.children = vec![child_node]; + let BorderOffsets { + left: left_offset, + right: right_offset, + top: top_offset, + bottom: bottom_offset, + } = self.border_offsets(); + + let vertical_offset = top_offset + bottom_offset; + let horizontal_offset = left_offset + right_offset; + + if bounds.max_height > vertical_offset && bounds.max_width > horizontal_offset { + let max_width = bounds.max_width - horizontal_offset; + let max_height = bounds.max_height - vertical_offset; + + let child_bounds = Bounds { + min_width: bounds.min_width, + min_height: bounds.min_height, + max_width, + max_height, + }; + let mut child_node = LayoutNode::default(); + let child_size = child.layout(child_bounds, &mut child_node); - self.outer_size(child_size) + child_node.rect = + Rect::new(left_offset, top_offset, child_size.width, child_size.height); + node.children = vec![child_node]; + + Size { + width: child_size.width + horizontal_offset, + height: child_size.height + vertical_offset, + } + } else { + Size { + width: 0, + height: 0, + } + } } else { Size { width: 0, @@ -167,3 +171,278 @@ where } } } + +#[cfg(test)] +mod tests { + use crate::tuine::Empty; + + use super::*; + + fn assert_border_offset(block: Block<(), Empty>, left: u16, right: u16, top: u16, bottom: u16) { + let offsets = block.border_offsets(); + assert_eq!(offsets.left, left, "left offset should be equal"); + assert_eq!(offsets.right, right, "right offset should be equal"); + assert_eq!(offsets.top, top, "top offset should be equal"); + assert_eq!(offsets.bottom, bottom, "bottom offset should be equal"); + } + + #[test] + fn empty_border_offset() { + let block: Block<(), Empty> = Block::with_child(Empty::default()).borders(Borders::empty()); + assert_border_offset(block, 0, 0, 0, 0); + } + + #[test] + fn all_border_offset() { + let block: Block<(), Empty> = Block::with_child(Empty::default()); + assert_border_offset(block, 1, 1, 1, 1); + } + + #[test] + fn horizontal_border_offset() { + let block: Block<(), Empty> = + Block::with_child(Empty::default()).borders(Borders::LEFT.union(Borders::RIGHT)); + assert_border_offset(block, 1, 1, 0, 0); + } + + #[test] + fn vertical_border_offset() { + let block: Block<(), Empty> = + Block::with_child(Empty::default()).borders(Borders::BOTTOM.union(Borders::TOP)); + assert_border_offset(block, 0, 0, 1, 1); + } + + #[test] + fn top_right() { + let block: Block<(), Empty> = + Block::with_child(Empty::default()).borders(Borders::RIGHT.union(Borders::TOP)); + assert_border_offset(block, 0, 1, 1, 0); + } + + #[test] + fn bottom_left() { + let block: Block<(), Empty> = + Block::with_child(Empty::default()).borders(Borders::BOTTOM.union(Borders::LEFT)); + assert_border_offset(block, 1, 0, 0, 1); + } + + #[test] + fn full_layout() { + let block: Block<(), Empty> = Block::with_child(Empty::default()); + let mut layout_node = LayoutNode::default(); + let bounds = Bounds { + min_width: 0, + min_height: 0, + max_width: 10, + max_height: 10, + }; + + assert_eq!( + block.layout(bounds, &mut layout_node), + Size { + width: 10, + height: 10, + }, + "the block should have dimensions (10, 10)." + ); + + assert_eq!( + layout_node.children[0].rect, + Rect { + x: 1, + y: 1, + width: 8, + height: 8 + }, + "the only child should have an offset of (1, 1), and dimensions (8, 8)" + ); + } + + #[test] + fn vertical_layout() { + let block: Block<(), Empty> = + Block::with_child(Empty::default()).borders(Borders::BOTTOM.union(Borders::TOP)); + let mut layout_node = LayoutNode::default(); + let bounds = Bounds { + min_width: 0, + min_height: 0, + max_width: 10, + max_height: 10, + }; + + assert_eq!( + block.layout(bounds, &mut layout_node), + Size { + width: 10, + height: 10, + }, + "the block should have dimensions (10, 10)." + ); + + assert_eq!( + layout_node.children[0].rect, + Rect { + x: 0, + y: 1, + width: 10, + height: 8 + }, + "the only child should have an offset of (0, 1), and dimensions (10, 8)" + ); + } + + #[test] + fn horizontal_layout() { + let block: Block<(), Empty> = + Block::with_child(Empty::default()).borders(Borders::LEFT.union(Borders::RIGHT)); + let mut layout_node = LayoutNode::default(); + let bounds = Bounds { + min_width: 0, + min_height: 0, + max_width: 10, + max_height: 10, + }; + + assert_eq!( + block.layout(bounds, &mut layout_node), + Size { + width: 10, + height: 10, + }, + "the block should have dimensions (10, 10)." + ); + + assert_eq!( + layout_node.children[0].rect, + Rect { + x: 1, + y: 0, + width: 8, + height: 10 + }, + "the only child should have an offset of (1, 0), and dimensions (8, 10)" + ); + } + + #[test] + fn irregular_layout_one() { + let block: Block<(), Empty> = + Block::with_child(Empty::default()).borders(Borders::LEFT.union(Borders::TOP)); + let mut layout_node = LayoutNode::default(); + let bounds = Bounds { + min_width: 0, + min_height: 0, + max_width: 10, + max_height: 10, + }; + + assert_eq!( + block.layout(bounds, &mut layout_node), + Size { + width: 10, + height: 10, + }, + "the block should have dimensions (10, 10)." + ); + + assert_eq!( + layout_node.children[0].rect, + Rect { + x: 1, + y: 1, + width: 9, + height: 9 + }, + "the only child should have an offset of (1, 1), and dimensions (9, 9)" + ); + } + + #[test] + fn irregular_layout_two() { + let block: Block<(), Empty> = + Block::with_child(Empty::default()).borders(Borders::BOTTOM.union(Borders::RIGHT)); + let mut layout_node = LayoutNode::default(); + let bounds = Bounds { + min_width: 0, + min_height: 0, + max_width: 10, + max_height: 10, + }; + + assert_eq!( + block.layout(bounds, &mut layout_node), + Size { + width: 10, + height: 10, + }, + "the block should have dimensions (10, 10)." + ); + + assert_eq!( + layout_node.children[0].rect, + Rect { + x: 0, + y: 0, + width: 9, + height: 9 + }, + "the only child should have an offset of (0, 0), and dimensions (9, 9)" + ); + } + + #[test] + fn irregular_layout_three() { + let block: Block<(), Empty> = + Block::with_child(Empty::default()).borders(Borders::RIGHT.union(Borders::TOP)); + let mut layout_node = LayoutNode::default(); + let bounds = Bounds { + min_width: 0, + min_height: 0, + max_width: 10, + max_height: 10, + }; + + assert_eq!( + block.layout(bounds, &mut layout_node), + Size { + width: 10, + height: 10, + }, + "the block should have dimensions (10, 10)." + ); + + assert_eq!( + layout_node.children[0].rect, + Rect { + x: 0, + y: 1, + width: 9, + height: 9 + }, + "the only child should have an offset of (0, 1), and dimensions (9, 9)" + ); + } + + #[test] + fn too_small_layout() { + let block: Block<(), Empty> = Block::with_child(Empty::default()); + let mut layout_node = LayoutNode::default(); + let bounds = Bounds { + min_width: 0, + min_height: 0, + max_width: 2, + max_height: 2, + }; + + assert_eq!( + block.layout(bounds, &mut layout_node), + Size { + width: 0, + height: 0, + }, + "the area should be 0" + ); + + assert_eq!(layout_node.children.len(), 0, "layout node should be empty"); + } +} diff --git a/src/tuine/component/base/flex/mod.rs b/src/tuine/component/base/flex/mod.rs index d4262069..99a8f555 100644 --- a/src/tuine/component/base/flex/mod.rs +++ b/src/tuine/component/base/flex/mod.rs @@ -191,14 +191,16 @@ impl TmpComponent for Flex { // 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. + // FIXME: 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. + // FIXME: For now, we'll cheat and just set it to be equal. current_size.height = bounds.min_height; } + // FIXME: Remove area 0 children + // 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! diff --git a/src/tuine/component/base/padding.rs b/src/tuine/component/base/padding.rs index e69de29b..c7c1ef9e 100644 --- a/src/tuine/component/base/padding.rs +++ b/src/tuine/component/base/padding.rs @@ -0,0 +1,16 @@ +use std::marker::PhantomData; + +use crate::tuine::TmpComponent; + +/// A [`Padding`] surrounds a child widget with spacing. +pub struct Padding +where + Child: TmpComponent, +{ + _pd: PhantomData, + padding_left: u16, + padding_right: u16, + padding_up: u16, + padding_down: u16, + child: Option, +} diff --git a/src/tuine/component/base/shortcut.rs b/src/tuine/component/base/shortcut.rs index 812a3d63..7300341f 100644 --- a/src/tuine/component/base/shortcut.rs +++ b/src/tuine/component/base/shortcut.rs @@ -8,8 +8,8 @@ use rustc_hash::FxHashMap; use tui::{backend::Backend, layout::Rect, Frame}; use crate::tuine::{ - Bounds, DrawContext, Event, Key, LayoutNode, Size, StateContext, StatefulComponent, Status, - TmpComponent, + Bounds, BuildContext, DrawContext, Event, Key, LayoutNode, Size, StateContext, + StatefulComponent, Status, TmpComponent, }; const MAX_TIMEOUT: Duration = Duration::from_millis(400); @@ -145,7 +145,7 @@ where type ComponentState = ShortcutState; - fn build(ctx: &mut crate::tuine::BuildContext<'_>, props: Self::Properties) -> Self { + fn build(ctx: &mut BuildContext<'_>, props: Self::Properties) -> Self { let (key, state) = ctx.register_and_mut_state::<_, Self::ComponentState>(Location::caller()); let mut forest: FxHashMap, bool> = FxHashMap::default(); diff --git a/src/tuine/component/base/text_table/mod.rs b/src/tuine/component/base/text_table/mod.rs index b29338e8..6b08c8d1 100644 --- a/src/tuine/component/base/text_table/mod.rs +++ b/src/tuine/component/base/text_table/mod.rs @@ -90,7 +90,7 @@ impl TextTable { 0 } else { // +1 for the spacing - width_remaining -= width + 1; + width_remaining = width_remaining.saturating_sub(width + 1); width } }) @@ -372,7 +372,7 @@ impl TmpComponent for TextTable { #[cfg(test)] mod tests { use crate::tuine::{ - text_table::SortType, StateMap, StatefulComponent, TextTableProps, BuildContext, + text_table::SortType, BuildContext, StateMap, StatefulComponent, TextTableProps, }; use super::{DataRow, TextTable}; diff --git a/src/tuine/component/base/time_graph.rs b/src/tuine/component/base/time_graph.rs index 6059dce7..9c899243 100644 --- a/src/tuine/component/base/time_graph.rs +++ b/src/tuine/component/base/time_graph.rs @@ -27,28 +27,3 @@ pub struct TimeGraph { } impl TimeGraph {} - -impl TmpComponent for TimeGraph { - fn draw( - &mut self, state_ctx: &mut StateContext<'_>, draw_ctx: &DrawContext<'_>, - frame: &mut Frame<'_, Backend>, - ) where - Backend: tui::backend::Backend, - { - todo!() - } - - fn on_event( - &mut self, state_ctx: &mut StateContext<'_>, draw_ctx: &DrawContext<'_>, event: Event, - messages: &mut Vec, - ) -> Status { - Status::Ignored - } - - fn layout(&self, bounds: Bounds, node: &mut LayoutNode) -> crate::tuine::Size { - crate::tuine::Size { - width: bounds.max_width, - height: bounds.max_height, - } - } -} diff --git a/src/tuine/component/mod.rs b/src/tuine/component/mod.rs index aaea0f32..b7a2224e 100644 --- a/src/tuine/component/mod.rs +++ b/src/tuine/component/mod.rs @@ -10,8 +10,7 @@ pub use stateful::*; pub mod banner; pub use banner::*; -// pub mod stateless; -// pub use stateless::*; + use enum_dispatch::enum_dispatch; use tui::Frame; diff --git a/src/tuine/context/context.rs b/src/tuine/context/context.rs new file mode 100644 index 00000000..ea06ce18 --- /dev/null +++ b/src/tuine/context/context.rs @@ -0,0 +1,35 @@ +use gapbuffer::GapBuffer; + +use crate::tuine::{Key, KeyCreator, State}; + +/// A [`Context`] is used to create a [`Component`](super::Component). +/// +/// The internal implementation is based on Jetpack Compose's [Positional Memoization](https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd), +/// in addition to [Crochet](https://github.com/raphlinus/crochet/blob/master/src/tree.rs) in its entirety. +pub struct Context { + component_key_creator: KeyCreator, + buffer: GapBuffer, +} + +enum Payload { + State(Box), + View, +} + +struct Item { + key: Key, + payload: Payload, +} + +enum Slot { + Begin(Item), + End, +} + +impl Context { + pub fn use_state(&self) {} + + pub fn start(&mut self) {} + + pub fn end(&mut self) {} +} diff --git a/src/tuine/context/mod.rs b/src/tuine/context/mod.rs index de4ee08e..904a52df 100644 --- a/src/tuine/context/mod.rs +++ b/src/tuine/context/mod.rs @@ -9,3 +9,6 @@ pub use build_context::BuildContext; pub mod state_context; pub use state_context::StateContext; + +pub mod context; +pub use context::Context; diff --git a/src/tuine/key.rs b/src/tuine/key.rs index 05e70152..16f65980 100644 --- a/src/tuine/key.rs +++ b/src/tuine/key.rs @@ -30,3 +30,19 @@ impl Key { } } } + +#[derive(Default, Clone, Copy, Debug)] +pub struct KeyCreator { + index: usize, +} + +impl KeyCreator { + pub fn new_key(&mut self, caller: impl Into) -> Key { + self.index += 1; + Key::new(caller, self.index) + } + + pub fn reset(&mut self) { + self.index = 0; + } +} diff --git a/src/tuine/layout/size.rs b/src/tuine/layout/size.rs index 74bbf4df..003ae8fe 100644 --- a/src/tuine/layout/size.rs +++ b/src/tuine/layout/size.rs @@ -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, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct Size { /// The width that the component has determined. pub width: u16, diff --git a/src/tuine/mod.rs b/src/tuine/mod.rs index f7ec3ede..111de69d 100644 --- a/src/tuine/mod.rs +++ b/src/tuine/mod.rs @@ -1,14 +1,22 @@ //! tuine is a wrapper around tui-rs that expands on it with state management and //! event handling. //! -//! tuine is inspired by a **ton** of other libraries and frameworks, like: +//! tuine is inspired by a **ton** of other libraries and frameworks: //! //! - [Crochet](https://github.com/raphlinus/crochet) +//! - [Dioxus](https://github.com/DioxusLabs/dioxus) //! - [Druid](https://github.com/linebender/druid) //! - [Flutter](https://flutter.dev/) //! - [Iced](https://github.com/iced-rs/iced) +//! - [Jetpack Compose](https://developer.android.com/jetpack/compose) //! - [React](https://reactjs.org/) //! - [Yew](https://yew.rs/) +//! +//! In addition, Raph Levien's post, +//! [*Towards principled reactive UI](https://raphlinus.github.io/rust/druid/2020/09/25/principled-reactive-ui.html), +//! was a fantastic source of information for someone like me who had basically zero knowledge heading in. +//! +//! None of this would be possible without these as reference points and sources of inspiration and learning! mod tui_rs; diff --git a/tests/invalid_config_tests.rs b/tests/invalid_config_tests.rs index f8908b5c..5ce0f9b5 100644 --- a/tests/invalid_config_tests.rs +++ b/tests/invalid_config_tests.rs @@ -25,7 +25,7 @@ fn test_empty_layout() { .arg("./tests/invalid_configs/empty_layout.toml") .assert() .failure() - .stderr(predicate::str::contains("Configuration file error")); // FIXME: [Urgent] Use a const for the error pattern + .stderr(predicate::str::contains("cannot be empty")); // FIXME: [Urgent] Use a const for the error pattern } #[test] -- cgit v1.2.3