Compare commits
7 Commits
0b4e1beb53
...
c3a14d6c33
Author | SHA1 | Date |
---|---|---|
Joe Bellus | c3a14d6c33 | |
Joe Bellus | 6abfc6aa54 | |
Joe Bellus | 972ddb57ca | |
Joe Bellus | 3e50d269e5 | |
Joe Bellus | 44baee56b0 | |
Joe Bellus | 3655f3269e | |
Joe Bellus | 99137d1708 |
|
@ -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,2 +1,3 @@
|
|||
/target
|
||||
/Cargo.lock
|
||||
.direnv/
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "arkham"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition = "2021"
|
||||
description = "TUI made simple"
|
||||
authors = ["Joe Bellus"]
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
# Document Title
|
||||
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
|
@ -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.
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
```
|
|
@ -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<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<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<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.
|
||||
</Alert>
|
||||
|
|
@ -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);
|
||||
}
|
||||
```
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
19
src/app.rs
19
src/app.rs
|
@ -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!(),
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
58
src/input.rs
58
src/input.rs
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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', " "));
|
||||
}
|
||||
}
|
||||
|
|
129
src/runes.rs
129
src/runes.rs
|
@ -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>,
|
||||
|
|
290
src/stack.rs
290
src/stack.rs
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
45
src/view.rs
45
src/view.rs
|
@ -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))));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue