Style — API Reference
The style data model. Three structs (Style, ComputedStyle, and the
helpers), plus the supporting types used inside them.
- Header:
Style.hpp - Namespace:
ZenitUI
For the cascade and transitions, see
Style System. For the .zstyle
grammar, see ZStyle.
1. Opt<T> — optional property wrapper
template <typename T>
struct Opt {
T value{};
bool is_set{false};
Opt() = default;
Opt(const T& v);
Opt& operator=(const T& v);
void reset();
T get_or(const T& fallback) const;
};
Every style property is an Opt<T>. is_set distinguishes "not
declared" from "declared to the default value". This is what makes the
cascade work: an unset property doesn't override a lower-priority one.
Methods
| Method | Description |
|---|---|
Opt(const T&) |
Constructs a set Opt. |
operator=(const T&) |
Assigns and sets is_set = true. |
reset() |
Sets is_set = false. The value is unchanged but ignored. |
get_or(fallback) |
Returns value if is_set, else fallback. |
Note: Opt<T> has no operator bool. Use is_set directly.
2. BUBBLE_STYLE_PROPS — the property macro
All properties of Style and ComputedStyle are defined by one macro:
#define BUBBLE_STYLE_PROPS(X) \
X(Value, width, Value::autoSize()) \
X(Value, height, Value::autoSize()) \
X(Value, minWidth, Value::autoSize()) \
X(Value, minHeight, Value::autoSize()) \
X(Value, maxWidth, Value::autoSize()) \
X(Value, maxHeight, Value::autoSize()) \
X(float, grow, 0.0f) \
X(float, shrink, 1.0f) \
X(Value, gap, Value(0.0f)) \
X(Spacing, margin, Spacing()) \
X(Spacing, padding, Spacing()) \
X(Align, alignH, Align::Auto) \
X(Align, alignV, Align::Auto) \
X(Align, itemsH, Align::Start) \
X(Align, itemsV, Align::Start) \
X(Justify, justify, Justify::Start) \
X(Align, textAlign, Align::Auto) \
X(Color, background, Colors::Blank) \
X(Color, color, Colors::White) \
X(Color, tint, Colors::White) \
X(TextureRef, backgroundTexture, TextureRef{}) \
X(BoxShadow, boxShadow, BoxShadow{}) \
X(std::string, effect, "") \
X(std::vector<FilterRef>, filters, {}) \
X(Color, borderColor, Colors::Blank) \
X(Value, borderWidth, Value(0.0f)) \
X(Value, radius, Value(0.0f)) \
X(float, opacity, 1.0f) \
X(float, scale, 1.0f) \
X(float, rotation, 0.0f) \
X(Value, translateX, Value(0.0f)) \
X(Value, translateY, Value(0.0f)) \
X(Value, fontSize, Value(20.0f)) \
X(Value, letterSpacing, Value(2.0f)) \
X(float, transitionTime, 0.15f) \
X(TransitionFunction, ease, TransitionFunction::Linear) \
X(Overflow, overflowX, Overflow::Visible) \
X(Overflow, overflowY, Overflow::Visible) \
X(Position, position, Position::Static) \
X(ZIndex, zIndex, ZIndex::Auto()) \
X(Value, top, Value::autoSize()) \
X(Value, left, Value::autoSize()) \
X(Value, right, Value::autoSize()) \
X(Value, bottom, Value::autoSize()) \
X(std::string, font, "")
Every operation on Style and ComputedStyle (overlay, operator==,
lerp, stylesDiffer, forEachSetStyleProp, propTable, etc.) uses
this macro. Adding a property means editing this macro and the parser
(applyStyleAttr in StyleAttr.hpp).
Property list (quick reference)
| Group | Properties |
|---|---|
| Sizing | width, height, minWidth, minHeight, maxWidth, maxHeight |
| Flex | grow, shrink, gap |
| Spacing | margin, padding |
| Alignment | alignH, alignV, itemsH, itemsV, justify, textAlign |
| Visual | background, color, tint, backgroundTexture, boxShadow, borderColor, borderWidth, radius, opacity |
| Effects | effect, filters |
| Transform | scale, rotation, translateX, translateY |
| Typography | fontSize, letterSpacing, font |
| Transition | transitionTime, ease |
| Overflow / position | overflowX, overflowY, position, zIndex, top, left, right, bottom |
All property names use camelCase in C++ and kebab-case in
.zstyle (borderWidth ↔ border-width).
3. Style — the declared style
struct Style {
#define X(T, name, def) Opt<T> name;
BUBBLE_STYLE_PROPS(X)
#undef X
Opt<std::vector<TransitionSpec>> transitions;
Opt<std::vector<AnimationRef>> animations;
std::unordered_map<std::string, std::string> customProps;
std::unordered_map<std::string, std::string> unresolvedProps;
Style& overlay(const Style& o);
};
Every property is an Opt<T>. Plus:
transitions— the parsedtransition:shorthand.animations— the parsedanimation:shorthand.customProps— custom properties (--name) stored as raw strings.unresolvedProps— properties whose values containedvar()and couldn't be typed at parse time.
overlay(const Style& o)
Copies every set property from o into *this. Unset properties
in o don't change *this. This is the core cascade operation.
Style a;
a.background = Colors::Red;
a.color = Colors::White;
Style b;
b.background = Colors::Blue; // only background
a.overlay(b);
// a.background == Blue, a.color == White
For customProps and unresolvedProps, the maps are merged
(overlay wins on conflicts). For transitions and animations, the
whole vector is replaced if o's is set.
Special behavior for unresolvedProps: when an overlay brings an
unresolved prop, clearStyleProp is called on the same name in *this
to clear any typed value. This ensures a typed value from a lower-
priority source doesn't win over a var()-containing value from a
higher-priority one.
4. ComputedStyle — the resolved style
struct ComputedStyle {
#define X(T, name, def) T name = def;
BUBBLE_STYLE_PROPS(X)
#undef X
std::vector<TransitionSpec> transitions;
std::vector<AnimationRef> animations;
std::unordered_map<std::string, std::string> customProps;
static ComputedStyle from(const Style& s,
const ComputedStyle* parent = nullptr,
const Style* root = nullptr);
};
Same property list, but each field is a concrete value (no Opt). Every
field has a default, so a ComputedStyle is always fully populated.
from(s, parent, root)
Resolves a Style into a ComputedStyle. Priority:
- If
s.name.is_set, use it. - Else if the property is inherited and
parentexists, useparent->name. - Else if
roothas the property set, useroot->name.value. - Else use the default.
Plus the overflow computed-value rule:
if (c.overflowX == Overflow::Visible && c.overflowY != Overflow::Visible)
c.overflowX = Overflow::Auto;
else if (c.overflowY == Overflow::Visible && c.overflowX != Overflow::Visible)
c.overflowY = Overflow::Auto;
If one axis is Visible and the other isn't, the Visible axis is
promoted to Auto. This makes clipping behavior predictable.
customProps inherit from the parent, then merge with the node's own
(winning on conflict).
Inheritance
Only these properties are inherited:
| Property | Reason |
|---|---|
font |
Text typography cascades. |
fontSize |
Same. |
color |
Same. |
letterSpacing |
Same. |
textAlign |
Same. |
The check is isInheritedProp(name) in Style.hpp:
inline bool isInheritedProp(std::string_view name) {
return name == "font" || name == "fontSize" || name == "color"
|| name == "letterSpacing" || name == "textAlign";
}
Everything else uses the local value or the default — no inheritance.
operator== / operator!=
Compares every field. Used by Layout::recomputeDirty to detect style
changes.
inline bool operator==(const ComputedStyle& a, const ComputedStyle& b);
inline bool operator!=(const ComputedStyle& a, const ComputedStyle& b);
5. Spacing
struct Spacing {
Value top, right, bottom, left;
Spacing();
Spacing(float all);
Spacing(Value all);
Spacing(Value v, Value h);
Spacing(Value v, Value h, Value b);
Spacing(Value t, Value r, Value b, Value l);
bool operator==(const Spacing& o) const;
bool operator!=(const Spacing& o) const;
};
The four-side value used by padding and margin. Follows CSS syntax:
| Constructor | Meaning |
|---|---|
Spacing(v) |
All sides = v. |
Spacing(v, h) |
Vertical = v, horizontal = h. |
Spacing(v, h, b) |
Top = v, horizontal = h, bottom = b. |
Spacing(t, r, b, l) |
Top, right, bottom, left. |
Note: these are constructed programmatically. In .zstyle, spacing
is parsed by parseSpacingToken, which produces the same result from
1 / 2 / 3 / 4 whitespace-separated values.
6. TransitionSpec
struct TransitionSpec {
std::string prop; // property name, or "all"
float duration{0.15f}; // seconds
TransitionFunction ease{TransitionFunction::Linear};
float delay{0.0f}; // seconds
};
One entry in Style::transitions. A transition: shorthand produces
one TransitionSpec per comma-separated item.
See Animations for the CSS semantics.
7. AnimationRef
struct AnimationRef {
std::string name; // keyframe name
float duration{1.0f}; // seconds
TransitionFunction ease{TransitionFunction::Linear};
float delay{0.0f}; // seconds
int iterations{1}; // -1 for infinite
bool alternate{false};
bool fillForwards{false};
bool blocksInput{false};
};
One entry in Style::animations. An animation: shorthand produces
one AnimationRef per comma-separated item.
8. Position, ZIndex, Overflow
Position
enum class Position {
Static, // in flow, no z-index stacking
Relative, // in flow, can have z-index
Absolute // out of flow, positioned by top/left/right/bottom
};
Relative currently behaves like Static for layout purposes (only
affects stacking). See Layout §7.
ZIndex
struct ZIndex {
bool isAuto{true};
int value{0};
ZIndex() = default;
ZIndex(int v);
static ZIndex Auto();
bool operator==(const ZIndex& o) const;
bool operator!=(const ZIndex& o) const;
};
isAuto = true means "no explicit z-index" — the node participates in
its parent's normal flow ordering. isAuto = false with value set
means the node is a stacking context (if position != Static).
Overflow
enum class Overflow {
Visible, // no clipping, no scrolling
Hidden, // clipping, no scrolling
Scroll, // clipping, scrollbar always visible
Auto // clipping, scrollbar only when needed
};
See Layout §8.
9. TextureRef
struct TextureRef {
std::string name;
int left{0}, top{0}, right{0}, bottom{0};
bool isNineSlice() const;
bool operator==(const TextureRef& o) const;
bool operator!=(const TextureRef& o) const;
};
A background texture reference. name is the asset name. The four
integers are nine-slice insets; if all zero, isNineSlice() returns
false and the texture is stretched.
Set in .zstyle:
background-texture: bubble; /* stretch */
background-texture: npatches 16 16 16 16; /* nine-slice */
10. SimpleSelector
struct SimpleSelector {
enum class Kind { Tag, Class, Id };
std::string name;
Kind kind{Kind::Tag};
// Extra components of a compound selector (`Button.btn-primary`,
// `.card.elevated`, `Button#save`). Each pair is (kind, name).
// All components must match the same node.
std::vector<std::pair<Kind, std::string>> extras;
bool requireHover{false};
bool requirePressed{false};
bool requireFocus{false};
bool requireDisabled{false};
bool requireChecked{false};
};
One token of a selector chain. A ThemeRule holds a chain of these:
the rightmost entry matches the node itself, the earlier ones match
ancestors (descendant selectors).
A token can be a compound selector: a tag, any number of classes,
and an id, all on the same node. In that case kind/name hold the
first component and extras holds the rest. Examples:
| Source | kind / name |
extras |
|---|---|---|
Button |
Tag / "Button" |
(empty) |
.btn-primary |
Class / "btn-primary" |
(empty) |
Button.btn-primary |
Tag / "Button" |
[(Class, "btn-primary")] |
.card.elevated |
Class / "card" |
[(Class, "elevated")] |
Button.btn-primary#save |
Tag / "Button" |
[(Class, "btn-primary"), (Id, "save")] |
States (:hover, :checked, …) always apply to the whole compound,
regardless of where they appear in the source.
11. FilterRef
struct FilterRef {
std::string name;
std::vector<std::string> args;
bool operator==(const FilterRef& o) const;
bool operator!=(const FilterRef& o) const;
};
inline bool operator==(const std::vector<FilterRef>& a,
const std::vector<FilterRef>& b);
inline bool operator!=(const std::vector<FilterRef>& a,
const std::vector<FilterRef>& b);
One filter in a filter: list. args is the raw argument strings.
filter: blur(4px), drop-shadow(2px, 2px, #000);
produces two FilterRef: {name="blur", args={"4px"}} and
{name="drop-shadow", args={"2px", "2px", "#000"}}.
12. BoxShadow
struct BoxShadow {
Value x{0.0f}, y{0.0f}, blur{0.0f};
Color color{Colors::Blank};
bool enabled{false};
bool operator==(const BoxShadow& o) const;
bool operator!=(const BoxShadow& o) const;
};
A box-shadow definition. enabled is true if the property was declared.
box-shadow: 4px 4px 8px #00000080;
is {x=4px, y=4px, blur=8px, color={0,0,0,128}, enabled=true}.
13. Property table
using PropValue = std::variant<float, Value, Color, Spacing, Align, Justify,
TransitionFunction, Overflow, Position, ZIndex,
std::string, TextureRef, std::vector<FilterRef>,
BoxShadow>;
struct PropDesc {
const char* name;
PropValue (*get)(const ComputedStyle&);
void (*set)(ComputedStyle&, const PropValue&);
};
inline const std::vector<PropDesc>& propTable();
inline const PropDesc* findProp(std::string_view name);
A runtime-discoverable table of all properties. Used by lerpValue,
and available for reflection / tooling.
findProp("width") returns a PropDesc* or null. The get / set
function pointers let you read or write a property by name.
14. Lerp functions
Scalar and unit types
float lerpProp(float a, float b, float t);
Color lerpProp(const Color& a, const Color& b, float t);
Value lerpProp(const Value& a, const Value& b, float t);
Spacing lerpProp(const Spacing& a, const Spacing& b, float t);
Numeric interpolation with the easing applied upstream.
For Value, auto on either side causes a snap (returns b if
t > 0, else a). Mixed units are interpolated term-by-term,
including units present in only one side.
The result of a mixed-unit lerp can have up to 7 terms — one per
Unit. For example, interpolating Px(0) with Percent(100) gives a
2-term result; a more unusual lerp across all 7 units gives a
7-term result. Value::terms is a SmallVector<Term, 2>, which
allocates to the heap once the third term is added. In practice this
is rare: transitions between two unit-consistent values keep the same
term count, and the lerp itself doesn't grow beyond the union of the
input units.
Enum-like types (snap)
template <typename E>
E lerpProp(E a, E b, float t); // enums snap
ZIndex lerpProp(const ZIndex& a, const ZIndex& b, float t);
std::string lerpProp(const std::string& a, const std::string& b, float t);
TextureRef lerpProp(const TextureRef& a, const TextureRef& b, float t);
BoxShadow lerpProp(const BoxShadow& a, const BoxShadow& b, float t);
std::vector<FilterRef> lerpProp(const std::vector<FilterRef>& a,
const std::vector<FilterRef>& b,
float t);
All return t > 0 ? b : a (snap at the first non-zero t).
lerpValue
PropValue lerpValue(const PropValue& a, const PropValue& b, float t);
Variant-dispatched lerp. If the two variants hold the same type, lerps;
otherwise returns a.
lerpStyle, lerpStyleTimed, lerpStyleParts
ComputedStyle lerpStyle(const ComputedStyle& a,
const ComputedStyle& b, float t);
ComputedStyle lerpStyleTimed(const ComputedStyle& a,
const ComputedStyle& b,
float realElapsed);
Style lerpStyleParts(const Style& a, const Style& b, float t);
Three variants:
lerpStyle— global lerp with a pre-normalizedt. All properties use the samet.lerpStyleTimed— per-property timing. Each property uses its own duration/ease fromb.transitions, withb.transitionTimeas fallback.realElapsedis the elapsed time in seconds.lerpStyleParts— forStyle(notComputedStyle). UseslerpOpt, which snaps if either side is unset. Used byStyleResolver::partFor.
15. Style manipulation helpers
overlayComputed(dst, src)
inline void overlayComputed(ComputedStyle& dst, const Style& src);
Applies a Style on top of a ComputedStyle. Every set property in
src overwrites the corresponding field in dst.
Used by Anim::overlayCssComputed to apply CSS keyframe values to the
node's render style.
forEachSetStyleProp(s, fn)
template <typename F>
inline void forEachSetStyleProp(const Style& s, F&& fn);
Calls fn(name) for each property that's set. name is a const char*.
hasStyleProp(s, name)
inline bool hasStyleProp(const Style& s, std::string_view name);
True if the named property is set.
copyStyleProp(dst, src, name)
inline bool copyStyleProp(Style& dst, const Style& src, std::string_view name);
If src.name is set, copies it to dst.name and returns true. Else
returns false without touching dst.
clearStyleProp(s, name)
inline bool clearStyleProp(Style& s, std::string_view name);
Resets the named property (is_set = false). Returns true if the
property was recognized.
stylesDiffer(a, b)
inline bool stylesDiffer(const Style& a, const Style& b);
Compares two Styles, distinguishing "unset" from "set to default".
Returns true if any property is set in exactly one, or set to different
values in both. Also compares animations.
Used by StyleResolver::partFor to decide whether to start a part
transition.
16. Common patterns
Reading a resolved property
const ComputedStyle& style = node->getStyle();
float w = style.width.resolveH(parentW, parentH);
float rPx = style.radius.resolveH(rect.width, rect.height);
Overlaying a style
Style base;
base.background = Colors::Red;
Style override;
override.background = Colors::Blue;
override.radius = Px(8);
base.overlay(override);
// base.background == Blue, base.radius == 8px
Applying a keyframe overlay
ComputedStyle renderStyle = node->getStyle();
Anim::overlayCssComputed(renderStyle, node->anim_.css);
Building a custom Style in a widget
Style s;
s.background = Colors::DarkGray;
s.radius = Px(6);
s.padding = Spacing(Px(8));
node->setInlineBase(s);
Iterating set properties (debugging)
forEachSetStyleProp(style, [](const char* name) {
std::printf("%s\n", name);
});
17. Pitfalls
overlay doesn't clear unset properties.
Style a; a.background = Colors::Red;
Style b; // empty
a.overlay(b);
// a.background is still Red
To clear, use clearStyleProp(a, "background") or reset() on the
specific Opt.
ComputedStyle::from uses the parent's value for inherited props
even if the parent has no explicit value.
Because ComputedStyle is always fully populated, an inherited
property always comes from the parent (or root, or default) — never
from a "no value" state. This is correct for CSS but can surprise
you if you expected inheritance to be optional.
Value lerp with auto snaps.
Value a = Auto();
Value b = Px(100);
lerpProp(a, b, 0.5f) == b // snap
There's no interpolation between "no value" and a concrete value.
stylesDiffer and ComputedStyle::operator== are not the same.
stylesDiffer(a, b)compares twoStyles, considering "setness".operator==(ComputedStyle, ComputedStyle)compares two resolved styles, where every field has a value.
stylesDiffer is for part transitions (where unset means "fall back"),
operator== is for dirty tracking.
unresolvedProps shadows typed values.
When a Style has both background (typed) and
unresolvedProps["background"] (a var() expression), the overlay
rule clears the typed value. This is intentional: the var() is
higher priority. Don't set both manually unless you know what you're
doing.
TransitionSpec::prop is a raw string.
There's no enum. The comparison in lerpStyleTimed is a string compare
against the property name ("opacity", "all", etc.). Typos silently
fall through to the default timing.
propTable is built once, on first call.
The static table is initialized on first use. Thread-safety of the
initialization is guaranteed by C++11's magic statics, but the returned
reference is const, and the table is read-only after init.
BoxShadow snaps under interpolation.
lerpProp(BoxShadow, BoxShadow, t) returns b if t > 0, else a.
So a transition on box-shadow jumps to the target at the first
non-zero t. It's visually a hard cut, not an interpolation.
The same applies to TextureRef, FilterRef lists, ZIndex, enums,
and strings.
lerpProp(Value, Value, t) can produce multi-term results.
Interpolating Px(0) to Percent(100) at t = 0.5 produces
{0.5 Pixel, 50 Percent} — a two-term value. This resolves fine but
has more terms than either input. For repeatedly-recomputed values
(e.g. in a transition tick), the term count stays bounded because the
lerp is between fixed endpoints, not accumulated.
The maximum term count is 7 (one per Unit), and it's reached only
when the two inputs have disjoint unit sets. In practice this doesn't
happen: a transition on width typically moves between two Px or
two Percent values, producing a result with the same term count as
the inputs.
Value::terms is a SmallVector<Term, 2>, so a 1-2 term result is
allocation-free. A 3+ term result allocates once, on the first push
that overflows the inline storage. See Unit for the
container's semantics and limits.
customProps are not part of operator==.
ComputedStyle::operator== compares the macro-generated properties
only. Two styles that differ only in customProps compare equal. This
is a known limitation — custom properties are treated as metadata, not
as part of the style identity.
Style::overlay merges customProps but not other maps.
customProps and unresolvedProps are merged key-by-key. Everything
else (including transitions and animations) is replaced wholesale
when the source is set.
forEachSetStyleProp doesn't iterate transitions / animations.
The macro only covers the ~45 typed properties. The two vectors and the custom props maps are skipped.
propTable doesn't include transitions / animations / customProps.
Same reason. The variant doesn't have a case for them.
18. See also
- Style System — the cascade in detail.
- ZStyle — the
.zstylegrammar. - Unit — the
Valuetype used by many properties. - Theme —
ThemeRuleand specificity. - Animations — transitions and keyframes.