Stack alignment

Stacks now have an alignment property, which allows them to align their
content.

Horizontal stacks can be aligned Top Bottom Center.

Vertical stacks can be aligned Left Right Center.

This is respected for both Stack::insert and Stack::component.

Alignment expects Stack::insert to contain a single line string, which
is a functional requirement for both Stack::insert and ViewContext::insert.
This commit is contained in:
Joe Bellus 2024-04-15 13:14:30 -04:00
parent 3e50d269e5
commit 972ddb57ca
6 changed files with 332 additions and 7 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "arkham"
version = "0.2.0"
version = "0.2.1"
edition = "2021"
description = "TUI made simple"
authors = ["Joe Bellus"]

View File

@ -7,7 +7,13 @@
pkgs = import nixpkgs { inherit system; };
in {
devShells.${system}.default = pkgs.mkShell {
packages = [ pkgs.rustc pkgs.cargo pkgs.rust-analyzer pkgs.rustfmt ];
packages = [
pkgs.rustc
pkgs.cargo
pkgs.rust-analyzer
pkgs.rustfmt
pkgs.cargo-watch
];
};
};
}

View File

@ -61,6 +61,7 @@ impl ViewContext {
container: self.container.clone(),
view: View::new(size),
position: Pos::from(0),
alignment: crate::stack::StackAlignment::Top,
}
}
@ -70,6 +71,7 @@ impl ViewContext {
container: self.container.clone(),
view: View::new(size),
position: Pos::from(0),
alignment: crate::stack::StackAlignment::Left,
}
}
@ -104,3 +106,16 @@ impl ViewContext {
}
}
}
#[cfg(test)]
pub mod tests {
use std::{cell::RefCell, rc::Rc};
use crate::container::Container;
use super::ViewContext;
pub fn context_fixture() -> ViewContext {
ViewContext::new(Rc::new(RefCell::new(Container::default())), (20, 20).into())
}
}

View File

@ -19,8 +19,16 @@ pub mod prelude {
geometry::{Pos, Rect, Size},
input::Keyboard,
runes::{Rune, Runes, ToRuneExt},
stack::StackAlignment,
theme::Theme,
};
pub use crossterm::event::KeyCode;
pub use crossterm::style::Color;
}
#[cfg(test)]
pub mod tests {
pub fn print_render_text(s: &String) {
println!("{}", s.replace('\0', " "));
}
}

View File

