Code/Core/Blob.cs
using System;
using System.Collections;
using System.Collections.Immutable;
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;

namespace Goo;

/// <summary>Renders a run of text. Style it with the text and common style properties.</summary>
public readonly partial record struct Text : IBlob
{
    public static BlobKind Kind => BlobKind.Text;
    /// <summary>The text to display.</summary>
    public string Content { get; init; }
    /// <summary>Stable identity across rebuilds. Set it when this blob can change position among its siblings, so the reconciler matches it to the same panel.</summary>
    public string? Key { get; init; }
    internal readonly StyleList _style;

    internal readonly Action<MousePanelEvent>? _onClick;
    internal readonly Action<MousePanelEvent>? _onMouseEnter;
    internal readonly Action<MousePanelEvent>? _onMouseLeave;
    internal readonly Action<MousePanelEvent>? _onMouseDown;
    internal readonly Action<MousePanelEvent>? _onMouseUp;
    internal readonly Action<MousePanelEvent>? _onMouseMove;

    /// <summary>Called when clicked.</summary>
    public Action<MousePanelEvent>? OnClick      { init => _onClick      = value; }
    /// <summary>Called when the pointer enters.</summary>
    public Action<MousePanelEvent>? OnMouseEnter { init => _onMouseEnter = value; }
    /// <summary>Called when the pointer leaves.</summary>
    public Action<MousePanelEvent>? OnMouseLeave { init => _onMouseLeave = value; }
    /// <summary>Called when a mouse button is pressed.</summary>
    public Action<MousePanelEvent>? OnMouseDown  { init => _onMouseDown  = value; }
    /// <summary>Called when a mouse button is released.</summary>
    public Action<MousePanelEvent>? OnMouseUp    { init => _onMouseUp    = value; }
    /// <summary>Called when the pointer moves over it.</summary>
    public Action<MousePanelEvent>? OnMouseMove  { init => _onMouseMove  = value; }

    public Text(string content)
    {
        Content = content;
        Key = null;
        _style = StyleList.Empty;
    }

    public bool Equals(Text other)
        => Content == other.Content && Key == other.Key && StyleList.ContentsEqual(_style, other._style);

    public override int GetHashCode()
        => HashCode.Combine(Content, Key);
    // _style and event handlers intentionally excluded; mirrors Container per StructIntentEqualityTests.

    void IBlob.WriteTo(ref Frame frame)
    {
        frame.Kind = BlobKind.Text;
        frame.Key = Key;
        frame.Content = Content;
        frame.Style = _style;
        frame.Children = null;
        frame.Texture = null;
        frame.Path = null;
        frame.Scene = null;
        frame.RenderOnce = false;
        frame.Paused = false;
        frame.Color = null;
        frame.Shape = default;
        frame.Points = null;
        frame.Material = null;
        frame.Uniforms = ImmutableArray<UniformValue>.Empty;
        frame.Draw     = null;
        frame.Placeholder   = null;
        frame.MaxLength     = null;
        frame.Disabled      = false;
        frame.Numeric       = false;
        frame.MinValue      = null;
        frame.MaxValue      = null;
        frame.NumberFormat  = null;
        frame.Multiline     = false;
        frame.OnChange      = null;
        frame.OnSubmit      = null;
        frame.OnFocus       = null;
        frame.OnBlur        = null;
        frame.OnCancel      = null;
        frame.IsControlled  = false;
        frame.ValueAndInitialTextBothSet = false;
        frame.Events.OnClick      = _onClick;
        frame.Events.OnMouseEnter = _onMouseEnter;
        frame.Events.OnMouseLeave = _onMouseLeave;
        frame.Events.OnMouseDown  = _onMouseDown;
        frame.Events.OnMouseUp    = _onMouseUp;
        frame.Events.OnMouseMove  = _onMouseMove;
    }
}

/// <summary>
/// The general-purpose layout blob. Holds children and exposes the full style surface:
/// layout, flex, spacing, border, background, filters, transforms, and state variants.
/// </summary>
public readonly partial record struct Container : IBlob, IEnumerable
{
    public static BlobKind Kind => BlobKind.Container;
    /// <summary>Stable identity across rebuilds. Set it when this blob can change position among its siblings, so the reconciler matches it to the same panel.</summary>
    public string? Key { get; init; }
    internal readonly StyleList _style;
    internal readonly Children _children;

    internal readonly Action<MousePanelEvent>? _onClick;
    internal readonly Action<MousePanelEvent>? _onMouseEnter;
    internal readonly Action<MousePanelEvent>? _onMouseLeave;
    internal readonly Action<MousePanelEvent>? _onMouseDown;
    internal readonly Action<MousePanelEvent>? _onMouseUp;
    internal readonly Action<MousePanelEvent>? _onMouseMove;

    /// <summary>Called when clicked.</summary>
    public Action<MousePanelEvent>? OnClick      { init => _onClick      = value; }
    /// <summary>Called when the pointer enters.</summary>
    public Action<MousePanelEvent>? OnMouseEnter { init => _onMouseEnter = value; }
    /// <summary>Called when the pointer leaves.</summary>
    public Action<MousePanelEvent>? OnMouseLeave { init => _onMouseLeave = value; }
    /// <summary>Called when a mouse button is pressed.</summary>
    public Action<MousePanelEvent>? OnMouseDown  { init => _onMouseDown  = value; }
    /// <summary>Called when a mouse button is released.</summary>
    public Action<MousePanelEvent>? OnMouseUp    { init => _onMouseUp    = value; }
    /// <summary>Called when the pointer moves over it.</summary>
    public Action<MousePanelEvent>? OnMouseMove  { init => _onMouseMove  = value; }

    internal readonly Action<DragEvent>?  _onDragStart;
    internal readonly Action<DragEvent>?  _onDrag;
    internal readonly Action<DragEvent>?  _onDragEnd;
    internal readonly Action<PanelEvent>? _onDragEnter;
    internal readonly Action<PanelEvent>? _onDragLeave;
    internal readonly Action<PanelEvent>? _onDrop;

    /// <summary>Called when a drag begins on this blob.</summary>
    public Action<DragEvent>?  OnDragStart { init => _onDragStart = value; }
    /// <summary>Called on each frame of an in-flight drag.</summary>
    public Action<DragEvent>?  OnDrag      { init => _onDrag      = value; }
    /// <summary>Called when a drag started on this blob ends.</summary>
    public Action<DragEvent>?  OnDragEnd   { init => _onDragEnd   = value; }
    /// <summary>Called when a dragged item enters this blob as a drop target.</summary>
    public Action<PanelEvent>? OnDragEnter { init => _onDragEnter = value; }
    /// <summary>Called when a dragged item leaves this blob as a drop target.</summary>
    public Action<PanelEvent>? OnDragLeave { init => _onDragLeave = value; }
    /// <summary>Called when a dragged item is dropped on this blob.</summary>
    public Action<PanelEvent>? OnDrop      { init => _onDrop      = value; }

    internal readonly Material?                    _material;
    internal readonly ImmutableArray<UniformValue> _uniforms;
    internal readonly Action<CommandList, Rect>?   _draw;

    /// <summary>A custom material to render this blob with. Pair with <see cref="Uniforms"/> to feed shader parameters.</summary>
    public Material?                    Material { get => _material; init => _material = value; }
    /// <summary>Shader uniform values passed to <see cref="Material"/>.</summary>
    public ImmutableArray<UniformValue> Uniforms { get => _uniforms; init => _uniforms = value; }
    /// <summary>A direct draw callback, given a command list and the blob's rect, for custom rendering.</summary>
    public Action<CommandList, Rect>?   Draw     { get => _draw;     init => _draw     = value; }

    public Container()
    {
        if (BuildContext._current == null)
            throw new InvalidOperationException(
                "Container constructed outside Build(). Containers are short-lived: " +
                "build them inside your Build() method, or in a helper function called from Build(). " +
                "Static Container fields and instances held across rebuilds are not supported.");
        _children = BuildContext._current.RentChildren();
        _style = StyleList.Empty;
        Key = null;
        _material = null;
        _uniforms = ImmutableArray<UniformValue>.Empty;
        _draw = null;
    }

    /// <summary>The child blobs added to this container during Build().</summary>
    public Children Children => _children;

    /// <summary>Adds a child blob. Called by collection-initializer syntax inside Build().</summary>
    public void Add<T>(in T child) where T : struct, IBlob => _children.Add(in child);

    // IEnumerable is required by C# collection-initializer syntax; iteration is not a
    // use case for Container, so this returns an empty enumerator.
    IEnumerator IEnumerable.GetEnumerator() => Array.Empty<object>().GetEnumerator();


    public bool Equals(Container other)
        => Key == other.Key
        && StyleList.ContentsEqual(_style, other._style)
        && ReferenceEquals(_material, other._material)
        && _draw == other._draw
        && UniformValue.SequenceEqual(_uniforms, other._uniforms);

    public override int GetHashCode()
        => Key?.GetHashCode() ?? 0;
    // _style, _children, event handlers, _material, _uniforms, and _draw intentionally excluded
    // from GetHashCode; Equals governs correctness -- see StructIntentEqualityTests.

    void IBlob.WriteTo(ref Frame frame)
    {
        frame.Kind = BlobKind.Container;
        frame.Key = Key;
        frame.Content = "";
        frame.Style = _style;
        frame.Children = _children;
        frame.Texture = null;
        frame.Path = null;
        frame.Scene = null;
        frame.RenderOnce = false;
        frame.Paused = false;
        frame.Color = null;
        frame.Shape = default;
        frame.Points = null;
        frame.Material = _material;
        frame.Uniforms = _uniforms;
        frame.Draw     = _draw;
        frame.Placeholder   = null;
        frame.MaxLength     = null;
        frame.Disabled      = false;
        frame.Numeric       = false;
        frame.MinValue      = null;
        frame.MaxValue      = null;
        frame.NumberFormat  = null;
        frame.Multiline     = false;
        frame.OnChange      = null;
        frame.OnSubmit      = null;
        frame.OnFocus       = null;
        frame.OnBlur        = null;
        frame.OnCancel      = null;
        frame.IsControlled  = false;
        frame.ValueAndInitialTextBothSet = false;
        frame.Events.OnClick      = _onClick;
        frame.Events.OnMouseEnter = _onMouseEnter;
        frame.Events.OnMouseLeave = _onMouseLeave;
        frame.Events.OnMouseDown  = _onMouseDown;
        frame.Events.OnMouseUp    = _onMouseUp;
        frame.Events.OnMouseMove  = _onMouseMove;
        frame.Events.OnDragStart  = _onDragStart;
        frame.Events.OnDrag       = _onDrag;
        frame.Events.OnDragEnd    = _onDragEnd;
        frame.Events.OnDragEnter  = _onDragEnter;
        frame.Events.OnDragLeave  = _onDragLeave;
        frame.Events.OnDrop       = _onDrop;
    }
}

