Follow this guide to learn the C++ API for reading, writing, and reacting to resolved values.
This guide covers the day-to-day C++ API: the object model, finding and reading values, writing them, and reacting to changes. It sits between the JSON guide (the data language) and the class reference (every method); the integration guide covers loading patterns per platform.
Everything in a loaded tree is an LObject:
LObject parent/child ownership, names, lifetime
├── LStyle a node: attributes + child styles
└── LAttribute a named, typed value (with optional states and a link)
An LObject owns its children (std::unique_ptr), knows its parent() and object_name(), and can enumerate typed children with find_children<T>(). You rarely construct objects by hand; JSON loading builds the tree, and programmatic creation goes through the factory:
LStyle* button = lMake<LStyle>(parent, "Button");
lMake<LAttribute>(button, "Fill", "#25272b");
lMake<LAttribute>(button, "Corner Radii", 3.0);
lMake<T>(parent, args...) constructs the object and parents it in one step. Prefer it over new plus add_child.
Attribute values are an LVariant: one of double, bool, LString, or std::vector<LString> (or empty). Numbers in JSON always load as double.
Covered fully in the integration guide; the short version:
#include <Layers/lcontroller.h>
lController.include("/path/to/styles"); // a directory of JSON
lController.include("/styles/lib", "MyLib"); // namespaced set
lController.root_style()->resolve_links(); // once, after ALL includes
lController is the global controller (one tree per process). resolve_links() walks every L: link once and stores direct pointers; reads after that are pointer-chasing, not path lookups.
A link that finds no target is reported to stderr during resolution, and the full list is queryable afterwards:
for (const LUnresolvedLink& u : lController.unresolved_links())
std::cout << "unresolved: " << u.link_path.c_str()
<< " on " << u.attribute_path.c_str() << "\n";
An empty list means every link landed; unresolved attributes fall back to their own values at read time, so this is the way to catch silent defaults early.
LStyle* window = lController.find_style("App/Main Window"); // path from the root
LStyle* button = window->find_item("Titlebar/Exit Button"); // path from a style
LAttribute* fill = button->find_attribute("Fill"); // exact name
Paths split on / only; an attribute name like Margins.Left is one exact name, dot included. All three return null on a miss, so guard before dereferencing. To enumerate instead of look up:
for (auto& [name, attr] : button->attributes()) { /* ... */ }
for (auto& [name, child] : button->children()) { /* ... */ }
as<T>() returns the final, composed value, following the whole chain:
double radii = button->find_attribute("Corner Radii")->as<double>();
LString fill = button->find_attribute("Fill")->as<LString>();
LString down = button->find_attribute("Fill")->as<LString>({"Pressed"});
The optional argument is the active state list. Resolution order for every read:
as<T>() with the wrong T throws (std::bad_variant_access). When the type is not guaranteed, use the checked accessor, which returns a pointer or null:
if (const double* radii = attr->as_if<double>())
use(*radii);
attr->set_value(42.0);
attr->set_value("#e8eaed");
attr->set_value(LVariant(LString("#e8eaed")));
Two behaviors worth internalizing:
set_value forwards to the link target. Writing to Button/Fill while it links to Theme/Tertiary changes the theme value itself (and therefore everything else linked to it). Break the link first (break_link()) if you want the attribute to hold its own local value instead.Links can also be rewired at runtime: create_link(other_attr) points an attribute at a new source, break_link() detaches it and lets it keep its own value.
Every attribute is observable:
LConnectionID conn = fill->on_change([] {
// fill's stored value changed
});
// ...
fill->disconnect_change(conn);
on_change fires when the attribute's stored value changes, and it propagates through links: writing a link's target notifies every attribute linked to it, transitively. Subscribe wherever is most convenient. For the mount-node pattern (theming, string tables), that can be the mount's attributes or any linked consumer of them; the theming example subscribes on a widget's linked Fill and fires when the theme is re-valued.
To see what would be affected by a change, ask the engine:
for (LAttribute* dep : theme_fg->dependent_attributes(true)) // true = transitive
std::cout << dep->path().c_str() << "\n";
on_link_change (with disconnect_link_change) fires when an attribute's link wiring changes (created, broken, or re-pointed), as opposed to its value.
Always disconnect callbacks when the subscriber goes away; connection IDs are cheap to store. Objects also expose on_destroyed for lifetime tracking.
A style can be patched at runtime with another style, non-destructively:
button->apply_style(no_radius_style); // values now read from the patch first
button->clear_style(); // original values restored
Applied values sit at the top of the read chain (step 1 above). This is the machinery behind user styles (lController.toggle_custom_style()); on_style_applied lets a consumer react when it happens. The feature is experimental; see the JSON guide's User styles section.
Parents own children. When an LAttribute is destroyed, links from its dependents are broken automatically (they fall back to their own values), and its callbacks die with it. The rules of thumb:
lMake so ownership is established immediately.LConnectionIDs and disconnect them when the observer outlives interest, or may die before the observed attribute.find_* as borrowed: the tree owns them.For every class and method, see the generated class reference on this site (start with LAttribute, LStyle, and LController).