Compare commits

...

7 Commits

Author SHA1 Message Date
Joe Bellus c3a14d6c33 Documentation
Added documentation to many public functions

Added Codex documentaiton for the project as a whole

Added build pipline for generating codex site
2024-04-19 01:44:24 -04:00
Joe Bellus 6abfc6aa54 Aded doc tests throughout 2024-04-18 15:07:21 -04:00
Joe Bellus 972ddb57ca Stack alignment
Stacks now have an alignment property, which allows them to align their
content.

Horizontal stacks can be aligned Top Bottom Center.

Vertical stacks can be aligned Left Right Center.

This is respected for both Stack::insert and Stack::component.

Alignment expects Stack::insert to contain a single line string, which
is a functional requirement for both Stack::insert and ViewContext::insert.
2024-04-15 13:14:30 -04:00
Joe Bellus 3e50d269e5 Fill functions now support a Color as an argument
Fill functions were modified to support Into<Rune> and
From<Color> was implemented for Rune and Runes. This allows
Passing a color to functions such as View::fill and View::fill_all.
to set the background color.
2024-04-13 00:23:40 -04:00
Joe Bellus 44baee56b0 Added shifting functions for Rect
Added Rect::expand and Rect::translate for conveniently mutating Rects
2024-04-13 00:23:01 -04:00
Joe Bellus 3655f3269e Added handling for modifier keys
Modifier key detection is now available in the keyboard resource
2024-04-13 00:22:40 -04:00
Joe Bellus 99137d1708 Added Nix flake 2024-04-13 00:22:20 -04:00
23 changed files with 1152 additions and 32 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

40
.github/workflows/docs.yml vendored Normal file
View File

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

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target
/Cargo.lock
.direnv/

View File

@ -1,6 +1,6 @@
[package]
name = "arkham"
version = "0.2.0"
version = "0.2.1"
edition = "2021"
description = "TUI made simple"
authors = ["Joe Bellus"]

15
docs/codex.yml Normal file
View File

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

2
docs/index.md Normal file
View File

@ -0,0 +1,2 @@
# Document Title

102
docs/overview/components.md Normal file
View File

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

View File

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

72
docs/overview/keyboard.md Normal file
View File

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

View File

@ -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<Person> = 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&lt;T&gt;`
- State objects use `State&lt;T&gt;`
## 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<People>) {
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<AppState>) {
ctx.insert(
(0, 0),
format!("Counter: {}", state.get().counter));
}
```
<Alert style="warning" title="Don't borrow state mutably more than once">
Under the hood state is provided inside `Rc&lt;RefCell&lt;T&gt;&gt;`.
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.
</Alert>

67
docs/overview/stacks.md Normal file
View File

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

View File