/// <summary>
/// Displays an image from a live <see cref="T:Sandbox.Texture"/> or an asset path. Set one source, never both.
/// Wrap in a <see cref="T:Goo.Container"/> for any style it does not expose.
/// </summary>
public readonly partial record struct Image : IBlob
{
    public static BlobKind Kind => BlobKind.Image;
    /// <summary>A live texture to display. Set this or <see cref="Path"/>, never both. Setting both renders nothing and warns.</summary>
    public Texture? Texture { get; init; }
    /// <summary>Path to an image asset, for example "ui/logo.png". Set this or <see cref="Texture"/>, never both. Setting both renders nothing and warns.</summary>
    public string?  Path    { get; init; }
    /// <summary>Stable identity across rebuilds. Set it when this blob can change position among its siblings, so the reconciler matches it to the same panel.</summary>
    public string?  Key     { get; init; }
    internal readonly StyleList _style;

    internal readonly Action<MousePanelEvent>? _onClick;
    internal readonly Action<MousePanelEvent>? _onMouseEnter;
    internal readonly Action<MousePanelEvent>? _onMouseLeave;
    internal readonly Action<MousePanelEvent>? _onMouseDown;
    internal readonly Action<MousePanelEvent>? _onMouseUp;
    internal readonly Action<MousePanelEvent>? _onMouseMove;

    /// <summary>Called when clicked.</summary>
    public Action<MousePanelEvent>? OnClick      { init => _onClick      = value; }
    /// <summary>Called when the pointer enters.</summary>
    public Action<MousePanelEvent>? OnMouseEnter { init => _onMouseEnter = value; }
    /// <summary>Called when the pointer leaves.</summary>
    public Action<MousePanelEvent>? OnMouseLeave { init => _onMouseLeave = value; }
    /// <summary>Called when a mouse button is pressed.</summary>
    public Action<MousePanelEvent>? OnMouseDown  { init => _onMouseDown  = value; }
    /// <summary>Called when a mouse button is released.</summary>
    public Action<MousePanelEvent>? OnMouseUp    { init => _onMouseUp    = value; }
    /// <summary>Called when the pointer moves over it.</summary>
    public Action<MousePanelEvent>? OnMouseMove  { init => _onMouseMove  = value; }

    public Image()
    {
        Texture = null;
        Path = null;
        Key = null;
        _style = StyleList.Empty;
    }

    public bool Equals(Image other)
        => ReferenceEquals(Texture, other.Texture)
        && Path == other.Path
        && Key == other.Key
        && StyleList.ContentsEqual(_style, other._style);

    public override int GetHashCode()
        => HashCode.Combine(Texture, Path, Key);
    // _style and event handlers intentionally excluded; mirrors Text/Container per StructIntentEqualityTests.

    void IBlob.WriteTo(ref Frame frame)
    {
        frame.Kind = BlobKind.Image;
        frame.Key = Key;
        frame.Content = "";
        frame.Style = _style;
        frame.Children = null;
        frame.Texture = Texture;
        frame.Path = Path;
        frame.Scene = null;
        frame.RenderOnce = false;
        frame.Paused = false;
        frame.Color = null;
        frame.Shape = default;
        frame.Points = null;
        frame.Material = null;
        frame.Uniforms = ImmutableArray<UniformValue>.Empty;
        frame.Draw     = null;
        frame.Placeholder   = null;
        frame.MaxLength     = null;
        frame.Disabled      = false;
        frame.Numeric       = false;
        frame.MinValue      = null;
        frame.MaxValue      = null;
        frame.NumberFormat  = null;
        frame.Multiline     = false;
        frame.OnChange      = null;
        frame.OnSubmit      = null;
        frame.OnFocus       = null;
        frame.OnBlur        = null;
        frame.OnCancel      = null;
        frame.IsControlled  = false;
        frame.ValueAndInitialTextBothSet = false;
        frame.Events.OnClick      = _onClick;
        frame.Events.OnMouseEnter = _onMouseEnter;
        frame.Events.OnMouseLeave = _onMouseLeave;
        frame.Events.OnMouseDown  = _onMouseDown;
        frame.Events.OnMouseUp    = _onMouseUp;
        frame.Events.OnMouseMove  = _onMouseMove;
    }
}

/// <summary>Renders a 3D <see cref="T:Sandbox.Scene"/> into the UI. Provide a live scene or a scene path.</summary>
public readonly partial record struct ScenePanel : IBlob
{
    public static BlobKind Kind => BlobKind.ScenePanel;
    /// <summary>A live scene to render. Set this or <see cref="ScenePath"/>.</summary>
    public Scene?  Scene      { get; init; }
    /// <summary>Path to a scene asset to render. Set this or <see cref="Scene"/>.</summary>
    public string? ScenePath  { get; init; }
    /// <summary>When true, renders the scene a single frame instead of every frame. Use for static previews.</summary>
    public bool    RenderOnce { get; init; }
    /// <summary>Stable identity across rebuilds. Set it when this blob can change position among its siblings, so the reconciler matches it to the same panel.</summary>
    public string? Key        { get; init; }
    internal readonly StyleList _style;

    internal readonly Action<MousePanelEvent>? _onClick;
    internal readonly Action<MousePanelEvent>? _onMouseEnter;
    internal readonly Action<MousePanelEvent>? _onMouseLeave;
    internal readonly Action<MousePanelEvent>? _onMouseDown;
    internal readonly Action<MousePanelEvent>? _onMouseUp;
    internal readonly Action<MousePanelEvent>? _onMouseMove;

    /// <summary>Called when clicked.</summary>
    public Action<MousePanelEvent>? OnClick      { init => _onClick      = value; }
    /// <summary>Called when the pointer enters.</summary>
    public Action<MousePanelEvent>? OnMouseEnter { init => _onMouseEnter = value; }
    /// <summary>Called when the pointer leaves.</summary>
    public Action<MousePanelEvent>? OnMouseLeave { init => _onMouseLeave = value; }
    /// <summary>Called when a mouse button is pressed.</summary>
    public Action<MousePanelEvent>? OnMouseDown  { init => _onMouseDown  = value; }
    /// <summary>Called when a mouse button is released.</summary>
    public Action<MousePanelEvent>? OnMouseUp    { init => _onMouseUp    = value; }
    /// <summary>Called when the pointer moves over it.</summary>
    public Action<MousePanelEvent>? OnMouseMove  { init => _onMouseMove  = value; }

    public ScenePanel()
    {
        Scene = null;
        ScenePath = null;
        RenderOnce = false;
        Key = null;
        _style = StyleList.Empty;
    }

    public bool Equals(ScenePanel other)
        => ReferenceEquals(Scene, other.Scene)
        && ScenePath == other.ScenePath
        && RenderOnce == other.RenderOnce
        && Key == other.Key
        && StyleList.ContentsEqual(_style, other._style);

    public override int GetHashCode()
        => HashCode.Combine(Scene, ScenePath, RenderOnce, Key);
    // _style and event handlers intentionally excluded; mirrors Image per StructIntentEqualityTests.

    void IBlob.WriteTo(ref Frame frame)
    {
        frame.Kind = BlobKind.ScenePanel;
        frame.Key = Key;
        frame.Content = "";
        frame.Style = _style;
        frame.Children = null;
        frame.Texture = null;
        frame.Scene = Scene;
        frame.Path = ScenePath;
        frame.RenderOnce = RenderOnce;
        frame.Paused = false;
        frame.Color = null;
        frame.Shape = default;
        frame.Points = null;
        frame.Material = null;
        frame.Uniforms = ImmutableArray<UniformValue>.Empty;
        frame.Draw     = null;
        frame.Placeholder   = null;
        frame.MaxLength     = null;
        frame.Disabled      = false;
        frame.Numeric       = false;
        frame.MinValue      = null;
        frame.MaxValue      = null;
        frame.NumberFormat  = null;
        frame.Multiline     = false;
        frame.OnChange      = null;
        frame.OnSubmit      = null;
        frame.OnFocus       = null;
        frame.OnBlur        = null;
        frame.OnCancel      = null;
        frame.IsControlled  = false;
        frame.ValueAndInitialTextBothSet = false;
        frame.Events.OnClick      = _onClick;
        frame.Events.OnMouseEnter = _onMouseEnter;
        frame.Events.OnMouseLeave = _onMouseLeave;
        frame.Events.OnMouseDown  = _onMouseDown;
        frame.Events.OnMouseUp    = _onMouseUp;
        frame.Events.OnMouseMove  = _onMouseMove;
    }
}

