tmp commit

This commit is contained in:
ClementTsang 2022-01-30 21:33:20 -05:00
parent d0a0cc5e8c
commit ef44e2cf32
15 changed files with 447 additions and 107 deletions

7
Cargo.lock generated

@ -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"

@ -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"

@ -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<Message, Child>
@ -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 BorderOffsets {
left: left_offset,
right: right_offset,
top: top_offset,
bottom: bottom_offset,
} = self.border_offsets();
let mut child_node = LayoutNode::default();
let child_size = child.layout(child_bounds, &mut child_node);
let vertical_offset = top_offset + bottom_offset;
let horizontal_offset = left_offset + right_offset;
child_node.rect = Rect::new(
inner_rect.x,
inner_rect.y,
child_size.width,
child_size.height,
);
node.children = vec![child_node];
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;
self.outer_size(child_size)
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);
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");
}
}

@ -191,14 +191,16 @@ impl<Message> TmpComponent<Message> for Flex<Message> {
// 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!

@ -0,0 +1,16 @@
use std::marker::PhantomData;
use crate::tuine::TmpComponent;
/// A [`Padding`] surrounds a child widget with spacing.
pub struct Padding<Child, Message>
where
Child: TmpComponent<Message>,
{
_pd: PhantomData<Message>,
padding_left: u16,
padding_right: u16,
padding_up: u16,
padding_down: u16,
child: Option<Child>,
}

@ -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<Vec<Event>, bool> = FxHashMap::default();

@ -90,7 +90,7 @@ impl<Message> TextTable<Message> {
0
} else {
// +1 for the spacing
width_remaining -= width + 1;
width_remaining = width_remaining.saturating_sub(width + 1);
width
}
})
@ -372,7 +372,7 @@ impl<Message> TmpComponent<Message> for TextTable<Message> {
#[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};

@ -27,28 +27,3 @@ pub struct TimeGraph {
}
impl TimeGraph {}
impl<Message> TmpComponent<Message> for TimeGraph {
fn draw<Backend>(
&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<Message>,
) -> 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,
}
}
}

@ -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;

@ -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<Slot>,
}
enum Payload {
State(Box<dyn State>),
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) {}
}

@ -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;

@ -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<Caller>) -> Key {
self.index += 1;
Key::new(caller, self.index)
}
pub fn reset(&mut self) {
self.index = 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, Debug, Default)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Size {
/// The width that the component has determined.
pub width: u16,

@ -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;

@ -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]