diff --git a/HellionChat/Ui/Components/MessageList.cs b/HellionChat/Ui/Components/MessageList.cs index c0189da..a29eb49 100644 --- a/HellionChat/Ui/Components/MessageList.cs +++ b/HellionChat/Ui/Components/MessageList.cs @@ -43,6 +43,12 @@ internal sealed class MessageList // config field that had stopped bounding anything. private float[] _heightScratch = []; + // Measured once per Draw rather than per row: it only moves when the clock + // format or the font does, and both of those are in the layout fingerprint. + private float _stampColumnWidth; + private bool _stampVisible; + private float _metaDrop; + // §6.2: setter-injection breaks the PayloadHandler → MainWindow → MessageList → PayloadHandler 3-cycle. // Wired by PayloadHandlerInitHostedService.StartAsync after both singletons exist. internal void AttachPayloadHandler(PayloadHandler handler) @@ -157,6 +163,8 @@ internal sealed class MessageList // and a runaway content-height computation. var compact = Plugin.Config.UseCompactDensity; + MeasureTimestampColumn(tab); + // B2: drop stale cached heights before the snapshot draw. Both densities // need this now -- compact rows are not constant height either, they wrap. // Width read here while it is valid. @@ -193,6 +201,72 @@ internal sealed class MessageList _handler?.Draw(); } + // The stamp column is fixed width so sender names line up under each other. + // It stays reserved even when the stamp is hidden -- otherwise a per-tab + // switch would change every row height in the tab, and the height cache would + // need to carry the wrap position rather than just the format. + private void MeasureTimestampColumn(Tab tab) + { + _stampVisible = tab.DisplayTimestamp; + + var meta = MetaFace(); + float sample; + using (meta.Push()) + sample = ImGui.CalcTextSize(TimestampColumn.SampleFor(Plugin.Config.Use24HourClock)).X; + + _stampColumnWidth = sample + ImGui.CalcTextSize(" ").X * 2f; + + // ImGui aligns a row by its top edge, so the smaller meta face would hang + // above the baseline of the body text beside it. + float bodyAscent; + using (BodyFace().Push()) + bodyAscent = ImGui.GetFont().Ascent; + + float metaAscent; + using (meta.Push()) + metaAscent = ImGui.GetFont().Ascent; + + _metaDrop = StyleEngine.BaselineMath.OffsetFor( + bodyAscent, + metaAscent, + StyleEngine.Metrics.Scale + ); + } + + // Both follow the same pair of settings every other push site follows. + private Dalamud.Interface.ManagedFontAtlas.IFontHandle BodyFace() => + Plugin.Config.FontsEnabled || Plugin.Config.UseHellionFont + ? _fonts.RegularFont! + : _fonts.Axis; + + private Dalamud.Interface.ManagedFontAtlas.IFontHandle MetaFace() => + Plugin.Config.FontsEnabled || Plugin.Config.UseHellionFont ? _fonts.MetaFont! : _fonts.Axis; + + // Same size as the body face, drawn heavier. With the game font selected + // there is no heavier variant, so the sender leans on channel colour alone. + private Dalamud.Interface.ManagedFontAtlas.IFontHandle SenderFace() => + Plugin.Config.FontsEnabled || Plugin.Config.UseHellionFont + ? _fonts.SenderFont! + : _fonts.Axis; + + // Draws the stamp into its column and leaves the cursor at the text column, + // whether or not anything was drawn. + private void DrawTimestampCell(Message message) + { + var origin = ImGui.GetCursorPos(); + + if (_stampVisible) + { + ImGui.SetCursorPosY(origin.Y + _metaDrop); + using (MetaFace().Push()) + ImGui.TextUnformatted(FormatTimestamp(message.Date)); + + ImGui.SameLine(0f, 0f); + } + + ImGui.SetCursorPos(origin with { X = origin.X + _stampColumnWidth }); + } + // B3-5: Discord-style full-width bar pinned to the bottom edge of the // visible region while the user is scrolled up. Geometry comes from window // pos + size (visible region), never from the content flow: when scrolled @@ -252,19 +326,20 @@ internal sealed class MessageList // channel brackets/colon as ChunkSource.None wrappers (MessageManager // .cs:300-314), so the separator is rendered by the chunks. 1.5.6 parity // (ChatLogWindow.cs:1965: DrawChunks(message.Sender) + SameLine). - var timestamp = FormatTimestamp(message.Date); + DrawTimestampCell(message); + if (message.Sender.Count > 0) { - ImGui.TextUnformatted($"{timestamp} "); - ImGui.SameLine(0f, 0f); - _chunkRenderer.DrawChunks(message.Sender, wrap: true, handler: _handler, lineWidth: 0f); - ImGui.SameLine(0f, 0f); - } - else - { - ImGui.TextUnformatted(timestamp); + using (SenderFace().Push()) + _chunkRenderer.DrawChunks( + message.Sender, + wrap: true, + handler: _handler, + lineWidth: 0f + ); ImGui.SameLine(0f, 0f); } + _chunkRenderer.DrawChunks(message.Content, wrap: true, handler: _handler, lineWidth: 0f); } @@ -383,18 +458,29 @@ internal sealed class MessageList // SameLine after the sender). The 1.5.6 channel-colour push on the // sender is deferred styling polish (masterplan §6 -> v1.9.0); plain // text here. - var timestamp = FormatTimestamp(message.Date); - if (message.Sender.Count > 0) + // A system message has no sender, so a header row would be a stamp on a + // line of its own -- an empty gesture. Those stay single-line in both + // densities; only a message with a sender gets the two-line treatment. + if (message.Sender.Count == 0) { - ImGui.TextUnformatted($"{timestamp} "); - ImGui.SameLine(0f, 0f); + DrawTimestampCell(message); + _chunkRenderer.DrawChunks( + message.Content, + wrap: true, + handler: _handler, + lineWidth: 0f + ); + return; + } + + DrawTimestampCell(message); + using (SenderFace().Push()) _chunkRenderer.DrawChunks(message.Sender, wrap: true, handler: _handler, lineWidth: 0f); - } - else - { - ImGui.TextUnformatted(timestamp); - } + + // Indented onto the text column so the body lines up under the name. + ImGui.Indent(_stampColumnWidth); _chunkRenderer.DrawChunks(message.Content, wrap: true, handler: _handler, lineWidth: 0f); + ImGui.Unindent(_stampColumnWidth); } private static string FormatTimestamp(DateTimeOffset date)