/// <summary>Renders an SVG asset, with an optional color override.</summary>
public readonly partial record struct SvgPanel : IBlob
{
    public static BlobKind Kind => BlobKind.SvgPanel;
    /// <summary>Path to the SVG asset to render, for example "ui/icon.svg".</summary>
    public string? Path  { get; init; }
    /// <summary>Optional color to tint the SVG, as a CSS color string. Leave null to use the SVG's own colors.</summary>
    public string? Color { get; init; }
    /// <summary>Stable identity across rebuilds. Set it when this blob can change position among its siblings, so the reconciler matches it to the same panel.</summary>
    public string? Key   { get; init; }
    internal readonly StyleList _style;

    internal readonly Action<MousePanelEvent>? _onClick;
    internal readonly Action<MousePanelEvent>? _onMouseEnter;
    internal readonly Action<MousePanelEvent>? _onMouseLeave;
    internal readonly Action<MousePanelEvent>? _onMouseDown;
    internal readonly Action<MousePanelEvent>? _onMouseUp;
    internal readonly Action<MousePanelEvent>? _onMouseMove;

    /// <summary>Called when clicked.</summary>
    public Action<MousePanelEvent>? OnClick      { init => _onClick      = value; }
    /// <summary>Called when the pointer enters.</summary>
    public Action<MousePanelEvent>? OnMouseEnter { init => _onMouseEnter = value; }
    /// <summary>Called when the pointer leaves.</summary>
    public Action<MousePanelEvent>? OnMouseLeave { init => _onMouseLeave = value; }
    /// <summary>Called when a mouse button is pressed.</summary>
    public Action<MousePanelEvent>? OnMouseDown  { init => _onMouseDown  = value; }
    /// <summary>Called when a mouse button is released.</summary>
    public Action<MousePanelEvent>? OnMouseUp    { init => _onMouseUp    = value; }
    /// <summary>Called when the pointer moves over it.</summary>
    public Action<MousePanelEvent>? OnMouseMove  { init => _onMouseMove  = value; }

    public SvgPanel()
    {
        Path = null;
        Color = null;
        Key = null;
        _style = StyleList.Empty;
    }

    public bool Equals(SvgPanel other)
        => Path == other.Path
        && Color == other.Color
        && Key == other.Key
        && StyleList.ContentsEqual(_style, other._style);

    public override int GetHashCode()
        => HashCode.Combine(Path, Color, Key);

    void IBlob.WriteTo(ref Frame frame)
    {
        frame.Kind = BlobKind.SvgPanel;
        frame.Key = Key;
        frame.Content = "";
        frame.Style = _style;
        frame.Children = null;
        frame.Texture = null;
        frame.Path = Path;
        frame.Color = Color;
        frame.Scene = null;
        frame.RenderOnce = false;
        frame.Paused = false;
        frame.Shape = default;
        frame.Points = null;
        frame.Material = null;
        frame.Uniforms = ImmutableArray<UniformValue>.Empty;
        frame.Draw     = null;
        frame.Placeholder   = null;
        frame.MaxLength     = null;
        frame.Disabled      = false;
        frame.Numeric       = false;
        frame.MinValue      = null;
        frame.MaxValue      = null;
        frame.NumberFormat  = null;
        frame.Multiline     = false;
        frame.OnChange      = null;
        frame.OnSubmit      = null;
        frame.OnFocus       = null;
        frame.OnBlur        = null;
        frame.OnCancel      = null;
        frame.IsControlled  = false;
        frame.ValueAndInitialTextBothSet = false;
        frame.Events.OnClick      = _onClick;
        frame.Events.OnMouseEnter = _onMouseEnter;
        frame.Events.OnMouseLeave = _onMouseLeave;
        frame.Events.OnMouseDown  = _onMouseDown;
        frame.Events.OnMouseUp    = _onMouseUp;
        frame.Events.OnMouseMove  = _onMouseMove;
    }
}

