Core/Op.cs
using System;
using System.Collections.Immutable;
using Sandbox;
using Sandbox.Rendering;
namespace Goo;
public enum OpKind : byte
{
CreateText,
UpdateText,
RemoveAt,
CreateContainer,
CreateImage,
UpdateImage,
CreateScenePanel,
UpdateScenePanel,
CreateSvgPanel,
UpdateSvgPanel,
CreateSector,
UpdateSector,
CreateArc,
UpdateArc,
CreatePolygon,
UpdatePolygon,
CreateWebPanel,
UpdateWebPanel,
CreateTextEntry,
UpdateTextEntry,
MoveAt,
SetStyle,
SetEvents,
SetDrawState,
}
// Discriminated value-type union: one struct with name-overlapped fields carries all op variants, so List<Op> is a poolable value buffer. BlobEvents (six delegate refs) is the heaviest variant (~64B/slot).
public struct Op : IEquatable<Op>
{
public OpKind Kind;
public int[] HostPath;
// Index meaning by kind: Create*/Update*/Remove* target index; MoveAt source index.
public int Index;
public int ToIndex;
// Text Content for CreateText/UpdateText; image Path for CreateImage/UpdateImage.
public string? StringPayload;
public Texture? Texture;
internal StyleList? Style;
internal BlobEvents Events;
// ScenePanel-only carry-fields. Scene is public to match Texture's posture
// (engine refs callers may inspect); RenderOnce is value-typed flag.
public Scene? Scene;
public bool RenderOnce;
// WebPanel-only carry-field; maps to engine WebSurface.InBackgroundMode.
public bool Paused;
// SvgPanel-only carry-field.
public string? Color;
// TextEntry carry-fields. StringPayload (already declared) carries the text value.
public string? Placeholder;
public int? MaxLength;
public bool Disabled;
public bool Numeric;
public float? MinValue;
public float? MaxValue;
public string? NumberFormat;
public bool Multiline;
public Action<string>? OnChange;
public Action<string>? OnSubmit;
// Interaction callbacks. OnBlur carries the final text; all rebound every Update.
public Action? OnFocus;
public Action<string>? OnBlur;
public Action? OnCancel;
// True in controlled mode (consumer set Value); false when only InitialText was set.
public bool IsControlled;
// Sector / Arc carry-field.
public ShapeParams Shape;
// Polygon carry-field. Points are unit-coord [0..1] vertices; treat as immutable.
public Vector2[]? Points;
// SetDrawState carry-fields (Container only in Plan 1).
public Material? DrawMaterial;
public ImmutableArray<UniformValue> DrawUniforms;
public Action<CommandList, Rect>? DrawCallback;
// Read-only semantic aliases so call sites can use the natural name for the variant.
public string? Content => StringPayload;
public string? Path => StringPayload;
public string? ScenePath => StringPayload;
public string? Url => StringPayload;
public string? Text => StringPayload;
public int FromIndex => Index;
public static Op CreateText(int index, string content, int[]? hostPath = null) =>
new() { Kind = OpKind.CreateText, Index = index, StringPayload = content, HostPath = hostPath ?? Array.Empty<int>() };
public static Op UpdateText(int index, string content, int[]? hostPath = null) =>
new() { Kind = OpKind.UpdateText, Index = index, StringPayload = content, HostPath = hostPath ?? Array.Empty<int>() };
public static Op RemoveAt(int index, int[]? hostPath = null) =>
new() { Kind = OpKind.RemoveAt, Index = index, HostPath = hostPath ?? Array.Empty<int>() };
public static Op CreateContainer(int index, int[]? hostPath = null) =>
new() { Kind = OpKind.CreateContainer, Index = index, HostPath = hostPath ?? Array.Empty<int>() };
public static Op CreateImage(int index, Texture? texture, string? path, int[]? hostPath = null) =>
new() { Kind = OpKind.CreateImage, Index = index, Texture = texture, StringPayload = path, HostPath = hostPath ?? Array.Empty<int>() };
public static Op UpdateImage(int index, Texture? texture, string? path, int[]? hostPath = null) =>
new() { Kind = OpKind.UpdateImage, Index = index, Texture = texture, StringPayload = path, HostPath = hostPath ?? Array.Empty<int>() };
public static Op CreateScenePanel(int index, Scene? scene, string? scenePath, bool renderOnce, int[]? hostPath = null) =>
new() {
Kind = OpKind.CreateScenePanel, Index = index,
Scene = scene, StringPayload = scenePath, RenderOnce = renderOnce,
HostPath = hostPath ?? Array.Empty<int>(),
};
public static Op UpdateScenePanel(int index, Scene? scene, string? scenePath, bool renderOnce, int[]? hostPath = null) =>
new() {
Kind = OpKind.UpdateScenePanel, Index = index,
Scene = scene, StringPayload = scenePath, RenderOnce = renderOnce,
HostPath = hostPath ?? Array.Empty<int>(),
};
public static Op CreateSvgPanel(int index, string? path, string? color, int[]? hostPath = null) =>
new() {
Kind = OpKind.CreateSvgPanel, Index = index,
StringPayload = path, Color = color,
HostPath = hostPath ?? Array.Empty<int>(),
};
public static Op UpdateSvgPanel(int index, string? path, string? color, int[]? hostPath = null) =>
new() {
Kind = OpKind.UpdateSvgPanel, Index = index,
StringPayload = path, Color = color,
HostPath = hostPath ?? Array.Empty<int>(),
};
public static Op CreateSector(int index, in ShapeParams shape, int[]? hostPath = null) =>
new() {
Kind = OpKind.CreateSector, Index = index,
Shape = shape,
HostPath = hostPath ?? Array.Empty<int>(),
};
public static Op UpdateSector(int index, in ShapeParams shape, int[]? hostPath = null) =>
new() {
Kind = OpKind.UpdateSector, Index = index,
Shape = shape,
HostPath = hostPath ?? Array.Empty<int>(),
};
public static Op CreateArc(int index, in ShapeParams shape, int[]? hostPath = null) =>
new() {
Kind = OpKind.CreateArc, Index = index,
Shape = shape,
HostPath = hostPath ?? Array.Empty<int>(),
};
public static Op UpdateArc(int index, in ShapeParams shape, int[]? hostPath = null) =>
new() {
Kind = OpKind.UpdateArc, Index = index,
Shape = shape,
HostPath = hostPath ?? Array.Empty<int>(),
};
public static Op CreatePolygon(int index, Vector2[]? points, int[]? hostPath = null) =>
new() {
Kind = OpKind.CreatePolygon, Index = index,
Points = points,
HostPath = hostPath ?? Array.Empty<int>(),
};
public static Op UpdatePolygon(int index, Vector2[]? points, int[]? hostPath = null) =>
new() {
Kind = OpKind.UpdatePolygon, Index = index,
Points = points,
HostPath = hostPath ?? Array.Empty<int>(),
};
public static Op CreateWebPanel(int index, string? url, bool paused = false, int[]? hostPath = null) =>
new() {
Kind = OpKind.CreateWebPanel, Index = index,
StringPayload = url, Paused = paused,
HostPath = hostPath ?? Array.Empty<int>(),
};
public static Op UpdateWebPanel(int index, string? url, bool paused = false, int[]? hostPath = null) =>
new() {
Kind = OpKind.UpdateWebPanel, Index = index,
StringPayload = url, Paused = paused,
HostPath = hostPath ?? Array.Empty<int>(),
};
public static Op CreateTextEntry(
int index,
string? text,
bool isControlled = false,
string? placeholder = null,
int? maxLength = null,
bool disabled = false,
bool numeric = false,
float? minValue = null,
float? maxValue = null,
string? numberFormat = null,
bool multiline = false,
Action<string>? onChange = null,
Action<string>? onSubmit = null,
Action? onFocus = null,
Action<string>? onBlur = null,
Action? onCancel = null,
int[]? hostPath = null) =>
new() {
Kind = OpKind.CreateTextEntry, Index = index,
StringPayload = text,
IsControlled = isControlled,
Placeholder = placeholder,
MaxLength = maxLength,
Disabled = disabled,
Numeric = numeric,
MinValue = minValue,
MaxValue = maxValue,
NumberFormat = numberFormat,
Multiline = multiline,
OnChange = onChange,
OnSubmit = onSubmit,
OnFocus = onFocus,
OnBlur = onBlur,
OnCancel = onCancel,
HostPath = hostPath ?? Array.Empty<int>(),
};
public static Op UpdateTextEntry(
int index,
string? text,
bool isControlled = false,
string? placeholder = null,
int? maxLength = null,
bool disabled = false,
bool numeric = false,
float? minValue = null,
float? maxValue = null,
string? numberFormat = null,
bool multiline = false,
Action<string>? onChange = null,
Action<string>? onSubmit = null,
Action? onFocus = null,
Action<string>? onBlur = null,
Action? onCancel = null,
int[]? hostPath = null) =>
new() {
Kind = OpKind.UpdateTextEntry, Index = index,
StringPayload = text,
IsControlled = isControlled,
Placeholder = placeholder,
MaxLength = maxLength,
Disabled = disabled,
Numeric = numeric,
MinValue = minValue,
MaxValue = maxValue,
NumberFormat = numberFormat,
Multiline = multiline,
OnChange = onChange,
OnSubmit = onSubmit,
OnFocus = onFocus,
OnBlur = onBlur,
OnCancel = onCancel,
HostPath = hostPath ?? Array.Empty<int>(),
};
public static Op MoveAt(int fromIndex, int toIndex, int[]? hostPath = null) =>
new() { Kind = OpKind.MoveAt, Index = fromIndex, ToIndex = toIndex, HostPath = hostPath ?? Array.Empty<int>() };
internal static Op SetStyle(StyleList style, int[]? hostPath = null) =>
new() { Kind = OpKind.SetStyle, Style = style, HostPath = hostPath ?? Array.Empty<int>() };
internal static Op SetEvents(BlobEvents events, int[]? hostPath = null) =>
new() { Kind = OpKind.SetEvents, Events = events, HostPath = hostPath ?? Array.Empty<int>() };
internal static Op SetDrawState(Material? material, ImmutableArray<UniformValue> uniforms, Action<CommandList, Rect>? drawCallback, int[]? hostPath = null) =>
new() {
Kind = OpKind.SetDrawState,
DrawMaterial = material,
DrawUniforms = uniforms.IsDefault ? ImmutableArray<UniformValue>.Empty : uniforms,
DrawCallback = drawCallback,
HostPath = hostPath ?? Array.Empty<int>(),
};
// Record-default equality: value-equal for primitives/strings, reference-equal for arrays/managed refs. HostPath defaults to the empty-array singleton so no-path factory calls compare equal.
public bool Equals(Op other)
{
if (Kind != other.Kind) return false;
if (!ReferenceEquals(HostPath, other.HostPath)) return false;
if (Index != other.Index) return false;
if (ToIndex != other.ToIndex) return false;
if (!string.Equals(StringPayload, other.StringPayload)) return false;
if (!ReferenceEquals(Texture, other.Texture)) return false;
if (!ReferenceEquals(Scene, other.Scene)) return false;
if (RenderOnce != other.RenderOnce) return false;
if (Paused != other.Paused) return false;
if (!string.Equals(Color, other.Color)) return false;
if (Shape != other.Shape) return false;
if (!ReferenceEquals(Points, other.Points)) return false;
if (!ReferenceEquals(Style, other.Style)) return false;
if (!BlobEvents.ContentsEqual(in Events, in other.Events)) return false;
if (!ReferenceEquals(DrawMaterial, other.DrawMaterial)) return false;
if (!ReferenceEquals(DrawCallback, other.DrawCallback)) return false;
if (!UniformValue.SequenceEqual(DrawUniforms, other.DrawUniforms)) return false;
if (!string.Equals(Placeholder, other.Placeholder)) return false;
if (MaxLength != other.MaxLength) return false;
if (Disabled != other.Disabled) return false;
if (Numeric != other.Numeric) return false;
if (MinValue != other.MinValue) return false;
if (MaxValue != other.MaxValue) return false;
if (!string.Equals(NumberFormat, other.NumberFormat)) return false;
if (Multiline != other.Multiline) return false;
if (!ReferenceEquals(OnChange, other.OnChange)) return false;
if (!ReferenceEquals(OnSubmit, other.OnSubmit)) return false;
if (!ReferenceEquals(OnFocus, other.OnFocus)) return false;
if (!ReferenceEquals(OnBlur, other.OnBlur)) return false;
if (!ReferenceEquals(OnCancel, other.OnCancel)) return false;
if (IsControlled != other.IsControlled) return false;
return true;
}
public override bool Equals(object? obj) => obj is Op o && Equals(o);
public override int GetHashCode() => HashCode.Combine(HashCode.Combine((byte)Kind, Index, ToIndex, StringPayload, Style, Scene, RenderOnce, Color), HashCode.Combine(Shape, Points));
public static bool operator ==(Op a, Op b) => a.Equals(b);
public static bool operator !=(Op a, Op b) => !a.Equals(b);
}