Internal/StatefulShapePanel.cs

A UI panel that can render shape masks either via a GPU SDF shader (for arcs/sectors) or a baked polygon texture, and that hosts mouse event handlers with deferred rebuild requests. It applies shader uniforms, updates a ShaderEffect, draws a single quad when using the SDF path, and dispatches mouse events through an EventDispatch helper.

Native Interop
using System;
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;

namespace Goo.Internal;

internal sealed class StatefulShapePanel : Panel, IPanelDraw, IStatefulEventHost
{
    internal Action<MousePanelEvent>? _onClick;
    internal Action<MousePanelEvent>? _onRightClick;
    internal Action<MousePanelEvent>? _onMiddleClick;
    internal Action<MousePanelEvent>? _onMouseEnter;
    internal Action<MousePanelEvent>? _onMouseLeave;
    internal Action<MousePanelEvent>? _onMouseDown;
    internal Action<MousePanelEvent>? _onMouseUp;
    internal Action<MousePanelEvent>? _onMouseMove;
    internal bool    _userSetPointerEvents;
    internal Action? _requestRebuild;
    public Action? RequestRebuild { set => _requestRebuild = value; }

    // Sector/Arc render via ui_shape.shader (GPU SDF) instead of a CPU-baked mask: no per-frame
    // re-bake on animated geometry, no fixed-resolution texture, crisp at any size. Polygon still
    // bakes a BackgroundImage mask (variable point count is awkward as a uniform; it rarely animates).
    ShaderEffect? _shapeEffect;
    Color _shapeColor = Color.White;

    public void ApplyEvents(in BlobEvents events)
    {
        _onClick      = events.OnClick;
        _onRightClick = events.OnRightClick;
        _onMiddleClick = events.OnMiddleClick;
        _onMouseEnter = events.OnMouseEnter;
        _onMouseLeave = events.OnMouseLeave;
        _onMouseDown  = events.OnMouseDown;
        _onMouseUp    = events.OnMouseUp;
        _onMouseMove  = events.OnMouseMove;
    }

    public bool HasEventHandlers =>
        _onClick != null || _onRightClick != null || _onMiddleClick != null || _onMouseEnter != null || _onMouseLeave != null ||
        _onMouseDown != null || _onMouseUp != null || _onMouseMove != null;

    public bool UserSetPointerEvents
    {
        get => _userSetPointerEvents;
        set => _userSetPointerEvents = value;
    }

    // Sector/Arc: drive the SDF shader's uniforms. Color arrives separately via SetShapeColor (it
    // comes from the style's BackgroundColor, not the shape params). DrawQuad passes Color.White, so
    // the colour has to be a uniform rather than a background tint.
    public void ApplyShape(BlobKind kind, in ShapeParams shape)
    {
        float inner, outer, corner;
        if (kind == BlobKind.Arc)
        {
            // Arc params are (StartAngle, EndAngle, centre Radius, Stroke); expand to inner/outer like BakeArc.
            float halfStroke = shape.D * 0.5f;
            inner  = MathF.Max(0f, shape.C - halfStroke);
            outer  = MathF.Min(1f, shape.C + halfStroke);
            corner = 0f;
        }
        else
        {
            inner  = shape.C;
            outer  = shape.D;
            corner = shape.E;
        }

        _shapeEffect ??= new ShaderEffect("shaders/ui_shape.shader");
        _shapeEffect["ShapeStart"]  = shape.A;
        _shapeEffect["ShapeEnd"]    = shape.B;
        _shapeEffect["ShapeInner"]  = inner;
        _shapeEffect["ShapeOuter"]  = outer;
        _shapeEffect["ShapeCorner"] = corner;
        _shapeEffect["ShapeColor"]  = _shapeColor;
        _shapeEffect.Warm();   // build the Material on the main thread; Draw (render thread) only reads the cache.

        // If this panel had been a Polygon, drop its baked mask so it does not draw behind the SDF.
        Style.BackgroundImage = null;
        MarkRenderDirty();
    }

    // Color for a Sector/Arc. No-op storage when this is a Polygon panel (no effect yet).
    public void SetShapeColor(Color color)
    {
        _shapeColor = color;
        if (_shapeEffect is not null)
        {
            _shapeEffect["ShapeColor"] = color;
            MarkRenderDirty();
        }
    }

    public void ApplyPolygon(Vector2[] points)
    {
        _shapeEffect = null;   // leave the GPU path; Polygon draws as a baked BackgroundImage.
        Style.BackgroundImage = ShapeTextureCache.GetOrBakePolygon(points);
        Style.BackgroundSizeX = Length.Percent(100);
        Style.BackgroundSizeY = Length.Percent(100);
    }

    // Re-record the one SDF quad each frame so animated geometry (changed shape params push new
    // uniforms) and panel moves/resizes stay in sync. One quad per shape per frame is trivial.
    public override void Tick()
    {
        base.Tick();
        if (_shapeEffect is not null)
            MarkRenderDirty();
    }

    void IPanelDraw.Draw(CommandList cl)
    {
        if (_shapeEffect is null) return;
        _shapeEffect.Apply(cl, Box.Rect);
        cl.DrawQuad(Box.Rect, _shapeEffect.Material, Color.White);
    }

    protected override void OnClick(MousePanelEvent e)       { base.OnClick(e);       EventDispatch.Fire(_onClick, e, _requestRebuild); }
    protected override void OnRightClick(MousePanelEvent e)  { base.OnRightClick(e);  EventDispatch.Fire(_onRightClick, e, _requestRebuild); }
    protected override void OnMiddleClick(MousePanelEvent e) { base.OnMiddleClick(e); EventDispatch.Fire(_onMiddleClick, e, _requestRebuild); }
    protected override void OnMouseOver(MousePanelEvent e)   { base.OnMouseOver(e);   EventDispatch.Fire(_onMouseEnter, e, _requestRebuild); }
    protected override void OnMouseOut(MousePanelEvent e)    { base.OnMouseOut(e);    EventDispatch.Fire(_onMouseLeave, e, _requestRebuild); }
    protected override void OnMouseDown(MousePanelEvent e)   { base.OnMouseDown(e);   EventDispatch.Fire(_onMouseDown, e, _requestRebuild); }
    protected override void OnMouseUp(MousePanelEvent e)     { base.OnMouseUp(e);     EventDispatch.Fire(_onMouseUp, e, _requestRebuild); }
    protected override void OnMouseMove(MousePanelEvent e)   { base.OnMouseMove(e);   EventDispatch.Fire(_onMouseMove, e, _requestRebuild); }
}