/// <summary>A pie wedge or ring segment. Spans from <see cref="StartAngle"/> to <see cref="EndAngle"/>, between <see cref="InnerRadius"/> and <see cref="OuterRadius"/>.</summary>
public readonly partial record struct Sector : IBlob
{
    public static BlobKind Kind => BlobKind.Sector;
    /// <summary>Angle where the wedge begins, in degrees.</summary>
    public float   StartAngle  { get; init; }
    /// <summary>Angle where the wedge ends, in degrees.</summary>
    public float   EndAngle    { get; init; }
    /// <summary>Inner edge of the ring, as a fraction (0 to 1) of the frame box. 0 gives a solid wedge.</summary>
    public float   InnerRadius { get; init; }
    /// <summary>Outer edge of the ring, as a fraction (0 to 1) of the frame box.</summary>
    public float   OuterRadius { get; init; }
    /// <summary>Corner rounding, as a fraction (0 to 1) of the outer radius. 0 gives sharp corners.</summary>
    public float   CornerRadius { get; init; }
    /// <summary>Stable identity across rebuilds. Set it when this blob can change position among its siblings, so the reconciler matches it to the same panel.</summary>
    public string? Key         { get; init; }
    internal readonly StyleList _style;

    internal readonly Action<MousePanelEvent>? _onClick;
    internal readonly Action<MousePanelEvent>? _onMouseEnter;
    internal readonly Action<MousePanelEvent>? _onMouseLeave;
    internal readonly Action<MousePanelEvent>? _onMouseDown;
    internal readonly Action<MousePanelEvent>? _onMouseUp;
    internal readonly Action<MousePanelEvent>? _onMouseMove;

    /// <summary>Called when clicked.</summary>
    public Action<MousePanelEvent>? OnClick      { init => _onClick      = value; }
    /// <summary>Called when the pointer enters.</summary>
    public Action<MousePanelEvent>? OnMouseEnter { init => _onMouseEnter = value; }
    /// <summary>Called when the pointer leaves.</summary>
    public Action<MousePanelEvent>? OnMouseLeave { init => _onMouseLeave = value; }
    /// <summary>Called when a mouse button is pressed.</summary>
    public Action<MousePanelEvent>? OnMouseDown  { init => _onMouseDown  = value; }
    /// <summary>Called when a mouse button is released.</summary>
    public Action<MousePanelEvent>? OnMouseUp    { init => _onMouseUp    = value; }
    /// <summary>Called when the pointer moves over it.</summary>
    public Action<MousePanelEvent>? OnMouseMove  { init => _onMouseMove  = value; }

    public Sector()
    {
        StartAngle = 0f;
        EndAngle = 0f;
        InnerRadius = 0f;
        OuterRadius = 0f;
        CornerRadius = 0f;
        Key = null;
        _style = StyleList.Empty;
    }

    public bool Equals(Sector other)
        => StartAngle == other.StartAngle
        && EndAngle == other.EndAngle
        && InnerRadius == other.InnerRadius
        && OuterRadius == other.OuterRadius
        && CornerRadius == other.CornerRadius
        && Key == other.Key
        && StyleList.ContentsEqual(_style, other._style);

    public override int GetHashCode()
        => HashCode.Combine(StartAngle, EndAngle, InnerRadius, OuterRadius, CornerRadius, Key);

    void IBlob.WriteTo(ref Frame frame)
    {
        frame.Kind = BlobKind.Sector;
        frame.Key = Key;
        frame.Content = "";
        frame.Style = _style;
        frame.Children = null;
        frame.Texture = null;
        frame.Path = null;
        frame.Scene = null;
        frame.RenderOnce = false;
        frame.Paused = false;
        frame.Color = null;
        frame.Shape = new ShapeParams { A = StartAngle, B = EndAngle, C = InnerRadius, D = OuterRadius, E = CornerRadius };
        frame.Points = null;
        frame.Material = null;
        frame.Uniforms = ImmutableArray<UniformValue>.Empty;
        frame.Draw     = null;
        frame.Placeholder   = null;
        frame.MaxLength     = null;
        frame.Disabled      = false;
        frame.Numeric       = false;
        frame.MinValue      = null;
        frame.MaxValue      = null;
        frame.NumberFormat  = null;
        frame.Multiline     = false;
        frame.OnChange      = null;
        frame.OnSubmit      = null;
        frame.OnFocus       = null;
        frame.OnBlur        = null;
        frame.OnCancel      = null;
        frame.IsControlled  = false;
        frame.ValueAndInitialTextBothSet = false;
        frame.Events.OnClick      = _onClick;
        frame.Events.OnMouseEnter = _onMouseEnter;
        frame.Events.OnMouseLeave = _onMouseLeave;
        frame.Events.OnMouseDown  = _onMouseDown;
        frame.Events.OnMouseUp    = _onMouseUp;
        frame.Events.OnMouseMove  = _onMouseMove;
    }
}

