UI helper class for the Prism editor that draws shared vector primitives (pills, chips, glows, rings, checkerboards, squiggles, badges, etc.). It exposes static methods that set Paint state and issue drawing commands to render common UI visuals used by the Prism surfaces.
namespace Editor.Prism.Ui;
/// <summary>
/// The shared drawing primitives every Prism surface is built from: pills, chips, glows, rings,
/// checkerboards, squiggles and gradient strokes.
/// <para>
/// Only valid inside an <c>OnPaint</c> or a <c>Paint.ToPixmap</c> scope. Every method takes explicit
/// colours — nothing here reaches into <see cref="PrismTheme"/> on its own, so a caller can tint a
/// primitive for a state (hover, error, unreachable) without the primitive knowing what a state is.
/// </para>
/// </summary>
public static class PrismPaint
{
// Painting is main-thread-only by contract (Paint asserts it), so these scratch buffers are shared
// rather than allocated per call. A wire with a conversion marker and a squiggled diagnostic are both
// drawn every repaint of every item that has one; on a several-hundred-node graph that was a few
// hundred short-lived arrays per frame for shapes whose vertex count never changes.
static readonly Vector2[] s_diamond = new Vector2[4];
static readonly List<Vector2> s_squiggle = new();
/// <summary>
/// A filled rounded rectangle whose bottom corners are square. Used for card headers, where the
/// card's own radius must be continued at the top and interrupted at the seam.
/// </summary>
public static void RoundedTop( Rect rect, float radius )
{
if ( rect.Height <= 0f || rect.Width <= 0f ) return;
radius = MathF.Min( radius, MathF.Min( rect.Width, rect.Height ) * 0.5f );
Paint.DrawRect( rect, radius );
Paint.DrawRect( new Rect( rect.Left, rect.Top + radius, rect.Width, rect.Height - radius ), 0f );
}
/// <summary>A filled rounded rectangle whose top corners are square.</summary>
public static void RoundedBottom( Rect rect, float radius )
{
if ( rect.Height <= 0f || rect.Width <= 0f ) return;
radius = MathF.Min( radius, MathF.Min( rect.Width, rect.Height ) * 0.5f );
Paint.DrawRect( rect, radius );
Paint.DrawRect( new Rect( rect.Left, rect.Top, rect.Width, rect.Height - radius ), 0f );
}
/// <summary>
/// A filled rounded rectangle. The workhorse behind chips, pills and cards.
/// <para>
/// A negative radius means "the theme's chip radius". It cannot be spelled as a default parameter
/// value, because <see cref="PrismTheme.RadiusChip"/> is a mutable token a theme file can change and
/// a default parameter value has to be a compile-time constant.
/// </para>
/// </summary>
public static void Pill( Rect rect, Color fill, float radius = -1f )
{
if ( radius < 0f ) radius = PrismTheme.RadiusChip;
Paint.ClearPen();
Paint.SetBrush( fill );
Paint.DrawRect( rect, radius );
}
/// <summary>A rounded rectangle with a fill and a one-pixel border. A negative radius means the theme's.</summary>
public static void Pill( Rect rect, Color fill, Color border, float radius = -1f,
float borderWidth = 1f )
{
if ( radius < 0f ) radius = PrismTheme.RadiusChip;
Paint.SetBrush( fill );
Paint.SetPen( border, borderWidth );
Paint.DrawRect( rect, radius );
}
/// <summary>
/// A small labelled chip: rounded background, optional leading icon, text. Returns the rectangle it
/// occupied so a caller can lay several out in a row.
/// </summary>
public static Rect Chip( Vector2 topRight, string text, Color foreground, Color background,
string icon = null, int fontSize = 10, int fontWeight = 500 )
{
Paint.SetFont( PrismTheme.FontFamily, fontSize, fontWeight, false, true );
var textWidth = string.IsNullOrEmpty( text ) ? 0f : Paint.MeasureText( text ).x;
var iconWidth = string.IsNullOrEmpty( icon ) ? 0f : fontSize + 3f;
var width = textWidth + iconWidth + ( textWidth > 0f ? 10f : 6f );
var height = fontSize + 7f;
var rect = new Rect( topRight.x - width, topRight.y, width, height );
Paint.ClearPen();
Paint.SetBrush( background );
Paint.DrawRect( rect, PrismTheme.RadiusChip );
var inner = rect.Shrink( 5f, 0f, 5f, 0f );
if ( !string.IsNullOrEmpty( icon ) )
{
Paint.SetPen( foreground );
Paint.DrawIcon( inner, icon, fontSize, TextFlag.LeftCenter );
inner.Left += iconWidth;
}
if ( !string.IsNullOrEmpty( text ) )
{
Paint.SetPen( foreground );
Paint.SetFont( PrismTheme.FontFamily, fontSize, fontWeight, false, true );
Paint.DrawText( inner, text, TextFlag.LeftCenter );
}
return rect;
}
/// <summary>
/// Concentric outer glow rings around a rectangle. Two layers at falling alpha read as a soft halo
/// without a blur, which the paint API does not offer.
/// <para>
/// The falloff is geometric so the default two layers land on exactly the 24 % and 10 % the design
/// language specifies, and a caller asking for more layers gets the same visual curve rather than a
/// linear ramp that washes out.
/// </para>
/// </summary>
public static void Glow( Rect rect, Color color, float radius, int layers = 2, float strength = 0.24f,
float spacing = 3f )
{
Paint.ClearBrush();
var alpha = strength;
for ( int i = 1; i <= layers; i++ )
{
if ( alpha > 0.002f )
{
Paint.SetPen( color.WithAlpha( color.a * alpha ), 1.5f );
Paint.DrawRect( rect.Grow( spacing * i ), radius + spacing * i );
}
alpha *= 0.42f;
}
}
/// <summary>
/// A soft drop shadow beneath a card. Three offset rounded rectangles at falling alpha; cheap, and
/// convincing at the zoom levels a graph canvas actually uses.
/// </summary>
public static void DropShadow( Rect rect, float radius, Color shadow, float distance = 3f, int layers = 3 )
{
Paint.ClearPen();
for ( int i = layers; i >= 1; i-- )
{
var spread = distance * i;
var alpha = shadow.a * ( 0.5f / i );
Paint.SetBrush( shadow.WithAlpha( alpha ) );
Paint.DrawRect( new Rect( rect.Left - spread * 0.5f, rect.Top + spread * 0.35f,
rect.Width + spread, rect.Height + spread * 0.5f ), radius + spread * 0.5f );
}
}
/// <summary>A filled circle of a given radius, centred on a point.</summary>
public static void Dot( Vector2 center, float radius, Color color )
{
Paint.ClearPen();
Paint.SetBrush( color );
Paint.DrawCircle( new Rect( center.x - radius, center.y - radius, radius * 2f, radius * 2f ) );
}
/// <summary>An unfilled circle of a given radius, centred on a point.</summary>
public static void Ring( Vector2 center, float radius, Color color, float width = 1.5f,
PenStyle style = PenStyle.Solid )
{
Paint.ClearBrush();
Paint.SetPen( color, width, style );
Paint.DrawCircle( new Rect( center.x - radius, center.y - radius, radius * 2f, radius * 2f ) );
}
/// <summary>
/// The transparency checkerboard a colour swatch sits on, so a low alpha reads as transparency
/// rather than as a dark colour. Drawn by hand rather than from an image so it scales with zoom.
/// </summary>
public static void Checkerboard( Rect rect, Color light, Color dark, float cell = 4f )
{
if ( rect.Width <= 0f || rect.Height <= 0f ) return;
Paint.ClearPen();
Paint.SetBrush( light );
Paint.DrawRect( rect, PrismTheme.RadiusChip );
Paint.SetBrush( dark );
var rows = (int)MathF.Ceiling( rect.Height / cell );
var cols = (int)MathF.Ceiling( rect.Width / cell );
for ( int y = 0; y < rows; y++ )
{
for ( int x = 0; x < cols; x++ )
{
if ( ( x + y ) % 2 == 0 ) continue;
var cx = rect.Left + x * cell;
var cy = rect.Top + y * cell;
var w = MathF.Min( cell, rect.Right - cx );
var h = MathF.Min( cell, rect.Bottom - cy );
if ( w <= 0f || h <= 0f ) continue;
Paint.DrawRect( new Rect( cx, cy, w, h ), 0f );
}
}
}
/// <summary>A swatch of a colour over a checkerboard, with a subtle border.</summary>
public static void Swatch( Rect rect, Color color, Color border )
{
Checkerboard( rect, Color.White.WithAlpha( 0.16f ), Color.Black.WithAlpha( 0.22f ) );
Paint.ClearPen();
Paint.SetBrush( color );
Paint.DrawRect( rect, PrismTheme.RadiusChip );
Paint.ClearBrush();
Paint.SetPen( border, 1f );
Paint.DrawRect( rect, PrismTheme.RadiusChip );
}
/// <summary>
/// The wavy underline that marks an error or warning span. Amplitude and period are fixed so two
/// adjacent squiggles stay in phase and read as one run.
/// </summary>
public static void Squiggle( Vector2 from, float width, Color color, float amplitude = 1.6f,
float period = 5f, float thickness = 1.2f )
{
if ( width <= 0f ) return;
var steps = Math.Max( 2, (int)( width / ( period * 0.5f ) ) );
s_squiggle.Clear();
for ( int i = 0; i <= steps; i++ )
{
var t = i / (float)steps;
var x = from.x + width * t;
var y = from.y + ( i % 2 == 0 ? -amplitude : amplitude );
s_squiggle.Add( new Vector2( x, y ) );
}
Paint.ClearBrush();
Paint.SetPen( color, thickness );
Paint.DrawLine( s_squiggle );
}
/// <summary>
/// A stroke whose colour changes along its length. Used for wires that cross a type boundary, so
/// the conversion is legible without reading the tooltip.
/// </summary>
public static void GradientStroke( Vector2 a, Vector2 b, Color from, Color to, float width )
{
Paint.SetBrushLinear( a, b, from, to );
Paint.SetPen( Color.Lerp( from, to, 0.5f ), width );
Paint.DrawLine( a, b );
}
/// <summary>A circular badge with an icon in it, for status marks on a card header.</summary>
public static void IconBadge( Vector2 center, float radius, string icon, Color foreground, Color background )
{
Dot( center, radius, background );
Paint.SetPen( foreground );
Paint.DrawIcon( new Rect( center.x - radius, center.y - radius, radius * 2f, radius * 2f ),
icon, radius * 1.4f, TextFlag.Center );
}
/// <summary>
/// The diamond drawn at the midpoint of a wire that performs a lossy or padded conversion. Small,
/// but it is the whole visible difference between Prism and an editor that truncates silently.
/// </summary>
public static void Diamond( Vector2 center, float size, Color fill, Color ring )
{
s_diamond[0] = new Vector2( center.x, center.y - size );
s_diamond[1] = new Vector2( center.x + size, center.y );
s_diamond[2] = new Vector2( center.x, center.y + size );
s_diamond[3] = new Vector2( center.x - size, center.y );
Paint.SetBrush( fill );
Paint.SetPen( ring, 1.25f );
Paint.DrawPolygon( s_diamond );
}
/// <summary>The X drawn at the dangling end of a broken connection.</summary>
public static void Cross( Vector2 center, float size, Color color, float width = 1.75f )
{
Paint.ClearBrush();
Paint.SetPen( color, width );
Paint.DrawLine( new Vector2( center.x - size, center.y - size ), new Vector2( center.x + size, center.y + size ) );
Paint.DrawLine( new Vector2( center.x + size, center.y - size ), new Vector2( center.x - size, center.y + size ) );
}
/// <summary>A one-pixel horizontal divider.</summary>
public static void Divider( Rect rect, Color color )
{
Paint.ClearBrush();
Paint.SetPen( color, 1f );
Paint.DrawLine( new Vector2( rect.Left, rect.Top ), new Vector2( rect.Right, rect.Top ) );
}
/// <summary>
/// Draw text elided to fit, in one call. Returns the rectangle the text actually occupied.
/// <para>
/// The width is measured before eliding rather than eliding unconditionally. <c>GetElidedText</c>
/// always builds a fresh string, and the overwhelming majority of the labels this draws — node
/// titles, port names, chips — already fit; on a card with a dozen labels that was a dozen strings
/// per repaint to discover nothing had to be shortened.
/// </para>
/// </summary>
public static Rect Text( Rect rect, string text, Color color, int size, int weight,
TextFlag flags = TextFlag.LeftCenter, bool italic = false )
{
if ( string.IsNullOrEmpty( text ) || rect.Width <= 1f ) return new Rect( rect.Position, 0f );
Paint.SetFont( PrismTheme.FontFamily, size, weight, italic, true );
Paint.SetPen( color );
var drawn = Paint.MeasureText( text ).x <= rect.Width
? text
: Paint.GetElidedText( text, rect.Width, ElideMode.Right, flags );
return Paint.DrawText( rect, drawn, flags );
}
/// <summary>Measure a run of text in the Prism UI font without drawing it.</summary>
public static float MeasureText( string text, int size, int weight, bool italic = false )
{
if ( string.IsNullOrEmpty( text ) ) return 0f;
Paint.SetFont( PrismTheme.FontFamily, size, weight, italic, true );
return Paint.MeasureText( text ).x;
}
/// <summary>
/// A horizontal progress/slider track with a filled portion. Used by the inline float editor and by
/// anything else that has to show a normalised value in very little space.
/// </summary>
public static void Track( Rect rect, float fraction, Color track, Color fill, float radius = 2f )
{
Paint.ClearPen();
Paint.SetBrush( track );
Paint.DrawRect( rect, radius );
fraction = Math.Clamp( fraction, 0f, 1f );
if ( fraction <= 0f ) return;
var filled = new Rect( rect.Left, rect.Top, MathF.Max( rect.Width * fraction, radius * 2f ), rect.Height );
Paint.SetBrush( fill );
Paint.DrawRect( filled, radius );
}
/// <summary>A two-state switch, drawn as a rounded track with a knob.</summary>
public static void Switch( Rect rect, bool on, Color track, Color knob, Color accent )
{
var radius = rect.Height * 0.5f;
Paint.ClearPen();
Paint.SetBrush( on ? accent : track );
Paint.DrawRect( rect, radius );
var knobRadius = radius - 2f;
var cx = on ? rect.Right - radius : rect.Left + radius;
Dot( new Vector2( cx, rect.Center.y ), knobRadius, knob );
}
/// <summary>
/// The short stub of wire that ties an inline value pill to its port handle, so a floating pill
/// still reads as belonging to a specific socket.
/// </summary>
public static void Stub( Vector2 from, Vector2 to, Color color, float width = 2f )
{
Paint.ClearBrush();
Paint.SetPen( color, width );
Paint.DrawLine( from, to );
}
}