Core/Reconciler.cs
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Goo;
public readonly record struct DiffResult(IReadOnlyList<Op> Ops, IReadOnlyList<string> Warnings);
// Lazy-rented diff output buffers. Identical-tree iterations never add an Op or warning,
// so we don't even touch the pool on the hot path. The pooled lists outlive Reconciler.Diff
// and are read by the caller; BuildContext.ReturnAll returns them after Applier.Apply runs.
internal struct DiffBuffers
{
public List<Op>? Ops;
public List<string>? Warnings;
public void AddOp(Op op) => (Ops ??= BuildContext.Current.RentOpList()).Add(op);
public void AddWarning(string warning) => (Warnings ??= BuildContext.Current.RentWarningList()).Add(warning);
}
public static class Reconciler
{
internal static DiffResult Diff<TRoot>(ref Fiber? rootFiber, in TRoot intentRoot)
where TRoot : struct, IBlob
{
DiffBuffers bufs = default;
var ctx = BuildContext.Current;
ctx.BeginDiff();
var path = ctx.RentPath();
try
{
Frame rootFrame = default;
intentRoot.WriteTo(ref rootFrame); // direct call, monomorphized - no cast, no boxing
DiffNode(ref rootFiber, ref rootFrame, 0, path, ref bufs);
return new DiffResult(
bufs.Ops ?? (IReadOnlyList<Op>)Array.Empty<Op>(),
bufs.Warnings ?? (IReadOnlyList<string>)Array.Empty<string>());
}
finally
{
ctx.ReturnPath(path);
}
}
static void DiffNode(ref Fiber? fiber, ref Frame frame, int index, List<int> path, ref DiffBuffers bufs)
{
if (frame.Kind == BlobKind.Cell)
{
DiffCell(ref fiber, ref frame, index, path, ref bufs);
return;
}
if (fiber is null)
{
fiber = AllocateFiberFor(in frame);
EmitCreate(in frame, fiber, index, path, ref bufs);
return;
}
if (fiber.Kind != frame.Kind)
{
var snap = Snapshot(path);
bufs.AddOp(Op.RemoveAt(index, snap));
TeardownTree(fiber);
fiber = AllocateFiberFor(in frame);
EmitCreate(in frame, fiber, index, path, ref bufs);
return;
}
switch (frame.Kind)
{
case BlobKind.Text:
if (fiber.Content != frame.Content)
{
bufs.AddOp(Op.UpdateText(index, frame.Content, Snapshot(path)));
fiber.Content = frame.Content;
}
if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
fiber.Key = frame.Key;
break;
case BlobKind.Container:
path.Add(index);
DiffChildren(fiber, frame.Children!, path, ref bufs);
if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
{
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
}
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
{
bool drawChanged =
!ReferenceEquals(fiber.Material, frame.Material) ||
fiber.Draw != frame.Draw ||
!UniformValue.SequenceEqual(fiber.Uniforms, frame.Uniforms);
if (drawChanged)
{
bufs.AddOp(Op.SetDrawState(frame.Material, frame.Uniforms, frame.Draw, Snapshot(path)));
fiber.Material = frame.Material;
fiber.Uniforms = frame.Uniforms;
fiber.Draw = frame.Draw;
}
}
fiber.Key = frame.Key;
path.RemoveAt(path.Count - 1);
break;
case BlobKind.Image:
if (!ReferenceEquals(fiber.Texture, frame.Texture) || fiber.Path != frame.Path)
{
if ((frame.Texture is not null) == (frame.Path is not null))
{
bufs.AddWarning($"Image at HostPath=[{string.Join(",", path)},{index}] must have exactly one of Texture/Path set; update emitted with both null.");
bufs.AddOp(Op.UpdateImage(index, null, null, Snapshot(path)));
}
else
{
bufs.AddOp(Op.UpdateImage(index, frame.Texture, frame.Path, Snapshot(path)));
}
fiber.Texture = frame.Texture;
fiber.Path = frame.Path;
}
if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
fiber.Key = frame.Key;
break;
case BlobKind.ScenePanel:
bool sceneChanged = !ReferenceEquals(fiber.Scene, frame.Scene) || fiber.Path != frame.Path;
bool renderOnceChanged = fiber.RenderOnce != frame.RenderOnce;
if (sceneChanged || renderOnceChanged)
{
if ((frame.Scene is not null) == (frame.Path is not null))
{
bufs.AddWarning($"ScenePanel at HostPath=[{string.Join(",", path)},{index}] must have exactly one of Scene/ScenePath set; update emitted with both null.");
bufs.AddOp(Op.UpdateScenePanel(index, null, null, frame.RenderOnce, Snapshot(path)));
}
else
{
bufs.AddOp(Op.UpdateScenePanel(index, frame.Scene, frame.Path, frame.RenderOnce, Snapshot(path)));
}
fiber.Scene = frame.Scene;
fiber.Path = frame.Path;
fiber.RenderOnce = frame.RenderOnce;
}
if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
fiber.Key = frame.Key;
break;
case BlobKind.SvgPanel:
bool pathChanged = fiber.Path != frame.Path;
bool colorChanged = fiber.Color != frame.Color;
if (pathChanged || colorChanged)
{
bufs.AddOp(Op.UpdateSvgPanel(index, frame.Path, frame.Color, Snapshot(path)));
fiber.Path = frame.Path;
fiber.Color = frame.Color;
}
if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
fiber.Key = frame.Key;
break;
case BlobKind.Sector:
if (fiber.Shape != frame.Shape)
{
bufs.AddOp(Op.UpdateSector(index, in frame.Shape, Snapshot(path)));
fiber.Shape = frame.Shape;
}
if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
fiber.Key = frame.Key;
break;
case BlobKind.Arc:
if (fiber.Shape != frame.Shape)
{
bufs.AddOp(Op.UpdateArc(index, in frame.Shape, Snapshot(path)));
fiber.Shape = frame.Shape;
}
if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
fiber.Key = frame.Key;
break;
case BlobKind.Polygon:
if (!ReferenceEquals(fiber.Points, frame.Points))
{
bufs.AddOp(Op.UpdatePolygon(index, frame.Points, Snapshot(path)));
fiber.Points = frame.Points;
}
if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
fiber.Key = frame.Key;
break;
case BlobKind.WebPanel:
if (fiber.Path != frame.Path || fiber.Paused != frame.Paused)
{
bufs.AddOp(Op.UpdateWebPanel(index, frame.Path, frame.Paused, Snapshot(path)));
fiber.Path = frame.Path;
fiber.Paused = frame.Paused;
}
if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
fiber.Key = frame.Key;
break;
case BlobKind.TextEntry:
{
if (frame.ValueAndInitialTextBothSet)
bufs.AddWarning(
$"TextEntry at HostPath=[{string.Join(",", path)},{index}] " +
"has both Value and InitialText set; using Value (controlled mode). " +
"Set only one: Value for controlled, InitialText for uncontrolled.");
if (frame.Multiline && frame.OnSubmit is not null)
bufs.AddWarning(
$"TextEntry at HostPath=[{string.Join(",", path)},{index}] " +
"has Multiline=true with an OnSubmit handler; OnSubmit will not fire " +
"(Enter inserts newline in multiline mode). Use OnChange to observe edits.");
bool placeholderChanged = fiber.Placeholder != frame.Placeholder;
bool maxLengthChanged = fiber.MaxLength != frame.MaxLength;
bool disabledChanged = fiber.Disabled != frame.Disabled;
bool numericChanged = fiber.Numeric != frame.Numeric;
bool minValueChanged = fiber.MinValue != frame.MinValue;
bool maxValueChanged = fiber.MaxValue != frame.MaxValue;
bool numberFormatChanged = fiber.NumberFormat != frame.NumberFormat;
bool multilineChanged = fiber.Multiline != frame.Multiline;
bool onChangeChanged = !ReferenceEquals(fiber.OnChange, frame.OnChange);
bool onSubmitChanged = !ReferenceEquals(fiber.OnSubmit, frame.OnSubmit);
bool onFocusChanged = !ReferenceEquals(fiber.OnFocus, frame.OnFocus);
bool onBlurChanged = !ReferenceEquals(fiber.OnBlur, frame.OnBlur);
bool onCancelChanged = !ReferenceEquals(fiber.OnCancel, frame.OnCancel);
bool isControlledChanged = fiber.IsControlled != frame.IsControlled;
// Controlled mode re-emits Value every render so the engine's HasFocus
// guard can no-op in-flight typing. Uncontrolled mode never re-emits
// the text on Update (InitialText is one-shot at Create time).
bool emit = frame.IsControlled
|| placeholderChanged || maxLengthChanged || disabledChanged
|| numericChanged || minValueChanged || maxValueChanged
|| numberFormatChanged || multilineChanged
|| onChangeChanged || onSubmitChanged
|| onFocusChanged || onBlurChanged || onCancelChanged
|| isControlledChanged;
if (emit)
{
// Uncontrolled mode carries the fiber's last-known text so the Applier has valid state either way.
string? textPayload = frame.IsControlled ? frame.Path : fiber.Path;
bufs.AddOp(Op.UpdateTextEntry(
index,
text: textPayload,
isControlled: frame.IsControlled,
placeholder: frame.Placeholder,
maxLength: frame.MaxLength,
disabled: frame.Disabled,
numeric: frame.Numeric,
minValue: frame.MinValue,
maxValue: frame.MaxValue,
numberFormat: frame.NumberFormat,
multiline: frame.Multiline,
onChange: frame.OnChange,
onSubmit: frame.OnSubmit,
onFocus: frame.OnFocus,
onBlur: frame.OnBlur,
onCancel: frame.OnCancel,
hostPath: Snapshot(path)));
// Always sync fiber state to frame after emit (covers callback
// ref rotation, controlled-value updates, all other fields).
fiber.Path = textPayload;
fiber.Placeholder = frame.Placeholder;
fiber.MaxLength = frame.MaxLength;
fiber.Disabled = frame.Disabled;
fiber.Numeric = frame.Numeric;
fiber.MinValue = frame.MinValue;
fiber.MaxValue = frame.MaxValue;
fiber.NumberFormat = frame.NumberFormat;
fiber.Multiline = frame.Multiline;
fiber.OnChange = frame.OnChange;
fiber.OnSubmit = frame.OnSubmit;
fiber.OnFocus = frame.OnFocus;
fiber.OnBlur = frame.OnBlur;
fiber.OnCancel = frame.OnCancel;
fiber.IsControlled = frame.IsControlled;
}
if (!StyleList.ContentsEqual(fiber.Style, frame.Style))
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
fiber.Key = frame.Key;
}
break;
}
}
// Cell: expand a self-owning stateful instance in place at this slot; its state lives on the fiber.
static void DiffCell(ref Fiber? fiber, ref Frame frame, int index, List<int> path, ref DiffBuffers bufs)
{
// 1. Reuse the fiber's instance when its runtime type matches frame.CellType, else dispose old and create fresh.
Cell instance;
if (fiber?.Instance is Cell prior && prior.GetType() == frame.CellType)
instance = prior;
else
{
if (fiber?.Instance is IDisposable replaced) replaced.Dispose();
instance = frame.CellFactory!();
}
// 2. Refresh props on the persistent instance.
frame.Configure?.Invoke(instance);
// 3. Wire the rebuild hook (root marks itself dirty).
instance.SetRebuildHook(BuildContext.Current.RootRebuild);
// 4. Detach before recursing so an inner-kind-change teardown cannot dispose the instance we re-attach.
if (fiber is not null) fiber.Instance = null;
// 5. Expand to the inner Blob and recurse at the same slot; the normal arms handle it.
Frame inner = default;
instance.ExpandInto(ref inner);
DiffNode(ref fiber, ref inner, index, path, ref bufs);
// 6. Re-attach the instance and stamp the cell's key onto the slot fiber (keyed reorder needs it).
// fiber is non-null here: every DiffNode arm assigns it before returning.
fiber!.Instance = instance;
fiber.Key = frame.Key;
}
// Skip when both sides are all-null. Otherwise emit only on a structural handler change (Delegate.Equals per slot): method-groups and instance-field closures compare equal and skip; local-capture closures differ each Build and correctly rewire.
static void EmitSetEventsIfDelta(in Frame frame, Fiber fiber, List<int> targetPath, ref DiffBuffers bufs)
{
if (!frame.Events.AnyNonNull && !fiber.PrevEvents.AnyNonNull) return;
if (BlobEvents.ContentsEqual(in fiber.PrevEvents, in frame.Events))
{
fiber.PrevEvents = frame.Events;
return;
}
bufs.AddOp(Op.SetEvents(frame.Events, Snapshot(targetPath)));
fiber.PrevEvents = frame.Events;
}
static void DiffChildren(Fiber parent, Children intentChildren, List<int> path, ref DiffBuffers bufs)
{
parent.Children ??= new List<Fiber>();
var prev = parent.Children;
int nextCount = intentChildren.Count;
bool prevAllUnkeyed = AllUnkeyed(prev);
bool nextAllUnkeyed = AllUnkeyed(in intentChildren);
bool allUnkeyed = prevAllUnkeyed && nextAllUnkeyed;
bool prevAllKeyed = AllKeyed(prev);
bool nextAllKeyed = AllKeyed(in intentChildren);
bool allKeyed = prevAllKeyed && nextAllKeyed;
if (allKeyed)
{
DiffKeyed(parent, in intentChildren, path, ref bufs);
return;
}
if (!allUnkeyed)
{
bufs.AddWarning($"Mixed keyed/unkeyed children at HostPath=[{string.Join(",", path)}]; use all-keyed or all-unkeyed children per list. {DescribeKeyMix(in intentChildren)}");
DiffPositional(parent, in intentChildren, path, ref bufs, warnLengthChange: false);
return;
}
DiffPositional(parent, in intentChildren, path, ref bufs, warnLengthChange: true);
}
static void DiffPositional(Fiber parent, in Children intentChildren, List<int> path, ref DiffBuffers bufs, bool warnLengthChange)
{
var prev = parent.Children!;
int nextCount = intentChildren.Count;
if (warnLengthChange && prev.Count != nextCount)
bufs.AddWarning($"Keyless child list at HostPath=[{string.Join(",", path)}] changed length {prev.Count}->{nextCount}.");
int common = Math.Min(prev.Count, nextCount);
for (int i = 0; i < common; i++)
{
Fiber? f = prev[i];
DiffNode(ref f, ref intentChildren[i], i, path, ref bufs);
prev[i] = f!;
}
for (int i = common; i < nextCount; i++)
{
Fiber? f = null;
DiffNode(ref f, ref intentChildren[i], i, path, ref bufs);
prev.Add(f!);
}
for (int i = prev.Count - 1; i >= common && prev.Count > nextCount; i--)
{
bufs.AddOp(Op.RemoveAt(i, Snapshot(path)));
TeardownTree(prev[i]);
prev.RemoveAt(i);
}
}
// Dispose every IDisposable Cell instance in a fiber tree; entry point for panel teardown on disable/remount.
internal static void TeardownFiberTree(Fiber? fiber)
{
if (fiber is not null) TeardownTree(fiber);
}
// Dispose any IDisposable Cell instance in a fiber subtree that is leaving the tree for good.
static void TeardownTree(Fiber fiber)
{
if (fiber.Instance is IDisposable d) d.Dispose();
if (fiber.Children is { } kids)
for (int i = 0; i < kids.Count; i++) TeardownTree(kids[i]);
}
static void DiffKeyed(Fiber parent, in Children intentChildren, List<int> path, ref DiffBuffers bufs)
{
var prev = parent.Children!;
int nextCount = intentChildren.Count;
var ctx = BuildContext.Current;
// prevByKey: identity lookup of the surviving Fiber for a key (stays valid through prev mutation).
// nextKeys: membership probe for the removal pass.
// The redundant "current" key mirror that the old code carried is gone; IndexOfKey scans prev directly.
var prevByKey = ctx.RentKeyedDict();
var nextKeys = ctx.RentKeySet();
try
{
foreach (var f in prev) prevByKey[f.Key!] = f;
for (int i = 0; i < nextCount; i++) nextKeys.Add(intentChildren[i].Key!);
for (int i = prev.Count - 1; i >= 0; i--)
{
if (!nextKeys.Contains(prev[i].Key!))
{
bufs.AddOp(Op.RemoveAt(i, Snapshot(path)));
TeardownTree(prev[i]);
prev.RemoveAt(i);
}
}
for (int newIdx = 0; newIdx < nextCount; newIdx++)
{
var nKey = intentChildren[newIdx].Key!;
if (!prevByKey.ContainsKey(nKey))
{
Fiber? f = null;
DiffNode(ref f, ref intentChildren[newIdx], newIdx, path, ref bufs);
prev.Insert(newIdx, f!);
prevByKey[nKey] = f!;
continue;
}
int oldIdx = IndexOfKey(prev, nKey);
if (oldIdx != newIdx)
{
bufs.AddOp(Op.MoveAt(oldIdx, newIdx, Snapshot(path)));
var moved = prev[oldIdx];
prev.RemoveAt(oldIdx);
prev.Insert(newIdx, moved);
}
Fiber existing = prevByKey[nKey];
Fiber? slot = existing;
DiffNode(ref slot, ref intentChildren[newIdx], newIdx, path, ref bufs);
prev[newIdx] = slot!;
}
}
finally
{
ctx.ReturnKeyedDict(prevByKey);
ctx.ReturnKeySet(nextKeys);
}
}
static int IndexOfKey(List<Fiber> prev, string key)
{
for (int i = 0; i < prev.Count; i++) if (prev[i].Key == key) return i;
return -1;
}
static void EmitCreate(in Frame frame, Fiber fiber, int index, List<int> path, ref DiffBuffers bufs)
{
switch (frame.Kind)
{
case BlobKind.Text:
bufs.AddOp(Op.CreateText(index, frame.Content, Snapshot(path)));
if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
break;
case BlobKind.Container:
{
bufs.AddOp(Op.CreateContainer(index, Snapshot(path)));
path.Add(index);
fiber.Children = new List<Fiber>();
var kids = frame.Children!;
for (int i = 0; i < kids.Count; i++)
{
Fiber? child = null;
DiffNode(ref child, ref kids[i], i, path, ref bufs);
fiber.Children.Add(child!);
}
if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
{
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
}
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
if (frame.Material is not null || !frame.Uniforms.IsDefaultOrEmpty || frame.Draw is not null)
{
bufs.AddOp(Op.SetDrawState(frame.Material, frame.Uniforms, frame.Draw, Snapshot(path)));
}
path.RemoveAt(path.Count - 1);
}
break;
case BlobKind.Image:
{
if ((frame.Texture is not null) == (frame.Path is not null))
{
bufs.AddWarning($"Image at HostPath=[{string.Join(",", path)}] must have exactly one of Texture/Path set; emitted with both null.");
bufs.AddOp(Op.CreateImage(index, null, null, Snapshot(path)));
}
else
{
bufs.AddOp(Op.CreateImage(index, frame.Texture, frame.Path, Snapshot(path)));
}
if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
fiber.Texture = frame.Texture;
fiber.Path = frame.Path;
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
}
break;
case BlobKind.ScenePanel:
{
if ((frame.Scene is not null) == (frame.Path is not null))
{
bufs.AddWarning($"ScenePanel at HostPath=[{string.Join(",", path)}] must have exactly one of Scene/ScenePath set; emitted with both null.");
bufs.AddOp(Op.CreateScenePanel(index, null, null, frame.RenderOnce, Snapshot(path)));
}
else
{
bufs.AddOp(Op.CreateScenePanel(index, frame.Scene, frame.Path, frame.RenderOnce, Snapshot(path)));
}
if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
fiber.Scene = frame.Scene;
fiber.Path = frame.Path;
fiber.RenderOnce = frame.RenderOnce;
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
}
break;
case BlobKind.SvgPanel:
{
bufs.AddOp(Op.CreateSvgPanel(index, frame.Path, frame.Color, Snapshot(path)));
if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
fiber.Path = frame.Path;
fiber.Color = frame.Color;
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
}
break;
case BlobKind.Sector:
{
bufs.AddOp(Op.CreateSector(index, in frame.Shape, Snapshot(path)));
if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
fiber.Shape = frame.Shape;
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
}
break;
case BlobKind.Arc:
{
bufs.AddOp(Op.CreateArc(index, in frame.Shape, Snapshot(path)));
if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
fiber.Shape = frame.Shape;
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
}
break;
case BlobKind.Polygon:
{
bufs.AddOp(Op.CreatePolygon(index, frame.Points, Snapshot(path)));
if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
fiber.Points = frame.Points;
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
}
break;
case BlobKind.WebPanel:
{
bufs.AddOp(Op.CreateWebPanel(index, frame.Path, frame.Paused, Snapshot(path)));
if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
fiber.Path = frame.Path;
fiber.Paused = frame.Paused;
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
}
break;
case BlobKind.TextEntry:
{
if (frame.ValueAndInitialTextBothSet)
bufs.AddWarning(
$"TextEntry at HostPath=[{string.Join(",", path)},{index}] " +
"has both Value and InitialText set; using Value (controlled mode). " +
"Set only one: Value for controlled, InitialText for uncontrolled.");
if (frame.Multiline && frame.OnSubmit is not null)
bufs.AddWarning(
$"TextEntry at HostPath=[{string.Join(",", path)},{index}] " +
"has Multiline=true with an OnSubmit handler; OnSubmit will not fire " +
"(Enter inserts newline in multiline mode). Use OnChange to observe edits.");
bufs.AddOp(Op.CreateTextEntry(
index,
text: frame.Path,
isControlled: frame.IsControlled,
placeholder: frame.Placeholder,
maxLength: frame.MaxLength,
disabled: frame.Disabled,
numeric: frame.Numeric,
minValue: frame.MinValue,
maxValue: frame.MaxValue,
numberFormat: frame.NumberFormat,
multiline: frame.Multiline,
onChange: frame.OnChange,
onSubmit: frame.OnSubmit,
onFocus: frame.OnFocus,
onBlur: frame.OnBlur,
onCancel: frame.OnCancel,
hostPath: Snapshot(path)));
if (frame.Style != StyleList.Empty && frame.Style.Count > 0)
{
path.Add(index);
bufs.AddOp(Op.SetStyle(frame.Style, Snapshot(path)));
UpdateFiberStyle(fiber, frame.Style);
path.RemoveAt(path.Count - 1);
}
fiber.Path = frame.Path;
fiber.Placeholder = frame.Placeholder;
fiber.MaxLength = frame.MaxLength;
fiber.Disabled = frame.Disabled;
fiber.Numeric = frame.Numeric;
fiber.MinValue = frame.MinValue;
fiber.MaxValue = frame.MaxValue;
fiber.NumberFormat = frame.NumberFormat;
fiber.Multiline = frame.Multiline;
fiber.OnChange = frame.OnChange;
fiber.OnSubmit = frame.OnSubmit;
fiber.OnFocus = frame.OnFocus;
fiber.OnBlur = frame.OnBlur;
fiber.OnCancel = frame.OnCancel;
fiber.IsControlled = frame.IsControlled;
path.Add(index);
EmitSetEventsIfDelta(in frame, fiber, path, ref bufs);
path.RemoveAt(path.Count - 1);
}
break;
default:
throw new InvalidOperationException($"Unknown BlobKind {frame.Kind}.");
}
}
static Fiber AllocateFiberFor(in Frame frame)
{
return new Fiber
{
Kind = frame.Kind,
Key = frame.Key,
Content = frame.Content,
Style = StyleList.Empty,
Children = frame.Kind == BlobKind.Container ? new List<Fiber>() : null,
Texture = frame.Texture,
Path = frame.Path,
Scene = frame.Scene,
RenderOnce = frame.RenderOnce,
Paused = frame.Paused,
Color = frame.Color,
Shape = frame.Shape,
Points = frame.Points,
Material = frame.Material,
Uniforms = frame.Uniforms.IsDefault ? ImmutableArray<UniformValue>.Empty : frame.Uniforms,
Draw = frame.Draw,
Placeholder = frame.Placeholder,
MaxLength = frame.MaxLength,
Disabled = frame.Disabled,
Numeric = frame.Numeric,
MinValue = frame.MinValue,
MaxValue = frame.MaxValue,
NumberFormat = frame.NumberFormat,
Multiline = frame.Multiline,
OnChange = frame.OnChange,
OnSubmit = frame.OnSubmit,
OnFocus = frame.OnFocus,
OnBlur = frame.OnBlur,
OnCancel = frame.OnCancel,
IsControlled = frame.IsControlled,
};
}
static void UpdateFiberStyle(Fiber fiber, StyleList incoming)
{
if (incoming == StyleList.Empty || incoming.Count == 0)
{
fiber.Style = StyleList.Empty;
return;
}
if (fiber.Style == StyleList.Empty)
fiber.Style = new StyleList();
fiber.Style.CopyFrom(incoming);
}
static bool AllUnkeyed(List<Fiber> fibers)
{
foreach (var f in fibers) if (f.Key is not null) return false;
return true;
}
static bool AllUnkeyed(in Children children)
{
for (int i = 0; i < children.Count; i++) if (children[i].Key is not null) return false;
return true;
}
static bool AllKeyed(List<Fiber> fibers)
{
foreach (var f in fibers) if (f.Key is null) return false;
return true;
}
static bool AllKeyed(in Children children)
{
for (int i = 0; i < children.Count; i++) if (children[i].Key is null) return false;
return true;
}
static int[] Snapshot(List<int> path) => BuildContext.Current.RentHostPath(path);
// Slow-path diagnostic for the mixed-keyed warning: lists which sibling indices are keyed vs unkeyed (truncated per side) so the developer can spot the offender.
static string DescribeKeyMix(in Children intentChildren)
{
const int MaxListed = 8;
var unkeyed = new System.Text.StringBuilder();
var keyed = new System.Text.StringBuilder();
int unkeyedCount = 0, keyedCount = 0;
for (int i = 0; i < intentChildren.Count; i++)
{
var key = intentChildren[i].Key;
if (key is null)
{
if (unkeyedCount < MaxListed)
{
if (unkeyedCount > 0) unkeyed.Append(',');
unkeyed.Append(i);
}
unkeyedCount++;
}
else
{
if (keyedCount < MaxListed)
{
if (keyedCount > 0) keyed.Append(',');
keyed.Append(i).Append(":\"").Append(key).Append('"');
}
keyedCount++;
}
}
if (unkeyedCount > MaxListed) unkeyed.Append(",...");
if (keyedCount > MaxListed) keyed.Append(",...");
return $"Intent ({intentChildren.Count} children): unkeyed at [{unkeyed}]; keyed at [{keyed}].";
}
}