/// <summary>A stroked arc line. Sweeps from <see cref="StartAngle"/> to <see cref="EndAngle"/> at a fixed <see cref="Radius"/>.</summary>
public readonly partial record struct Arc : IBlob
{
    public static BlobKind Kind => BlobKind.Arc;
    /// <summary>Angle where the arc begins, in degrees.</summary>
    public float   StartAngle  { get; init; }
    /// <summary>Angle where the arc ends, in degrees.</summary>
    public float   EndAngle    { get; init; }
    /// <summary>Radius of the arc, as a fraction (0 to 1) of the frame box.</summary>
    public float   Radius      { get; init; }
    /// <summary>Thickness of the arc stroke.</summary>
    public float   StrokeWidth { get; init; }
    /// <summary>Stable identity across rebuilds. Set it when this blob can change position among its siblings, so the reconciler matches it to the same panel.</summary>
    public string? Key         { get; init; }
    internal readonly StyleList _style;

    internal readonly Action<MousePanelEvent>? _onClick;
    internal readonly Action<MousePanelEvent>? _onMouseEnter;
    internal readonly Action<MousePanelEvent>? _onMouseLeave;
    internal readonly Action<MousePanelEvent>? _onMouseDown;
    internal readonly Action<MousePanelEvent>? _onMouseUp;
    internal readonly Action<MousePanelEvent>? _onMouseMove;

    /// <summary>Called when clicked.</summary>
    public Action<MousePanelEvent>? OnClick      { init => _onClick      = value; }
    /// <summary>Called when the pointer enters.</summary>
    public Action<MousePanelEvent>? OnMouseEnter { init => _onMouseEnter = value; }
    /// <summary>Called when the pointer leaves.</summary>
    public Action<MousePanelEvent>? OnMouseLeave { init => _onMouseLeave = value; }
    /// <summary>Called when a mouse button is pressed.</summary>
    public Action<MousePanelEvent>? OnMouseDown  { init => _onMouseDown  = value; }
    /// <summary>Called when a mouse button is released.</summary>
    public Action<MousePanelEvent>? OnMouseUp    { init => _onMouseUp    = value; }
    /// <summary>Called when the pointer moves over it.</summary>
    public Action<MousePanelEvent>? OnMouseMove  { init => _onMouseMove  = value; }

    public Arc()
    {
        StartAngle = 0f;
        EndAngle = 0f;
        Radius = 0f;
        StrokeWidth = 0f;
        Key = null;
        _style = StyleList.Empty;
    }

    public bool Equals(Arc other)
        => StartAngle == other.StartAngle
        && EndAngle == other.EndAngle
        && Radius == other.Radius
        && StrokeWidth == other.StrokeWidth
        && Key == other.Key
        && StyleList.ContentsEqual(_style, other._style);

    public override int GetHashCode()
        => HashCode.Combine(StartAngle, EndAngle, Radius, StrokeWidth, Key);

    void IBlob.WriteTo(ref Frame frame)
    {
        frame.Kind = BlobKind.Arc;
        frame.Key = Key;
        frame.Content = "";
        frame.Style = _style;
        frame.Children = null;
        frame.Texture = null;
        frame.Path = null;
        frame.Scene = null;
        frame.RenderOnce = false;
        frame.Paused = false;
        frame.Color = null;
        frame.Shape = new ShapeParams { A = StartAngle, B = EndAngle, C = Radius, D = StrokeWidth };
        frame.Points = null;
        frame.Material = null;
        frame.Uniforms = ImmutableArray<UniformValue>.Empty;
        frame.Draw     = null;
        frame.Placeholder   = null;
        frame.MaxLength     = null;
        frame.Disabled      = false;
        frame.Numeric       = false;
        frame.MinValue      = null;
        frame.MaxValue      = null;
        frame.NumberFormat  = null;
        frame.Multiline     = false;
        frame.OnChange      = null;
        frame.OnSubmit      = null;
        frame.OnFocus       = null;
        frame.OnBlur        = null;
        frame.OnCancel      = null;
        frame.IsControlled  = false;
        frame.ValueAndInitialTextBothSet = false;
        frame.Events.OnClick      = _onClick;
        frame.Events.OnMouseEnter = _onMouseEnter;
        frame.Events.OnMouseLeave = _onMouseLeave;
        frame.Events.OnMouseDown  = _onMouseDown;
        frame.Events.OnMouseUp    = _onMouseUp;
        frame.Events.OnMouseMove  = _onMouseMove;
    }
}

// Polygon: arbitrary-vertex filled shape baked into a 512x512 alpha mask. Unit coords in [0,1], treated as immutable (the cache key holds the reference). Click hit-testing uses the bounding rect (engine limitation).
/// <summary>A filled shape defined by arbitrary vertices in unit coordinates.</summary>
public readonly partial record struct Polygon : IBlob
{
    public static BlobKind Kind => BlobKind.Polygon;
    /// <summary>The vertices, in unit coordinates from 0 to 1 (origin top-left, x right, y down). Treated as immutable: do not mutate the array after assigning it.</summary>
    public Vector2[]? Points { get; init; }
    /// <summary>Stable identity across rebuilds. Set it when this blob can change position among its siblings, so the reconciler matches it to the same panel.</summary>
    public string?    Key    { get; init; }
    internal readonly StyleList _style;

    internal readonly Action<MousePanelEvent>? _onClick;
    internal readonly Action<MousePanelEvent>? _onMouseEnter;
    internal readonly Action<MousePanelEvent>? _onMouseLeave;
    internal readonly Action<MousePanelEvent>? _onMouseDown;
    internal readonly Action<MousePanelEvent>? _onMouseUp;
    internal readonly Action<MousePanelEvent>? _onMouseMove;

    /// <summary>Called when clicked.</summary>
    public Action<MousePanelEvent>? OnClick      { init => _onClick      = value; }
    /// <summary>Called when the pointer enters.</summary>
    public Action<MousePanelEvent>? OnMouseEnter { init => _onMouseEnter = value; }
    /// <summary>Called when the pointer leaves.</summary>
    public Action<MousePanelEvent>? OnMouseLeave { init => _onMouseLeave = value; }
    /// <summary>Called when a mouse button is pressed.</summary>
    public Action<MousePanelEvent>? OnMouseDown  { init => _onMouseDown  = value; }
    /// <summary>Called when a mouse button is released.</summary>
    public Action<MousePanelEvent>? OnMouseUp    { init => _onMouseUp    = value; }
    /// <summary>Called when the pointer moves over it.</summary>
    public Action<MousePanelEvent>? OnMouseMove  { init => _onMouseMove  = value; }

    public Polygon()
    {
        Points = null;
        Key = null;
        _style = StyleList.Empty;
    }

    public bool Equals(Polygon other)
        => ReferenceEquals(Points, other.Points)
        && Key == other.Key
        && StyleList.ContentsEqual(_style, other._style);

    public override int GetHashCode()
        => HashCode.Combine(Points, Key);

    void IBlob.WriteTo(ref Frame frame)
    {
        frame.Kind = BlobKind.Polygon;
        frame.Key = Key;
        frame.Content = "";
        frame.Style = _style;
        frame.Children = null;
        frame.Texture = null;
        frame.Path = null;
        frame.Scene = null;
        frame.RenderOnce = false;
        frame.Paused = false;
        frame.Color = null;
        frame.Shape = default;
        frame.Points = Points;
        frame.Material = null;
        frame.Uniforms = ImmutableArray<UniformValue>.Empty;
        frame.Draw     = null;
        frame.Placeholder   = null;
        frame.MaxLength     = null;
        frame.Disabled      = false;
        frame.Numeric       = false;
        frame.MinValue      = null;
        frame.MaxValue      = null;
        frame.NumberFormat  = null;
        frame.Multiline     = false;
        frame.OnChange      = null;
        frame.OnSubmit      = null;
        frame.OnFocus       = null;
        frame.OnBlur        = null;
        frame.OnCancel      = null;
        frame.IsControlled  = false;
        frame.ValueAndInitialTextBothSet = false;
        frame.Events.OnClick      = _onClick;
        frame.Events.OnMouseEnter = _onMouseEnter;
        frame.Events.OnMouseLeave = _onMouseLeave;
        frame.Events.OnMouseDown  = _onMouseDown;
        frame.Events.OnMouseUp    = _onMouseUp;
        frame.Events.OnMouseMove  = _onMouseMove;
    }
}

