Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/engine/ui/document.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,12 @@ void endStyleMountBatch();
// presents to the panel would show unstyled content; the render path uses this to
// suppress that flush until styles land. Defined in style.cpp.
bool styleMountBatchActive();
// Queues the class styles of a newly mounted root. The mounted root is what
// `:root` matches and where a stylesheet's custom properties live, but nothing
// in building it (createView + an id attribute) queues its own recompute, so
// `:root { --x: ... }` never landed and every var(--x) below resolved to
// nothing. Deferred and coalesced under an open batch, where it becomes the
// one top-most root; immediate otherwise. Defined in style.cpp.
void noteMountedRootStyle(int root);

} // namespace gea::embedded::ui
11 changes: 9 additions & 2 deletions packages/engine/ui/layout.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1129,8 +1129,15 @@ class FlexLayoutPass {
}
pen += gap + marginL + cn.layout.width + marginR;

if (soleRun && cn.type == NodeType::Text && !hasExplicitWidth(cn)) {
// text-align needs a box as wide as the line box to align inside.
// text-align needs a box as wide as the line box to align inside — but
// ONLY the alignments that actually consume the extra width. `start`
// (alignedOffset's 0 case) draws at the box's left edge either way, so
// widening is invisible, while the widened box becomes the run's
// layout.width and feeds the parent's shrink-to-fit: a `<button>Hours`
// with no width stopped hugging its label and took the whole row (the
// weather forecast tabs, which then pushed `Days` off-screen).
const bool alignmentNeedsLineBox = cn.style.text_align == 1 || cn.style.text_align == 2;
if (soleRun && cn.type == NodeType::Text && alignmentNeedsLineBox && !hasExplicitWidth(cn)) {
const int full = contentW - marginL - marginR;
if (full > cn.layout.width) cn.layout.width = clampInt16(full);
}
Expand Down
84 changes: 77 additions & 7 deletions packages/engine/ui/style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2766,6 +2766,22 @@ CssRule makeStaticCompiledCssRule(StaticStyleSelectorKind selectorKind,
return rule;
}

// A direct property rule is applied through its compiled value, so its
// declaration only says which pass it belongs to. The font metrics are resolved
// first, by a pass that picks its rules by declaration (setsFontMetrics), and
// every later pass drops them as settled (setStyleValue's g_resolvedFontNode
// guard). Registered as Ignored, a static `font-weight: 600` therefore never
// landed anywhere: every node kept 400 on every target.
CssDeclarationId declarationForDirectProperty(Property property)
{
switch (property) {
case Property::FontId: return CssDeclarationId::FontFamily;
case Property::FontSize: return CssDeclarationId::FontSize;
case Property::FontWeight: return CssDeclarationId::FontWeight;
default: return CssDeclarationId::Ignored;
}
}

CssRule makeDirectPropertyCssRule(StaticStyleSelectorKind selectorKind,
const char *selector,
Property property,
Expand All @@ -2775,7 +2791,7 @@ CssRule makeDirectPropertyCssRule(StaticStyleSelectorKind selectorKind,
return makeStaticCompiledCssRule(selectorKind,
selector,
CssRuleProperty::Other,
CssDeclarationId::Ignored,
declarationForDirectProperty(property),
storeDirectPropertyCompiledValue(property, value),
media);
}
Expand All @@ -2785,10 +2801,18 @@ CssRule makeDirectPropertyGroupCssRule(StaticStyleSelectorKind selectorKind,
std::initializer_list<StaticStylePropertyValue> properties,
const char *media)
{
// A group that carries a font metric joins the font pass whole. Its other
// properties are plain values, so applying them early is harmless: the
// ordinary pass replays the cascade over them in order.
CssDeclarationId declaration = CssDeclarationId::Ignored;
for (const StaticStylePropertyValue &entry : properties) {
const CssDeclarationId candidate = declarationForDirectProperty(entry.property);
if (candidate != CssDeclarationId::Ignored) declaration = candidate;
}
return makeStaticCompiledCssRule(selectorKind,
selector,
CssRuleProperty::Other,
CssDeclarationId::Ignored,
declaration,
storeDirectPropertyGroupCompiledValue(properties),
media);
}
Expand Down Expand Up @@ -5765,10 +5789,16 @@ ResolvedCssLength resolveCompiledLengthForNodeDetailed(const CssLengthSpec &leng
if (tryResolveStaticLengthExpressionCached(handle, expression, nodeId, axis, depth, cached)) return cached;
// The ordinary dynamic cache keys containing-block dimensions. Font metrics
// are another dependency, so never reuse that cache for font-relative input.
// A var() is a third: its value is an ancestor's custom property, and a
// node styled before it is parented (templates build children first)
// resolves to nothing under the same handle, basis and axis -- an entry
// that would then answer every later resolution, layout's included, and
// `.screen { padding: var(--safe-x) }` stayed at 0 that way.
const bool fontRelative = g_fontSizeBasisNode >= 0 || lengthDependsOnFont(length, nodeId);
if (!fontRelative && tryResolveDynamicLengthExpressionCached(handle, nodeId, axis, cached)) return cached;
const bool cacheable = !fontRelative && !compiledLengthSpecHasCustomRuntimeInputs(length, depth);
if (cacheable && tryResolveDynamicLengthExpressionCached(handle, nodeId, axis, cached)) return cached;
ResolvedCssLength resolved = resolveCompiledLengthExpressionForNode(expression, nodeId, axis, depth);
if (!fontRelative) storeDynamicLengthExpressionCached(handle, nodeId, axis, resolved);
if (cacheable) storeDynamicLengthExpressionCached(handle, nodeId, axis, resolved);
return resolved;
}

Expand Down Expand Up @@ -8549,7 +8579,8 @@ bool propertyAffectsDescendantStyle(Property property)
property == Property::LineHeight || property == Property::LineHeightExpression || property == Property::LineHeightMultiplier ||
property == Property::TextAlign ||
property == Property::TextTransform ||
property == Property::WhiteSpace || property == Property::Visibility || property == Property::BorderWidth ||
property == Property::WhiteSpace || property == Property::Visibility || property == Property::PointerEvents ||
property == Property::BorderWidth ||
(property >= Property::BorderTopWidth && property <= Property::BorderLeftWidth);
}

Expand All @@ -8574,6 +8605,10 @@ void applyInheritedStyleDefaults(int node)
style.text_transform = parentStyle.text_transform;
style.white_space = parentStyle.white_space;
style.visibility = parentStyle.visibility;
// pointer-events inherits in CSS: `.overlay { pointer-events: none }` lets
// hits fall through its images too, which is how a decorative layer stays
// out of the way of what sits under it.
style.pointer_events = parentStyle.pointer_events;
}

