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.
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); }
}