Skip to main content

Module layer

Module layer 

Source
Expand description

Auto-promotion of UI subtrees to composited layers.

A node whose style makes it a layer root (opacity present on a node with children unless groupAlpha: false, a non-empty filter chain, or cache: "always") has its whole subtree captured into an offscreen atlas by a custom render pass and drawn back as one quad — so opacity fades the subtree as a group (web semantics) instead of folding into each node’s own colors (which shows overlapping children through each other).

Captures are cached: a clean layer (no content dirt this frame — see LayerContentDirt, resolve_layer_repaints) skips its capture pass entirely and composites last frame’s texture. The layer root’s own translation and group alpha are composite-time parameters, so translate/opacity animation of a promoted subtree costs no re-capture — promotion is the will-change pattern, an optimization rather than a tax.

Captures are also clip-independent: ancestor clipping (a scroll container or the viewport) never reaches the captured pixels — members are captured under interior clips only (the cascade restarted at the layer root, clip) and the ancestor clip clamps the composite quad at draw time instead (web semantics: overflow clips the filtered result). This is what makes the translation-invariance above actually hold under scroll: without it, a capture taken while clipped would be served stale after scrolling into view. An offscreen layer still captures when dirty (its quad just draws nothing) — the accepted cost is invisible re-captures for continuously-animated offscreen subtrees.

Promotion is a render-side concern: the subtree stays in the main UI tree (layout, picking, refs, animations untouched); promoting inserts the PromotedLayer marker on the existing entity and demoting removes it. The render half lives in render and works entirely through public bevy_ui_render seams — stock extraction/queue/prepare run untouched; a post-queue system moves the subtree’s already-queued phase items into a per-layer synthetic view rendered to the atlas. See render for details.

Extensibility contract: each future promotion rule (transform3d, backdrop) is one evaluator producing one PromotionReasons flag plus composite parameters the render pass forwards without interpreting. Promotion is !reasons.is_empty(); demotion is the flags emptying.

Modules§

clip
Interior clips — the main-world half of clip-independent layer capture.
pick3d
Transformed picking for transform3d layers.
render
Render-world half of layer compositing — a custom pass over stock bevy_ui_render, public API only (no fork). Mechanism per frame:
transform3d
The composite-time 3D transform on a promoted layer (transform3d style).

Structs§

LayerCaptureRect
The layer’s capture rectangle in physical pixels, in the same screen space as UiGlobalTransform — the node’s border box, inflated on every side by the node’s quantized filter outset (blur reads/writes beyond the border box; see crate::filters::quantize_outset). Recomputed every frame after layout by sync_layer_geometry; consumed by extraction. v1 clips the capture to this box (web opacity does not clip — known divergence, diag-warned).
LayerContentDirt
Frame-scoped content-dirt inbox for the layer capture cache. Every site that mutates a node’s rendered appearance pushes the entity here (see mark_content_dirty); resolve_layer_repaints drains it in PostUpdate, once LayerMembership is this frame’s — the taps themselves can’t resolve node → layer, membership isn’t valid yet.
LayerGroupAlpha
The composite-time alpha of a promoted subtree (applied once to the whole captured group). Separate from PromotedLayer so per-frame writes from the animation/transition paths don’t look like promotion-state changes.
LayerMembership
Which layer root each node under a promoted subtree belongs to (ancestor-or-self, nearest wins — so nested layers map their interior, including the inner root’s own paint, to the inner root). Rebuilt every frame after layout; extracted to the render world to route stolen phase items. A layer root’s composite quad is the one thing that routes by Self::enclosing instead (it draws inside the parent layer’s capture, or the screen when there is none).
LayerMeta
Per-layer observability row in LayersRegistry. Identity fields are written by evaluate_layer_promotions; geometry fields (capture_rect/depth) and the live group_alpha are refreshed by sync_layer_geometry; cache stats by resolve_layer_repaints.
LayerRepaintState
Per-layer repaint decisions, rebuilt from scratch every frame by resolve_layer_repaints — nothing persists to be cleared, so render extraction (which runs at the sync point after the whole main frame) always reads the same frame’s state.
LayersRegistry
Public registry of currently promoted layers — the observability surface auto-promotion comes with (promotion cost is invisible in JSX; this is where to see what promoted and why). Tests assert on it; a future devtools “layers” tab is a pure consumer.
PromotedLayer
Marker on a promoted layer root. Insert = promote, remove = demote — nothing else about the entity changes.
PromotionReasons
Why a node is promoted — one bit per rule, OR’d together. A node is promoted iff any bit is set; it demotes when the set empties.

Functions§

evaluate_layer_promotions
The single writer of promotion state. Drains the bridge’s dirty set (fed by the op-apply hooks), re-evaluates promotion_reasons per node, and flips the PromotedLayer/LayerGroupAlpha markers + the registry + bridge.promoted_layers. Ordered after apply_js_ops and before the interaction/transition/animation appliers so every later alpha writer this frame sees the final promotion state.
fold_member_geometry
Fold one member’s root-relative geometry into a layer’s content hash: translation relative to the layer root (so moving the whole layer cancels exactly and never re-captures), the affine’s linear part (member scale / rotation), and the laid-out size. Quantized (1/64 px positions, 1/1024 matrix entries) so float ulp noise — (a+d)-(b+d) isn’t bit-exact — can’t flap the hash. Visit order is encoded implicitly by the fold sequence, so reorders change the hash too.
mark_content_dirty
Tap helper for EntityCommands call sites (style apply, op arms): queue a push of this entity into LayerContentDirt. Queued (not immediate) because the op-apply sites only hold EntityCommands; the push lands at the next command flush, well before the PostUpdate resolver. A missing resource (external app without the plugin) degrades to a no-op.
promotion_reasons
The promotion rule set — pure, one flag per rule (see the module doc’s extensibility contract). PromotionReasons::FORCED is cache: "always" in the base style, gated only on element eligibility (no visual semantics of its own — no child or groupAlpha gate). PromotionReasons::FILTER is a non-empty filter chain in the base style OR any hover/press/focus variant, gated the same way (the effect is subtree-wide by definition, and a filtered leaf is valid). Like opacity the union is presence-based: interaction must never flip promotion, so a hover-only filter promotes eagerly — the layer (and its capture) exists before the first hover. PromotionReasons::OPACITY:
resolve_layer_repaints
Turn this frame’s dirt into per-layer repaint decisions. Runs in PostUpdate after sync_layer_geometry (membership + geometry hashes are this frame’s) and after bevy_ui’s text systems (Changed<TextLayoutInfo> must see this frame’s reshapes). Render extraction reads the result at the sync point; the state is rebuilt from scratch next frame, so nothing needs clearing across frames.
sync_layer_geometry
Recomputes each promoted layer’s capture rect (inflated by the node’s quantized filter outset — blur reads/writes beyond the border box, and the composite quad, texture allocation, and synthetic-view ortho all derive from this rect), the subtree membership map, and each layer’s content-geometry hash (see fold_member_geometry). Also warns (filterBleed) when a nested filtered layer’s inflated rect escapes its enclosing layer’s capture — v1 clips there, losing part of the bleed. Runs in PostUpdate after bevy_ui layout so ComputedNode / UiGlobalTransform are this frame’s values.
watch_layer_image_assets
<image> textures arrive asynchronously — no op, no bevy-react write site — so watch the asset events and dirty the owning layer of any node using a touched image. (One frame late for loads, which are async anyway; canvas uploads are also Modified here, double-covering their direct tap.)