feat(style-engine): add DrawListExtensions primitives

Custom-drawing primitives consumed by upcoming components: DrawHoverSheen
with sweep tracking via a static dictionary scoped to constant element-id
keys, DrawGlowBorder using squared-fade layer rects, DrawSlipPolygon as
unsafe stackalloc six-point chamfered polygon, and DrawHonorificHeader for
the crown plus bracketed title. BuildSlipPolygon is internal so the build
suite can pin the geometry without spinning up an ImGui frame.
This commit is contained in:
2026-05-23 17:04:33 +02:00
parent b8299a90ca
commit cd9e43c183
@@ -0,0 +1,154 @@
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.ManagedFontAtlas;
using HellionChat.Themes;
using HellionChat.Util;
namespace HellionChat.Ui.StyleEngine;
// Custom-drawing primitives for the v2.x style layer. Callers feed RGBA
// uints (typically resolved via TokenResolver) and these methods convert to
// ABGR before delegating to ImDrawList. Hover-sheen state lives in a small
// static dictionary keyed by constant strings — keep keys constant and
// scope to static UI elements so the per-key footprint stays bounded.
internal static class DrawListExtensions
{
private const float SheenDurationSeconds = 0.65f;
private static readonly Dictionary<string, DateTime> SheenStarts = new();
public static void DrawHoverSheen(
this ImDrawListPtr dl,
Vector2 min,
Vector2 max,
uint accentRgba,
string elementId,
bool hovered
)
{
if (!hovered)
{
// Reset so re-hover restarts the sweep instead of catching the
// tail end of a stale animation.
SheenStarts.Remove(elementId);
return;
}
if (!SheenStarts.TryGetValue(elementId, out var started))
{
started = DateTime.UtcNow;
SheenStarts[elementId] = started;
}
var elapsed = (DateTime.UtcNow - started).TotalSeconds;
if (elapsed > SheenDurationSeconds)
return;
var t = (float)(elapsed / SheenDurationSeconds);
var alpha = (byte)Math.Round(0x40 * (1f - t));
var sheenAbgr = ((uint)alpha << 24) | 0x00FFFFFFu;
var sweepX = min.X + (max.X - min.X) * t;
dl.AddRectFilled(
new Vector2(sweepX - 12f, min.Y),
new Vector2(sweepX + 12f, max.Y),
sheenAbgr,
2f
);
// Accent currently unused — reserved for a tinted-sweep variant that
// tracks the element's accent hue. Keeping it in the signature so
// call-sites don't churn when the tinted path lands.
_ = accentRgba;
}
public static void DrawGlowBorder(
this ImDrawListPtr dl,
Vector2 min,
Vector2 max,
uint colorRgba,
float thickness = 1f,
int layers = 5
)
{
var r = (byte)((colorRgba >> 24) & 0xFFu);
var g = (byte)((colorRgba >> 16) & 0xFFu);
var b = (byte)((colorRgba >> 8) & 0xFFu);
var a = (byte)(colorRgba & 0xFFu);
for (var i = 0; i < layers; i++)
{
var fade = 1f - (i / (float)layers);
// Squared fade so outer layers vanish faster than a linear taper
// would suggest — keeps the glow halo from looking like a band.
var layerAlpha = (byte)Math.Round(a * fade * fade);
var layerAbgr = ((uint)layerAlpha << 24) | ((uint)b << 16) | ((uint)g << 8) | r;
var offset = i + 1;
dl.AddRect(
new Vector2(min.X - offset, min.Y - offset),
new Vector2(max.X + offset, max.Y + offset),
layerAbgr,
3f,
ImDrawFlags.None,
thickness
);
}
}
public static unsafe void DrawSlipPolygon(
this ImDrawListPtr dl,
Vector2 min,
Vector2 max,
uint colorRgba,
float chamfer
)
{
Span<Vector2> pts = stackalloc Vector2[6];
BuildSlipPolygon(min, max, chamfer, pts);
var abgr = ColourUtil.RgbaToAbgr(colorRgba);
fixed (Vector2* p = pts)
dl.AddConvexPolyFilled(p, 6, abgr);
}
// Geometry-only helper (no ImGui, no allocation) so the build suite can
// pin the slip-card shape without standing up an ImGui frame.
internal static void BuildSlipPolygon(
Vector2 min,
Vector2 max,
float chamfer,
Span<Vector2> pts
)
{
var bound = Math.Min(max.X - min.X, max.Y - min.Y) * 0.5f;
var c = Math.Max(0f, Math.Min(chamfer, bound));
// Clockwise wrap; bottom-left corner replaced by a 45-degree cut.
pts[0] = new Vector2(min.X + c, min.Y);
pts[1] = new Vector2(max.X, min.Y);
pts[2] = new Vector2(max.X, max.Y);
pts[3] = new Vector2(min.X + c, max.Y);
pts[4] = new Vector2(min.X, max.Y - c);
pts[5] = new Vector2(min.X, min.Y + c);
}
public static void DrawHonorificHeader(
this ImDrawListPtr dl,
Vector2 origin,
string title,
Theme theme,
IFontHandle fontAwesomeFont,
float gap = 4f
)
{
var crownAbgr = ColourUtil.RgbaToAbgr(theme.Colors.Identity);
var titleAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary);
var crownGlyph = FontAwesomeIcon.Crown.ToIconString();
// FontAwesome must wrap the crown specifically — the bracketed title
// renders in the regular text font, so the push has to be tight.
float crownWidth;
using (fontAwesomeFont.Push())
{
crownWidth = ImGui.CalcTextSize(crownGlyph).X;
dl.AddText(origin, crownAbgr, crownGlyph);
}
dl.AddText(origin + new Vector2(crownWidth + gap, 0f), titleAbgr, $"«{title}»");
}
}