Module tui

Module tui 

Source
Expand description

An immediate mode UI framework for terminals.

§Why immediate mode?

This uses an “immediate mode” design, similar to ImGui. The reason for this is that I expect the UI needs for any terminal application to be fairly minimal, and for that purpose an immediate mode design is much simpler to use.

So what’s “immediate mode”? The primary alternative is called “retained mode”. The difference is that when you create a button in this framework in one frame, and you stop telling this framework in the next frame, the button will vanish. When you use a regular retained mode UI framework, you create the button once, set up callbacks for when it is clicked, and then stop worrying about it.

The downside of immediate mode is that your UI code may become cluttered. The upside however is that you cannot leak UI elements, you don’t need to worry about lifetimes nor callbacks, and that simple UIs are simple to write.

More importantly though, the primary reason for this is that the lack of callbacks means we can use this design across a plain C ABI, which we’ll need once plugins come into play. GTK’s g_signal_connect shows that the alternative can be rather cumbersome.

§Design overview

While this file is fairly lengthy, the overall algorithm is simple. On the first frame ever:

  • Prepare an empty arena_next.
  • Parse the incoming input::Input which should be a resize event.
  • Create a new Context instance and give it the caller.
  • Now the caller will draw their UI with the Context by calling the various Context UI methods, such as Context::block_begin() and Context::block_end(). These two are the basis which all other UI elements are built upon by the way. Each UI element that is created gets allocated onto arena_next and inserted into the UI tree. That tree works exactly like the DOM tree in HTML: Each node in the tree has a parent, children, and siblings. The tree layout at the end is then a direct mirror of the code “layout” that created it.
  • Once the caller is done and drops the Context, it’ll secretly call report_context_completion. This causes a number of things:
    • The DOM tree that was built is stored in prev_tree.
    • A hashmap of all nodes is built and stored in prev_node_map.
    • arena_next is swapped with arena_prev.
    • Each UI node is measured and laid out.
  • Now the caller is expected to repeat this process with a None input event until Tui::needs_settling() returns false. This is necessary, because when Context::button() returns true in one frame, it may change the state in the caller’s code and require another frame to be drawn.
  • Finally a call to Tui::render() will render the UI tree into the framebuffer and return VT output.

On every subsequent frame the process is similar, but one crucial element of any immediate mode UI framework is added: Now when the caller draws their UI, the various Context UI elements have access to prev_node_map and the previously built UI tree. This allows the UI framework to reuse the previously computed layout for hit tests, caching scroll offsets, and so on.

In the end it looks very similar:

  • Prepare an empty arena_next.
  • Parse the incoming input::Input
    • BUT now we can hit-test mouse clicks onto the previously built UI tree. This way we can delegate focus on left mouse clicks.
  • Create a new Context instance and give it the caller.
  • The caller draws their UI with the Context
    • BUT we can preserve the UI state across frames.
  • Continue rendering until Tui::needs_settling() returns false.
  • And the final call to Tui::render().

§Classnames and node IDs

So how do we find which node from the previous tree correlates to the current node? Each node needs to be constructed with a “classname”. The classname is hashed with the parent node ID as the seed. This derived hash is then used as the new child node ID. Under the assumption that the collision likelihood of the hash function is low, this serves as true IDs.

This has the nice added property that finding a node with the same ID guarantees that all of the parent nodes must have equivalent IDs as well. This turns “is the focus anywhere inside this subtree” into an O(1) check.

The reason “classnames” are used is because I was hoping to add theming in the future with a syntax similar to CSS (simplified, however).

§Example

use edit::helpers::Size;
use edit::input::Input;
use edit::tui::*;
use edit::{arena, arena_format};

struct State {
    counter: i32,
}

fn main() {
    arena::init(128 * 1024 * 1024).unwrap();

    // Create a `Tui` instance which holds state across frames.
    let mut tui = Tui::new().unwrap();
    let mut state = State { counter: 0 };
    let input = Input::Resize(Size { width: 80, height: 24 });

    // Pass the input to the TUI.
    {
        let mut ctx = tui.create_context(Some(input));
        draw(&mut ctx, &mut state);
    }

    // Continue until the layout has settled.
    while tui.needs_settling() {
        let mut ctx = tui.create_context(None);
        draw(&mut ctx, &mut state);
    }

    // Render the output.
    let scratch = arena::scratch_arena(None);
    let output = tui.render(&*scratch);
    println!("{}", output);
}

fn draw(ctx: &mut Context, state: &mut State) {
    ctx.table_begin("classname");
    {
        ctx.table_next_row();

        // Thanks to the lack of callbacks, we can use a primitive
        // if condition here, as well as in any potential C code.
        if ctx.button("button", "Click me!", ButtonStyle::default()) {
            state.counter += 1;
        }

        // Similarly, formatting and showing labels is straightforward.
        // It's impossible to forget updating the label this way.
        ctx.label("label", &arena_format!(ctx.arena(), "Counter: {}", state.counter));
    }
    ctx.table_end();
}

Structs§

ButtonStyle
Controls the style with which a button label renders
Context
Context is a temporary object that is created for each frame. Its primary purpose is to build a UI tree.
FloatSpec
Controls the position of the floater. See Context::attr_float.
ModifierTranslations
In order for the TUI to show the correct Ctrl/Alt/Shift translations, this struct lets you set them.
Tui
There’s two types of lifetimes the TUI code needs to manage:

Enums§

Anchor
Controls to which node the floater is anchored.
ListSelection
Informs you about the change that was made to the list selection.
Overflow
Controls the text overflow behavior of a label when the text doesn’t fit the container.
Position
Controls the position of a node relative to its parent.