bool applyNumberDeclarationWithSource(NodeHandle node,
Expand Down Expand Up @@ -14950,6 +14985,7 @@ struct ParentStyleSnapshot {
std::uint8_t text_transform;
std::uint8_t white_space;
std::uint8_t visibility;
std::int8_t pointer_events;
std::array<int, 4> border_widths;
};

Expand All @@ -14960,6 +14996,7 @@ ParentStyleSnapshot snapshotParentStyle(const ComputedStyle &s)
static_cast<std::int16_t>(s.line_height), s.line_height_multiplier,
static_cast<std::uint8_t>(s.text_align), static_cast<std::uint8_t>(s.text_transform),
static_cast<std::uint8_t>(s.white_space), static_cast<std::uint8_t>(s.visibility),
s.pointer_events,
{computedBorderWidth(s, 0), computedBorderWidth(s, 1), computedBorderWidth(s, 2), computedBorderWidth(s, 3)}};
}

Expand All @@ -14968,7 +15005,8 @@ bool parentStylesDiffer(const ParentStyleSnapshot &a, const ParentStyleSnapshot
return a.text_color != b.text_color || a.text_alpha != b.text_alpha || a.font_id != b.font_id || a.font_size != b.font_size ||
a.font_weight != b.font_weight || a.line_height != b.line_height || a.line_height_multiplier != b.line_height_multiplier ||
a.text_align != b.text_align || a.text_transform != b.text_transform ||
a.white_space != b.white_space || a.visibility != b.visibility || a.border_widths != b.border_widths;
a.white_space != b.white_space || a.visibility != b.visibility || a.pointer_events != b.pointer_events ||
a.border_widths != b.border_widths;
}

inline void listInsertUnique(CssAtomSmallList &v, CssAtomId s)
Expand Down Expand Up @@ -16217,6 +16255,17 @@ bool styleMountBatchActive()
return g_styleMountBatchActive;
}

void noteMountedRootStyle(int root)
{
auto &state = treeState();
if (root < 0 || root >= state.nodeCount) return;
// Under the mount batch this only records the root as pending; being the
// ancestor of everything else queued, it subsumes them and the batch's
// incremental walk styles the root first, so its custom properties are in
// place before any descendant's var() is resolved.
recomputeSubtreeClassStyles(root);
}

void endStyleMountBatch()
{
if (!g_styleMountBatchActive) return;
Expand Down Expand Up @@ -16262,7 +16311,28 @@ void endStyleMountBatch()
break;
}
}
if (!covered) topRoots.push_back(node);
if (covered) continue;
// The document root is created by Document, not by the app, so a mount
// never marks it pending — yet it is what :root matches, and :root is
// where custom properties are declared. Recomputing the app's subtree
// without it means every descendant var() resolves against a root that
// has no properties yet: the lookup misses, and a miss latches 0 into the
// computed style (a `padding: var(--safe-x)` silently became 0). Pull the
// root in, but only while it has never been styled — that is the first
// mount. Later incremental updates keep their narrow roots, so a theme
// switch does not turn into a whole-tree recompute.
int ultimate = node;
while (state.nodes[ultimate].parent >= 0) ultimate = state.nodes[ultimate].parent;
const bool rootNeedsFirstStyle =
ultimate != node && ultimate >= 0 && ultimate < kMaxNodes && !g_nodeRefs[ultimate].tracked;
if (rootNeedsFirstStyle) {
if (!g_pendingRecomputeMark.contains(ultimate)) {
g_pendingRecomputeMark.insert(ultimate);
topRoots.push_back(ultimate);
}
continue;
}
topRoots.push_back(node);
}

// Incremental recompute: a near-root class change (e.g. a theme switch) only
Expand Down
5 changes: 5 additions & 0 deletions packages/engine/ui/tree_render.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1626,9 +1626,14 @@ namespace gea::embedded::ui
return;
state.refreshSerial++;

const int previousRoot = state.mountedRoot;
state.mountedRoot = root;
state.mountedWidth = width;
state.mountedHeight = height;
// A root mounted for the first time has never had its class styles
// computed: `:root` rules (custom properties above all) apply to it and
// nowhere else. The styled re-mount at boot passes the same root again.
if (previousRoot != root) noteMountedRootStyle(root);

// While the initial style batch is open, per-node class styles haven't been
// applied yet, so laying out now produces UNSTYLED, content-sized boxes. That
Expand Down
Loading