Runtime Content¶
maya's DSL is compile-time by default, but real applications need dynamic data — values that change per frame, user input, API responses, computed strings. maya provides several mechanisms to inject runtime content into the compile-time tree.
text() — Runtime Text Node¶
text() creates a RuntimeTextNode<S> — a proper DSL node that accepts pipe
styling, just like compile-time t<"...">:
text("hello") // RuntimeTextNode<string_view>
text(std::to_string(count)) // RuntimeTextNode<string>
text(42) // RuntimeTextNode<int>
text(3.14) // RuntimeTextNode<double>
text("Error!") | Bold | Fg<255,80,80> // Styled — pipe works!
Signatures¶
template <typename S>
auto text(S&& content, Style s = {}) -> RuntimeTextNode<decay_t<S>>;
template <typename S>
auto text(S&& content, Style s, TextWrap w) -> RuntimeTextNode<decay_t<S>>;
Numeric Content¶
text() handles integers and floats directly — no std::to_string() needed:
Internally it uses std::to_chars() for zero-allocation formatting.
Text Wrapping¶
Control how text wraps when it exceeds the container width:
text("Long text...", Style{}, TextWrap::Wrap) // Word wrap (default)
text("Long text...", Style{}, TextWrap::TruncateEnd) // "Long te..."
text("Long text...", Style{}, TextWrap::TruncateMiddle) // "Lon...xt"
text("Long text...", Style{}, TextWrap::TruncateStart) // "...g text"
text("Long text...", Style{}, TextWrap::NoWrap) // Overflow
Piping Styles¶
RuntimeTextNode supports the same | pipe operator as TextNode:
text("OK") | Bold | Fg<80, 220, 120> // Compile-time style tags
text("Error") | Bold | Fg<255, 80, 80>
text(count) | Dim
The pipe merges the tag's CTStyle into the node's runtime Style:
template <typename S, CTStyle V>
auto operator|(RuntimeTextNode<S> n, StyTag<V>) {
n.style = n.style.merge(V.runtime());
return n;
}
Runtime Styles¶
For fully dynamic styling (colors computed at runtime), pass a Style object:
auto color = is_ok ? Color::rgb(80, 220, 120) : Color::rgb(255, 80, 80);
text("Status", Style{}.with_bold().with_fg(color))
dyn() — Runtime Escape Hatch¶
dyn() wraps a lambda that returns an Element. It's the general-purpose
escape from the compile-time tree into runtime logic:
When to Use dyn()¶
Use dyn() when you need:
- Conditional rendering — show different elements based on state
- Access to runtime variables — capture by reference from the enclosing scope
- Complex element construction — build trees with runtime BoxBuilder
- Theme-aware styling — access
ctx.themecolors
// Conditional rendering
dyn([&] {
if (loading) return text("Loading...") | Dim;
return text("Ready") | Bold | Fg<80, 220, 120>;
})
// Complex runtime construction (vstack/hstack available from maya::dsl)
dyn([&] {
return vstack()
.border(BorderStyle::Round)
.border_color(theme_border())
.border_text("CPU", BorderTextPos::Top)
.padding(0, 1, 0, 1)(
text(sparkline(cpu_hist, width), Style{}.with_fg(accent)),
text(fmt("%4.1f%%", cpu_avg), Style{}.with_bold().with_fg(gauge_color(cpu_avg)))
);
})
dyn() Returns a DynNode¶
The lambda is called during .build(). It must return something convertible to
Element — any node type works since they all have operator Element().
dyn() Inside Compile-Time Trees¶
The power is mixing dyn() with static DSL nodes:
auto ui = v(
t<"Dashboard"> | Bold | Fg<100, 180, 255>, // compile-time
sep, // compile-time
dyn([&] { return text(uptime_str); }), // runtime
dyn([&] { return text(cpu_text, cpu_style); }), // runtime
space, // compile-time
t<"[q] quit"> | Dim // compile-time
) | border_<Round> | pad<1>;
map() — Range Projection¶
map() projects a range of values into a list of elements:
std::vector<std::string> items = {"alpha", "beta", "gamma"};
map(items, [](const auto& s) { return text(s) | Dim; })
Signature¶
template <std::ranges::range R, typename Proj>
auto map(R&& range, Proj&& proj) -> MapNode<decay_t<R>, decay_t<Proj>>;
The Projection Function¶
The projection receives each element of the range and returns a Node or Element:
// Return a styled RuntimeTextNode (it's a Node)
map(names, [](const auto& name) { return text(name) | Bold; })
// Return a compound element via DSL
map(items, [](const auto& item) {
return h(
text(item.label) | Dim,
text(item.value) | Bold
);
})
// Return a raw Element
map(items, [](const auto& item) {
return hstack()(text(item.name), text(item.value));
})
map() With Indices¶
Use std::views for indexed iteration:
auto indexed = items | std::views::enumerate;
map(indexed, [](const auto& pair) {
auto [i, item] = pair;
return h(text(std::to_string(i) + ". "), text(item));
});
Inside a Layout¶
auto list = v(
t<"Users:"> | Bold,
map(users, [](const User& u) {
return h(
text(u.name) | Fg<100, 180, 255>,
space,
text(u.status) | Dim
);
})
) | border_<Round> | pad<1>;
Dynamic Child Lists — vector\<Element> in v()/h()¶
v() and h() accept std::vector<Element> (or any ElementRange) directly
as children. The vector's elements are flattened into the container — no wrapper
needed:
std::vector<Element> rows;
for (const auto& item : data) {
rows.push_back(text(item.name) | Bold);
}
// Vector flattened alongside static nodes:
auto ui = v(
t<"Items"> | Bold,
rows, // each Element becomes a child
t<"---"> | Dim
) | border_<Round>;
Implicit Conversion — All Nodes are Elements¶
Every DSL node type has operator Element(), so you can freely push DSL nodes
into a vector<Element>:
std::vector<Element> panels;
panels.push_back(t<"Static text">);
panels.push_back(text("Dynamic") | Bold);
panels.push_back(h(t<"A">, t<"B">) | border_<Single>);
panels.push_back(v(inner_rows) | pad<1>);
auto grid = h(panels);
When to Use What¶
| Pattern | When to use |
|---|---|
v(node1, node2) |
Fixed children known at compile time |
v(vector) |
Dynamic child list built at runtime |
v(node1, vector, node2) |
Mix of static header/footer with dynamic body |
map(range, proj) |
Transform a data collection into elements |
vstack().border(...)(...) |
Runtime border colors, titles, or advanced layout |
Mixing Static and Dynamic Patterns¶
All patterns in this section work in both run() and run<P>(). In simple
run(), these trees are returned from the render closure. In Program apps,
they are returned from view(const Model&). The mixing of static and dynamic
nodes is the same either way.
Pattern: Static Shell + Dynamic Body¶
auto ui = v(
t<"App Title"> | Bold | Fg<100, 180, 255>, // static
sep, // static
dyn([&] { return build_body(); }), // dynamic body
space, // static
t<"[q]quit [r]refresh"> | Dim // static
) | border_<Round> | pad<1>;
Pattern: Dynamic Data Table¶
auto table = v(
t<"PID USER CPU% MEM%"> | Bold,
sep,
map(processes, [](const Proc& p) {
return h(
text(std::to_string(p.pid)),
text(p.user),
text(std::to_string(p.cpu), Style{}.with_fg(gauge_color(p.cpu))),
text(std::to_string(p.mem))
);
})
) | border_<Round> | pad<0, 1>;
Pattern: Conditional Panels¶
auto ui = v(
t<"Status"> | Bold,
dyn([&] -> Element {
if (state == State::Loading) {
return v(
text(spinner(frame)) | Bold,
text("Please wait...") | Dim
); // implicit conversion via operator Element()
}
return v(
text("Complete") | Bold | Fg<80, 220, 120>,
text(result_message)
);
})
) | border_<Round> | pad<1>;
Pattern: Live Sparklines¶
dyn([&] {
std::string spark;
for (float v : history)
spark += block_char(v); // Unicode block elements
return text(spark, Style{}.with_fg(Color::rgb(80, 200, 255)));
})
Performance Notes¶
-
Compile-time nodes (
t<>,v(),h(), style pipes): zero runtime overhead. Template parameters encode everything;.build()is a direct construction. -
text(): One heap allocation for the string content (or zero if the source is a
string_view). Style is a small value type. -
dyn(): Lambda call + whatever the lambda does. Keep lambdas lightweight — avoid building huge trees inside a single
dyn(). -
map(): Allocates a
vector<Element>sized to the range. If the range issized_range, it reserves upfront. The projection is called once per element. -
Rebuild per frame: In continuous rendering (
fps > 0), the entire tree is rebuilt each frame — this applies to both simplerun()andrun<P>(). maya's diff engine ensures only changed cells are written to the terminal. Don't worry about rebuilding — it's designed for it.