// WebPanel: embeds an engine Chromium webview. The Url is the only content surface;
// background-image and cursor are owned by the engine WebPanel itself per frame, so
// the common-only facade keeps both out by construction.
/// <summary>Embeds a web view via the engine's Chromium webview. <see cref="Url"/> is its only content surface.</summary>
public readonly partial record struct WebPanel : IBlob
{
    public static BlobKind Kind => BlobKind.WebPanel;
    /// <summary>The URL to load in the web view.</summary>
    public string? Url { get; init; }
    // Maps to engine WebSurface.InBackgroundMode.
    /// <summary>When true, throttles the view's script and repaint and pauses its media. Use for offscreen or hidden views.</summary>
    public bool Paused { get; init; }
    /// <summary>Stable identity across rebuilds. Set it when this blob can change position among its siblings, so the reconciler matches it to the same panel.</summary>
    public string? Key { get; init; }
    internal readonly StyleList _style;

    internal readonly Action<MousePanelEvent>? _onClick;
    internal readonly Action<MousePanelEvent>? _onMouseEnter;
    internal readonly Action<MousePanelEvent>? _onMouseLeave;
    internal readonly Action<MousePanelEvent>? _onMouseDown;
    internal readonly Action<MousePanelEvent>? _onMouseUp;
    internal readonly Action<MousePanelEvent>? _onMouseMove;

    /// <summary>Called when clicked.</summary>
    public Action<MousePanelEvent>? OnClick      { init => _onClick      = value; }
    /// <summary>Called when the pointer enters.</summary>
    public Action<MousePanelEvent>? OnMouseEnter { init => _onMouseEnter = value; }
    /// <summary>Called when the pointer leaves.</summary>
    public Action<MousePanelEvent>? OnMouseLeave { init => _onMouseLeave = value; }
    /// <summary>Called when a mouse button is pressed.</summary>
    public Action<MousePanelEvent>? OnMouseDown  { init => _onMouseDown  = value; }
    /// <summary>Called when a mouse button is released.</summary>
    public Action<MousePanelEvent>? OnMouseUp    { init => _onMouseUp    = value; }
    /// <summary>Called when the pointer moves over it.</summary>
    public Action<MousePanelEvent>? OnMouseMove  { init => _onMouseMove  = value; }

    public WebPanel()
    {
        Url = null;
        Paused = false;
        Key = null;
        _style = StyleList.Empty;
    }

    public bool Equals(WebPanel other)
        => Url == other.Url
        && Paused == other.Paused
        && Key == other.Key
        && StyleList.ContentsEqual(_style, other._style);

    public override int GetHashCode()
        => HashCode.Combine(Url, Paused, Key);

    void IBlob.WriteTo(ref Frame frame)
    {
        frame.Kind = BlobKind.WebPanel;
        frame.Key = Key;
        frame.Content = "";
        frame.Style = _style;
        frame.Children = null;
        frame.Texture = null;
        frame.Path = Url;
        frame.Scene = null;
        frame.RenderOnce = false;
        frame.Paused = Paused;
        frame.Color = null;
        frame.Shape = default;
        frame.Points = null;
        frame.Material = null;
        frame.Uniforms = ImmutableArray<UniformValue>.Empty;
        frame.Draw     = null;
        frame.Placeholder   = null;
        frame.MaxLength     = null;
        frame.Disabled      = false;
        frame.Numeric       = false;
        frame.MinValue      = null;
        frame.MaxValue      = null;
        frame.NumberFormat  = null;
        frame.Multiline     = false;
        frame.OnChange      = null;
        frame.OnSubmit      = null;
        frame.OnFocus       = null;
        frame.OnBlur        = null;
        frame.OnCancel      = null;
        frame.IsControlled  = false;
        frame.ValueAndInitialTextBothSet = false;
        frame.Events.OnClick      = _onClick;
        frame.Events.OnMouseEnter = _onMouseEnter;
        frame.Events.OnMouseLeave = _onMouseLeave;
        frame.Events.OnMouseDown  = _onMouseDown;
        frame.Events.OnMouseUp    = _onMouseUp;
        frame.Events.OnMouseMove  = _onMouseMove;
    }
}

