diff --git a/HellionChat/Ui/StyleEngine/DrawListExtensions.cs b/HellionChat/Ui/StyleEngine/DrawListExtensions.cs new file mode 100644 index 0000000..1d0545f --- /dev/null +++ b/HellionChat/Ui/StyleEngine/DrawListExtensions.cs @@ -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 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 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 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}»"); + } +}