@ -6,6 +6,16 @@ use crate::{
view::View,
};
#[derive(Debug, Clone, Copy, Default)]
pub enum StackAlignment {
#[default]
Left,
Right,
Center,
Top,
Bottom,
}
#[derive(Debug, Clone, Copy)]
pub enum StackDirection {
Vertical,
@ -18,9 +28,14 @@ pub struct Stack {
pub(crate) container: Rc<RefCell<Container>>,
pub(crate) view: View,
pub(crate) position: Pos,
pub(crate) alignment: StackAlignment,
}
impl Stack {
pub fn alignment(&mut self, alignment: StackAlignment) {
self.alignment = alignment;
}
pub fn component<F, Args, S>(&mut self, size: S, f: F)
where
F: crate::prelude::Callable<Args>,
@ -28,22 +43,124 @@ impl Stack {
S: Into<Size>,
{
let size = size.into();
let pos = match self.direction {
StackDirection::Vertical => {
if size.width != self.view.size().width {
match self.alignment {
StackAlignment::Left | StackAlignment::Top | StackAlignment::Bottom => {
self.position
}
StackAlignment::Right => Pos::new(
self.position.x + self.view.size().width - size.width,
self.position.y,
),
StackAlignment::Center => {
let view_width = self.view.size().width as f32;
let diff = view_width - size.width as f32;
Pos::new(
self.position.x + (diff / 2.0).floor() as usize,
self.position.y,
)
}
}
} else {
self.position
}
}
StackDirection::Horizontal => {
if size.height != self.view.size().height {
match self.alignment {
StackAlignment::Top | StackAlignment::Left | StackAlignment::Right => {
self.position
}
StackAlignment::Bottom => Pos::new(
self.position.x,
self.position.y + self.view.size().height - size.height,
),
StackAlignment::Center => {
let view_height = self.view.size().height as f32;
let diff = view_height - size.height as f32;
Pos::new(
self.position.x,
self.position.y + (diff / 2.0).floor() as usize,
)
}
}
} else {
self.position
}
}
};
let mut context = ViewContext::new(self.container.clone(), size);
f.call(&mut context, Args::from_container(&self.container.borrow()));
self.view.apply(self.position, &context.view);
self.view.apply(pos, &context.view);
self.position += match self.direction {
StackDirection::Vertical => Pos::new(0, size.height),
StackDirection::Horizontal => Pos::new(size.width, 0),
};
}
/// Insert a set a runes, such as a string, into the stack.
pub fn insert<R: Into<Runes>>(&mut self, value: R) {
let runes: Runes = value.into();
let l = runes.len();
self.view.insert(self.position, runes);
let size = Size::new(runes.len(), 1);
let pos = match self.direction {
StackDirection::Vertical => {
if size.width != self.view.size().width {
match self.alignment {
StackAlignment::Left | StackAlignment::Top | StackAlignment::Bottom => {
self.position
}
StackAlignment::Right => Pos::new(
self.position.x + self.view.size().width - size.width,
self.position.y,
),
StackAlignment::Center => {
let view_width = self.view.size().width as f32;
let diff = view_width - size.width as f32;
Pos::new(
self.position.x + (diff / 2.0).floor() as usize,
self.position.y,
)
}
}
} else {
self.position
}
}
StackDirection::Horizontal => {
if size.height != self.view.size().height {
match self.alignment {
StackAlignment::Top | StackAlignment::Left | StackAlignment::Right => {
self.position
}
StackAlignment::Bottom => Pos::new(
self.position.x,
self.position.y + self.view.size().height - size.height,
),
StackAlignment::Center => {
let view_height = self.view.size().height as f32;
let diff = view_height - size.height as f32;
Pos::new(
self.position.x,
self.position.y + (diff / 2.0).floor() as usize,
)
}
}
} else {
self.position
}
}
};
self.view.insert(pos, runes);
self.position += match self.direction {
StackDirection::Vertical => Pos::new(0, 1),
StackDirection::Horizontal => Pos::new(l, 0),
StackDirection::Horizontal => Pos::new(size.width, 0),
};
}
}
@ -53,3 +170,168 @@ impl Callable<()> for Stack {
ctx.apply((0, 0), &self.view);
}
}
#[cfg(test)]
mod tests {
use crate::prelude::{StackAlignment, ViewContext};
#[test]
fn test_vertical_insert() {
let ctx = crate::context::tests::context_fixture();
let mut stack = ctx.vertical_stack((10, 2).into());
stack.insert("one");
stack.insert("two");
assert_eq!(
stack.view.render_text(),
"one\0\0\0\0\0\0\0\ntwo\0\0\0\0\0\0\0\n".to_string()
);
}
#[test]
fn test_horizontal_insert() {
let ctx = crate::context::tests::context_fixture();
let mut stack = ctx.horizontal_stack((10, 1).into());
stack.insert("one");
stack.insert("two");
assert_eq!(stack.view.render_text(), "onetwo\0\0\0\0\n".to_string());
}
#[test]
fn test_component() {
let ctx = crate::context::tests::context_fixture();
let mut stack = ctx.horizontal_stack((10, 2).into());
stack.component((10, 2), |ctx: &mut ViewContext| {
ctx.insert((3, 1), "one");
});
assert_eq!(
stack.view.render_text(),
"\0\0\0\0\0\0\0\0\0\0\n\0\0\0one\0\0\0\0\n".to_string()
);
}
#[test]
fn test_align_left() {
let ctx = crate::context::tests::context_fixture();
let mut stack = ctx.vertical_stack((10, 2).into());
stack.component((5, 1), |ctx: &mut ViewContext| {
ctx.insert((0, 0), "one");
});
stack.insert("two");
assert_eq!(
stack.view.render_text(),
"one\0\0\0\0\0\0\0\ntwo\0\0\0\0\0\0\0\n".to_string()
);
}
#[test]
fn test_align_right() {
let ctx = crate::context::tests::context_fixture();
let mut stack = ctx.vertical_stack((10, 3).into());
stack.alignment = StackAlignment::Right;
stack.insert("one");
stack.component((5, 1), |ctx: &mut ViewContext| {
ctx.insert((0, 0), "one");
});
stack.component((10, 1), |ctx: &mut ViewContext| {
ctx.insert((0, 0), "two");
});
let res = "\0\0\0\0\0\0\0one\n\0\0\0\0\0one\0\0\ntwo\0\0\0\0\0\0\0\n".to_string();
crate::tests::print_render_text(&stack.view.render_text());
println!("---");
crate::tests::print_render_text(&res);
assert_eq!(stack.view.render_text(), res);
}
#[test]
fn test_align_center_v() {
let ctx = crate::context::tests::context_fixture();
let mut stack = ctx.vertical_stack((10, 3).into());
stack.alignment = StackAlignment::Center;
stack.insert("one");
stack.component((5, 1), |ctx: &mut ViewContext| {
ctx.insert((0, 0), "one");
});
stack.component((10, 1), |ctx: &mut ViewContext| {
ctx.insert((0, 0), "two");
});
let res = "\0\0\0one\0\0\0\0\n\0\0one\0\0\0\0\0\ntwo\0\0\0\0\0\0\0\n".to_string();
crate::tests::print_render_text(&stack.view.render_text());
println!("---");
crate::tests::print_render_text(&res);
assert_eq!(stack.view.render_text(), res);
}
#[test]
fn test_align_top() {
let ctx = crate::context::tests::context_fixture();
let mut stack = ctx.horizontal_stack((9, 6).into());
stack.component((3, 1), |ctx: &mut ViewContext| {
ctx.insert((0, 0), "one");
});
stack.component((3, 3), |ctx: &mut ViewContext| {
ctx.insert((0, 1), "two");
});
stack.insert("one");
let res = "one\0\0\0one\n\0\0\0two\0\0\0\n\0\0\0\0\0\0\0\0\0\n\0\0\0\0\0\0\0\0\0\n\0\0\0\0\0\0\0\0\0\n\0\0\0\0\0\0\0\0\0\n"
.to_string();
crate::tests::print_render_text(&stack.view.render_text());
println!("---");
crate::tests::print_render_text(&res);
assert_eq!(stack.view.render_text(), res.to_string());
}
#[test]
fn test_align_bottom() {
let ctx = crate::context::tests::context_fixture();
let mut stack = ctx.horizontal_stack((9, 6).into());
stack.alignment(StackAlignment::Bottom);
stack.component((3, 1), |ctx: &mut ViewContext| {
ctx.insert((0, 0), "one");
});
stack.component((3, 3), |ctx: &mut ViewContext| {
ctx.insert((0, 1), "two");
});
stack.insert("one");
let res = "\0\0\0\0\0\0\0\0\0\n\0\0\0\0\0\0\0\0\0\n\0\0\0\0\0\0\0\0\0\n\0\0\0\0\0\0\0\0\0\n\0\0\0two\0\0\0\none\0\0\0one\n"
.to_string();
crate::tests::print_render_text(&stack.view.render_text());
println!("---");
crate::tests::print_render_text(&res);
assert_eq!(stack.view.render_text(), res);
}
#[test]
fn test_align_center_h() {
let ctx = crate::context::tests::context_fixture();
let mut stack = ctx.horizontal_stack((9, 6).into());
stack.alignment(StackAlignment::Center);
stack.component((3, 1), |ctx: &mut ViewContext| {
ctx.insert((0, 0), "one");
});
stack.component((3, 3), |ctx: &mut ViewContext| {
ctx.insert((0, 1), "two");
});
stack.insert("one");
let res = "\0\0\0\0\0\0\0\0\0\n\0\0\0\0\0\0\0\0\0\nonetwoone\n\0\0\0\0\0\0\0\0\0\n\0\0\0\0\0\0\0\0\0\n\0\0\0\0\0\0\0\0\0\n"
.to_string();
crate::tests::print_render_text(&stack.view.render_text());
println!("---");
crate::tests::print_render_text(&res);
assert_eq!(stack.view.render_text(), res);
}
}

View File

@ -57,7 +57,7 @@ impl View {
// The width of the view.
pub fn width(&self) -> usize {
self.0.first().unwrap().len()
self.0.first().map(|i| i.len()).unwrap_or_default()
}
/// The height of the view.
@ -130,6 +130,20 @@ impl View {
}
}
}
#[cfg(test)]
pub fn render_text(&self) -> String {
self.0.iter().fold(String::new(), |mut acc, line| {
acc.push_str(
&line
.into_iter()
.map(|r| r.content.unwrap_or_default())
.collect::<String>(),
);
acc.push('\n');
acc
})
}
}
#[cfg(test)]