@ -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) {

View File

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

27
flake.lock Normal file
View File

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1712791164,
"narHash": "sha256-3sbWO1mbpWsLepZGbWaMovSO7ndZeFqDSdX0hZ9nVyw=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "1042fd8b148a9105f3c0aca3a6177fd1d9360ba5",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

19
flake.nix Normal file
View File

@ -0,0 +1,19 @@
{
description = "JSON documentation explorer";
inputs = { nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; };
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in {
devShells.${system}.default = pkgs.mkShell {
packages = [
pkgs.rustc
pkgs.cargo
pkgs.rust-analyzer
pkgs.rustfmt
pkgs.cargo-watch
];
};
};
}

View File

@ -34,7 +34,21 @@ impl Renderer {
}
/// The app is the core container for the application logic, resources,
/// state, and run loop.
/// state, and run loop.
///
/// Setting up a basic application:
///
/// ```no_run
/// use arkham::prelude::*;
///
/// fn main() {
/// App::new(root_view).run();
/// }
///
/// fn root_view(ctx: &mut ViewContext) {
/// ctx.insert((2,2), "Hello World");
/// }
/// ```
pub struct App<F, Args>
where
F: Callable<Args>,
@ -147,12 +161,14 @@ where
pub fn run(&mut self) -> anyhow::Result<()> {
self.container.borrow_mut().bind(Res::new(Terminal));
self.container.borrow_mut().bind(Res::new(Keyboard::new()));
let _ = ctrlc::set_handler(|| {
let mut out = std::io::stdout();
let _ = terminal::disable_raw_mode();
let _ = execute!(out, terminal::LeaveAlternateScreen, cursor::Show);
std::process::exit(0);
});
let mut out = std::io::stdout();
execute!(out, terminal::EnterAlternateScreen, cursor::Hide)?;
terminal::enable_raw_mode()?;
@ -190,6 +206,7 @@ where
let container = self.container.borrow();
let kb = container.get::<Res<Keyboard>>().unwrap();
kb.set_key(key_event.code);
kb.set_modifiers(key_event.modifiers);
}
Event::Mouse(_) => todo!(),
Event::Paste(_) => todo!(),

View File

@ -55,21 +55,29 @@ impl ViewContext {
self.rerender = true;
}
pub fn vertical_stack(&self, size: Size) -> Stack {
pub fn vertical_stack<S>(&self, size: S) -> Stack
where
S: Into<Size>,
{
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<S>(&self, size: S) -> Stack
where
S: Into<Size>,
{
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,
}
}
@ -104,3 +112,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())
}
}

View File

@ -1,6 +1,9 @@
use std::ops::{Add, AddAssign, Sub};
/// Pos represents a coordinate position within the termianl screen.
///
/// *NOTE* Most functions accept a value that can be converted into a Pos.
/// For these a simple tuple of coordinates is sufficient.
#[derive(Debug, Clone, Copy)]
pub struct Pos {
pub x: usize,
@ -8,6 +11,16 @@ pub struct Pos {
}
impl Pos {
/// Generate a new Pos from a given set of coordinates.
///
/// Example:
///
/// ```
/// use arkham::prelude::*;
/// let pos = Pos::new(3,1);
/// assert_eq!(pos.x, 3);
/// assert_eq!(pos.y, 1);
/// ```
pub fn new(x: usize, y: usize) -> Self {
Self { x, y }
}
@ -45,7 +58,27 @@ impl AddAssign<Pos> for Pos {
}
}
// An area that can be operated on.
/// An area that can be operated on.
///
/// ```
/// use arkham::prelude::*;
///
/// let s = Size::new(3,3);
/// assert_eq!(s.width, 3);
/// assert_eq!(s.height, 3);
/// ```
///
/// Sizes can be added and subtracted to mutate them easily:
///
/// ```
/// use arkham::prelude::*;
///
/// let s1 = Size::new(3,3);
/// let s2 = Size::new(0,1);
/// let s = s1 - s2;
/// assert_eq!(s.width, 3);
/// assert_eq!(s.height, 2);
/// ```
#[derive(Debug, Clone, Copy)]
pub struct Size {
pub width: usize,
@ -171,6 +204,38 @@ impl Rect {
size: size.into(),
}
}
/// Move the Rect's origin, without chaging its size.
///
/// Example:
///
/// ```
/// use arkham::prelude::*;
///
/// let mut rect = Rect::new((0,0), (15,5));
/// rect.translate(5,0);
/// assert_eq!(rect.pos.x, 5);
/// ```
pub fn translate(&mut self, x: i32, y: i32) {
self.pos.x = (self.pos.x as i32 + x).max(0) as usize;
self.pos.y = (self.pos.y as i32 + y).max(0) as usize;
}
/// Change the Rect's size without altering its position.
///
/// Example:
///
/// ```
/// use arkham::prelude::*;
///
/// let mut rect = Rect::new((0,0), (15,5));
/// rect.expand(5,0);
/// assert_eq!(rect.size.width, 20);
/// ```
pub fn expand(&mut self, width: i32, height: i32) {
self.size.width = (self.size.width as i32 + width).max(1) as usize;
self.size.height = (self.size.height as i32 + height).max(1) as usize;
}
}
impl From<Size> for Rect {
@ -178,3 +243,13 @@ impl From<Size> for Rect {
Rect::with_size(value)
}
}
impl<P, S> From<(P, S)> for Rect
where
P: Into<Pos>,
S: Into<Size>,
{
fn from(value: (P, S)) -> Self {
Rect::new(value.0.into(), value.1.into())
}
}