/// <summary>A text input wrapping the engine's TextEntry. Set <see cref="Value"/> for a controlled field driven by your state, or <see cref="InitialText"/> for an uncontrolled one; never both.</summary>
public readonly partial record struct TextEntry : IBlob
{
    public static BlobKind Kind => BlobKind.TextEntry;

    /// <summary>The field text in controlled mode, re-emitted each render to track your state. Set this or <see cref="InitialText"/>, never both.</summary>
    public string? Value        { get; init; }
    /// <summary>The starting text in uncontrolled mode, set once when the field is created. Set this or <see cref="Value"/>, never both.</summary>
    public string? InitialText  { get; init; }
    /// <summary>Hint text shown when the field is empty.</summary>
    public string? Placeholder  { get; init; }
    /// <summary>Maximum number of characters allowed. Null for no limit.</summary>
    public int?    MaxLength    { get; init; }
    /// <summary>When true, the field is read-only and does not accept input.</summary>
    public bool    Disabled     { get; init; }
    /// <summary>When true, the field accepts and wraps multiple lines.</summary>
    public bool    Multiline    { get; init; }
    /// <summary>When true, the field accepts numeric input only.</summary>
    public bool    Numeric      { get; init; }
    /// <summary>Smallest accepted value when <see cref="Numeric"/> is true.</summary>
    public float?  MinValue     { get; init; }
    /// <summary>Largest accepted value when <see cref="Numeric"/> is true.</summary>
    public float?  MaxValue     { get; init; }
    /// <summary>Format string applied to the value when <see cref="Numeric"/> is true.</summary>
    public string? NumberFormat { get; init; }
    /// <summary>Stable identity across rebuilds. Set it when this blob can change position among its siblings, so the reconciler matches it to the same panel.</summary>
    public string? Key          { get; init; }

    internal readonly StyleList _style;

    internal readonly Action<string>? _onChange;
    internal readonly Action<string>? _onSubmit;
    internal readonly Action? _onFocus;
    internal readonly Action<string>? _onBlur;
    internal readonly Action? _onCancel;
    internal readonly Action<MousePanelEvent>? _onClick;
    internal readonly Action<MousePanelEvent>? _onMouseEnter;
    internal readonly Action<MousePanelEvent>? _onMouseLeave;
    internal readonly Action<MousePanelEvent>? _onMouseDown;
    internal readonly Action<MousePanelEvent>? _onMouseUp;
    internal readonly Action<MousePanelEvent>? _onMouseMove;

    /// <summary>Called on each edit, with the new text.</summary>
    public Action<string>?           OnChange     { init => _onChange     = value; }
    /// <summary>Called when the user submits the field, for example by pressing Enter, with the text.</summary>
    public Action<string>?           OnSubmit     { init => _onSubmit     = value; }
    /// <summary>Called when the field gains focus.</summary>
    public Action?                   OnFocus      { init => _onFocus      = value; }
    /// <summary>Called when the field loses focus, with its final text.</summary>
    public Action<string>?           OnBlur       { init => _onBlur       = value; }
    /// <summary>Called when the user cancels editing, for example by pressing Escape.</summary>
    public Action?                   OnCancel     { init => _onCancel     = value; }
    /// <summary>Called when clicked.</summary>
    public Action<MousePanelEvent>?  OnClick      { init => _onClick      = value; }
    /// <summary>Called when the pointer enters.</summary>
    public Action<MousePanelEvent>?  OnMouseEnter { init => _onMouseEnter = value; }
    /// <summary>Called when the pointer leaves.</summary>
    public Action<MousePanelEvent>?  OnMouseLeave { init => _onMouseLeave = value; }
    /// <summary>Called when a mouse button is pressed.</summary>
    public Action<MousePanelEvent>?  OnMouseDown  { init => _onMouseDown  = value; }
    /// <summary>Called when a mouse button is released.</summary>
    public Action<MousePanelEvent>?  OnMouseUp    { init => _onMouseUp    = value; }
    /// <summary>Called when the pointer moves over it.</summary>
    public Action<MousePanelEvent>?  OnMouseMove  { init => _onMouseMove  = value; }

    public TextEntry()
    {
        Value = null;
        InitialText = null;
        Placeholder = null;
        MaxLength = null;
        Disabled = false;
        Multiline = false;
        Numeric = false;
        MinValue = null;
        MaxValue = null;
        NumberFormat = null;
        Key = null;
        _style = StyleList.Empty;
    }

    public bool Equals(TextEntry other)
        => Value == other.Value
        && InitialText == other.InitialText
        && Placeholder == other.Placeholder
        && MaxLength == other.MaxLength
        && Disabled == other.Disabled
        && Multiline == other.Multiline
        && Numeric == other.Numeric
        && MinValue == other.MinValue
        && MaxValue == other.MaxValue
        && NumberFormat == other.NumberFormat
        && Key == other.Key
        && StyleList.ContentsEqual(_style, other._style);

    public override int GetHashCode()
        => HashCode.Combine(Value, InitialText, Placeholder, MaxLength, Disabled, Numeric, Multiline, Key);

    void IBlob.WriteTo(ref Frame frame)
    {
        frame.Kind = BlobKind.TextEntry;
        frame.Key = Key;
        frame.Content = "";
        frame.Style = _style;
        frame.Children = null;
        frame.Texture = null;
        // Path slot carries the text value (controlled-or-initial). Reconciler reads
        // IsControlled to decide whether to re-emit on Update.
        frame.Path = Value ?? InitialText;
        frame.IsControlled = Value is not null;
        frame.ValueAndInitialTextBothSet = Value is not null && InitialText is not null;
        frame.Scene = null;
        frame.RenderOnce = false;
        frame.Paused = false;
        frame.Color = null;
        frame.Shape = default;
        frame.Points = null;
        frame.Material = null;
        frame.Uniforms = ImmutableArray<UniformValue>.Empty;
        frame.Draw = null;
        frame.Placeholder = Placeholder;
        frame.MaxLength = MaxLength;
        frame.Disabled = Disabled;
        frame.Numeric = Numeric;
        frame.MinValue = MinValue;
        frame.MaxValue = MaxValue;
        frame.NumberFormat = NumberFormat;
        frame.Multiline = Multiline;
        frame.OnChange = _onChange;
        frame.OnSubmit = _onSubmit;
        frame.OnFocus = _onFocus;
        frame.OnBlur = _onBlur;
        frame.OnCancel = _onCancel;
        frame.Events.OnClick      = _onClick;
        frame.Events.OnMouseEnter = _onMouseEnter;
        frame.Events.OnMouseLeave = _onMouseLeave;
        frame.Events.OnMouseDown  = _onMouseDown;
        frame.Events.OnMouseUp    = _onMouseUp;
        frame.Events.OnMouseMove  = _onMouseMove;
    }
}