diff --git a/HellionChat/Ui/StyleEngine/RowSurfaceScope.cs b/HellionChat/Ui/StyleEngine/RowSurfaceScope.cs new file mode 100644 index 0000000..825e792 --- /dev/null +++ b/HellionChat/Ui/StyleEngine/RowSurfaceScope.cs @@ -0,0 +1,75 @@ +using System.Numerics; +using Dalamud.Bindings.ImGui; + +namespace HellionChat.Ui.StyleEngine; + +// Draws a surface behind a row whose height is only known after the text has been +// drawn. Text goes into channel 1, the fill into channel 0, and the merge puts +// the fill underneath. +// +// Only needed on the frame after the height cache is dropped. Every other frame +// the cached height is already correct -- a chat message does not change height +// after its first measurement -- so the caller draws the fill directly and never +// comes near this. +// +// Three things make draw channels sharp: +// +// Nesting a splitter into itself asserts, and Dalamud does not compile asserts +// out. The user would get an error dialog, not a glitch. Hence the depth count. +// +// A forgotten merge is not a dropped frame, it is permanent: the commands stay +// in the channel buffers and never reach the draw list, and the next frame's +// split walks into the assert. Hence the finally. +// +// And the clip rect carries over on a channel switch, so the fill inherits +// whatever was clipping the text. +internal static class RowSurfaceScope +{ + [ThreadStatic] + private static int _depth; + + [ThreadStatic] + private static ImDrawListPtr _drawList; + + internal static bool IsActive => _depth > 0; + + internal static Scope Push() + { + if (_depth == 0) + { + _drawList = ImGui.GetWindowDrawList(); + _drawList.ChannelsSplit(2); + // Foreground is the resting state: everything that is not explicitly + // painting a surface draws where it expects to. + _drawList.ChannelsSetCurrent(1); + } + + _depth++; + return new Scope(); + } + + // Paints into the background channel and returns immediately to the + // foreground, so a caller can never leave the channel switched. + internal static void Fill(Vector2 min, Vector2 max, uint abgr, float rounding) + { + if (!IsActive) + return; + + _drawList.ChannelsSetCurrent(0); + _drawList.AddRectFilled(min, max, abgr, rounding); + _drawList.ChannelsSetCurrent(1); + } + + internal readonly struct Scope : IDisposable + { + public void Dispose() + { + _depth--; + if (_depth > 0) + return; + + _drawList.ChannelsMerge(); + _drawList = default; + } + } +}