doc strings and doc tests

This commit is contained in:
Joe Bellus 2023-05-29 20:46:21 -04:00
parent aaa51179ad
commit 3388ae4096
16 changed files with 311 additions and 39 deletions

View File

@ -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"

View File

@ -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),
);
}

View File

@ -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),
);
}

31
examples/keyboard.rs Normal file
View File

@ -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<Keyboard>) {
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),
);
}

15
examples/simple.rs Normal file
View File

@ -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),
);
}

19
examples/theme.rs Normal file
View File

@ -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<Theme>) {
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),
);
}

View File

@ -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<F, Args>
where
F: Callable<Args>,
@ -30,6 +32,9 @@ where
F: Callable<Args>,
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<F, Args> {
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<MyResource>) {
/// 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<T: Any>(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<MyState>) {
/// thing.get_mut().value += 1;
/// ctx.insert(0,format!("Value is {}", thing.get().value));
/// }
/// ````
pub fn insert_state<T: Any>(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()));

View File

@ -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<TypeId, Box<dyn Any>>,
}
impl Container {
pub fn bind<T: Any>(&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<T: Any>(&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<T: Any>(&self) -> Option<&T> {
self.bindings
.get(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast_ref())
}
/// Call a function while providing dependency injcetion.
pub fn call<F, Args>(&self, view: &mut ViewContext, callable: &F)
where
F: Callable<Args>,
Args: FromContainer,
{
callable.call(view, Args::from_container(self));
let _ = callable.call(view, Args::from_container(self));
}
}
pub trait Callable<Args> {
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<T: ?Sized>(Rc<RefCell<T>>);
impl<T> State<T> {
/// 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<T> {
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<T> {
RefCell::borrow(&self.0)
}
@ -73,6 +98,9 @@ impl<T: ?Sized + 'static> FromContainer for State<T> {
}
}
/// 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<T: ?Sized>(Rc<T>);
@ -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<Args> {
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 {}

View File

@ -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<RefCell<Container>>,
@ -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<RefCell<Container>>, size: Size) -> Self {
let view = View::new(size);
Self { view, container }
}
pub fn component<F, Args>(&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<F, Args, R>(&mut self, rect: R, f: F)
where
F: Callable<Args>,
Args: FromContainer,
R: Into<Rect>,
{
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<P>(&mut self, pos: P, rune: Rune)
where
P: Into<Pos>,

View File

@ -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<usize> 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,

View File

@ -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<RefCell<Option<KeyCode>>>,

View File

@ -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;

View File

@ -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<char>,
@ -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<Rune>);

View File

@ -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,

View File

@ -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<Vec<Rune>>);
@ -21,6 +26,7 @@ impl std::ops::Deref for View {
}
impl View {
/// Construct a new view for a given region size.
pub fn new<T>(size: T) -> Self
where
T: Into<Size>,
@ -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<Item = &Vec<Rune>> {
self.0.iter()
}
/// Apply another view onto this view at a given position.
pub fn apply<P: Into<Pos>>(&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<R>(&mut self, rect: R, color: Color)
where
R: Into<Rect>,
{
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<R>(&mut self, rect: R, rune: Rune)
where
R: Into<Rect>,
{
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<P: Into<Pos>, S: Into<Runes>>(&mut self, pos: P, value: S) {
let Pos { x, y } = pos.into();
let runes: Runes = value.into();

View File

@ -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<Runes>, selection_index: usize) -> impl FnOnce(&mut ViewContext, Res<Theme>) {
move |ctx: &mut ViewContext, theme: Res<Theme>| {
for (idx, item) in items.into_iter().enumerate() {
ctx.insert((0, idx), item.clone());
}
}
}