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::Inputwhich should be a resize event. - Create a new
Contextinstance and give it the caller. - Now the caller will draw their UI with the
Contextby calling the variousContextUI methods, such asContext::block_begin()andContext::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 ontoarena_nextand 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 callreport_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_nextis swapped witharena_prev.- Each UI node is measured and laid out.
- The DOM tree that was built is stored in
- Now the caller is expected to repeat this process with a
Noneinput event untilTui::needs_settling()returns false. This is necessary, because whenContext::button()returnstruein 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
Contextinstance 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§
- Button
Style - 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.
- Float
Spec - Controls the position of the floater. See
Context::attr_float. - Modifier
Translations - 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.
- List
Selection - 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.