diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..02df51e --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,40 @@ +name: Publish Documentation + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Build + run: | + curl --proto '=https' --tlsv1.2 -LsSf https://github.com/5Sigma/codex/releases/latest/download/Codex-installer.sh | sh + codex -r docs build + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/dist + + deploy: + needs: build + permissions: + pages: write + id-token: write + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/docs/codex.yml b/docs/codex.yml new file mode 100644 index 0000000..ff45530 --- /dev/null +++ b/docs/codex.yml @@ -0,0 +1,15 @@ +# Project name +name: Arkham CLI Library +# The author displayed on the title page during PDF generation +author: ~ +# The path relative to the project root where the compiled static site files +# will be placed +build_path: dist +# The URL to the code repository for the project. If specified a link will appear +# in the site header. +repo_url: https://github.com/5sigma/arkham +# The URL to the main project page. If specified a home link will be displayed +# in the header. +project_url: ~ +# Base URL can be set if the site is hosted in a sub path. +base_url: /arkham diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..1a893be --- /dev/null +++ b/docs/index.md @@ -0,0 +1,2 @@ +# Document Title + diff --git a/docs/overview/components.md b/docs/overview/components.md new file mode 100644 index 0000000..8423507 --- /dev/null +++ b/docs/overview/components.md @@ -0,0 +1,102 @@ +--- +title: Designing Components +subtitle: Overview +menu_position: 1 +--- + + +# Component overview + +A component in Arkham is a piece of rendering logic that could be reusable, or could be used for organization. +They are simple functions that accept a `ViewContext` reference, alongside any injected objects. See [Dependency Injection](resources) + +A simple component might look like this: + +```Rust +fn seperator(ctx: &mut ViewContext) { + let width = ctx.size().width; + let sep_chars = "-".repeat(ctx.size().width); + ctx.insert(sep_chars); +} +``` + +## Components with parameters + +Reusable components will need the ability to pass parameters into them. This is done by returning the component function from within another function. + +```Rust +fn name_field(name: &str) -> impl Fn(&mut ViewContext) { + move |ctx:&mut ViewContext| { + ctx.insert((0,0), "Name:"); + ctx.insert((10,0), name); + } +} +``` + +## Understanding component sizing and positioning + +When a component is used it is allocated a specific `Rect` that it can render to. +The total dimensions for the component are available in `ViewContext::size()`. +Components are also rendered at a specific position (the upper left corner). +Inside a component its coordinates are relative to itself, its upper left corner is (0,0). + + +## Using components + +A component can render other components inside them. + +In this example we can also see the component positioning and sizing. +The first parameter to `ViewContext::component` is a Rect for the component. +This is the position in the parent component it will render its top left corner +to and the size the component is allowed to render to. + +The first parameter to `Rect::new` is the `Pos` for the rect, the coordinates +of its top left corner. From the perspective of the `field` component, +the content is inserted at y=0. However, when the component is placed in +the `container` component, its upper left coordinate is specified and the +coordinates of the `field` component become relative to the specified position +in the `container` component. + +The second parameter to `Rect::new` is the `Size` of the `Rect`. Its width and height. + +```Rust +fn field(key: &str, value: &str) -> impl Fn(&mut ViewContext) { + move |ctx: &mut ViewContext| { + ctx.insert((0,0), format!("{}:", key)); + ctx.insert((10,0), value); + } +} + +fn container(ctx: &mut ViewContext) { + ctx.component(Rect::new((0,0), (10,1)), field("Name", "Alice") + ctx.component(Rect::new((0,1), (10,1)), field("Age", "22") +} + +``` + +The `container` component will render to the following: + +``` +Name: Alice +Age: 22 +``` + + +## Components as closures + +When you need something more complex than `ViewContext::insert`, but don't want to +build a whole separate component. Components can be defined inline using a closure. + + +```Rust +fn container(ctx: &mut ViewContext) { + let size = ctx.size(); + ctx.component( + Rect::new((0,0), (size.width, size.height /2)) + |ctx: &mut ViewContext| { + ctx.fill_all(Color::Blue); + } + ); +} +``` + diff --git a/docs/overview/getting-started.md b/docs/overview/getting-started.md new file mode 100644 index 0000000..8db76f2 --- /dev/null +++ b/docs/overview/getting-started.md @@ -0,0 +1,65 @@ +--- +title: Getting Started +subtitle: Overview +menu_position: 0 +--- + +# Starting a new project + +To setup a new project use `cargo new` and `cargo add arkham` to add the dependency. + +```Shell +cargo new my_project +cd my_project +cago add arkham +``` + +# Import the arkham prelude + +Add the following line to the top of _my_project/src/main.rs_ to import all the Arkham members. + +```rust +use arkham::prelude::*; +``` + +# Setup the root view + +Arkham requires a _Root View_ component that will act as a container view for the application. Views in Arkham are simple functions. +We can add a root view function to _my_project/src/main.rs_ +A simple root view may look like this: + +```Rust +fn root_view(ctx &mut ViewContext) { + ctx.insert((5,5), "Hello World"); +} +``` + +# Setup the application + +In our `main` function we can setup the application and run it, beginning the run loop. Replace the main function in _my_project/src/main.rs_ with the following: + +```Rust +fn main() { + App::new(root_view).run().unwrap(); +} +``` + + +# The full main.rs + +The full main.rs now looks like this: + +```Rust +use arkham::prelude::*; + +fn main() { + App::new(root_view).run().unwrap(); +} + +fn root_view(ctx &mut ViewContext) { + ctx.insert((5,5), "Hello World"); +} +``` + + +This can now be run with `cargo run` and a hello world app, complete with a default 'q' hotkey to quit, will run. diff --git a/docs/overview/keyboard.md b/docs/overview/keyboard.md new file mode 100644 index 0000000..83067af --- /dev/null +++ b/docs/overview/keyboard.md @@ -0,0 +1,72 @@ +--- +title: Keyboard Input +subtitle: Overview +menu_position: 3 +--- + +# Reading keyboard input + +Keyboard input is provided by a `Keyboard` resource. This is +automatically available. To read input from keyboard events +accept the resource as a parameter for a component function +and check the current state. + +```Rust +fn show_keypress(ctx &mut ViewContext, kb: Res) { + if let Some(c) = kb.char() { + ctx.insert(0, format!("Key press: {}", c)); + } +} +``` + +## Reading modifier keys + +Modifier key state is provided within the keyboard resource. + + +```Rust +fn check_key(ctx &mut ViewContext, kb: Res) { + if kb.char() == Some('d') && kb.control() { + ctx.insert(0, "Key Pressed") + } else { + ctx.insert(0, "Key NOT Pressed") + } +} +``` + + +# Full Keyboard example + +```Rust +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(((10, 10), (30, 1)), hello_world); + ctx.component(((10, 11), (20, 1)), show_key_press); + ctx.component((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/docs/overview/resources.md b/docs/overview/resources.md new file mode 100644 index 0000000..83bd64b --- /dev/null +++ b/docs/overview/resources.md @@ -0,0 +1,98 @@ +--- +title: Resources & State +subtitle: Overview +menu_position: 2 +--- + +# Dependency Injection + +Being able to access your own objects and state is +important for any application. Arkham focuses heavily +on making this ergonomic and easy. + +There are two types of injectable objects a _State_ +object and _Resource_ object. The main difference is +Resource objects are provided immutable and State +objects are provided with the ability to borrow both +as mutable and immutable references. + +## Defining an injectable object + +_Resources_ and _state_ are added during application startup +and are global to the application. A single state object can +be used to maintain the full state of the application and +individual components can read and write to the sections they +need. + +Resources and state must have unique _Type_. Only one instance +any type can be inserted. + + +```Rust + +pub struct Person { + pub name: String, + pub age: u16 +} + +#[derive(Default)] +pub struct AppState { + pub counter: usize, +} + +let people: Vec = load_people(); + +App::new(root_view) + .insert_resource(people) + .insert_state(AppState::default()) + .run(); + +``` + +# Using resources and state + +Injectables are provided automatically to any component that accepts them. +Accepting them requires the use of a wrapper component depending on which +it is. + +- Resources use `Res<T>` +- State objects use `State<T>` + +## Using a resource + +Including the resource in the function arguments automatically provides +the object inside a `Res` wrapper, which derefs to the underlying object. + +```Rust +fn my_component(ctx: &mut ViewContext, people: Res) { + for (idx, person) in people.iter().enumerate() { + ctx.insert((0, idx), person.name); + } +} +``` + +## Using state + + +Including the resource in the function arguments automatically provides +the object inside a `State` wrapper. The state wrapper has two primary +functions `State::get` which returns immutable access to the state +and `State::get_mut` which returns a mutable reference. + +```Rust +fn my_component(ctx: &mut ViewContext, state: State) { + ctx.insert( + (0, 0), + format!("Counter: {}", state.get().counter)); +} +``` + + + Under the hood state is provided inside `Rc<RefCell<T>>`. + Take care not to call `State::get_mut`, which is effectively calling `RefCell::borrow_mut` more than + once at a time. + + This includes holding it and then calling a sub component that attempts to access state again. + Scope calls to `State::get_mut` so they live as short as possible and clone out objects if needed. + + diff --git a/docs/overview/stacks.md b/docs/overview/stacks.md new file mode 100644 index 0000000..b164b40 --- /dev/null +++ b/docs/overview/stacks.md @@ -0,0 +1,67 @@ +--- +title: Stacks +subtitle: Overview +menu_position: 4 +--- + +# Stack Components + +Stacks are a convenient layout component available through the `ViewContext`. +They allow a number of components to be easily positioned next to each other. + +Stacks can be either _horizontal_ or _vertical_. + +To use a stack initialize it from the context and use the `Stack::insert` +and `Stack:component` functions. The `Stack::component` function requires only a +`Size` and not a `Rect` like the `ViewContext`. This is because the stack will +automatically handle its positioning. + + +```Rust +let size = ctx.size(); +let mut stack = ctx.vertical_stack(Size::new(100, 100)); +stack.component((size.width, 2), my_component); +// We can pass size here and it will be automtically +// converted to a Rect with position (0,0). +ctx.component(size, stack); +``` + +## Alignment + +Stacks can also have a specified alignment. This will modify the +positioning of the sub components so they align in the given direction. + +Vertical stacks can have Left, Center, Right alignments +Horizontal stacks can have Top, Center, Bottom alignments + +```Rust +let size = ctx.size(); +let mut stack = ctx.vertical_stack(Size::new(100, 100)); +stack.alignment(StackAlignment::Center); +``` + +# Full stack example + +```Rust +use arkham::prelude::*; + +fn main() { + let _ = App::new(root).run(); +} + +fn root(ctx: &mut ViewContext) { + let mut stack = ctx.vertical_stack((100, 100)); + for _ in 0..10 { + stack.component((ctx.size().width, 1), list_item); + } + ctx.component((0, (100, 100)), stack); +} + +fn list_item(ctx: &mut ViewContext) { + let size = ctx.size(); + let mut hstack = ctx.horizontal_stack((ctx.size().width, 1)); + hstack.insert("> "); + hstack.insert("line 1"); + ctx.component(size, hstack); +} +``` diff --git a/examples/keyboard.rs b/examples/keyboard.rs index f8acd9a..100b885 100644 --- a/examples/keyboard.rs +++ b/examples/keyboard.rs @@ -7,9 +7,9 @@ fn main() { 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); + ctx.component(((10, 10), (30, 1)), hello_world); + ctx.component(((10, 11), (20, 1)), show_key_press); + ctx.component((0, (size.width, 1)), quit_nag); } fn hello_world(ctx: &mut ViewContext) { diff --git a/examples/stack.rs b/examples/stack.rs index 11c43eb..0fb874e 100644 --- a/examples/stack.rs +++ b/examples/stack.rs @@ -5,13 +5,17 @@ fn main() { } fn root(ctx: &mut ViewContext) { - let mut stack = ctx.vertical_stack(Size::new(100, 100)); + let mut stack = ctx.vertical_stack((100, 100)); for _ in 0..10 { - stack.component(Size::new(ctx.size().width, 2), list_item); + stack.component((ctx.size().width, 1), list_item); } - ctx.component(Rect::new(0, (100, 100)), stack); + ctx.component((0, (100, 100)), stack); } fn list_item(ctx: &mut ViewContext) { - ctx.insert(0, "line 1"); + let size = ctx.size(); + let mut hstack = ctx.horizontal_stack((ctx.size().width, 1)); + hstack.insert("> "); + hstack.insert("line 1"); + ctx.component(size, hstack); } diff --git a/src/app.rs b/src/app.rs index f9c85a7..9f208a1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -38,7 +38,7 @@ impl Renderer { /// /// Setting up a basic application: /// -/// ``` +/// ```no_run /// use arkham::prelude::*; /// /// fn main() { diff --git a/src/context.rs b/src/context.rs index 62eb540..2c91b8b 100644 --- a/src/context.rs +++ b/src/context.rs @@ -55,21 +55,27 @@ impl ViewContext { self.rerender = true; } - pub fn vertical_stack(&self, size: Size) -> Stack { + pub fn vertical_stack(&self, size: S) -> Stack + where + S: Into, + { Stack { direction: crate::stack::StackDirection::Vertical, container: self.container.clone(), - view: View::new(size), + view: View::new(size.into()), position: Pos::from(0), alignment: crate::stack::StackAlignment::Top, } } - pub fn horizontal_stack(&self, size: Size) -> Stack { + pub fn horizontal_stack(&self, size: S) -> Stack + where + S: Into, + { Stack { direction: crate::stack::StackDirection::Horizontal, container: self.container.clone(), - view: View::new(size), + view: View::new(size.into()), position: Pos::from(0), alignment: crate::stack::StackAlignment::Left, } diff --git a/src/geometry.rs b/src/geometry.rs index d569b10..27f2d4a 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -243,3 +243,13 @@ impl From for Rect { Rect::with_size(value) } } + +impl From<(P, S)> for Rect +where + P: Into, + S: Into, +{ + fn from(value: (P, S)) -> Self { + Rect::new(value.0.into(), value.1.into()) + } +} diff --git a/src/input.rs b/src/input.rs index 6810db3..15cbe3f 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,6 +1,6 @@ use std::{cell::RefCell, rc::Rc}; -use crossterm::event::{KeyCode, KeyModifiers, ModifierKeyCode}; +use crossterm::event::{KeyCode, KeyModifiers}; /// Keyboard can be used as an injectable resource that provides information /// about the current keyboard state. This is the primary mechanism by which @@ -24,22 +24,31 @@ impl Keyboard { Self::default() } - pub fn set_key(&self, k: KeyCode) { + /// Set the keyboard state to indicate a specific keycode is pressed + pub(crate) fn set_key(&self, k: KeyCode) { *self.key.borrow_mut() = Some(k); } - pub fn set_modifiers(&self, modifiers: KeyModifiers) { + /// Set the keyboard state to indicate specific modifier keys are pressed + pub(crate) fn set_modifiers(&self, modifiers: KeyModifiers) { *self.modifiers.borrow_mut() = modifiers; } + /// Resets the keyboard state. This can be used after accepting + /// a keypress within a component to prevent further components from + /// registering the keypress event pub fn reset(&self) { *self.key.borrow_mut() = None; } + /// Retruns the keycode that is current pressed, or None if there are + /// no currently pressed keys pub fn code(&self) -> Option { *self.key.borrow() } + /// Returns the char value of the pressed key. Returns None if no key + /// is currently pressed, or if the key does not have a char value. pub fn char(&self) -> Option { if let Some(KeyCode::Char(c)) = *self.key.borrow() { Some(c) @@ -48,26 +57,32 @@ impl Keyboard { } } + /// Returns true if the shift key is current pressed pub fn shift(&self) -> bool { self.modifiers.borrow().contains(KeyModifiers::SHIFT) } + /// Returns true if the control key is current pressed pub fn control(&self) -> bool { self.modifiers.borrow().contains(KeyModifiers::CONTROL) } + /// Returns true if the alt key is current pressed pub fn alt(&self) -> bool { self.modifiers.borrow().contains(KeyModifiers::ALT) } + /// Returns true if the super key is current pressed pub fn super_key(&self) -> bool { self.modifiers.borrow().contains(KeyModifiers::SUPER) } + /// Returns true if the hyper key is current pressed pub fn hyper(&self) -> bool { self.modifiers.borrow().contains(KeyModifiers::HYPER) } + /// Returns true if the meta key is current pressed pub fn meta(&self) -> bool { self.modifiers.borrow().contains(KeyModifiers::META) } diff --git a/src/stack.rs b/src/stack.rs index cbecc01..aa8d567 100644 --- a/src/stack.rs +++ b/src/stack.rs @@ -178,7 +178,7 @@ mod tests { #[test] fn test_vertical_insert() { let ctx = crate::context::tests::context_fixture(); - let mut stack = ctx.vertical_stack((10, 2).into()); + let mut stack = ctx.vertical_stack((10, 2)); stack.insert("one"); stack.insert("two"); assert_eq!( @@ -190,7 +190,7 @@ mod tests { #[test] fn test_horizontal_insert() { let ctx = crate::context::tests::context_fixture(); - let mut stack = ctx.horizontal_stack((10, 1).into()); + let mut stack = ctx.horizontal_stack((10, 1)); stack.insert("one"); stack.insert("two"); assert_eq!(stack.view.render_text(), "onetwo\0\0\0\0\n".to_string()); @@ -199,7 +199,7 @@ mod tests { #[test] fn test_component() { let ctx = crate::context::tests::context_fixture(); - let mut stack = ctx.horizontal_stack((10, 2).into()); + let mut stack = ctx.horizontal_stack((10, 2)); stack.component((10, 2), |ctx: &mut ViewContext| { ctx.insert((3, 1), "one"); }); @@ -212,7 +212,7 @@ mod tests { #[test] fn test_align_left() { let ctx = crate::context::tests::context_fixture(); - let mut stack = ctx.vertical_stack((10, 2).into()); + let mut stack = ctx.vertical_stack((10, 2)); stack.component((5, 1), |ctx: &mut ViewContext| { ctx.insert((0, 0), "one"); }); @@ -226,7 +226,7 @@ mod tests { #[test] fn test_align_right() { let ctx = crate::context::tests::context_fixture(); - let mut stack = ctx.vertical_stack((10, 3).into()); + let mut stack = ctx.vertical_stack((10, 3)); stack.alignment = StackAlignment::Right; stack.insert("one"); stack.component((5, 1), |ctx: &mut ViewContext| { @@ -248,7 +248,7 @@ mod tests { #[test] fn test_align_center_v() { let ctx = crate::context::tests::context_fixture(); - let mut stack = ctx.vertical_stack((10, 3).into()); + let mut stack = ctx.vertical_stack((10, 3)); stack.alignment = StackAlignment::Center; stack.insert("one"); stack.component((5, 1), |ctx: &mut ViewContext| { @@ -270,7 +270,7 @@ mod tests { #[test] fn test_align_top() { let ctx = crate::context::tests::context_fixture(); - let mut stack = ctx.horizontal_stack((9, 6).into()); + let mut stack = ctx.horizontal_stack((9, 6)); stack.component((3, 1), |ctx: &mut ViewContext| { ctx.insert((0, 0), "one"); }); @@ -292,7 +292,7 @@ mod tests { #[test] fn test_align_bottom() { let ctx = crate::context::tests::context_fixture(); - let mut stack = ctx.horizontal_stack((9, 6).into()); + let mut stack = ctx.horizontal_stack((9, 6)); stack.alignment(StackAlignment::Bottom); stack.component((3, 1), |ctx: &mut ViewContext| { ctx.insert((0, 0), "one"); @@ -315,7 +315,7 @@ mod tests { #[test] fn test_align_center_h() { let ctx = crate::context::tests::context_fixture(); - let mut stack = ctx.horizontal_stack((9, 6).into()); + let mut stack = ctx.horizontal_stack((9, 6)); stack.alignment(StackAlignment::Center); stack.component((3, 1), |ctx: &mut ViewContext| { ctx.insert((0, 0), "one");