diff --git a/Cargo.toml b/Cargo.toml index 7633b59..4de9f5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,25 @@ name = "arkham" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[example]] +name = "simple" +path = "examples/simple.rs" + +[[example]] +name = "component_functions" +path = "examples/component_functions.rs" + +[[example]] +name = "theme" +path = "examples/theme.rs" + +[[example]] +name = "component_params" +path = "examples/component_params.rs" + +[[example]] +name = "keyboard" +path = "examples/keyboard.rs" [dependencies] anyhow = "1.0.71" diff --git a/examples/component_functions.rs b/examples/component_functions.rs new file mode 100644 index 0000000..088de5e --- /dev/null +++ b/examples/component_functions.rs @@ -0,0 +1,24 @@ +use arkham::prelude::*; + +fn main() { + App::new(root).run().expect("couldnt launch app"); +} + +fn root(ctx: &mut ViewContext) { + let size = ctx.size(); + ctx.fill(size, Rune::new().bg(Color::DarkGrey)); + ctx.component(Rect::new((10, 10), (20, 1)), hello_world); + ctx.component(Rect::new(0, (size.width, 1)), quit_nag); +} + +fn hello_world(ctx: &mut ViewContext) { + ctx.insert(0, "Hello World"); +} + +fn quit_nag(ctx: &mut ViewContext) { + let size = ctx.size(); + ctx.insert( + ((size.width / 2) - 7, 0), + "Press Q to Quit".to_runes().fg(Color::Red), + ); +} diff --git a/examples/component_params.rs b/examples/component_params.rs new file mode 100644 index 0000000..28e538f --- /dev/null +++ b/examples/component_params.rs @@ -0,0 +1,26 @@ +use arkham::prelude::*; + +fn main() { + App::new(root).run().expect("couldnt launch app"); +} + +fn root(ctx: &mut ViewContext) { + let size = ctx.size(); + ctx.fill(size, Rune::new().bg(Color::DarkGrey)); + ctx.component(Rect::new((10, 10), (20, 1)), say_hello("Alice")); + ctx.component(Rect::new(0, (size.width, 1)), quit_nag); +} + +fn say_hello(name: &'static str) -> impl Fn(&mut ViewContext) { + move |ctx: &mut ViewContext| { + ctx.insert((0, 0), format!("Hello, {}", name)); + } +} + +fn quit_nag(ctx: &mut ViewContext) { + let size = ctx.size(); + ctx.insert( + ((size.width / 2) - 7, 0), + "Press Q to Quit".to_runes().fg(Color::Red), + ); +} diff --git a/examples/keyboard.rs b/examples/keyboard.rs new file mode 100644 index 0000000..f8acd9a --- /dev/null +++ b/examples/keyboard.rs @@ -0,0 +1,31 @@ +use arkham::prelude::*; + +fn main() { + App::new(root).run().expect("couldnt launch app"); +} + +fn root(ctx: &mut ViewContext) { + let size = ctx.size(); + ctx.fill(size, Rune::new().bg(Color::DarkGrey)); + ctx.component(Rect::new((10, 10), (30, 1)), hello_world); + ctx.component(Rect::new((10, 11), (20, 1)), show_key_press); + ctx.component(Rect::new(0, (size.width, 1)), quit_nag); +} + +fn hello_world(ctx: &mut ViewContext) { + ctx.insert(0, "Hello World, Press a key"); +} + +fn show_key_press(ctx: &mut ViewContext, kb: Res) { + if let Some(c) = kb.char() { + ctx.insert(0, format!("Key press: {}", c)); + } +} + +fn quit_nag(ctx: &mut ViewContext) { + let size = ctx.size(); + ctx.insert( + ((size.width / 2) - 7, 0), + "Press Q to Quit".to_runes().fg(Color::Red), + ); +} diff --git a/examples/simple.rs b/examples/simple.rs new file mode 100644 index 0000000..daa0b54 --- /dev/null +++ b/examples/simple.rs @@ -0,0 +1,15 @@ +use arkham::prelude::*; + +fn main() { + App::new(root).run().expect("couldnt launch app"); +} + +fn root(ctx: &mut ViewContext) { + let size = ctx.size(); + ctx.fill(size, Rune::new().bg(Color::DarkGrey)); + ctx.insert((10, 10), "Hello World"); + ctx.insert( + ((size.width / 2) - 7, 0), + "Press Q to Quit".to_runes().fg(Color::Red), + ); +} diff --git a/examples/theme.rs b/examples/theme.rs new file mode 100644 index 0000000..b7284e9 --- /dev/null +++ b/examples/theme.rs @@ -0,0 +1,19 @@ +use arkham::prelude::*; + +fn main() { + App::new(root) + .insert_resource(Theme::default()) + .run() + .expect("couldnt launch app"); +} + +fn root(ctx: &mut ViewContext, theme: Res) { + let size = ctx.size(); + ctx.paint(size, theme.bg_primary); + ctx.paint(Rect::new((5, 5), size - 10), theme.bg_secondary); + ctx.insert((10, 10), "Hello World"); + ctx.insert( + ((size.width / 2) - 7, 0), + "Press Q to Quit".to_runes().fg(theme.fg), + ); +} diff --git a/src/app.rs b/src/app.rs index f162fa3..41617ce 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,7 +2,7 @@ use std::{any::Any, cell::RefCell, io::Write, marker::PhantomData, rc::Rc}; use crossterm::{ cursor, - event::{Event, KeyCode, KeyEventKind, KeyModifiers}, + event::{Event, KeyCode, KeyEventKind}, execute, queue, terminal, }; @@ -14,6 +14,8 @@ use crate::{ use super::input::Keyboard; +/// The app is the core container for the application logic, resources, +/// state, and run loop. pub struct App where F: Callable, @@ -30,6 +32,9 @@ where F: Callable, Args: FromContainer, { + /// Constructs a new App objcet. This object uses a builder pattern and + /// should be finalized with App::run(). which will start a blocking run + /// loop and perform the initial screen setup and render. pub fn new(root: F) -> App { let container = Rc::new(RefCell::new(Container::default())); let size = terminal::size().unwrap(); @@ -42,26 +47,76 @@ where } } + /// 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. + /// Interior mutability must be used for anything that requires an internal + /// state. + /// + /// Example: + /// ``` + /// use arkham::prelude::*; + /// struct MyResource { + /// value: i32 + /// } + /// + /// fn main() { + /// App::new(root).insert_resource(MyResource { value: 12 }); + /// } + /// + /// fn root(ctx: &mut ViewContext, thing: Res) { + /// ctx.insert(0,format!("Value is {}", thing.value)); + /// } + /// ```` + /// Alternatively, App::insert_state can be used to insert a state object, + /// that can be borrowed mutable. pub fn insert_resource(self, v: T) -> Self { self.container.borrow_mut().bind(Res::new(v)); self } + /// Insert a stateful object that can be injected into component functions + /// unlike App::insert_resource, this value can be borrowed mutably and + /// is meant to store application state. + /// + /// Example: + /// ``` + /// use arkham::prelude::*; + /// struct MyState { + /// value: i32 + /// } + /// + /// fn main() { + /// App::new(root).insert_state(MyState { value: 12 }); + /// } + /// + /// fn root(ctx: &mut ViewContext, thing: State) { + /// thing.get_mut().value += 1; + /// ctx.insert(0,format!("Value is {}", thing.get().value)); + /// } + /// ```` pub fn insert_state(self, v: T) -> Self { self.container.borrow_mut().bind(State::new(v)); self } + /// Repairs the terminal state so it operates properly. fn teardown(&self) { let mut out = std::io::stdout(); let _ = terminal::disable_raw_mode(); let _ = execute!(out, terminal::LeaveAlternateScreen, cursor::Show); } + /// Executes the main run loop. This should be called to start the + /// application logic. + /// + /// This function will block while it reads events and performs render + /// cycles. pub fn run(&mut self) -> anyhow::Result<()> { self.container.borrow_mut().bind(Res::new(Terminal)); self.container.borrow_mut().bind(Res::new(Keyboard::new())); diff --git a/src/container.rs b/src/container.rs index 8d5562a..b981bea 100644 --- a/src/container.rs +++ b/src/container.rs @@ -12,50 +12,75 @@ pub enum ArkhamState { Noop, } +/// The container stores typed resource and state objects and provides +/// them to component functions. #[derive(Default)] pub struct Container { bindings: HashMap>, } impl Container { - pub fn bind(&mut self, val: T) { + /// insert a type binding into the container. This is used to provide an + /// object to functions executed by Container::call. + /// + /// App::insert_ressource and App::isnert_state proxies to this function. + pub(crate) fn bind(&mut self, val: T) { self.bindings.insert(val.type_id(), Box::new(val)); } + /// Get an object from the store by its type. This is a utility function + /// to extract an object directly, instead of using the container to + /// inject objects into a function's arguments. pub fn get(&self) -> Option<&T> { self.bindings .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, { - callable.call(view, Args::from_container(self)); + let _ = callable.call(view, Args::from_container(self)); } } -pub trait Callable { - fn call(&self, view: &mut ViewContext, args: Args) -> ArkhamResult; -} - -pub trait FromContainer { - fn from_container(container: &Container) -> Self; -} - +/// A wrapper for state objcets. This internally holds a reference counted +/// poitner to the object and is used when injecting itno functions. pub struct State(Rc>); impl State { + /// Create a new state wrapper. pub fn new(val: T) -> Self { State(Rc::new(RefCell::new(val))) } + /// Returns a mutable reference to the underlying state object. + /// + /// Example: + /// ``` + /// use arkham::prelude::*; + /// struct MyState(i32); + /// + /// let state = State::new(MyState(4)); + /// state.get_mut().0 = 6; + /// assert_eq!(state.get().0, 6); + /// ``` pub fn get_mut(&self) -> std::cell::RefMut { RefCell::borrow_mut(&self.0) } + // Returns an immutable reference to the underlying state object. + /// Example: + /// ``` + /// use arkham::prelude::*; + /// struct MyState(i32); + /// + /// let state = State::new(MyState(4)); + /// assert_eq!(state.get().0, 4); + /// ``` pub fn get(&self) -> std::cell::Ref { RefCell::borrow(&self.0) } @@ -73,6 +98,9 @@ impl FromContainer for State { } } +/// A wrapper for resources stored within the app. This wrapper is returned +/// when objects are injected into component functions and provide immutable +/// access #[derive(Debug)] pub struct Res(Rc); @@ -119,6 +147,19 @@ where } } +/// 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 { + fn from_container(container: &Container) -> Self; +} + impl FromContainer for () { #[inline] fn from_container(_container: &Container) -> Self {} diff --git a/src/context.rs b/src/context.rs index 5fcbf33..fb1970b 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,9 +1,6 @@ use std::{cell::RefCell, rc::Rc}; -use crate::{ - container::{Callable, FromContainer}, - widget::Widget, -}; +use crate::container::{Callable, FromContainer}; use super::{ container::Container, @@ -12,6 +9,9 @@ use super::{ view::View, }; +/// ViewContext represents the display context for a given area. +/// it maintains the drawing state for the region internally and is used +/// to generate a final view that is eventually rendered. pub struct ViewContext { pub view: View, pub container: Rc>, @@ -32,28 +32,34 @@ impl std::ops::Deref for ViewContext { } impl ViewContext { + /// Constructs a new ViewConext for a given area. A container reference + /// must also be passedo, so that component functions called + /// from the context are injectable. pub fn new(container: Rc>, size: Size) -> Self { let view = View::new(size); Self { view, container } } - pub fn component(&mut self, rect: Rect, f: F) + /// Execute a component function. The passed function will receive a new + /// ViewContext for its size and can be injected with arguments. + /// The context given to the component function will then be applied to + /// the parent ViewContext at a given position. + pub fn component(&mut self, rect: R, f: F) where F: Callable, Args: FromContainer, + R: Into, { + 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); } - pub fn widget(&mut self, rect: Rect, mut widget: impl Widget) { - let mut context = ViewContext::new(self.container.clone(), rect.size); - widget.ui(&mut context); - self.view.apply(rect.pos, context.view); - } - + /// Set a specific rune to a specific position. This function can be used + /// to set a signle character. To set multiple runes at a time see the + /// View::insert function. pub fn set_rune

(&mut self, pos: P, rune: Rune) where P: Into, diff --git a/src/geometry.rs b/src/geometry.rs index 773a51c..1eb8492 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -1,5 +1,6 @@ use std::ops::{Add, Sub}; +/// Pos represents a coordinate position within the termianl screen. #[derive(Debug, Clone, Copy)] pub struct Pos { pub x: usize, @@ -21,6 +22,7 @@ impl From for Pos { } } +// An area that can be operated on. #[derive(Debug, Clone, Copy)] pub struct Size { pub width: usize, @@ -98,6 +100,8 @@ impl From<(i32, i32)> for Size { } } +/// 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)] pub struct Rect { pub pos: Pos, diff --git a/src/input.rs b/src/input.rs index 014383f..50ff0f9 100644 --- a/src/input.rs +++ b/src/input.rs @@ -2,6 +2,9 @@ use std::{cell::RefCell, rc::Rc}; use crossterm::event::KeyCode; +/// Keyboard can be used as an injectable resource that provides information +/// about the current keyboard state. This is the primary mechanism by which +/// applications can respond to keyboard input from users. #[derive(Debug, Default)] pub struct Keyboard { key: Rc>>, diff --git a/src/lib.rs b/src/lib.rs index f537f57..06df12f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,6 @@ mod runes; pub mod symbols; mod theme; mod view; -mod widget; pub mod prelude { pub use super::{ @@ -18,7 +17,6 @@ pub mod prelude { input::Keyboard, runes::{Rune, Runes, ToRuneExt}, theme::Theme, - widget::Widget, }; pub use crossterm::event::KeyCode; pub use crossterm::style::Color; diff --git a/src/runes.rs b/src/runes.rs index 3476d52..170a0db 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -3,6 +3,8 @@ use crossterm::{ style::{Attribute, Color, Print, SetAttribute, SetBackgroundColor, SetForegroundColor}, }; +/// Rune repesents the state of the screen at a specific position. It stores +/// the character content and styling information that will be rendered. #[derive(Clone, Copy, Default, Eq, PartialEq)] pub struct Rune { pub content: Option, @@ -61,6 +63,8 @@ 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)] pub struct Runes(Vec); diff --git a/src/theme.rs b/src/theme.rs index e68d515..b5a9575 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,5 +1,11 @@ use crossterm::style::Color; +/// Theme is a simple theme provider. This structure is nothing special. It +/// simply holds some general styling information and can be inserted as a +/// resource into the application. +/// +/// If you would like to use different style names just make your own structure +/// which meets your needs and add it as a resource with App::insert_resource. #[derive(Debug)] pub struct Theme { pub bg_primary: Color, diff --git a/src/view.rs b/src/view.rs index 2f40fdd..cd67e3f 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,8 +1,13 @@ +use crossterm::style::Color; + use crate::{ geometry::{Pos, Rect, Size}, runes::{Rune, Runes}, }; +/// 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)] pub struct View(pub Vec>); @@ -21,6 +26,7 @@ impl std::ops::Deref for View { } impl View { + /// Construct a new view for a given region size. pub fn new(size: T) -> Self where T: Into, @@ -29,10 +35,12 @@ impl View { Self(vec![vec![Rune::default(); size.width]; size.height]) } + /// Return an iterator for all runes in the view. pub fn iter(&self) -> impl Iterator> { self.0.iter() } + /// Apply another view onto this view at a given position. pub fn apply>(&mut self, pos: P, view: View) { let pos = pos.into(); for (y, line) in view.0.iter().enumerate() { @@ -46,16 +54,38 @@ impl View { } } + // The width of the view. pub fn width(&self) -> usize { self.0.first().unwrap().len() } + + /// The height of the view. pub fn height(&self) -> usize { self.0.len() } + + /// The region size of the view. pub fn size(&self) -> Size { (self.width(), self.height()).into() } - pub fn fill(&mut self, rect: Rect, rune: Rune) { + + /// Paint is a conveinence method for filling a region ith a given color. + /// This is done by using the passed color as the background color and + /// filling the region with ' ' characters. + pub fn paint(&mut self, rect: R, color: Color) + where + R: Into, + { + self.fill(rect, Rune::new().content(' ').bg(color)); + } + + /// Fill a region of the view with a single rune, repeating it in every + /// position. + pub fn fill(&mut self, rect: R, rune: Rune) + where + R: Into, + { + let rect = rect.into(); for y in rect.pos.y..(rect.size.height + rect.pos.y).min(self.0.len()) { for x in rect.pos.x..(rect.size.width + rect.pos.x).min(self.0[y].len()) { let _ = std::mem::replace(&mut self.0[y][x], rune); @@ -63,6 +93,11 @@ impl View { } } + /// Insert a string at the specific position in the view. Each chacter is + /// mapped to a rune and placed starting at the position given and + /// continueing to the right + /// + /// This function performs no wrapping of any kind. pub fn insert, S: Into>(&mut self, pos: P, value: S) { let Pos { x, y } = pos.into(); let runes: Runes = value.into(); diff --git a/src/widget/mod.rs b/src/widget/mod.rs deleted file mode 100644 index 40bf396..0000000 --- a/src/widget/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::prelude::{Res, Runes, Theme, ViewContext}; - -pub trait Widget { - fn ui(&mut self, ctx: &mut ViewContext); -} - -fn list(items: Vec, selection_index: usize) -> impl FnOnce(&mut ViewContext, Res) { - move |ctx: &mut ViewContext, theme: Res| { - for (idx, item) in items.into_iter().enumerate() { - ctx.insert((0, idx), item.clone()); - } - } -}