UI applier for Goo, it walks a panel tree and applies a list of operations (create/update/remove/move/style/events/draw/etc) to construct and update Sandbox UI panels and virtual lists. It resolves style declarations into engine PanelStyle fields and wires event callbacks and virtual row mounting.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;
using Goo.Internal;
using EnginePanelTransform = Sandbox.UI.PanelTransform;
namespace Goo;
internal static class Applier
{
// Fast path takes the concrete List<Op> via CollectionsMarshal.AsSpan so the JIT
// emits an indexed-span loop, avoiding the boxed IEnumerator<Op> and per-element
// 64B copy that an interface-typed foreach over a value-type Op would incur.
public static void Apply(Panel root, IReadOnlyList<Op> ops)
{
if (ops is List<Op> list)
{
var span = CollectionsMarshal.AsSpan(list);
for (int i = 0; i < span.Length; i++)
ApplyTimed(root, in span[i]);
return;
}
foreach (var op in ops)
ApplyTimed(root, in op);
}
static void ApplyTimed(Panel root, in Op op)
{
if (!Perf.Enabled) { ApplyOne(root, in op); return; }
long t = System.Diagnostics.Stopwatch.GetTimestamp();
ApplyOne(root, in op);
Perf.RecordOp(op.Kind, System.Diagnostics.Stopwatch.GetTimestamp() - t);
}
static void ApplyOne(Panel root, in Op op)
{
var host = WalkPath(root, op.HostPath);
switch (op.Kind)
{
case OpKind.CreateText:
{
var label = host.AddChild(new StatefulLabel
{
IsRich = op.IsRich,
Text = op.StringPayload!,
});
host.SetChildIndex(label, op.Index);
break;
}
case OpKind.UpdateText:
{
if (host.GetChild(op.Index, false) is Label label)
{
label.IsRich = op.IsRich;
label.Text = op.StringPayload!;
}
break;
}
case OpKind.CreateContainer:
{
var panel = host.AddChild(new StatefulDrawPanel());
// Default FlexDirection; SetStyle overrides if the StyleList carries one.
panel.Style.FlexDirection = FlexDirection.Row;
host.SetChildIndex(panel, op.Index);
break;
}
case OpKind.CreateImage:
{
var img = host.AddChild(new StatefulImage());
if (op.StringPayload is not null)
img.SetTexture(op.StringPayload);
else if (op.Texture is not null)
img.Texture = op.Texture;
host.SetChildIndex(img, op.Index);
break;
}
case OpKind.UpdateImage:
{
if (host.GetChild(op.Index, false) is Sandbox.UI.Image img)
{
if (op.StringPayload is not null)
img.SetTexture(op.StringPayload);
else if (op.Texture is not null)
img.Texture = op.Texture;
}
break;
}
case OpKind.CreateScenePanel:
{
// ScenePath ctor path triggers engine's RenderScene.Load(SceneLoadOptions) and engine owns the scene.
// Scene-ref path uses the parameterless ctor and assigns RenderScene, which flips _ownsScene = false.
var scenePanel = op.StringPayload is not null
? new StatefulScenePanel(op.StringPayload)
: new StatefulScenePanel();
if (op.StringPayload is null && op.Scene is not null)
scenePanel.RenderScene = op.Scene;
scenePanel.RenderOnce = op.RenderOnce;
host.AddChild(scenePanel);
host.SetChildIndex(scenePanel, op.Index);
break;
}
case OpKind.UpdateScenePanel:
{
if (host.GetChild(op.Index, false) is Sandbox.UI.ScenePanel scenePanel)
{
// ScenePath hot-swap: re-load via SceneLoadOptions on the existing RenderScene.
// Scene-ref hot-swap: assign RenderScene (disposes prior owned scene if any).
if (op.StringPayload is not null)
{
var options = new SceneLoadOptions { ShowLoadingScreen = false };
if (options.SetScene(op.StringPayload))
scenePanel.RenderScene.Load(options);
}
else if (op.Scene is not null)
{
scenePanel.RenderScene = op.Scene;
}
scenePanel.RenderOnce = op.RenderOnce;
}
break;
}
case OpKind.CreateSvgPanel:
{
var svg = new StatefulSvgPanel();
if (op.StringPayload is not null)
svg.Src = op.StringPayload;
if (op.Color is not null)
svg.Color = op.Color;
host.AddChild(svg);
host.SetChildIndex(svg, op.Index);
break;
}
case OpKind.UpdateSvgPanel:
{
if (host.GetChild(op.Index, false) is Sandbox.UI.SvgPanel svg)
{
svg.Src = op.StringPayload;
svg.Color = op.Color;
}
break;
}
case OpKind.CreateSector:
{
var p = new StatefulShapePanel();
// Shapes with no explicit size collapse to 0 under the engine's
// default Yoga flex layout. Fill the parent unless the user
// declared a Width/Height (SetStyle path preserves this default).
p.Style.Width = Length.Percent(100);
p.Style.Height = Length.Percent(100);
host.AddChild(p);
p.RectHitTest = op.Shape.F > 0f;
p.ApplyShape(BlobKind.Sector, in op.Shape);
host.SetChildIndex(p, op.Index);
break;
}
case OpKind.UpdateSector:
{
if (host.GetChild(op.Index, false) is StatefulShapePanel p)
{
p.RectHitTest = op.Shape.F > 0f;
p.ApplyShape(BlobKind.Sector, in op.Shape);
}
break;
}
case OpKind.CreateArc:
{
var p = new StatefulShapePanel();
p.Style.Width = Length.Percent(100);
p.Style.Height = Length.Percent(100);
host.AddChild(p);
p.RectHitTest = op.Shape.F > 0f;
p.ApplyShape(BlobKind.Arc, in op.Shape);
host.SetChildIndex(p, op.Index);
break;
}
case OpKind.UpdateArc:
{
if (host.GetChild(op.Index, false) is StatefulShapePanel p)
{
p.RectHitTest = op.Shape.F > 0f;
p.ApplyShape(BlobKind.Arc, in op.Shape);
}
break;
}
case OpKind.CreatePolygon:
{
var p = new StatefulShapePanel();
p.Style.Width = Length.Percent(100);
p.Style.Height = Length.Percent(100);
host.AddChild(p);
p.RectHitTest = op.Shape.F > 0f;
if (op.Points is not null && op.Points.Length >= 3)
p.ApplyPolygon(op.Points);
host.SetChildIndex(p, op.Index);
break;
}
case OpKind.UpdatePolygon:
{
if (host.GetChild(op.Index, false) is StatefulShapePanel p)
{
p.RectHitTest = op.Shape.F > 0f;
if (op.Points is not null && op.Points.Length >= 3)
p.ApplyPolygon(op.Points);
else
p.Style.BackgroundImage = null;
}
break;
}
case OpKind.CreateWebPanel:
{
var wp = new StatefulWebPanel();
// WebPanel needs PointerEvents to receive mouse/wheel/key events; default to All so a bare one is interactive (preserved unless the user overrides).
wp.Style.PointerEvents = PointerEvents.All;
// Engine writes the webview texture to BackgroundImage without a size; force 100% so it fills the panel instead of centering with black bars.
wp.Style.BackgroundSizeX = Length.Percent(100);
wp.Style.BackgroundSizeY = Length.Percent(100);
if (op.StringPayload is not null)
wp.Url = op.StringPayload;
if (op.Paused)
wp.Surface.InBackgroundMode = true;
host.AddChild(wp);
host.SetChildIndex(wp, op.Index);
break;
}
case OpKind.UpdateWebPanel:
{
if (host.GetChild(op.Index, false) is Sandbox.UI.WebPanel wp)
{
wp.Url = op.StringPayload;
wp.Surface.InBackgroundMode = op.Paused;
}
break;
}
case OpKind.CreateTextEntry:
{
var te = new StatefulTextEntry();
// TextEntry needs PointerEvents to receive focus on click; default to All so a bare one is interactive (preserved unless the user overrides).
te.Style.PointerEvents = PointerEvents.All;
// Create-time uses the Text setter (safe: not focused yet, so the Value-setter HasFocus guard is irrelevant; the Update arm uses Value for controlled mode).
if (op.StringPayload is not null)
te.Text = op.StringPayload;
if (op.Placeholder is not null)
te.Placeholder = op.Placeholder;
if (op.MaxLength.HasValue)
te.MaxLength = op.MaxLength.Value;
te.Disabled = op.Disabled;
te.Numeric = op.Numeric;
te.MinLength = op.MinLength;
te.CharacterRegex = op.CharacterRegex;
te.StringRegex = op.StringRegex;
if (op.MinValue.HasValue)
te.MinValue = op.MinValue.Value;
if (op.MaxValue.HasValue)
te.MaxValue = op.MaxValue.Value;
if (op.NumberFormat is not null)
te.NumberFormat = op.NumberFormat;
te.Multiline = op.Multiline;
te._onChange = op.OnChange;
te._onSubmit = op.OnSubmit;
te._onFocus = op.OnFocus;
te._onBlur = op.OnBlur;
te._onCancel = op.OnCancel;
te._canEnterChar = op.CanEnterChar;
te._validate = op.Validate;
te._onValidationChanged = op.OnValidationChanged;
// OnChange/OnSubmit live outside BlobEvents, so a text-only entry (no mouse
// handlers) never gets a SetEvents op. Wire RequestRebuild here too so typing
// auto-rebuilds. Same policy the SetEvents arm reads from BuildContext.
var teCtx = BuildContext._current;
te.RequestRebuild = (teCtx != null && teCtx.AutoRebuildOnEvents) ? teCtx.RootRebuild : null;
// Compute initial validity now so the 'invalid' class / callback reflect the starting text.
te.RecomputeValidation();
host.AddChild(te);
host.SetChildIndex(te, op.Index);
break;
}
case OpKind.UpdateTextEntry:
{
if (host.GetChild(op.Index, false) is Sandbox.UI.TextEntry te)
{
var ste = te as Goo.Internal.StatefulTextEntry;
te.Placeholder = op.Placeholder;
te.MaxLength = op.MaxLength;
te.Disabled = op.Disabled;
te.Numeric = op.Numeric;
te.MinLength = op.MinLength;
te.CharacterRegex = op.CharacterRegex;
te.StringRegex = op.StringRegex;
te.MinValue = op.MinValue;
te.MaxValue = op.MaxValue;
te.NumberFormat = op.NumberFormat;
te.Multiline = op.Multiline;
if (ste != null)
{
ste._onChange = op.OnChange;
ste._onSubmit = op.OnSubmit;
ste._onFocus = op.OnFocus;
ste._onBlur = op.OnBlur;
ste._onCancel = op.OnCancel;
ste._canEnterChar = op.CanEnterChar;
ste._validate = op.Validate;
ste._onValidationChanged = op.OnValidationChanged;
// See CreateTextEntry: keep RequestRebuild wired in case this entry
// never receives a SetEvents op (no mouse handlers in BlobEvents).
var steCtx = BuildContext._current;
ste.RequestRebuild = (steCtx != null && steCtx.AutoRebuildOnEvents) ? steCtx.RootRebuild : null;
}
// Controlled mode re-emits Value every render (the engine's HasFocus guard
// no-ops mid-typing). Done AFTER all validation config is applied so the
// OnValueChanged it triggers validates against the current rules and predicate;
// uncontrolled mode never touches text (engine owns it after DefaultText).
if (op.IsControlled)
te.Value = op.StringPayload ?? "";
// Recompute for the config-only-change case where no controlled Value set
// fired OnValueChanged; flip-gated, so a redundant call is a harmless no-op.
ste?.RecomputeValidation();
}
break;
}
case OpKind.CreateEmbed:
{
var cfg = op.Embed!;
var hosted = cfg.Create();
// Embed-hosted panels are intrinsically interactive (WebPanel treatment): the marker
// class routes the style pass to All, and ??= keeps a bare embed clickable under
// Goo's None-stamped ancestors. An explicit declaration (blob or ctor) still wins.
hosted.AddClass("goo-embed");
hosted.Style.PointerEvents ??= PointerEvents.All;
host.AddChild(hosted);
host.SetChildIndex(hosted, op.Index);
cfg.Update?.Invoke(hosted);
break;
}
case OpKind.UpdateEmbed:
{
if (host.GetChild(op.Index, false) is { } hosted)
op.Embed!.Update?.Invoke(hosted);
break;
}
case OpKind.CreateVirtual:
{
var cfg = op.Virtual!;
// Both modes default to the fiber-backed Tier-2 panels (recycle + rediff);
// cfg.Panel is the escape hatch back to a stock engine panel.
var vp = cfg.Panel?.Invoke()
?? (cfg.ItemSize.y > 0
? new GooVirtualGrid { ItemSize = cfg.ItemSize }
: cfg.ItemHeight > 0
? (BaseVirtualPanel)new GooVirtualList { ItemHeight = cfg.ItemHeight }
: new GooVirtualList());
// Scroll viewport must be hittable or the wheel never arrives; SetStyle re-derives this.
vp.Style.PointerEvents = PointerEvents.All;
WireVirtual(vp, cfg);
host.AddChild(vp);
host.SetChildIndex(vp, op.Index);
if (cfg.Items is not null)
vp.SetItems(cfg.Items);
break;
}
case OpKind.UpdateVirtual:
{
if (host.GetChild(op.Index, false) is BaseVirtualPanel vp)
{
var cfg = op.Virtual!;
WireVirtual(vp, cfg);
if (cfg.Items is not null)
vp.SetItems(cfg.Items);
else
vp.Clear();
}
break;
}
case OpKind.RemoveAt:
{
host.GetChild(op.Index, false)?.Delete(true);
break;
}
case OpKind.MoveAt:
{
var child = host.GetChild(op.Index, false);
if (child is not null)
host.SetChildIndex(child, op.ToIndex);
break;
}
case OpKind.SetStyle:
{
ApplyAllDeclaredFields(host, host.Style, op.Style!);
break;
}
case OpKind.SetEvents:
{
// HostPath for SetEvents resolves directly to the target panel
// (not its parent), so `host` here is the target.
if (host is IStatefulEventHost evHost)
{
evHost.ApplyEvents(op.Events);
// Auto-rebuild-after-dispatch: wire the panel's rebuild request from the
// owning GooPanel, unless it opted out. BuildContext._current is the
// owning panel's context for the whole Apply pass.
var ctx = BuildContext._current;
evHost.RequestRebuild = (ctx != null && ctx.AutoRebuildOnEvents) ? ctx.RootRebuild : null;
// Apply-or-clear: a one-way All write would leave a panel that lost its
// handlers hittable forever, so the engine never hands the mouse back to the
// game (Panel.WantsMouseInput scans for All). Clear to None when handlers go.
if (!evHost.UserSetPointerEvents)
{
if (op.Events.AnyNonNull)
host.Style.PointerEvents = PointerEvents.All;
else if (host is not Sandbox.UI.WebPanel and not Sandbox.UI.TextEntry
&& (host is not IStatefulHost sh || !sh.HasActiveStateVariants))
host.Style.PointerEvents = PointerEvents.None;
}
}
break;
}
case OpKind.SetDraw:
{
if (host is StatefulDrawPanel p)
p._draw = op.DrawCallback;
break;
}
case OpKind.SetEffect:
{
if (host is StatefulDrawPanel p)
{
p._effect = op.Effect;
// Create the Material here on the main thread; Draw (render thread) cannot.
op.Effect?.Warm();
}
break;
}
case OpKind.SetLayoutTransition:
{
if (host is StatefulDrawPanel p)
p.SetLayoutTransition(op.LayoutTransition);
break;
}
}
}
// Rows are one-shot blob mounts; the rebuild hook is the OWNING panel's, captured at apply
// time, so a click in a row rebuilds the outer tree (which refreshes rows via Items).
static void WireVirtual(BaseVirtualPanel vp, VirtualConfig cfg)
{
vp.OnLastCell = cfg.OnLastRow;
var row = cfg.Row;
var ctx = BuildContext._current;
var rebuild = (ctx != null && ctx.AutoRebuildOnEvents) ? ctx.RootRebuild : null;
// Fiber-backed panels own their own row diffing; OnCreateCell never fires on them.
if (vp is GooVirtualPanel gvp)
{
gvp.Row = row;
gvp.RootRebuild = rebuild;
return;
}
if (row is null) { vp.OnCreateCell = null; return; }
vp.OnCreateCell = (panel, data) => RowMounter.MountInto(panel, row, data, rebuild);
}
// One-pass resolution scratch for ApplyAllDeclaredFields: forward fill keeps last-declared-wins,
// and every Try* probe below becomes an O(1) read instead of a reverse scan of the whole
// declared list per field (~130 probes per SetStyle). Apply runs on the main thread only.
const int StyleFieldCount = (int)StyleField.Disabled + 1;
static readonly StyleValue[] _resolvedValues = new StyleValue[StyleFieldCount];
static readonly bool[] _resolvedDeclared = new bool[StyleFieldCount];
static void ResolveDeclaredFields(StyleList style)
{
Array.Clear(_resolvedValues);
Array.Clear(_resolvedDeclared);
for (int i = 0; i < style.Count; i++)
{
ref var e = ref style[i];
_resolvedDeclared[(int)e.Field] = true;
_resolvedValues[(int)e.Field] = e.Value;
}
}
static bool TryResolved(StyleField f, out StyleValue value)
{
value = _resolvedValues[(int)f];
return _resolvedDeclared[(int)f];
}
static void ApplyAllDeclaredFields(Panel host, PanelStyle target, StyleList style)
{
ResolveDeclaredFields(style);
// Track whether the user declared PointerEvents in their style; the SetEvents handler
// uses this flag to decide whether to auto-gate to PointerEvents.All when any handler is set.
if (host is IStatefulEventHost evHost)
evHost.UserSetPointerEvents = _resolvedDeclared[(int)StyleField.PointerEvents];
Length? padding = TryLen(StyleField.Padding);
Length? paddingLeft = TryLen(StyleField.PaddingLeft) ?? padding;
Length? paddingTop = TryLen(StyleField.PaddingTop) ?? padding;
Length? paddingRight = TryLen(StyleField.PaddingRight) ?? padding;
Length? paddingBottom = TryLen(StyleField.PaddingBottom) ?? padding;
Length? margin = TryLen(StyleField.Margin);
Length? marginLeft = TryLen(StyleField.MarginLeft) ?? margin;
Length? marginTop = TryLen(StyleField.MarginTop) ?? margin;
Length? marginRight = TryLen(StyleField.MarginRight) ?? margin;
Length? marginBottom = TryLen(StyleField.MarginBottom) ?? margin;
Length? gap = TryLen(StyleField.Gap);
Length? rowGap = TryLen(StyleField.RowGap) ?? gap;
Length? columnGap = TryLen(StyleField.ColumnGap) ?? gap;
Length? borderRadius = TryLen(StyleField.BorderRadius);
Length? borderTopLeftRadius = TryLen(StyleField.BorderTopLeftRadius) ?? borderRadius;
Length? borderTopRightRadius = TryLen(StyleField.BorderTopRightRadius) ?? borderRadius;
Length? borderBottomRightRadius = TryLen(StyleField.BorderBottomRightRadius) ?? borderRadius;
Length? borderBottomLeftRadius = TryLen(StyleField.BorderBottomLeftRadius) ?? borderRadius;
// BorderColor fan-out: shorthand default applied to per-edge reads.
Color? borderColor = TryColor(StyleField.BorderColor);
Color? borderLeftColor = TryColor(StyleField.BorderLeftColor) ?? borderColor;
Color? borderTopColor = TryColor(StyleField.BorderTopColor) ?? borderColor;
Color? borderRightColor = TryColor(StyleField.BorderRightColor) ?? borderColor;
Color? borderBottomColor = TryColor(StyleField.BorderBottomColor) ?? borderColor;
// BorderWidth fan-out: shorthand default applied to per-edge reads.
Length? borderWidth = TryLen(StyleField.BorderWidth);
Length? borderLeftWidth = TryLen(StyleField.BorderLeftWidth) ?? borderWidth;
Length? borderTopWidth = TryLen(StyleField.BorderTopWidth) ?? borderWidth;
Length? borderRightWidth = TryLen(StyleField.BorderRightWidth) ?? borderWidth;
Length? borderBottomWidth = TryLen(StyleField.BorderBottomWidth) ?? borderWidth;
// FlexDirection is non-nullable on PanelStyle; default to Row when undeclared.
target.FlexDirection = TryFlex(StyleField.FlexDirection) ?? FlexDirection.Row;
target.JustifyContent = TryJustify(StyleField.JustifyContent);
target.AlignItems = TryAlign(StyleField.AlignItems);
target.Display = TryDisplay(StyleField.Display);
// Shape panels need to fill their parent by default so they don't collapse
// to 0 under flex layout; explicit user Width/Height still wins.
target.Width = TryLen(StyleField.Width)
?? (host is Goo.Internal.StatefulShapePanel ? Length.Percent(100) : (Length?)null);
target.Height = TryLen(StyleField.Height)
?? (host is Goo.Internal.StatefulShapePanel ? Length.Percent(100) : (Length?)null);
// Shapes draw via the IPanelDraw effect quad: BackgroundColor rides the ShapeColor uniform (set below), not the engine background path.
bool _isShape = host is Goo.Internal.StatefulShapePanel;
bool _isWebPanel = host is Goo.Internal.StatefulWebPanel;
bool _isTextEntry = host is Goo.Internal.StatefulTextEntry;
bool _isImage = host is Goo.Internal.StatefulImage;
bool _isVirtual = host is BaseVirtualPanel;
// State variants (hover/active/focus). Read up here because the inline base
// colors depend on them: the engine cascade merges inline styles after all
// stylesheet rules, so a channel with a variant must move its declared base
// into the variant sheet and leave the inline slot null, or the variant
// never visibly flips. The pointer-events policy below also needs them.
Color? hoverBg = TryColor(StyleField.HoverBackgroundColor);
Color? activeBg = TryColor(StyleField.ActiveBackgroundColor);
Color? focusBg = TryColor(StyleField.FocusBackgroundColor);
Color? hoverFg = TryColor(StyleField.HoverFontColor);
Color? activeFg = TryColor(StyleField.ActiveFontColor);
Color? focusFg = TryColor(StyleField.FocusFontColor);
bool hasBgVariant = hoverBg.HasValue || activeBg.HasValue || focusBg.HasValue;
bool hasFgVariant = hoverFg.HasValue || activeFg.HasValue || focusFg.HasValue;
bool hasAnyVariant = hasBgVariant || hasFgVariant;
StateController.SplitBaseColors(
TryColor(StyleField.BackgroundColor),
TryColor(StyleField.FontColor),
hasBgVariant, hasFgVariant, _isShape,
out var inlineBg, out var inlineFg,
out var sheetBg, out var sheetFg);
target.BackgroundColor = inlineBg;
target.Padding = padding;
target.PaddingLeft = paddingLeft; target.PaddingTop = paddingTop;
target.PaddingRight = paddingRight; target.PaddingBottom = paddingBottom;
target.Margin = margin;
target.MarginLeft = marginLeft; target.MarginTop = marginTop;
target.MarginRight = marginRight; target.MarginBottom = marginBottom;
target.RowGap = rowGap; target.ColumnGap = columnGap;
target.BorderTopLeftRadius = borderTopLeftRadius;
target.BorderTopRightRadius = borderTopRightRadius;
target.BorderBottomRightRadius = borderBottomRightRadius;
target.BorderBottomLeftRadius = borderBottomLeftRadius;
// Align properties.
target.AlignContent = TryAlign(StyleField.AlignContent);
target.AlignSelf = TryAlign(StyleField.AlignSelf);
// Aspect ratio.
target.AspectRatio = TrySingle(StyleField.AspectRatio);
// Backdrop filter properties.
target.BackdropFilterBlur = TryLen(StyleField.BackdropFilterBlur);
target.BackdropFilterBrightness = TryLen(StyleField.BackdropFilterBrightness);
target.BackdropFilterContrast = TryLen(StyleField.BackdropFilterContrast);
target.BackdropFilterHueRotate = TryLen(StyleField.BackdropFilterHueRotate);
target.BackdropFilterInvert = TryLen(StyleField.BackdropFilterInvert);
target.BackdropFilterSaturate = TryLen(StyleField.BackdropFilterSaturate);
target.BackdropFilterSepia = TryLen(StyleField.BackdropFilterSepia);
// Background properties.
target.BackgroundAngle = TryLen(StyleField.BackgroundAngle);
target.BackgroundBlendMode = TryString(StyleField.BackgroundBlendMode);
// Shapes draw via the effect quad, not the engine background path; leave their BackgroundImage/Size untouched. Other panels pass through (undeclared clears).
if (!_isShape)
target.BackgroundImage = TryTexture(StyleField.BackgroundImage);
target.BackgroundPlaybackPaused = TryBool(StyleField.BackgroundPlaybackPaused);
target.BackgroundPositionX = TryLen(StyleField.BackgroundPositionX);
target.BackgroundPositionY = TryLen(StyleField.BackgroundPositionY);
// Image defaults to NoRepeat so ObjectFit.Contain's aspect-preserved empty bars
// don't get tiled by the engine's Repeat default (engine-fact-objectfit-contain-tiles:
// Panel.Draw.cs DrawBackgroundTexture defaults BackgroundRepeat to Repeat when null).
// Explicit user value still wins. Mirrors the WebPanel BackgroundSize default below.
target.BackgroundRepeat = TryBackgroundRepeat(StyleField.BackgroundRepeat)
?? (_isImage ? BackgroundRepeat.NoRepeat : (BackgroundRepeat?)null);
if (!_isShape)
{
// WebPanel needs BackgroundSize=100% so the engine-managed webview texture
// fills the panel instead of being centered at native size with black bars
// around it. Explicit user value still wins.
target.BackgroundSizeX = TryLen(StyleField.BackgroundSizeX)
?? (_isWebPanel ? Length.Percent(100) : (Length?)null);
target.BackgroundSizeY = TryLen(StyleField.BackgroundSizeY)
?? (_isWebPanel ? Length.Percent(100) : (Length?)null);
}
// Shapes route colour through the ShapeColor uniform (all kinds), so BackgroundTint never applies to them; push the uniform + hover knobs here.
if (_isShape)
{
var shapePanel = (Goo.Internal.StatefulShapePanel)host;
shapePanel.SetShapeColor(TryColor(StyleField.BackgroundColor) ?? Color.White);
shapePanel.SetHover(TryColor(StyleField.HoverBackgroundColor), TryInt32(StyleField.TransitionMs) ?? 0);
}
target.BackgroundTint = TryColor(StyleField.BackgroundTint);
// Border color: shorthand first, per-edge override.
target.BorderColor = borderColor;
target.BorderLeftColor = borderLeftColor; target.BorderTopColor = borderTopColor;
target.BorderRightColor = borderRightColor; target.BorderBottomColor = borderBottomColor;
// Border image properties.
target.BorderImageFill = TryBorderImageFill(StyleField.BorderImageFill);
target.BorderImageRepeat = TryBorderImageRepeat(StyleField.BorderImageRepeat);
target.BorderImageSource = TryTexture(StyleField.BorderImageSource);
target.BorderImageTint = TryColor(StyleField.BorderImageTint);
target.BorderImageWidthBottom = TryLen(StyleField.BorderImageWidthBottom);
target.BorderImageWidthLeft = TryLen(StyleField.BorderImageWidthLeft);
target.BorderImageWidthRight = TryLen(StyleField.BorderImageWidthRight);
target.BorderImageWidthTop = TryLen(StyleField.BorderImageWidthTop);
// Border width: shorthand first, per-edge override.
target.BorderWidth = borderWidth;
target.BorderLeftWidth = borderLeftWidth; target.BorderTopWidth = borderTopWidth;
target.BorderRightWidth = borderRightWidth; target.BorderBottomWidth = borderBottomWidth;
// Position edges.
target.Bottom = TryLen(StyleField.Bottom);
target.Left = TryLen(StyleField.Left);
target.Right = TryLen(StyleField.Right);
target.Top = TryLen(StyleField.Top);
// Caret.
target.CaretColor = TryColor(StyleField.CaretColor);
// Cursor. TextEntry defaults to "text" so consumers don't have to opt in
// to the I-beam; explicit user value wins.
target.Cursor = TryString(StyleField.Cursor)
?? (_isTextEntry ? "text" : null);
// Filter properties.
target.FilterBlur = TryLen(StyleField.FilterBlur);
target.FilterBorderColor = TryColor(StyleField.FilterBorderColor);
target.FilterBorderWidth = TryLen(StyleField.FilterBorderWidth);
target.FilterBrightness = TryLen(StyleField.FilterBrightness);
target.FilterContrast = TryLen(StyleField.FilterContrast);
target.FilterHueRotate = TryLen(StyleField.FilterHueRotate);
target.FilterInvert = TryLen(StyleField.FilterInvert);
target.FilterSaturate = TryLen(StyleField.FilterSaturate);
target.FilterSepia = TryLen(StyleField.FilterSepia);
target.FilterTint = TryColor(StyleField.FilterTint);
// Flex properties.
target.FlexBasis = TryLen(StyleField.FlexBasis);
target.FlexGrow = TrySingle(StyleField.FlexGrow);
target.FlexShrink = TrySingle(StyleField.FlexShrink);
target.FlexWrap = TryWrap(StyleField.FlexWrap);
// Font properties.
target.FontColor = inlineFg;
target.FontFamily = TryString(StyleField.FontFamily);
target.FontSize = TryLen(StyleField.FontSize);
target.FontSmooth = TryFontSmooth(StyleField.FontSmooth);
target.FontStyle = TryFontStyle(StyleField.FontStyle);
target.FontVariantNumeric = TryFontVariantNumeric(StyleField.FontVariantNumeric);
target.FontWeight = TryInt32(StyleField.FontWeight);
// Image rendering.
target.ImageRendering = TryImageRendering(StyleField.ImageRendering);
// Letter and line spacing.
target.LetterSpacing = TryLen(StyleField.LetterSpacing);
target.LineHeight = TryLen(StyleField.LineHeight);
// Mask properties.
target.MaskAngle = TryLen(StyleField.MaskAngle);
target.MaskImage = TryTexture(StyleField.MaskImage);
target.MaskMode = TryMaskMode(StyleField.MaskMode);
target.MaskPositionX = TryLen(StyleField.MaskPositionX);
target.MaskPositionY = TryLen(StyleField.MaskPositionY);
target.MaskRepeat = TryBackgroundRepeat(StyleField.MaskRepeat);
target.MaskScope = TryMaskScope(StyleField.MaskScope);
target.MaskSizeX = TryLen(StyleField.MaskSizeX);
target.MaskSizeY = TryLen(StyleField.MaskSizeY);
// Min/max size.
target.MaxHeight = TryLen(StyleField.MaxHeight);
target.MaxWidth = TryLen(StyleField.MaxWidth);
target.MinHeight = TryLen(StyleField.MinHeight);
target.MinWidth = TryLen(StyleField.MinWidth);
// Mix blend mode.
target.MixBlendMode = TryString(StyleField.MixBlendMode);
// Object fit.
target.ObjectFit = TryObjectFit(StyleField.ObjectFit);
// Opacity.
target.Opacity = TrySingle(StyleField.Opacity);
// Order.
target.Order = TryInt32(StyleField.Order);
// Outline properties.
target.OutlineColor = TryColor(StyleField.OutlineColor);
target.OutlineOffset = TryLen(StyleField.OutlineOffset);
target.OutlineWidth = TryLen(StyleField.OutlineWidth);
// Engine Overflow is a facade writing both axis fields; coalesce or the per-axis nulls wipe it.
// Virtual panels default to Scroll (their ctor value) so a user style write cannot kill scrolling.
var overflow = TryOverflowMode(StyleField.Overflow)
?? (_isVirtual ? OverflowMode.Scroll : (OverflowMode?)null);
target.OverflowX = TryOverflowMode(StyleField.OverflowX) ?? overflow;
target.OverflowY = TryOverflowMode(StyleField.OverflowY) ?? overflow;
// Perspective origin.
target.PerspectiveOriginX = TryLen(StyleField.PerspectiveOriginX);
target.PerspectiveOriginY = TryLen(StyleField.PerspectiveOriginY);
// Pointer events: WebPanel and TextEntry default to All (intrinsically interactive); also gate to All when handlers exist so a style-only rebuild does not clobber the events-driven gate. A handler-less panel with a state variant stays capturing (unset); a fully inert panel resolves to None so it is pointer-transparent. Explicit value wins.
bool hasHandlers = host is IStatefulEventHost peHost && peHost.HasEventHandlers;
target.PointerEvents = TryPointerEvents(StyleField.PointerEvents)
?? (_isVirtual ? PointerEvents.All
: PointerEventsPolicy.Resolve(null, _isWebPanel, hasHandlers, hasStateVariant: hasAnyVariant, isTextEntry: _isTextEntry,
isEmbed: host.HasClass("goo-embed")));
target.Position = TryPositionMode(StyleField.Position)
?? (_isVirtual ? PositionMode.Relative : (PositionMode?)null);
// Box shadow: PanelStyle.BoxShadow is a mutable list field, not a self-diffing setter,
// so diff here and mark dirty manually on change.
var boxShadow = TryShadow(StyleField.BoxShadow);
if (boxShadow is { } bs)
{
if (target.BoxShadow.Count != 1 || !target.BoxShadow[0].Equals(bs))
{
target.BoxShadow.Clear();
target.BoxShadow.Add(bs);
target.Dirty();
}
}
else if (target.BoxShadow.Count > 0)
{
target.BoxShadow.Clear();
target.Dirty();
}
// Sound.
target.SoundIn = TryString(StyleField.SoundIn);
target.SoundOut = TryString(StyleField.SoundOut);
// Text properties.
target.TextAlign = TryTextAlign(StyleField.TextAlign);
target.TextBackgroundAngle = TryLen(StyleField.TextBackgroundAngle);
target.TextDecorationColor = TryColor(StyleField.TextDecorationColor);
target.TextDecorationLine = TryTextDecoration(StyleField.TextDecorationLine);
target.TextDecorationSkipInk = TryTextSkipInk(StyleField.TextDecorationSkipInk);
target.TextDecorationStyle = TryTextDecorationStyle(StyleField.TextDecorationStyle);
target.TextDecorationThickness = TryLen(StyleField.TextDecorationThickness);
target.TextFilter = TryFilterMode(StyleField.TextFilter);
target.TextLineThroughOffset = TryLen(StyleField.TextLineThroughOffset);
target.TextOverflow = TryTextOverflow(StyleField.TextOverflow);
target.TextOverlineOffset = TryLen(StyleField.TextOverlineOffset);
target.TextStrokeColor = TryColor(StyleField.TextStrokeColor);
target.TextStrokeWidth = TryLen(StyleField.TextStrokeWidth);
target.TextTransform = TryTextTransform(StyleField.TextTransform);
target.TextUnderlineOffset = TryLen(StyleField.TextUnderlineOffset);
// Transform.
target.Transform = TryPanelTransform(StyleField.Transform);
// Transform origin.
target.TransformOriginX = TryLen(StyleField.TransformOriginX);
target.TransformOriginY = TryLen(StyleField.TransformOriginY);
// White space and word properties.
target.WhiteSpace = TryWhiteSpace(StyleField.WhiteSpace);
target.WordBreak = TryWordBreak(StyleField.WordBreak);
target.WordSpacing = TryLen(StyleField.WordSpacing);
// Z-index, scaled so a declared value dominates sibling document order.
target.ZIndex = ZIndexScaling.Scale(TryInt32(StyleField.ZIndex));
int? transMs = TryInt32(StyleField.TransitionMs);
if (host is IStatefulHost stateful)
{
if (hasAnyVariant)
stateful.ApplyStateVariants(
baseBg: sheetBg,
baseFg: sheetFg,
hoverBg: hoverBg, activeBg: activeBg, focusBg: focusBg,
hoverFg: hoverFg, activeFg: activeFg, focusFg: focusFg,
transitionMs: transMs);
else
// Reused panels can carry a prior occupant's variant sheet; shed it
// so its stale base/:hover background does not bleed through.
stateful.ClearStateVariants();
}
// Disabled is the final word: state variants force PointerEvents.All for hover delivery, so this must run after them.
bool disabled = TryBool(StyleField.Disabled) == true;
if (disabled)
{
target.PointerEvents = PointerEvents.None;
target.Opacity = Container.ResolveDisabledOpacity(target.Opacity);
}
}
// Each Try* ends with (T?)null not null: a bare null lets the ternary resolve through engine types' implicit string operator and NPE. See engine-fact memories.
static Length? TryLen(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.Length ? v.LengthVal : (Length?)null;
static Color? TryColor(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.Color ? v.ColorVal : (Color?)null;
static FlexDirection? TryFlex(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.FlexDirection ? ((FlexDirection)v.EnumVal) : (FlexDirection?)null;
static Justify? TryJustify(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.Justify ? ((Justify)v.EnumVal) : (Justify?)null;
static Align? TryAlign(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.Align ? ((Align)v.EnumVal) : (Align?)null;
static DisplayMode? TryDisplay(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.DisplayMode ? ((DisplayMode)v.EnumVal) : (DisplayMode?)null;
static string? TryString(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.String ? (string?)v.RefVal : null;
static Shadow? TryShadow(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.Shadow ? (Shadow)v.RefVal! : (Shadow?)null;
static float? TrySingle(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.Single ? System.BitConverter.Int32BitsToSingle(v.RefSlot) : (float?)null;
static bool? TryBool(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.Boolean ? v.RefSlot != 0 : (bool?)null;
static int? TryInt32(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.Int32 ? v.RefSlot : (int?)null;
static Texture? TryTexture(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.Texture ? (Texture?)v.RefVal : null;
static EnginePanelTransform? TryPanelTransform(StyleField f)
{
if (!TryResolved(f, out var v)) return null;
if (v.Kind != StyleValueKind.PanelTransform) return null;
if (v.RefVal is not ImmutableList<EnginePanelTransform.Entry> list) return null;
return new EnginePanelTransform { List = list };
}
static Wrap? TryWrap(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.Wrap ? ((Wrap)v.EnumVal) : (Wrap?)null;
static BackgroundRepeat? TryBackgroundRepeat(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.BackgroundRepeat ? ((BackgroundRepeat)v.EnumVal) : (BackgroundRepeat?)null;
static BorderImageFill? TryBorderImageFill(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.BorderImageFill ? ((BorderImageFill)v.EnumVal) : (BorderImageFill?)null;
static BorderImageRepeat? TryBorderImageRepeat(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.BorderImageRepeat ? ((BorderImageRepeat)v.EnumVal) : (BorderImageRepeat?)null;
static FontSmooth? TryFontSmooth(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.FontSmooth ? ((FontSmooth)v.EnumVal) : (FontSmooth?)null;
static FontStyle? TryFontStyle(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.FontStyle ? ((FontStyle)v.EnumVal) : (FontStyle?)null;
static FontVariantNumeric? TryFontVariantNumeric(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.FontVariantNumeric ? ((FontVariantNumeric)v.EnumVal) : (FontVariantNumeric?)null;
static ImageRendering? TryImageRendering(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.ImageRendering ? ((ImageRendering)v.EnumVal) : (ImageRendering?)null;
static MaskMode? TryMaskMode(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.MaskMode ? ((MaskMode)v.EnumVal) : (MaskMode?)null;
static MaskScope? TryMaskScope(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.MaskScope ? ((MaskScope)v.EnumVal) : (MaskScope?)null;
static ObjectFit? TryObjectFit(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.ObjectFit ? ((ObjectFit)v.EnumVal) : (ObjectFit?)null;
static OverflowMode? TryOverflowMode(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.OverflowMode ? ((OverflowMode)v.EnumVal) : (OverflowMode?)null;
static PointerEvents? TryPointerEvents(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.PointerEvents ? ((PointerEvents)v.EnumVal) : (PointerEvents?)null;
static PositionMode? TryPositionMode(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.PositionMode ? ((PositionMode)v.EnumVal) : (PositionMode?)null;
static TextAlign? TryTextAlign(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.TextAlign ? ((TextAlign)v.EnumVal) : (TextAlign?)null;
static TextDecoration? TryTextDecoration(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.TextDecoration ? ((TextDecoration)v.EnumVal) : (TextDecoration?)null;
static TextDecorationStyle? TryTextDecorationStyle(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.TextDecorationStyle ? ((TextDecorationStyle)v.EnumVal) : (TextDecorationStyle?)null;
static TextSkipInk? TryTextSkipInk(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.TextSkipInk ? ((TextSkipInk)v.EnumVal) : (TextSkipInk?)null;
static FilterMode? TryFilterMode(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.FilterMode ? ((FilterMode)v.EnumVal) : (FilterMode?)null;
static TextOverflow? TryTextOverflow(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.TextOverflow ? ((TextOverflow)v.EnumVal) : (TextOverflow?)null;
static TextTransform? TryTextTransform(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.TextTransform ? ((TextTransform)v.EnumVal) : (TextTransform?)null;
static WhiteSpace? TryWhiteSpace(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.WhiteSpace ? ((WhiteSpace)v.EnumVal) : (WhiteSpace?)null;
static WordBreak? TryWordBreak(StyleField f)
=> TryResolved(f, out var v) && v.Kind == StyleValueKind.WordBreak ? ((WordBreak)v.EnumVal) : (WordBreak?)null;
static Panel WalkPath(Panel root, int[] path)
{
var cur = root;
foreach (var idx in path)
{
cur = cur.GetChild(idx, false);
if (cur is null)
throw new InvalidOperationException(
$"Applier.WalkPath: child index {idx} is null while traversing HostPath=[{string.Join(",", path)}].");
}
return cur;
}
}