Files
HellionChat/HellionChat/Ui/Components/MessageList.cs
T

171 lines
6.1 KiB
C#

using System.Globalization;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
using HellionChat.Util;
namespace HellionChat.Ui.Components;
// Virtualised message list. Compact mode reuses ImGuiListClipper because
// rows have a constant line height; card mode falls back to a linear
// render with a per-message height cache and an IsItemVisible skip path
// so off-screen rows place a Dummy of the cached height rather than
// running the full render again.
internal sealed class MessageList
{
private const float CompactRowHeight = 18f;
private readonly FontManager _fonts;
private readonly ChunkRenderer _chunkRenderer;
private PayloadHandler? _handler;
// §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)
{
_handler = handler;
}
public MessageList(FontManager fonts, ChunkRenderer chunkRenderer)
{
_fonts = fonts;
_chunkRenderer = chunkRenderer;
}
public void Draw(Tab tab)
{
if (!_fonts.FontsReady)
{
ImGui.TextUnformatted("Loading fonts…");
return;
}
// No own ImRaii.Child here — MainWindow already wraps the message
// area in one. Nesting would give the window two stacked scrolls
// and a runaway content-height computation.
using var messages = tab.Messages.GetReadOnly(3);
var compact = Plugin.Config.UseCompactDensity;
// Track whether the user was pinned to the bottom before this frame
// so newly arriving rows do not yank them up. The check runs against
// the parent child's scroll state, which is the one MainWindow owns.
var pinnedToBottom = ImGui.GetScrollY() >= ImGui.GetScrollMaxY() - 1f;
if (compact)
DrawCompact(messages);
else
DrawCard(tab, messages);
if (pinnedToBottom)
ImGui.SetScrollHereY(1f);
// OpenPopup in Click() and BeginPopup here share the ##hellion-main-area scope -> Popup-ID matches.
_handler?.Draw();
}
private void DrawCompact(IReadOnlyList<Message> messages)
{
unsafe
{
var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper());
try
{
clipper.Begin(messages.Count, CompactRowHeight);
while (clipper.Step())
{
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
DrawCompactRow(messages[i]);
}
clipper.End();
}
finally
{
clipper.Destroy();
}
}
}
private void DrawCompactRow(Message message)
{
// B2-1/B2-2: render the sender through DrawChunks (the name-aware path
// that applies WorldSuffixMode/NameFormMode via ForDisplay), not as a
// flat SenderSource.TextValue string. message.Sender already carries the
// 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);
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);
ImGui.SameLine(0f, 0f);
}
_chunkRenderer.DrawChunks(message.Content, wrap: true, handler: _handler, lineWidth: 0f);
}
private void DrawCard(Tab tab, IReadOnlyList<Message> messages)
{
var tabId = tab.Identifier;
for (var i = 0; i < messages.Count; i++)
{
var msg = messages[i];
// Cached row: place a Dummy of the known height and skip the
// full render path if the row is off-screen. Mirrors the
// v1.5.6 Card-Mode pattern in ChatLogWindow.DrawMessages.
msg.Height.TryGetValue(tabId, out var cachedHeight);
if (cachedHeight is float h)
{
var beforeDummy = ImGui.GetCursorPos();
ImGui.Dummy(new Vector2(10f, h));
var visible = ImGui.IsItemVisible();
msg.IsVisible[tabId] = visible;
if (!visible)
continue;
ImGui.SetCursorPos(beforeDummy);
}
var before = ImGui.GetCursorPosY();
DrawCardRow(msg);
var after = ImGui.GetCursorPosY();
msg.Height[tabId] = after - before;
}
}
private void DrawCardRow(Message message)
{
// B2-1/B2-2: sender via DrawChunks (name-aware path), on its own line
// with content below — 1.5.6 card parity (ChatLogWindow.cs:1913, no
// 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)
{
ImGui.TextUnformatted($"{timestamp} ");
ImGui.SameLine(0f, 0f);
_chunkRenderer.DrawChunks(message.Sender, wrap: true, handler: _handler, lineWidth: 0f);
}
else
{
ImGui.TextUnformatted(timestamp);
}
_chunkRenderer.DrawChunks(message.Content, wrap: true, handler: _handler, lineWidth: 0f);
}
private static string FormatTimestamp(DateTimeOffset date)
{
var local = date.ToLocalTime();
return Plugin.Config.Use24HourClock
? local.ToString("HH:mm", CultureInfo.InvariantCulture)
: local.ToString("h:mm tt", CultureInfo.InvariantCulture);
}
}