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