diff --git a/HellionChat/Ui/Components/MessageList.cs b/HellionChat/Ui/Components/MessageList.cs index a29eb49..368200a 100644 --- a/HellionChat/Ui/Components/MessageList.cs +++ b/HellionChat/Ui/Components/MessageList.cs @@ -414,6 +414,13 @@ internal sealed class MessageList { var msg = messages[i]; var before = ImGui.GetCursorPosY(); + + // The cached height is not an estimate here: a chat message does not + // change height after its first measurement, so last frame's value is + // this frame's value. That is what lets the surface go down before + // the text instead of needing a draw-channel detour. + DrawRowSurface(heights[i]); + drawRow(msg); if (frozen) continue; @@ -432,6 +439,63 @@ internal sealed class MessageList ); } + // Hover fill plus a 2px accent bar on the left edge. The gradient runs from + // the accent at a tenth opacity into nothing about seventy percent across, + // which is why it takes two rectangles: AddRectFilledMultiColor has no + // rounding parameter, so the rounded base goes down first and the gradient + // sits inside it. + private void DrawRowSurface(float height) + { + if (height <= 0f) + return; + + var top = ImGui.GetCursorScreenPos(); + PaintRowSurface(ImGui.GetWindowDrawList(), top, height, direct: true); + } + + private void FillRowSurface(Vector2 top, float height) + { + if (height <= 0f) + return; + + PaintRowSurface(ImGui.GetWindowDrawList(), top, height, direct: false); + } + + private void PaintRowSurface(ImDrawListPtr dl, Vector2 top, float height, bool direct) + { + var scale = StyleEngine.Metrics.Scale; + var width = ImGui.GetContentRegionAvail().X; + if (width <= 0f) + return; + + var min = top; + var max = top + new Vector2(width, height); + + var hovered = ImGui.IsWindowHovered() && ImGui.IsMouseHoveringRect(min, max); + var key = (uint)HashCode.Combine(top.Y, height); + var amount = StyleEngine.HoverState.Query(key, hovered); + if (amount <= 0.01f) + return; + + var theme = Plugin.Instance.ThemeRegistry.Active; + var accent = theme.Colors.Accent; + var rounding = 2f * scale; + + var wash = ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(accent), 0.07f * amount); + if (direct) + dl.AddRectFilled(min, max, wash, rounding); + else + StyleEngine.RowSurfaceScope.Fill(min, max, wash, rounding); + + // The bar is two pixels wide, so it has no visible corners to round. + var bar = ColourUtil.ApplyAlpha(ColourUtil.RgbaToAbgr(accent), amount); + var barMax = new Vector2(min.X + 2f * scale, max.Y); + if (direct) + dl.AddRectFilled(min, barMax, bar, 0f); + else + StyleEngine.RowSurfaceScope.Fill(min, barMax, bar, 0f); + } + // First-frame / post-invalidation fallback: draw + measure every row into the // cache so the next frame can take the planned path. The settle gate on the // layout fingerprint is what keeps a resize drag from landing here every frame. @@ -441,12 +505,24 @@ internal sealed class MessageList Action drawRow ) { + // No row has a cached height on this frame, so the surface cannot be + // drawn ahead of the text. Channels let it go down afterwards and still + // land underneath. Without this the whole list would flash bare for one + // frame after every resize. + using var surfaces = StyleEngine.RowSurfaceScope.Push(); + foreach (var msg in messages) { var before = ImGui.GetCursorPosY(); + var top = ImGui.GetCursorScreenPos(); + drawRow(msg); + var after = ImGui.GetCursorPosY(); - msg.Height[tabId] = after - before; + var height = after - before; + FillRowSurface(top, height); + + msg.Height[tabId] = height; msg.IsVisible[tabId] = ImGui.IsItemVisible(); } }