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

#![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.

  1. state.field.set(new_value)
  2. *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 Strings 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.

See supported types

// 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 Components 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 or false
  • 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 (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 spans 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:

  1. top left
  2. top
  3. top right
  4. right
  5. bottom right
  6. bottom
  7. bottom left
  8. 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 expands.

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

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.

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

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