From 972ddb57ca2f185609e7436358278b329d4fd119 Mon Sep 17 00:00:00 2001 From: Joe Bellus Date: Mon, 15 Apr 2024 13:14:30 -0400 Subject: [PATCH] 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. --- Cargo.toml | 2 +- flake.nix | 8 +- src/context.rs | 15 +++ src/lib.rs | 8 ++ src/stack.rs | 290 ++++++++++++++++++++++++++++++++++++++++++++++++- src/view.rs | 16 ++- 6 files changed, 332 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a29a232..2a045f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "arkham" -version = "0.2.0" +version = "0.2.1" edition = "2021" description = "TUI made simple" authors = ["Joe Bellus"] diff --git a/flake.nix b/flake.nix index 3bf4a1e..9df531e 100644 --- a/flake.nix +++ b/flake.nix @@ -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 + ]; }; }; } diff --git a/src/context.rs b/src/context.rs index f463b87..62eb540 100644 --- a/src/context.rs +++ b/src/context.rs @@ -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()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 0b4e175..569a531 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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', " ")); + } +} diff --git a/src/stack.rs b/src/stack.rs index b48396c..cbecc01 100644 --- a/src/stack.rs +++ b/src/stack.rs @@ -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>, 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(&mut self, size: S, f: F) where F: crate::prelude::Callable, @@ -28,22 +43,124 @@ impl Stack { S: Into, { 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>(&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); + } +} diff --git a/src/view.rs b/src/view.rs index f6e1e74..6aac287 100644 --- a/src/view.rs +++ b/src/view.rs @@ -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::(), + ); + acc.push('\n'); + acc + }) + } } #[cfg(test)]