View File

@ -1,13 +1,22 @@
use std::{cell::RefCell, rc::Rc};
use crossterm::event::KeyCode;
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
/// applications can respond to keyboard input from users.
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct Keyboard {
key: Rc<RefCell<Option<KeyCode>>>,
modifiers: Rc<RefCell<KeyModifiers>>,
}
impl Default for Keyboard {
fn default() -> Self {
Self {
key: Rc::new(RefCell::new(None)),
modifiers: Rc::new(RefCell::new(KeyModifiers::empty())),
}
}
}
impl Keyboard {
@ -15,18 +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);
}
/// 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<KeyCode> {
*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<char> {
if let Some(KeyCode::Char(c)) = *self.key.borrow() {
Some(c)
@ -34,4 +56,34 @@ impl Keyboard {
None
}
}
/// 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)
}
}

View File

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

View File

@ -41,30 +41,82 @@ impl From<char> for Rune {
}
}
impl From<Color> for Rune {
fn from(value: Color) -> Self {
Rune {
content: Some(' '),
bg: Some(value),
..Default::default()
}
}
}
impl Rune {
/// Create a new empty Rune. This can be used with the settings functions as a _builder_ pattern
///
/// Example:
/// ```
/// use arkham::prelude::*;
/// let rune:Rune = Rune::new().bg(Color::Blue).fg(Color::White).bold();
/// ```
pub fn new() -> Self {
Self::default()
}
/// Set the content of the rune. The rune's content is a single character.
///
/// Example:
/// ```
/// use arkham::prelude::*;
/// let rune = Rune::new().content('A');
/// assert_eq!(rune.content, Some('A'));
/// ```
pub fn content(mut self, content: char) -> Self {
self.content = Some(content);
self
}
/// Set the background color of the rune.
///
/// Example:
/// ```
/// use arkham::prelude::*;
/// let rune = Rune::new().bg(Color::Green);
/// assert_eq!(rune.bg, Some(Color::Green));
/// ```
pub fn bg(mut self, bg: Color) -> Self {
self.bg = Some(bg);
self
}
/// Set the text color of the rune.
///
/// Example:
/// ```
/// use arkham::prelude::*;
/// let rune = Rune::new().fg(Color::Green);
/// assert_eq!(rune.fg, Some(Color::Green));
/// ```
pub fn fg(mut self, fg: Color) -> Self {
self.fg = Some(fg);
self
}
/// Set the text color of the rune.
///
/// Example:
/// ```
/// use arkham::prelude::*;
/// let rune = Rune::new().fg(Color::Green);
/// assert_eq!(rune.fg, Some(Color::Green));
/// ```
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
pub fn render<W>(self, out: &mut W) -> anyhow::Result<()>
/// Renders a Print command into the terminal's output queue
pub(crate) fn render<W>(self, out: &mut W) -> anyhow::Result<()>
where
W: std::io::Write,
{
@ -85,10 +137,17 @@ impl Rune {
}
}
/// Runes represetns a series of runes. This is generally used to convert
/// Runes represents a series of runes. This is generally used to convert
/// strings into Runes and apply styling information to them.
///
/// Building runes from a string:
///
/// ```
/// use arkham::prelude::*;
/// let runes = "This is a test string".to_runes().fg(Color::White);
/// ```
#[derive(Clone, Debug, Default)]
pub struct Runes(Vec<Rune>);
pub struct Runes(pub(crate) Vec<Rune>);
impl std::ops::Deref for Runes {
type Target = Vec<Rune>;
@ -117,26 +176,70 @@ impl<T: ToString> From<T> for Runes {
}
impl Runes {
/// Create a new runes collection from a vector of Rune.
pub fn new(runes: Vec<Rune>) -> Self {
Self(runes)
}
pub fn fg(self, color: Color) -> Self {
self.set_fg(Some(color))
}
pub fn set_fg(mut self, color: Option<Color>) -> Self {
/// Set the text color of the rune.
///
/// Example:
/// ```
/// use arkham::prelude::*;
/// let runes = "blue".to_runes().fg(Color::Blue);
/// assert!(runes.iter().all(|r| r.fg == Some(Color::Blue)))
/// ```
pub fn fg(mut self, color: Color) -> Self {
for r in self.0.iter_mut() {
r.fg = color;
r.fg = Some(color);
}
self
}
/// Set the text color of the rune.
///
/// Example:
/// ```
/// use arkham::prelude::*;
/// let mut runes = "on blue".to_runes().bg(Color::Blue);
/// let runes = runes.clear_fg();
/// assert!(runes.iter().all(|r| r.fg == None))
pub fn clear_fg(mut self) -> Self {
for r in self.0.iter_mut() {
r.fg = None;
}
self
}
/// Set the text color of the rune.
///
/// Example:
/// ```
/// use arkham::prelude::*;
/// let runes = "on blue".to_runes().bg(Color::Blue);
/// assert!(runes.iter().all(|r| r.bg == Some(Color::Blue)))
/// ```
pub fn bg(mut self, color: Color) -> Self {
for r in self.0.iter_mut() {
r.bg = Some(color);
}
self
}
/// Set the text color of the rune.
///
/// Example:
/// ```
/// use arkham::prelude::*;
/// let mut runes = "on blue".to_runes().bg(Color::Blue);
/// let runes = runes.clear_bg();
/// assert!(runes.iter().all(|r| r.bg == None))
pub fn clear_bg(mut self) -> Self {
for r in self.0.iter_mut() {
r.bg = None;
}
self
}
pub fn bold(mut self) -> Self {
for r in self.0.iter_mut() {
r.bold = true;
@ -144,6 +247,14 @@ impl Runes {
self
}
/// Append runes or a string displayable object to the Runes
///
/// Example:
/// ```
/// use arkham::prelude::*;
/// let mut runes = "This is a test string. ".to_runes();
/// runes.add("This is a colored string".to_runes().fg(Color::Blue));
/// runes.add("This is another basic string");
pub fn add<R>(&mut self, runes: R)
where
R: Into<Runes>,

View File

@ -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<RefCell<Container>>,
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<F, Args, S>(&mut self, size: S, f: F)
where
F: crate::prelude::Callable<Args>,
@ -28,22 +43,124 @@ impl Stack {
S: Into<Size>,
{
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<R: Into<Runes>>(&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));
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));
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));
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));
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));
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));
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));
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));
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));
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);
}
}

View File

@ -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.
@ -82,11 +82,27 @@ impl View {
/// 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)
pub fn fill<R, U>(&mut self, rect: R, rune: U)
where
R: Into<Rect>,
U: Into<Rune>,
{
let rect = rect.into();
let rune = rune.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);
}
}
}
/// Fill the entire view context with a rune
pub fn fill_all<R>(&mut self, rune: R)
where
R: Into<Rune>,
{
let rune = rune.into();
let rect = Rect::new((0, 0), self.size());
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);
@ -114,10 +130,26 @@ 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::<String>(),
);
acc.push('\n');
acc
})
}
}
#[cfg(test)]
mod tests {
use crossterm::style::Color;
use crate::{geometry::Rect, runes::Rune};
use super::View;
@ -212,4 +244,13 @@ mod tests {
assert_eq!(view.0[2][1].content, Some('X'));
assert_eq!(view.0[2][2].content, Some('X'));
}
#[test]
pub fn test_color_fill() {
let mut view = View::new((3, 3));
view.fill_all(Color::Red);
assert!(view
.iter()
.all(|rs| rs.iter().all(|r| r.bg == Some(Color::Red))));
}
}