Introduction
Anathema is a library for building text user interfaces using composable components, with a custom markup language.
It was created with the intent to give developers a fast and easy way to build text user interfaces (TUI) and ship the template(s) alongside the application, giving the end user the option to customise the layout.
By separating the layout from the rest of the application, reducing the amount of code needed to express your design, and featuring hot reloading it becomes incredibly fast to iterate over the design.
Do note that AML is a markup language with some basic control flow and is not a reactive programming language.
Why
- Templates can be shipped with the application, giving the end user the ability to customise the entire layout of the application.
- Easy to prototype with the markup language and runtime.
Why not
Anathema does not excel when it comes to building dynamic layouts such as splitting buffers during runtime for your next Vim clone.
Editor support for AML
There is currently only basic syntax highlighting for vim: https://github.com/togglebyte/aml.vim.
Alternatives
Getting started
Install
Add anathema
to your Cargo.toml file.
...
[dependencies]
anathema = { git = "https://github.com/togglebyte/anathema/" }
Note
Even though efforts are made to keep this guide up to date there are possibilities of changes being made and published before they reach this guide.
At the time of writing, Anathema should be considered alpha (but almost beta!).
A basic example
Render a border around three lines of text, placing the text in the middle of the terminal.
// src/main.rs
use anathema::prelude::{Backend, Document, TuiBackend};
use anathema::runtime::Runtime;
fn main() {
let doc = Document::new("@index");
let mut backend = TuiBackend::builder()
.enable_alt_screen()
.enable_raw_mode()
.hide_cursor()
.finish()
.unwrap();
backend.finalize();
let mut builder = Runtime::builder(doc, &backend);
builder.default::<()>("index", "templates/index.aml").unwrap();
builder
.finish(&mut backend, |mut runtime, backend| runtime.run(backend))
.unwrap();
}
// templates/index.aml
align [alignment: "center"]
border
vstack
text [foreground: "yellow"] "You"
text [foreground: "red"] "are"
text [foreground: "blue"] "great!"
Prelude
Anathema has two convenience modules:
The prelude
can be used to bootstrap your application, as it contains the
Runtime
, Document
etc.
#![allow(unused)] fn main() { use anathema::prelude::*; }
and components
is suitable for modules that only contains Component
s.
#![allow(unused)] fn main() { use anathema::components::*; }
See the crate documentation for more information.
Components
A component exposes event handling and state management.
Any type that implements the anathema::component::Component
trait is a component.
This trait has no required methods, however note that there are two required associated types:
- State
- Message
If the component does not need to handle state or receive messages these can be set to the unit type.
use anathema::component::Component;
struct MyComponent;
impl Component for MyComponent {
type State = (); // <-\
// -- Set to () to ignore
type Message = (); // <-/
}
A component has to be registered with the runtime before it can be used in the template.
Template syntax
The component name is prefixed with an @
sign: @component_name
, followed by
optional associated events and attributes.
@ means it's a component
| Component name Attribute name
| | Component event | Attribute value
| | | Parent event | |
V V V V V V
@component (click->parent_click) [attrib: "value"]
Usage
Use a component in a template by prefixing the tag with the @
sign:
border
@my_comp
Focus and receiving events
Assuming .enable_mouse()
on the runtime, mouse events are sent to all components,
however key events are only sent to the component that currently has focus.
It is possible to assign focus to a component from another component using
context.components.by_name("my_component").set_focus()
.
For more information about querying components see Messaging.
set_focus
takes an attribute name and a value. It is possible to call
set_focus
on multiple components within a single frame. This will result in
multiple components receiving both on_focus
and on_blur
until the last
component in the focus queue which only receives on_focus
.
Example
The following example would set focus on @component_a
as it has a matching id.
It's not required to name the attribute id
, it can be any alphanumerical value
(as long as it doesn't start with a number).
vstack
@component_a [id: 1]
@component_b [id: 2]
@component_c [id: "not a number"]
fn on_key(
&mut self,
key: KeyEvent,
state: &mut Self::State,
mut elements: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
context.components.by_attribute("id", 1).focus();
}
State
A state is a struct with named fields.
Each field has to be wrapped in a Value
, e.g
#![allow(unused)] fn main() { name: Value<String> }
or
#![allow(unused)] fn main() { names: Value<List<String>> }
Updating state
There are two ways to update a field on State
.
state.field.set(new_value)
*state.field.to_mut() = new_value
Important note about state values
When assigning a new value to a state, do not create a new instance of Value<T>.
This is bad:
my_state.value = Value::new(123)
This is how it should be done:
my_state.some_value.set(123);
// or
*my_state.another_value.to_mut() = 123;
Attributes
A component can have state (mutable access) and attributes.
Passing attributes to a component is done the same way as for elements:
@my_comp [key: "this is a value"]
To use the attributes in the component refer to the keys passed into the component:
// component template
text "the value is " attributes.key
Accessing attributes from a component
It's possible to access attributes from within the component, using context.attributes
.
Example
@comp [key: 123]
#![allow(unused)] fn main() { fn on_key( &mut self, key: KeyEvent, state: &mut Self::State, mut elements: Elements<'_, '_>, mut context: Context<'_, Self::State>, ) { let v: i64 = context.attributes.get_as::<i64>("key").unwrap(); } }
The following types currently supports get_as<T>
:
i8
i16
i32
i64
isize
u8
u16
u32
u64
usize
&String
Color
Hex
Display
It's possible to add additional types that can be used as attributes as long as
the type implements TryFrom<&ValueKind<'_>> for T
and From<T> for ValueKind<'_>
where T
is a custom type.
struct MyAttribute(String);
impl TryFrom<&ValueKind<'_>> for MyAttribute {
type Error = ();
fn try_from(value: &ValueKind<'_>) -> Result<Self, Self::Error> {
let Some(s) = value.as_str() else { return Err(()) };
Ok(MyAttribute(s))
}
}
impl From<MyAttribute> for ValueKind<'_> {
fn from(value: MyAttribute) -> Self {
ValueKind::Str(value.0.into())
}
}
State
State is anything that implements the State
trait. It can be accessed in the template using state.<value>
.
Example
use anathema::state::{State, Value, List};
#[derive(State)]
struct MyState {
name: Value<String>,
numbers: Value<List<usize>>,
}
impl MyState {
pub fn new() -> Self {
Self {
name: String::from("Lilly").into(),
numbers: List::empty(),
}
}
}
let mut my_state = MyState::new();
my_state.numbers.push_back(1);
my_state.numbers.push_back(2);
runtime.component("my_comp", "path/to/template.aml", MyComponent, my_state);
Ignore fields
To store fields on the state that should be ignored by templates and widgets,
decorate the fields with #[anathema(ignore)]
.
Example
#![allow(unused)] fn main() { #[derive(State)] struct MyState { word: Value<String>, #[anathema(ignore)] ignored_value: usize, } }
Rename fields
To rename fields on the state that should have a different name in the template
decorate the fields with #[anathema(rename = "new_name")]
.
Example
#![allow(unused)] fn main() { #[derive(State)] struct MyState { #[anathema(rename = "string")] chars: Value<String>, } }
Event handling
For a component to receive events it has to enable event handling by implementing one or more of the following methods:
on_mount
Event called when a component is added to the tree.
fn on_init(
&mut self,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) { }
on_unmount
Event called when a component is removed from the tree and returned to component storage.
fn on_unmount(
&mut self,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) { }
on_key
Accept a key event and a mutable reference to the state.
fn on_key(
&mut self,
key: KeyEvent,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) { }
on_mouse
Accept a mouse event and a mutable reference to the state.
fn on_mouse(
&mut self,
mouse: MouseEvent,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) { }
on_focus
The component gained focus.
fn on_focus(
&mut self,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) { }
on_blur
The component lost focus.
fn on_blur(
&mut self,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {}
on_tick
Issued every frame by the runtime.
By default every component has ??
.
To stop a component from receiving tick events set ??
to false.
fn on_tick(
&mut self,
state: &mut Self::State,
mut children: Children<'_, '_>,
context: Context<'_, '_, Self::State>,
dt: Duration,
) {
}
on_resize
Issues when the backend is resized.
fn on_resize(
&mut self,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
}
on_event
This is issued if a child component emits an event using context.publish()
.
fn on_event(
&mut self,
event: &mut UserEvent<'_>,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
}
Example
use anathema::widgets::components::events::{KeyCode, KeyEvent, MouseEvent};
use anathema::widgets::Elements;
impl Component for MyComponent {
type Message = ();
type State = MyState;
fn on_key(
&mut self,
key: KeyEvent,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
// Get mutable access to the name
let mut name = state.name.to_mut();
if let Some(c) = key.get_char() {
name.push(c);
}
if let KeyCode::Enter = key.code {
name.clear();
}
}
fn on_mouse(
&mut self,
mouse: MouseEvent,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
// Mouse event
}
}
Messages and focus
Communication between components is done either with component queries or an
Emitter
.
An Emitter
is used to send a message to any recipient and can be used outside
of the runtime (e.g from another thread etc.).
The recipient id is returned when calling component
.
The Emitter
has two functions:
emit
(use in a sync context)emit_async
(use in an async context)
If the recipient no longer exist the message will be lost.
Implement the message
method of the Component
trait for a component to be
able to receive messages.
The following example sets the component to accept String
s and starts a thread
that sends a String
to the component every second.
Example
use anathema::runtime::{Emitter, Runtime};
impl Component for MyComponent {
type Message = String; // <- accept strings
type State = MyState;
fn on_message(
&mut self,
message: Self::Message,
state: &mut Self::State,
mut elements: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
state.messages.push_back(message);
}
}
// Send a string to a recipient every second
fn send_messages(emitter: Emitter, recipient: ComponentId<String>) {
let mut counter = 0;
loop {
emitter.emit(recipient, format!("{counter} message"));
counter += 1;
std::thread::sleep_ms(1000);
}
}
// Get the component id when registering the component
let recipient = runtime.component(
"my_comp",
"path/to/template.aml",
MyComponent::new(),
MyState::new()
);
let emitter = runtime.emitter();
std::thread::spawn(move || {
send_messages(emitter, recipient);
});
Internal messaging
It is possible to send a message from one component to another without using an emitter.
Using the Context
it's possible to send a message to the first components that fits
the query. Note that only one instance of the value will be sent, even if the
query matches multiple components.
Example
An example of a component sending a String
to another component.
// Component accepting messages
impl Component for ReceiverComponent {
type Message = String; // <- accept strings
type State = MyState;
fn on_message(
&mut self,
message: Self::Message,
state: &mut Self::State,
mut elements: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
state.messages.push_back(message);
}
}
// Component sending messages
impl Component for SenderComponent {
type Message = ();
type State = ();
fn on_message(
&mut self,
message: Self::Message,
state: &mut Self::State,
mut elements: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
context.components.by_name("receiver").send(String::from("What a lovely sweater you have"));
}
}
runtime.component("receiver", "path/to/template.aml", ReceiverComponent, ()).unwrap();
runtime.component("sender", "path/to/template.aml", SenderComponent, ()).unwrap();
Component querying and focus
By name
context.components.by_name("my_component").set_focus()
.
Example
fn on_key(
&mut self,
key: KeyEvent,
state: &mut Self::State,
mut elements: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
context.components.by_attribute("id", 1).focus();
context.components.by_name("my_component").set_focus();
}
By attribute
context.components.by_attribute("id", 1).send("sandwich".to_string())
.
Example
fn on_key(
&mut self,
key: KeyEvent,
state: &mut Self::State,
mut elements: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
context.components.by_attribute("id", 1).send("sandwich".to_string());
}
Element and Component query
Both component events and messages provide an element query that can be used to access elements and attributes of child components.
Attributes can be read and written to.
Querying elements
There are currently three element queries:
by_tag
by_attribute
at_position
A query can be followed by an additional query, or first
or each
.
first
will stop at the first found element, whereas each
will call the
closure for each element.
Example
children.elements()
.by_tag("border")
.by_attribute("button", true).
.as_position(pos)
.each(|element, attributes| {
// Each element and it's associated attributes
});
Cast an element
It's possible to cast an element to a specific widget using the to
method.
The to
method returns a &mut T
.
For an immutable reference use to_ref
.
If the element type is unknown use try_to
and try_to_ref
respectively.
Example
fn on_key(
&mut self,
key: KeyEvent,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
children
.elements()
.by_tag("overflow")
.by_attribute("abc", 123)
.first(|el, _| {
let overflow = el.to::<Overflow>();
overflow.scroll_up();
});
}
Filter elements
Example
fn on_mouse(
&mut self,
mouse: MouseEvent,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
children
.elements()
.by_attribute("abc", 123)
.each(|el, attributes| {
attributes.set("background", "green");
});
}
There are three methods to query the elements inside the component:
by_tag
This is the element tag name in the template, e.g text
or overflow
.
children
.elements()
.by_tag("text")
.each(|el, attributes| {
attributes.set("background", "green");
});
by_attribute
This is an attribute with a matching value on any element.
children
.elements()
.by_attribute("background", "green")
.each(|el, attributes| {
attributes.set("background", "red");
});
at_position
fn on_mouse(
&mut self,
mouse: MouseEvent,
state: &mut Self::State,
mut children: Elements<'_, '_>,
context: Context<'_, Self::State>,
) {
children
.elements()
.at_position(mouse.pos())
.each(|el, attributes| {
attributes.set("background", "red");
});
}
Setting attribute values
It is possible to assign an attribute to an element using the set
function on the attributes.
// Component event
fn on_key(
&mut self,
key: KeyEvent,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
children
.elements()
.by_tag("position")
.each(|el, attrs| {
attrs.set("background", Color::Red);
});
}
Getting values from attributes
To get values from attributes use the get_as
function.
E.e context.attributes.get_as::<&str>("name").unwrap();
Note that integers in the template can be auto cast to any integer type.
E.g i64
can be cast to a u8
.
However integers can not be cast to floats, and floats can not be cast to integers.
// Component event
fn on_key(
&mut self,
key: KeyEvent,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
children.elements()
.by_tag("position")
.each(|el, attrs| {
let boolean = attrs.get_as::<bool>("is_true").unwrap();
let string = attrs.get("background").unwrap().as_str().unwrap();
});
}
Querying components
There are currently four component queries:
by_name
by_id
by_attribute
at_position
// Component event
fn on_key(
&mut self,
key: KeyEvent,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
children
.component()
.by_name("my_component")
.each(|id, comp, attrs| {
let boolean = attrs.get_as::<bool>("is_true").unwrap();
let string = attrs.get("background").unwrap().as_str().unwrap();
});
}
Third party components
Components can be made reusable by implementing placeholders and associated functions.
A component can have named placeholders for external children:
// The template for @mycomponent
border
$my_children
When using a component with named children use the placeholder name to inject the elements:
for x in values
@mycomponent
$my_children
text "hello world"
Note that if no placeholder name is given a default of $children
is assumed:
for x in values
@mycomponent
text "hello world"
// The template for @mycomponent
border
$children
Events
Publishing events from a component is done with context.publish(ident, T)
where ident
is a string slice with the name of the event and T
is the data
that should be sent.
fn on_key(
&mut self,
key: KeyEvent,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
// Third party component emitting an event
context.publish("my_event", String::from("hello parents"));
}
Given the following template example:
vstack
@thirdparty (my_event->event_a)
@thirdparty (my_event->event_b)
And in the component using the third party component:
fn on_event(
&mut self,
event: &mut UserEvent<'_>,
state: &mut Self::State,
mut children: Children<'_, '_>,
mut context: Context<'_, '_, Self::State>,
) {
match event.name() {
"event_a" => {
let data: &String = event.data::<String>();
// all parents can get this event
}
"event_b" => {
let data: &String = event.data::<String>();
// no parent will get this event
event.stop_propagation();
}
_ => () // ignored
}
}
In the above example the component is emitting an event named "my_event". This is mapped to either "event_a" or "event_b" depending on which component is publishing the event.
To see the emitting components event name (in this case "my_event") use
event.internal_ident
.
The UserEvent
type contains information on which component sent it.
The name of the component can be found with event.sender
.
The id of the component is the event.sender_id
.
This is useful when using children.components().by_id(event.sender_id, |comp, attribs| { })
.
Runtime
use anathema::runtime::Runtime;
use anathema::backend::tui::TuiBackend;
let doc = Document::new("text 'hello world'");
let mut backend = TuiBackend::builder()
.enable_alt_screen()
.enable_raw_mode()
.enable_mouse()
.hide_cursor()
.finish()
.unwrap();
// finalize the backend (enable alt mode, ...)
backend.finalize();
let mut builder = Runtime::builder(doc, &backend);
runtime.fps(30); // default
runtime.finish(&mut backend, |rt, backend| rt.run(backend)).unwrap();
Registering components
Before components can be used in a template they have to be registered with the runtime.
let builder = Runtime::builder(document, &backend);
let component_id = builder.component(
"my_comp", // <- tag
"template.aml", // <- template
MyComponent, // <- component instance
ComponentState, // <- state
);
You can use default
if your component and its state implements Default.
builder.default::<C>(comp, template)
is equivalent to builder.component(comp, template, C::default(), C::State::default())
.
File path vs embedded template
If the template is passed as a string it is assumed to be a path and hot reloading will be enabled.
To to pass a template (rather than a path to a template) call to_template
on
the template string:
static TEMPLATE: &str = include_str!("template.aml");
let component_id = builder.component(
"my_comp",
TEMPLATE.to_template(),
MyComponent,
ComponentState,
);
Hot reload
Hot reloading only works with templates that are file paths.
Important note about state values
Hot reloading will not remember prototype components, so any component generated with a for-loop for instance, will not have its state retained.Hot reloading won't work on components that store internal state either, such as
the Canvas
widget.
To disable hot reloading:
runtime.hot_reload(false);
To enable hot reloading:
runtime.hot_reload(true);
Multiple instances of a component
To repeatedly use a component in a template, e.g:
vstack
@my_comp
@my_comp
@my_comp
The component has to be registered as a prototype using prototype
(instead of component
):
#![allow(unused)] fn main() { builder.prototype( "comp", "text 'this is a template'", || MyComponent::new(), || MyState::new() ); }
The main difference between registering a singular component vs a prototype is the closure creating an instance of the component and the state, rather than passing the actual component instance and state into the function.
Also note that prototypes does not have a component id and can not have messages emitted to them. However, messages can be sent using component queries.
If a component and state is empty (zero-sized) and does not have any special functionality, it can be registered with the template
function: builder.template("name", "path/to/template.aml");
.
This is equivalent to builder.prototype(comp, template, || (), || ())
Global Events
To register a global event handler on a runtime builder use the with_global_event_handler
function.
#![allow(unused)] fn main() { Runtime::builder(doc, &backend) .with_global_event_handler(|event, tabindex, components| { Some(event) }); }
The arguments are of types Event
, &mut TabIndex
and &mut DeferredComponents
.
Return the event that is supposed to propagate to the component that currently has focus.
When returning None
, no event will be triggered.
By default event.is_ctrl_c()
will return Some(Event::Stop)
.
If a custom event handler is used this has to be added manually if the behaviour
is desired.
To move focus forwards and backwards between components use tabindex.next()
and tabindex.prev()
respectively.
Configuring the runtime
fps
Default: 30
The number of frames to (try to) render per second
Backend
Configuring the backend
The following settings are available:
enable_raw_mode
Raw mode prevents inputs from being forwarded to the screen (the event system will pick them up but the terminal will not try to print them).
enable_mouse
Enable mouse support in the terminal (if it's supported).
enable_alt_screen
Creates an alternate screen for rendering. Restores the previous content of the terminal to its previous state when the runtime ends.
hide_cursor
Hides the cursor.
clear
Clears the screen. Useful when not enabling an alt screen (which will be cleared by default).
full_screen
function
As a convenience it's possible to call TuiBackend::full_screen()
.
This is the same as writing:
let mut backend = Self::builder()
.enable_alt_screen()
.enable_raw_mode()
.hide_cursor()
.finish()
.unwrap();
backend.finalize();
Templates
Anathema has a template language to describe user interfaces. This makes it possible to ship the template(s) together with the compiled binary so the application can be customised without having to be recompiled.
Do note that AML (Anathema Markup Language) is not a programming language, therefore control flow is limited.
The element syntax looks as follows: <element> <attributes> <value>
Element name
| Start of (optional) attributes
| | Attribute name
| | | Attribute value
| | | | Attribute separator End of attributes
| | | | | | Values / Text
| | | | | | |
| | | | | | |------+-+-+
| | | | | | | | | |
element [optional: "attribute", separated: "by a comma"] "text" 1 2 ident
Elements don't receive events (only Component
s do) and should be thought of as
something that is only drawn to screen.
There is for instance no "input" element. To represent an input the component would handle the key press events and simply pass the collected values as a string via state.
Comments
Use //
to add comments to a template:
text "I will render"
// text "I will not"
Attributes
Element attributes are optional.
Attributes consists of a collection of ident
, :
, and value
.
An ident
has to start with a letter (a-zA-Z
) and can contain one or more _
.
Example:
element [attribute_a: "some string", attribute_b: false]
An attribute name is always an ident, however the value can be anything that
implements State
.
Default value types:
A literal value can be one of the following:
- string:
"Empty vessel, under the sun"
- integer:
123
- float:
1.23
- hex:
#fab
or#ffaabb
- boolean:
true
orfalse
- list:
[1, 2, 3]
- map:
{"key": "value"}
To access a state or attribute value, use an ident
(a string without spaces that is not
wrapped in quotes). Note that state values are under the state
variable and attribute values are under the attributes
variable.
text "string literal"
text "`ident` in the component state: " state.ident
text "`ident` in the component attributes: " attributes.ident
Elements and children
Some elements can have one or more children.
Example: a border element with a text element inside
border
text "look, a border"
Either
The ?
symbol can be used to have a fallback value.
See Either for specifics on how this works as the behaviour differs between static values (defined in templates) and dynamic values (such as attributes and states).
text state.maybe_value ? attributes.maybe_this ? "there was no value"
Constants / Globals
Constants are global and regardless of where in a template they are declared they will be hoisted to the top.
Example
The following example will print 1
text glob
let glob = 1
The following example will print 1
then 2
, as the inner
component will override the
outer components global.
// main.aml
let glob = 1
vstack
text glob
@inner
// inner.aml
let glob = 2
text glob
The following example will print 2
, as the inner
component will override the
outer components global.
// main.aml
let glob = glob
@inner
// inner.aml
let glob = 2
text glob
Loops
It's possible to loop over the elements of a collection.
The syntax for loops are for <element> in <collection>
.
Example: looping over values
for value in [1, 2, "hello", 3]
text value
Elements generated by the loop can be thought of as belonging to the parent element as the loop it self is not a element. See example below.
Example: mixing loops and elements
vstack
text "start"
for val in [1, 2, 3]
text "some value: " val "."
text "end"
The above example would render:
start
some value: 1.
some value: 2.
some value: 3.
end
For-loops also expose a loop
variable that is set to the current loop iteration.
Example: loop
variable
vstack
for val in ["a", "b", "c", "d"]
text "#" loop ": " val
The above example would render:
#0: a
#1: b
#2: c
#3: d
Note: For-loops do not work inside attributes, and can only produce elements.
If / Else
It's possible to use if
and else
to determine what elements to render.
Example:
if true
text "It's true!"
if value > 10
text "Larger than ten"
else if value > 5
text "Larger than five but less than ten"
else
text "It's a small value..."
Conditional theming
Changing colors and style depending on a condition can be achieved by declaring a constant. All constants are global.
let THEME = {
enabled: { bg: "grey" },
disabled: { bg: "reset" },
}
text [background: THEME["disabled"].bg] "Hello"
Since boolean values can translate to numbers it's also possible to use them as index lookups.
let THEME = [
// False
{ bg: "grey" },
// True
{ bg: "reset" },
]
text [background: THEME[state.some_bool].bg] "Hello, tea"
Operators
- Equality
==
- Greater than
>
- Greater than or equals
>=
- Less than
<
- Less than or equals
<=
- And
&&
- Or
||
- Addition
+
- Subtraction
-
- Multiplication
*
- Division
/
- Remainder (Modulo)
%
- Negation
!
- Either
?
(Uses the right value if the left value does not exist)
Note: just like for-loops it's not possible to use if / else with attributes.
To determine the value of an attribute depending on some condition this should be handled by the state.
Either
The Either operator: ?
can be used to provide fallback values.
Only dynamic values (attributes and state) are subject to truthiness
checks; Static values (defined using let
in a template) are never (even if they are false).
Does this exist?
and is it truthy?
/ \
/ \
yes no -------+
| |
V V
text state.value ? "default"
Example
This will always use the static value, and print false
:
text false ? "hello"
However
text state.maybe_false ? "hello"
Would print hello
if maybe_false
is any of the values in the fallback table:
Fallback table
These values causes fallback.
null
0
""
[]
{}
false
This means that any state or attribute value that resolves to a value listed above will use the fallback.
Switch / Case
It's possible to use switch
/ case
to determine what elements to render.
The default
branch has to be the last one, and is not required.
Example
switch value
case 1: text "one"
case 2: text "two"
default: text "default"
With
A with
makes it possible to scope an expression for nodes
Example
let THEME = [
{ fg: "red", bool: true },
{ fg: "green", bool: false },
{ fg: "blue", bool: true },
];
border
with theme as colors[state.some_count % 2 == 0]
// Refering to `theme` instead of repeating `colors[state.some_count % 2 == 0]`
text [foreground: theme.fg] "hello "
span [bold: theme.bold] "world"
Functions
AML have a few functions built-in and it's possible to add custom functions.
A function can either be called as a member function: "string".to_upper()
or a
free function: to_upper("string")
Do note that calling a function as a member function on a collection
to_upper(string)
Convert a string to upper case.
Example
text "It's teatime".to_upper()
Will output: "IT'S TEATIME".
to_lower(string)
Convert a string to lower case.
Example
text "It's teatime".to_lower()
Will output: "it's teatime".
to_str(arg)
Convert any value to a string representation.
Example
let greetings = {
"1": "Hello",
"2": "Hi",
}
text greetings[to_str(2)]
Will output: "Hi".
to_int(arg)
Try to convert any value to an integer.
Boolean true
will convert to 1
, and false
will convert to 0
.
Floating point values will be truncated.
to_int(1.99999)
will be converted to 1
.
Example
let greetings = [
"Hello",
"Hi",
]
text greetings[to_int(true)]
Will output: "Hi".
to_float(arg)
Try to convert any value to a floating point number.
Boolean true
will convert to 1.0
, and false
will convert to 0.0
.
Example
text to_float(123)
Will output: "123".
round(number, decimal_count=0)
Round a floating point value. Trying to round any other type will return null.
If no decimal_count
is provided 0
is the default.
Example
vstack
text round(1.1234, 2)
text round(1.1234)
Will output:
1.12
1
This function will pad with zeroes:
text round(1.12, 5)
Will output: 1.12000
contains(haystack, needle)
Returns true if the haystack contains the needle.
Example
vstack
text contains([1, 2, 3], 2)
text "hello world".contains("lo")
Will output:
true
true
Adding custom functions
It's possible to add custom functions to the runtime.
All function have the same signature in Rust:
fn contains<'a>(args: &[ValueKind<'a>]) -> ValueKind<'a> {
...
}
Example
A function that adds two values
use anathema::resolver::ValueKind;
fn add<'a>(args: &[ValueKind<'a>]) -> ValueKind<'a> {
if args.len() != 2 {
return ValueKind::Null;
}
let args = args[0].to_int().zip(args[1].to_int());
match args {
Some((lhs, rhs)) => ValueKind::Int(lhs + rhs),
None => ValueKind::Null
}
}
Adding the function to the runtime
Use the register_function
method on the runtime to add your custom function.
It is not possible to overwrite existing functions, and trying to do so will return an error.
Example
builder.register_function("add", add).unwrap();
In the template:
text add(1, 2)
Elements
Even though it's possible to create custom elements Anathema aims to provide the necessary building blocks to represent almost any layout without the need to do so.
Default attributes
Elements that draw them selves (e.g text
, span
and border
) support these default
attributes:
foreground
Foreground colour
Valid values:
- hex:
#ffaabb
- string: "green"
background
Background colour (see foreground
for valid values)
bold
Valid values:
true
or false
italic
Valid values:
true
or false
display
Changes the behaviour of the rendering and layout step.
show
is default and renders the element.
hide
will not paint the element, but it will be part of the layout,
exclude
excludes it from layout as well, thus the element won't take up any space and be hidden.
Valid Values:
show
, hide
or exclude
fill
Fill the unpainted space with a string.
Example:
border [width: 10, height: 5, fill: "+-"]
text "Hello"
┌────────┐
│Hello-+-│
│+-+-+-+-│
│+-+-+-+-│
└────────┘
Default widgets
The following is a list of available widgets and their template tags:
- Text (template tag:
text
) - Span (template tag:
span
) - Border (template tag:
border
) - Alignment (template tag:
align
) - VStack (template tag:
vstack
) - HStack (template tag:
hstack
) - ZStack (template tag:
zstack
) - Expand (template tag:
expand
) - Spacer (template tag:
spacer
) - Position (template tag:
position
) - Overflow (template tag:
overflow
) - Canvas (template tag:
canvas
) - Container (template tag:
container
) - Column (template tag:
column
) - Row (template tag:
row
) - Padding (template tag:
padding
)
Text (text
)
Displays text.
String literals can be wrapped in either "
or '
.
To add styles to text in the middle of a string use a span
element as the
text
element only accepts span
s as children. Any other element will be
ignored.
Example
text [foreground: "red"] "I'm a little sausage"
Attributes
wrap
Default is to wrap on word boundaries such as space and hyphen, and this method
of wrapping will be used if no wrap
attribute is given.
Valid values:
"break"
: the text will wrap once it can no longer fit
text_align
Note that text align will align the text within the element. The text element will size it self according to its constraint.
To right align text to the right side of the screen therefore requires the use of the alignment widget in combination with the text align attribute.
Default: left
Valid values:
"left"
"right"
"centre"
|"center"
Example of right aligned text
border [width: 5 + 2]
text [text_align: "right"] "hello you"
┌─────┐
│hello│
│ you│
└─────┘
Example of centre aligned text
border [width: 5 + 2]
text [text_align: "centre"] "hello you"
┌─────┐
│hello│
│ you │
└─────┘
Span (span
)
The span
element is used to style text on the same line.
Span can not be used on its own, it has to belong to a text
element and does
not accept children.
text "start"
span "-middle-"
span "end"
start-middle-end
Border (border
)
The border accepts only a single child.
Example
border
text "What a border!"
┌──────────────┐
│What a border!│
└──────────────┘
Attributes
width
The fixed width of the border
height
The fixed height of the border
min_width
The minimum width of the border
min_height
The minimum height of the border
sides
Valid values:
"top"
"right"
"bottom"
"left"
Sides specifies which sides of the border should be drawn.
This can be written as an individual side:
border [sides: "left"]
text "What a border!"
│What a border!
or
border [sides: "top"]
text "What a border!"
──────────────
What a border!
It can also be written as a list:
border [sides: ["left", "top"]]
text "What a border!"
┌──────────────
│What a border!
border_style
The default border style is "thin"
.
For a thicker border style the value "tick"
can be used:
border [border_style: "thick"]
text "What a border!"
╔══════════════╗
║What a border!║
╚══════════════╝
It's also possible to customise the border style by providing an eight character string as input, where each character represents a section of the border in the following order:
- top left
- top
- top right
- right
- bottom right
- bottom
- bottom left
- left
See the following example:
border [border_style: "12345678"]
text "What a border!"
1222222222222223
8What a border!4
7666666666666665
Alignment (align
)
Alignment will inflate the wrapping element to use all the constraints.
This means it's not recommended to put an alignment widget inside an
unconstrained widget such as a Overflow
.
The alignment accepts at most one child.
Example
border [width: 16, height: 5]
align [alignment: "centre"]
text "centre"
┌──────────────┐
│ │
│ centre │
│ │
└──────────────┘
Attributes
alignment
Valid values:
"top_left"
"top"
"top_right"
"right"
"bottom_right"
"bottom"
"bottom_left"
"left"
"centre"
or"center"
VStack (vstack
)
Vertically stack elements.
Accepts many children.
Example
vstack
text "one"
text "two"
one
two
Attributes
width
Fixed width
height
Fixed height
min_width
Minimum width
min_height
Minimum height
HStack (hstack
)
Horizontally stack elements.
Accepts many children.
Example
hstack
text "one"
text "two"
onetwo
Attributes
width
Fixed width
height
Fixed height
min_width
Minimum width
min_height
Minimum height
ZStack (zstack
)
Stack elements on top of each other.
Accepts many children.
Example
zstack
text "333"
text "22"
text "1"
123
Attributes
width
Fixed width
height
Fixed height
min_width
Minimum width
min_height
Minimum height
Row (row
)
Horizontal lay out elements where all the elements are centred vertically.
Row accepts multiple children.
Example
row
text "a"
border
text "b"
text "c"
┌─┐
a│b│c
└─┘
Column (column
)
Vertical layout of elements where all the elements are centred horizontally.
Column accepts multiple children.
Example
column
text "a"
border
text "b"
text "c"
a
┌─┐
│b│
└─┘
c
Expand (expand
)
Expand the element to fill the remaining space.
Accepts one child.
The layout process works as follows:
First all elements that are not expand
or spacer
will be laid out.
The remaining space will be distributed between expand
then spacer
in that
order, meaning if one expand
exists followed by a spacer
the expand
will
consume all remaining space, leaving the spacer
zero sized.
The size is distributed evenly between all expand
s.
To alter the distribution factor set the factor
attribute.
Example
border [width: 10, height: 11]
vstack
expand
border
expand
text "top"
expand
border
expand
text "bottom"
text "footer"
┌────────┐
│┌──────┐│
││top ││
││ ││
│└──────┘│
│┌──────┐│
││bottom││
││ ││
│└──────┘│
│footer │
└────────┘
Attributes
factor
The factor decides the amount of space to distribute between the expand
s.
Given a height of 3
and two expand
widgets, the height would be divided by
two.
If one of the expand
widgets had a factor
of two, then it would receive 2
of the total height, and the remaining widget would receive 1
.
axis
Expand along an axis.
Valid values:
"horz"
|"horizontal"
"vert"
|"vertical"
Position (position
)
Absolute or relative position of the child.
Accepts at most one child.
Attributes
placement
Either relative
(default)
or absolute
left
Position the left side of the element with an offset of the given value.
right
Position the right side of the element with an offset of the given value.
border [width: 10, height: 5]
position [top: 0, right: 0, placement: "relative"]
text "Hi"
┌────────┐
│ Hi│
│ │
│ │
└────────┘
top
Position the top side of the element with an offset of the given value.
bottom
Position the bottom side of the element with an offset of the given value.
Spacer (spacer
)
A spacer should only be used inside an hstack
or a vstack
.
The spacer
element will push the size value along a given axis to consume the
remaining available space.
A vertical spacer has no horizontal size, and a horizontal spacer has no vertical size.
The spacer
accepts no children and has no visible rendering.
Example
border
hstack
text "Hi"
spacer
┌─────────────────────────┐
│Hi │
└─────────────────────────┘
without the spacer:
border
hstack
text "Hi"
┌──┐
│Hi│
└──┘
Overflow (overflow
)
The Overflow
allows the elements to overflow along a given axis.
It's important to note that the overflow
is an unbounded element.
This means that elements can be laid out infinitely along a given axis.
Providing the overflow
with a very large collection would produce a very large element tree.
An Overflow
offset has to be changed via events rather than template values (if
the offset came from a State
it wouldn't be possible to know when the offset
was exceeded should it change as a result of a key press for example. Even though the
Offset
could clamp the offset, the offset it self would not be clamped).
Example
border [height: 4, width: 10]
overflow
text "1"
text "2"
text "3"
text "4"
┌────────┐
│1 │
│2 │
└────────┘
struct MyComponent {
// ...
fn on_key(
&mut self,
key: KeyEvent,
state: &mut Self::State,
mut elements: Elements<'_, '_>,
mut context: Context<'_, Self::State>,
) {
if let Some(c) = key.get_char() {
elements.by_tag("overflow").first(|el, _| {
let overflow = el.to::<Overflow>();
match c {
'k' => overflow.scroll_up(),
'j' => overflow.scroll_down(),
_ => {}
}
});
}
}
}
Attributes
direction
Specifies the direction to lay out the elements.
Default value: "forwards"
Valid values:
"back"
or"backwards"
or"backward"
"fwd"
or"forwards"
or"forward"
border [height: 5, width: 10]
overflow [direction: "backward"]
text "1"
text "2"
┌────────┐
│ │
│2 │
│1 │
└────────┘
axis
Specify along which axis to layout the children.
Valid values:
"horz"
|"horizontal"
"vert"
|"vertical"
clamp
Clamp the offset, preventing the content to scroll out of view
Default value: true
unconstrained
If this is set to true, both axis are unconstrained. If this is false, only the given axis is unconstrained.
Default value: false
false
Methods
scroll_up()
Scroll the Overflow
forward.
This is analogous to Overflow::scroll(Direction::Forward, 1)
.
scroll_up_by(n: u32)
Scroll the Overflow
forward n
number of lines.
This is analogous to Overflow::scroll(Direction::Forward, n)
.
scroll_down()
Scroll the overflow
backward.
This is analogous to Overflow::scroll(Direction::Backward, 1)
.
scroll_down_by(n: u32)
Scroll the overflow
backward n
number of lines.
This is analogous to Overflow::scroll(Direction::Backward, n)
.
scroll_right()
Scroll the overflow
forward.
This is analogous to Overflow::scroll(Direction::Forward, 1)
.
scroll_right_by(n: u32)
Scroll the overflow
forward n
number of lines.
This is analogous to Overflow::scroll(Direction::Forward, n)
.
scroll_left()
Scroll the overflow
backward.
This is analogous to Overflow::scroll(Direction::Backward, 1)
.
scroll_left_by(n: u32)
Scroll the overflow
backward n
number of lines.
This is analogous to Overflow::scroll(Direction::Backward, n)
.
scroll_to(pos: Pos)
Scroll the overflow
to a given position.
Padding (padding
)
Add padding around an element
Padding only accepts a single child.
Specifying only the padding
attribute will apply the value to all sides.
This can be overridden by specifying a side, such as bottom
, right
etc.
border
padding [padding: 1]
text "What a border!"
┌────────────────┐
│ │
│ What a border! │
│ │
└────────────────┘
Attributes
padding
Value applied to all sides of the element.
This value will be overridden by any specifics, such as left
or top
etc.
top
Top padding
right
Right side padding
bottom
Bottom padding
left
Left side padding
Canvas (canvas
)
A canvas is an element that can be drawn on.
The canvas accepts no children.
The canvas should be manipulated in Rust code:
The element will consume all available space and expand to fit inside the parent.
Hot reloading will clear the canvas as the cells are not stored as state.
Since components (but not prototype components) are restored upon hot reloading it
is possible to save the buffer from the canvas to the component using
canvas.take_buffer()
using unmount
on the component, and to restore it using
mount
.
restore, take buffer
Example
#![allow(unused)] fn main() { use anathema::backend::tui::{Color, Style}; fn on_key( &mut self, key: KeyEvent, state: &mut Self::State, elements: Elements<'_, '_>, ) { let mut style = Style::reset(); style.fg = Some(Color::Red); elements.by_tag("canvas").first(|el, _| { let canvas = el.to::<Canvas>(); canvas.put('a', style, (0, 0)); canvas.put('b', style, (1, 1)); canvas.put('c', style, (2, 2)); }); } }
border [width: 16, height: 5]
canvas
┌──────────────┐
│ a │
│ b │
│ c │
└──────────────┘
Attributes
width
height
Methods
erase(pos: impl Into<LocalPos>)
Erase a character at a given position in local canvas space
get(pos: impl Into<LocalPos>) -> Option<(&mut char, &mut CanvasAttribs)>
Get a mutable reference to the character and attributes at a given position.
put(c: char, style: Style, pos: LocalPos)
Put a character with a style at a given position.
translate(pos: Pos) -> LocalPos
Translate global coords to local (to the canvas) coords.
clear()
Clear the canvas
take_buffer()
Remove the buffer from the canvas
restore(buffer)
Sets a buffer to the canvas
Container (container
)
A container element.
The container accepts at most one child.
Example
border [width: 16, height: 5]
container [width: 1]
text "ab"
┌──────────────┐
│a │
│b │
│ │
└──────────────┘
Attributes
width
height
min_width
min_height
max_width
max_height
background
Additional components
A collection of common components are available.
To use these components add anathema-extras
as a dependency to Cargo.toml
[dependencies]
anathema-extras = { version = "0.1" }
Input
A single line input field.
#![allow(unused)] fn main() { builder .default::<Input>("input", Input::template()) .unwrap(); }
The input component can be registered as a prototype as well.
#![allow(unused)] fn main() { builder .prototype("input", Input::template(), Input::new, InputState::new) .unwrap(); }
Supported events:
The following events are provided:
on_enter
The enter key was pressed.
This event publishes anathema_extras::Text
, which implements Deref<str>
.
on_change
A change was made to the text (insert or remove).
This event publishes anathema_extras::InputChange
.
on_focus
The input component gained focus.
This event publishes ()
.
on_blur
The input component lost focus.
This event publishes ()
.
Attributes
clear_on_enter
If the desired outcome is to retain the text when the enter key is pressed set this attribute to false.
@input [clear_on_enter: false]
Example
vstack
@input (on_enter->update_label_a)
@input (on_enter->update_label_b)
Button
Recipes
Various recipes for solving certain common scenarios.
- Routing views
- Async
- Themes (not done yet)
Routing views
Routing can be done with a combination of state and switch
.
Using a switch / case
in the template we can router.
Component
We store the route name as a String
.
The component key change event can be used to select the route.
In this case a route is just a String
.
Pressing a
will set the route to "a"
, and pressing b
will set the route to "b"
.
Any other option will set it to "home"
.
#![allow(unused)] fn main() { use anathema::component::*; use anathema::prelude::*; #[derive(Debug, Default)] struct Router; impl Component for Router { type Message = (); type State = String; fn on_key(&mut self, key: KeyEvent, state: &mut Self::State, _: Children<'_, '_>, _: Context<'_, '_, Self::State>) { match key.get_char() { Some('a') => *state = "a".to_string(), Some('b') => *state = "b".to_string(), _ => *state = "home".to_string(), } } } }
Template
Since the route is the state (a string) we can use switch
on the state
and see if it's either "a"
or "b"
, otherwise fallback to @home
.
border
switch state
case "a": @a
case "b": @b
default: @home
Setting up the runtime
Finally register both "home", "a" and "b" as templates.
fn main() {
let doc = Document::new("@index");
let mut backend = TuiBackend::full_screen();
let mut builder = Runtime::builder(doc, &backend);
// Add routes
builder.component("index", "templates/index.aml", Router, String::new()).unwrap();
builder.template("a", "text 'hello from A'".to_template()).unwrap();
builder.template("b", "text 'hello from B'".to_template()).unwrap();
builder.template("home", "text 'default'".to_template()).unwrap();
let res = builder.finish(&mut backend, |runtime, backend| runtime.run(backend));
if let Err(e) = res {
eprintln!("{e}");
}
}
Async and Anathema
Since the UI Runtime
is blocking, spawn a secondary thread for our async
runtime to run on.
The Runtime
can not move between threads as the Value
s are not send.
That's why it's easier, if access to component ids and emitters are required, to
run the async code on a separate thread from the main thread.
Setting up for async
Create a function that starts an async runtime in a new thread.
Using a component id and an emitter it is possible to send messages into the Anathema runtime.
In this example we assume there is a component that accepts a usize
as its
Message
type.
pub fn run_async(emitter: Emitter, component: ComponentId<usize>) {
thread::spawn(move || {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async move {
// async code here
emitter.emit_async(component, 123).await;
});
});
}
Setup the UI
Setup the Anathema runtime on the main thread, generate component ids and emitters and pass them to the async runtime via the aforementioned function.
use anathema::component::*;
use anathema::prelude::*;
fn main() {
// -----------------------------------------------------------------------------
// - Setup UI -
// -----------------------------------------------------------------------------
let doc = Document::new("@index");
let mut backend = TuiBackend::full_screen();
let mut builder = Runtime::builder(doc, &backend);
let component_id = builder.default::<MyComponent>("index", "templates/index.aml").unwrap();
let emitter = builder.emitter();
// -----------------------------------------------------------------------------
// - Setup async -
// -----------------------------------------------------------------------------
run_async(emitter, component_id);
// -----------------------------------------------------------------------------
// - Start UI -
// -----------------------------------------------------------------------------
builder.finish(&mut backend, |runtime, backend| runtime.run(backend)).unwrap();
}
Themes
Add recipe here...