From ba7703e4c2b845618f8df2163ec4d4e226719542 Mon Sep 17 00:00:00 2001 From: Joe Bellus Date: Sat, 2 Sep 2023 22:46:50 -0400 Subject: [PATCH] sync commit --- Cargo.toml | 13 +++++++++ examples/navigation.rs | 48 ++++++++++++++++++++++++++++++++ examples/paragraph.rs | 14 ++++++++++ examples/stack.rs | 17 ++++++++++++ src/app.rs | 24 ++++++++-------- src/components/mod.rs | 2 ++ src/components/paragraph.rs | 38 +++++++++++++++++++++++++ src/container.rs | 50 +++++++-------------------------- src/context.rs | 42 +++++++++++++++++++++++++--- src/geometry.rs | 44 ++++++++++++++++++++++++++++- src/lib.rs | 3 ++ src/plugins/keybind.rs | 1 + src/plugins/mod.rs | 13 +++++++++ src/runes.rs | 51 +++++++++++++++++++++++++++++++--- src/stack.rs | 55 +++++++++++++++++++++++++++++++++++++ src/view.rs | 14 ++++++---- 16 files changed, 363 insertions(+), 66 deletions(-) create mode 100644 examples/navigation.rs create mode 100644 examples/paragraph.rs create mode 100644 examples/stack.rs create mode 100644 src/components/mod.rs create mode 100644 src/components/paragraph.rs create mode 100644 src/plugins/keybind.rs create mode 100644 src/plugins/mod.rs create mode 100644 src/stack.rs diff --git a/Cargo.toml b/Cargo.toml index 4de9f5a..6410b29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,20 @@ path = "examples/component_params.rs" name = "keyboard" path = "examples/keyboard.rs" +[[example]] +name = "navigation" +path = "examples/navigation.rs" + +[[example]] +name = "stack" +path = "examples/stack.rs" + +[[example]] +name = "paragraph" +path = "examples/paragraph.rs" + [dependencies] anyhow = "1.0.71" crossterm = "0.26.1" ctrlc = "3.3.1" +textwrap = "0.16.0" diff --git a/examples/navigation.rs b/examples/navigation.rs new file mode 100644 index 0000000..ba4d82e --- /dev/null +++ b/examples/navigation.rs @@ -0,0 +1,48 @@ +use arkham::prelude::*; + +#[derive(Copy, Clone)] +enum Route { + Main, + Secondary, +} + +fn main() { + let _ = App::new(root).insert_state(Route::Main).run(); +} + +fn root(ctx: &mut ViewContext, route: State) { + let size = ctx.size(); + let r = *route.get(); + ctx.insert((0, 0), "Press Q to quit"); + match r { + Route::Main => { + ctx.component(Rect::new((0, 1), size), main_route); + } + Route::Secondary => { + ctx.component(Rect::new((0, 1), size), secondary_route); + } + } +} + +fn main_route(ctx: &mut ViewContext, kb: Res, route: State) { + ctx.insert( + 0, + "Welcome to the main screen, press 2 to goto the secondary screen", + ); + if kb.char() == Some('2') { + *route.get_mut() = Route::Secondary; + ctx.render(); + } +} + +fn secondary_route(ctx: &mut ViewContext, kb: Res, route: State) { + ctx.insert( + 0, + "Welcome to the secondary screen, press 1 to goto the main screen", + ); + + if kb.char() == Some('1') { + *route.get_mut() = Route::Main; + ctx.render(); + } +} diff --git a/examples/paragraph.rs b/examples/paragraph.rs new file mode 100644 index 0000000..87afae9 --- /dev/null +++ b/examples/paragraph.rs @@ -0,0 +1,14 @@ +use arkham::prelude::*; + +fn main() { + let _ = App::new(root).run(); +} + +fn root(ctx: &mut ViewContext) { + let mut stack = ctx.vertical_stack(ctx.size()); + let p = arkham::components::Paragraph::new("Rust is a multi-paradigm, general-purpose programming language that emphasizes performance, type safety, and concurrency. It enforces memory safety—ensuring that all references point to valid memory—without requiring the use of a garbage collector or reference counting present in other memory-safe languages."); + stack.component(Size::new(100_usize, p.height(100)), p); + let p = arkham::components::Paragraph::new("Rust is a multi-paradigm, general-purpose programming language that emphasizes performance, type safety, and concurrency. It enforces memory safety—ensuring that all references point to valid memory—without requiring the use of a garbage collector or reference counting present in other memory-safe languages."); + stack.component(Size::new(100_usize, p.height(100)), p); + ctx.component(ctx.size(), stack); +} diff --git a/examples/stack.rs b/examples/stack.rs new file mode 100644 index 0000000..11c43eb --- /dev/null +++ b/examples/stack.rs @@ -0,0 +1,17 @@ +use arkham::prelude::*; + +fn main() { + let _ = App::new(root).run(); +} + +fn root(ctx: &mut ViewContext) { + let mut stack = ctx.vertical_stack(Size::new(100, 100)); + for _ in 0..10 { + stack.component(Size::new(ctx.size().width, 2), list_item); + } + ctx.component(Rect::new(0, (100, 100)), stack); +} + +fn list_item(ctx: &mut ViewContext) { + ctx.insert(0, "line 1"); +} diff --git a/src/app.rs b/src/app.rs index 41617ce..98a394c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -43,15 +43,10 @@ where container, root, main_view, - args: PhantomData::default(), + args: PhantomData, } } - /// Used to change the root function that is used during each render cycle. - pub fn change_root(&mut self, root: F) { - self.root = root; - } - /// Insert a resource which can be injected into component functions. /// /// This resource can only be accessed immutably by reference. @@ -131,12 +126,19 @@ where terminal::enable_raw_mode()?; loop { - let mut context = - ViewContext::new(self.container.clone(), terminal::size().unwrap().into()); + loop { + let mut context = + ViewContext::new(self.container.clone(), terminal::size().unwrap().into()); - self.container.borrow().call(&mut context, &self.root); - self.main_view.apply((0, 0), context.view); - self.render()?; + self.root + .call(&mut context, Args::from_container(&self.container.borrow())); + self.main_view.apply((0, 0), &context.view); + self.render()?; + + if !context.rerender { + break; + } + } self.container .borrow() diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..1f526a4 --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,2 @@ +mod paragraph; +pub use paragraph::Paragraph; diff --git a/src/components/paragraph.rs b/src/components/paragraph.rs new file mode 100644 index 0000000..1ad76da --- /dev/null +++ b/src/components/paragraph.rs @@ -0,0 +1,38 @@ +use std::ops::Deref; + +use crossterm::style::Color; + +use crate::prelude::{Callable, ToRuneExt}; + +#[derive(Debug)] +pub struct Paragraph { + content: String, + fg: Option, + bg: Option, +} + +impl Paragraph { + pub fn new(content: &str) -> Self { + Self { + content: content.to_string(), + fg: None, + bg: None, + } + } + pub fn height(&self, width: usize) -> usize { + textwrap::wrap(&self.content, width).len() + } +} + +impl Callable<()> for Paragraph { + fn call(&self, view: &mut crate::prelude::ViewContext, _args: ()) { + let lines = textwrap::wrap(&self.content, view.width()); + let mut stack = view.vertical_stack(view.size()); + for line in lines.iter() { + let l = line.deref().to_runes(); + + stack.insert(line); + } + view.component(view.size(), stack); + } +} diff --git a/src/container.rs b/src/container.rs index b981bea..5c3dce0 100644 --- a/src/container.rs +++ b/src/container.rs @@ -7,14 +7,10 @@ use std::{ }; use crate::context::ViewContext; -type ArkhamResult = anyhow::Result; -pub enum ArkhamState { - Noop, -} /// The container stores typed resource and state objects and provides /// them to component functions. -#[derive(Default)] +#[derive(Default, Debug)] pub struct Container { bindings: HashMap>, } @@ -36,15 +32,6 @@ impl Container { .get(&TypeId::of::()) .and_then(|boxed| boxed.downcast_ref()) } - - /// Call a function while providing dependency injcetion. - pub fn call(&self, view: &mut ViewContext, callable: &F) - where - F: Callable, - Args: FromContainer, - { - let _ = callable.call(view, Args::from_container(self)); - } } /// A wrapper for state objcets. This internally holds a reference counted @@ -136,24 +123,23 @@ impl FromContainer for Res { } } +/// Callable must be implemented for functions that can be used as component +/// functions. They are given a ViewContext for the component function and +/// injectable arguments. +pub trait Callable { + fn call(&self, view: &mut ViewContext, args: Args); +} + impl Callable<()> for Func where Func: Fn(&mut ViewContext), { #[inline] - fn call(&self, view: &mut ViewContext, _args: ()) -> ArkhamResult { + fn call(&self, view: &mut ViewContext, _args: ()) { (self)(view); - Ok(ArkhamState::Noop) } } -/// Callable must be implemented for functions that can be used as component -/// functions. They are given a ViewContext for the component function and -/// injectable arguments. -pub trait Callable { - fn call(&self, view: &mut ViewContext, args: Args) -> ArkhamResult; -} - /// FromContainer must be implmented for objects that can be injected into /// component functions. This includes the Res and State structs. pub trait FromContainer { @@ -172,9 +158,8 @@ macro_rules! callable_tuple ({ $($param:ident)* } => { { #[inline] #[allow(non_snake_case)] - fn call(&self, view: &mut ViewContext , ($($param,)*): ($($param,)*)) -> ArkhamResult{ + fn call(&self, view: &mut ViewContext , ($($param,)*): ($($param,)*)) { (self)(view, $($param,)*); - Ok(ArkhamState::Noop) } } }); @@ -217,18 +202,3 @@ tuple_from_tm! { A B C D E F G H I } tuple_from_tm! { A B C D E F G H I J } tuple_from_tm! { A B C D E F G H I J K } tuple_from_tm! { A B C D E F G H I J K L } - -#[cfg(test)] -mod tests { - use std::{cell::RefCell, rc::Rc}; - - use crate::{container::Container, prelude::ViewContext}; - - #[test] - fn test_no_params() { - fn test_f(_ctx: &mut ViewContext) {} - let container = Rc::new(RefCell::new(Container::default())); - let mut ctx = ViewContext::new(container.clone(), (1, 1).into()); - container.borrow().call(&mut ctx, &test_f); - } -} diff --git a/src/context.rs b/src/context.rs index fb1970b..f463b87 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,6 +1,9 @@ use std::{cell::RefCell, rc::Rc}; -use crate::container::{Callable, FromContainer}; +use crate::{ + container::{Callable, FromContainer}, + stack::Stack, +}; use super::{ container::Container, @@ -15,6 +18,7 @@ use super::{ pub struct ViewContext { pub view: View, pub container: Rc>, + pub(crate) rerender: bool, } impl std::ops::DerefMut for ViewContext { @@ -38,7 +42,35 @@ impl ViewContext { pub fn new(container: Rc>, size: Size) -> Self { let view = View::new(size); - Self { view, container } + Self { + view, + container, + rerender: false, + } + } + + /// Notify the application to rerender the view. This is useful after a + /// state change that might affect other views. + pub fn render(&mut self) { + self.rerender = true; + } + + pub fn vertical_stack(&self, size: Size) -> Stack { + Stack { + direction: crate::stack::StackDirection::Vertical, + container: self.container.clone(), + view: View::new(size), + position: Pos::from(0), + } + } + + pub fn horizontal_stack(&self, size: Size) -> Stack { + Stack { + direction: crate::stack::StackDirection::Horizontal, + container: self.container.clone(), + view: View::new(size), + position: Pos::from(0), + } } /// Execute a component function. The passed function will receive a new @@ -53,8 +85,10 @@ impl ViewContext { { let rect = rect.into(); let mut context = ViewContext::new(self.container.clone(), rect.size); - self.container.borrow().call(&mut context, &f); - self.view.apply(rect.pos, context.view); + let args = Args::from_container(&self.container.borrow()); + f.call(&mut context, args); + self.view.apply(rect.pos, &context.view); + self.rerender = context.rerender; } /// Set a specific rune to a specific position. This function can be used diff --git a/src/geometry.rs b/src/geometry.rs index 1eb8492..33cdb31 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -1,4 +1,4 @@ -use std::ops::{Add, Sub}; +use std::ops::{Add, AddAssign, Sub}; /// Pos represents a coordinate position within the termianl screen. #[derive(Debug, Clone, Copy)] @@ -7,6 +7,12 @@ pub struct Pos { pub y: usize, } +impl Pos { + pub fn new(x: usize, y: usize) -> Self { + Self { x, y } + } +} + impl From<(usize, usize)> for Pos { fn from(value: (usize, usize)) -> Self { Self { @@ -22,6 +28,23 @@ impl From for Pos { } } +impl Add for Pos { + type Output = Pos; + + fn add(mut self, rhs: Pos) -> Self::Output { + self.x += rhs.x; + self.y += rhs.y; + self + } +} + +impl AddAssign for Pos { + fn add_assign(&mut self, rhs: Pos) { + self.x += rhs.x; + self.y += rhs.y; + } +} + // An area that can be operated on. #[derive(Debug, Clone, Copy)] pub struct Size { @@ -29,6 +52,12 @@ pub struct Size { pub height: usize, } +impl Size { + pub fn new(width: usize, height: usize) -> Self { + Self { width, height } + } +} + impl Add for Size { type Output = Size; @@ -100,6 +129,15 @@ impl From<(i32, i32)> for Size { } } +impl From for Size { + fn from(value: i32) -> Self { + Self { + width: value as usize, + height: value as usize, + } + } +} + /// An area of the screen with a given size and postiion. The position /// represents the top-left corner of the rectangle. #[derive(Debug, Clone, Copy)] @@ -120,6 +158,10 @@ impl Rect { } } + pub fn zero() -> Self { + Self::new(0, 0) + } + pub fn with_size(size: S) -> Self where S: Into, diff --git a/src/lib.rs b/src/lib.rs index 06df12f..64efa49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,12 @@ mod app; +pub mod components; mod container; mod context; mod geometry; mod input; +mod plugins; mod runes; +mod stack; pub mod symbols; mod theme; mod view; diff --git a/src/plugins/keybind.rs b/src/plugins/keybind.rs new file mode 100644 index 0000000..8f0016b --- /dev/null +++ b/src/plugins/keybind.rs @@ -0,0 +1 @@ +pub struct KeybindPlugin; diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs new file mode 100644 index 0000000..88fd47a --- /dev/null +++ b/src/plugins/mod.rs @@ -0,0 +1,13 @@ +use crate::prelude::ViewContext; + +pub mod keybind; + +pub trait Pluigin { + fn build(ctx: ViewContext); + + #[allow(unused_variables)] + fn before_render(ctx: ViewContext) {} + + #[allow(unused_variables)] + fn after_render(ctx: ViewContext) {} +} diff --git a/src/runes.rs b/src/runes.rs index 170a0db..8db9c36 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -1,6 +1,8 @@ use crossterm::{ queue, - style::{Attribute, Color, Print, SetAttribute, SetBackgroundColor, SetForegroundColor}, + style::{ + Attribute, Color, Print, ResetColor, SetAttribute, SetBackgroundColor, SetForegroundColor, + }, }; /// Rune repesents the state of the screen at a specific position. It stores @@ -20,6 +22,25 @@ impl std::fmt::Debug for Rune { } } +impl std::ops::Add for Rune { + type Output = Rune; + + fn add(self, mut rhs: Rune) -> Self::Output { + rhs.fg = rhs.fg.or(self.fg); + rhs.bg = rhs.bg.or(self.bg); + rhs + } +} + +impl From for Rune { + fn from(value: char) -> Self { + Rune { + content: Some(value), + ..Default::default() + } + } +} + impl Rune { pub fn new() -> Self { Self::default() @@ -48,6 +69,7 @@ impl Rune { W: std::io::Write, { if let Some(content) = self.content { + queue!(out, ResetColor)?; if let Some(c) = self.fg { queue!(out, SetForegroundColor(c))?; } @@ -65,7 +87,7 @@ impl Rune { /// Runes represetns a series of runes. This is generally used to convert /// strings into Runes and apply styling information to them. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct Runes(Vec); impl std::ops::Deref for Runes { @@ -76,6 +98,12 @@ impl std::ops::Deref for Runes { } } +impl From for Runes { + fn from(value: Rune) -> Self { + Runes::new(vec![value]) + } +} + impl From for Runes { fn from(value: T) -> Self { Runes( @@ -89,12 +117,20 @@ impl From for Runes { } impl Runes { - pub fn fg(mut self, color: Color) -> Self { + pub fn new(runes: Vec) -> Self { + Self(runes) + } + pub fn fg(self, color: Color) -> Self { + self.set_fg(Some(color)) + } + + pub fn set_fg(mut self, color: Option) -> Self { for r in self.0.iter_mut() { - r.fg = Some(color); + r.fg = color; } self } + pub fn bg(mut self, color: Color) -> Self { for r in self.0.iter_mut() { r.bg = Some(color); @@ -107,6 +143,13 @@ impl Runes { } self } + + pub fn add(&mut self, runes: R) + where + R: Into, + { + self.0.append(&mut runes.into().0); + } } pub trait ToRuneExt { diff --git a/src/stack.rs b/src/stack.rs new file mode 100644 index 0000000..b48396c --- /dev/null +++ b/src/stack.rs @@ -0,0 +1,55 @@ +use std::{cell::RefCell, rc::Rc}; + +use crate::{ + container::Container, + prelude::{Callable, Pos, Runes, Size, ViewContext}, + view::View, +}; + +#[derive(Debug, Clone, Copy)] +pub enum StackDirection { + Vertical, + Horizontal, +} + +#[derive(Debug)] +pub struct Stack { + pub(crate) direction: StackDirection, + pub(crate) container: Rc>, + pub(crate) view: View, + pub(crate) position: Pos, +} + +impl Stack { + pub fn component(&mut self, size: S, f: F) + where + F: crate::prelude::Callable, + Args: crate::prelude::FromContainer, + S: Into, + { + let size = size.into(); + 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.position += match self.direction { + StackDirection::Vertical => Pos::new(0, size.height), + StackDirection::Horizontal => Pos::new(size.width, 0), + }; + } + + pub fn insert>(&mut self, value: R) { + let runes: Runes = value.into(); + let l = runes.len(); + self.view.insert(self.position, runes); + self.position += match self.direction { + StackDirection::Vertical => Pos::new(0, 1), + StackDirection::Horizontal => Pos::new(l, 0), + }; + } +} + +impl Callable<()> for Stack { + fn call(&self, ctx: &mut ViewContext, _args: ()) { + ctx.apply((0, 0), &self.view); + } +} diff --git a/src/view.rs b/src/view.rs index cd67e3f..fd758ac 100644 --- a/src/view.rs +++ b/src/view.rs @@ -8,7 +8,7 @@ use crate::{ /// A renderable region. View stores the renderable state of an area of the /// screen. Views can be combined together to achieve a finalized view that /// repsresents the entire screens next render. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct View(pub Vec>); impl std::ops::DerefMut for View { @@ -41,13 +41,14 @@ impl View { } /// Apply another view onto this view at a given position. - pub fn apply>(&mut self, pos: P, view: View) { + pub fn apply>(&mut self, pos: P, view: &View) { let pos = pos.into(); for (y, line) in view.0.iter().enumerate() { if self.0.len() > y + pos.y { for (x, rune) in line.iter().enumerate() { if rune.content.is_some() && self.0[y].len() > x + pos.x { - let _ = std::mem::replace(&mut self.0[y + pos.y][x + pos.x], *rune); + let rune = (self.0[y + pos.y][x + pos.x]) + *rune; + let _ = std::mem::replace(&mut self.0[y + pos.y][x + pos.x], rune); } } } @@ -108,7 +109,8 @@ impl View { .take((line_len - x as i32).max(0) as usize) .enumerate() { - let _ = std::mem::replace(&mut line[x + i], *c); + let rune = line[x + i] + *c; + let _ = std::mem::replace(&mut line[x + i], rune); } } } @@ -172,7 +174,7 @@ mod tests { let mut view1 = View::new((3, 4)); view1.fill(Rect::new((1, 1), (2, 2)), Rune::new().content('X')); let mut view2 = View::new((3, 4)); - view2.apply((0, 1), view1); + view2.apply((0, 1), &view1); dbg!(&view2.0); assert_eq!(view2.0[0][0].content, None); assert_eq!(view2.0[0][1].content, None); @@ -196,7 +198,7 @@ mod tests { let mut view0 = View::new((5, 5)); view0.fill(Rect::new((1, 1), (4, 4)), Rune::new().content('X')); let mut view = View::new((3, 3)); - view.apply((0, 0), view0); + view.apply((0, 0), &view0); dbg!(&view.0); assert_eq!(view.0[0][0].content, None); assert_eq!(view.0[0][1].content, None);