171 lines
6.1 KiB
C#
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);
|
|
}
|
|
}
|