feat(chat): give the timestamp its own column, and the show-timestamps box its effect back
The stamp used to be text at the head of the line with two spaces after it, so
every sender name started wherever the previous stamp happened to end. It sits in
a fixed column now, measured once per draw from the widest shape the current
format can produce, and the names line up.
The column stays reserved when the stamp is hidden. Collapsing it would make a
per-tab switch change every row height in that tab, and the height cache would
have to carry wrap positions rather than just the format.
tab.DisplayTimestamp has a reader again. It was in 1.5.6 at two call sites and
lost both when cf4705e retired the old chat window; the tab editor has been
writing a setting nobody read since. Same class of defect the last cycle spent
itself on, found in passing here.
The sender draws in the heavier face and the stamp in the smaller one, both
dropped onto the body baseline -- ImGui aligns a row by its top edge, so without
that the stamp would hang. All three faces follow the same FontsEnabled or
UseHellionFont pair every other push site follows; with the game font selected
there is no heavier or smaller variant and the row falls back to one face.
Card density gets the two-line treatment only where there is a sender. A system
message has none, so a header row would be a stamp alone on a line -- an empty
gesture. Those stay single-line in both densities.
This commit is contained in:
@@ -43,6 +43,12 @@ internal sealed class MessageList
|
|||||||
// config field that had stopped bounding anything.
|
// config field that had stopped bounding anything.
|
||||||
private float[] _heightScratch = [];
|
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.
|
// §6.2: setter-injection breaks the PayloadHandler → MainWindow → MessageList → PayloadHandler 3-cycle.
|
||||||
// Wired by PayloadHandlerInitHostedService.StartAsync after both singletons exist.
|
// Wired by PayloadHandlerInitHostedService.StartAsync after both singletons exist.
|
||||||
internal void AttachPayloadHandler(PayloadHandler handler)
|
internal void AttachPayloadHandler(PayloadHandler handler)
|
||||||
@@ -157,6 +163,8 @@ internal sealed class MessageList
|
|||||||
// and a runaway content-height computation.
|
// and a runaway content-height computation.
|
||||||
var compact = Plugin.Config.UseCompactDensity;
|
var compact = Plugin.Config.UseCompactDensity;
|
||||||
|
|
||||||
|
MeasureTimestampColumn(tab);
|
||||||
|
|
||||||
// B2: drop stale cached heights before the snapshot draw. Both densities
|
// B2: drop stale cached heights before the snapshot draw. Both densities
|
||||||
// need this now -- compact rows are not constant height either, they wrap.
|
// need this now -- compact rows are not constant height either, they wrap.
|
||||||
// Width read here while it is valid.
|
// Width read here while it is valid.
|
||||||
@@ -193,6 +201,72 @@ internal sealed class MessageList
|
|||||||
_handler?.Draw();
|
_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
|
// 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
|
// visible region while the user is scrolled up. Geometry comes from window
|
||||||
// pos + size (visible region), never from the content flow: when scrolled
|
// 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
|
// channel brackets/colon as ChunkSource.None wrappers (MessageManager
|
||||||
// .cs:300-314), so the separator is rendered by the chunks. 1.5.6 parity
|
// .cs:300-314), so the separator is rendered by the chunks. 1.5.6 parity
|
||||||
// (ChatLogWindow.cs:1965: DrawChunks(message.Sender) + SameLine).
|
// (ChatLogWindow.cs:1965: DrawChunks(message.Sender) + SameLine).
|
||||||
var timestamp = FormatTimestamp(message.Date);
|
DrawTimestampCell(message);
|
||||||
|
|
||||||
if (message.Sender.Count > 0)
|
if (message.Sender.Count > 0)
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted($"{timestamp} ");
|
using (SenderFace().Push())
|
||||||
ImGui.SameLine(0f, 0f);
|
_chunkRenderer.DrawChunks(
|
||||||
_chunkRenderer.DrawChunks(message.Sender, wrap: true, handler: _handler, lineWidth: 0f);
|
message.Sender,
|
||||||
ImGui.SameLine(0f, 0f);
|
wrap: true,
|
||||||
}
|
handler: _handler,
|
||||||
else
|
lineWidth: 0f
|
||||||
{
|
);
|
||||||
ImGui.TextUnformatted(timestamp);
|
|
||||||
ImGui.SameLine(0f, 0f);
|
ImGui.SameLine(0f, 0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
_chunkRenderer.DrawChunks(message.Content, wrap: true, handler: _handler, lineWidth: 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
|
// SameLine after the sender). The 1.5.6 channel-colour push on the
|
||||||
// sender is deferred styling polish (masterplan §6 -> v1.9.0); plain
|
// sender is deferred styling polish (masterplan §6 -> v1.9.0); plain
|
||||||
// text here.
|
// text here.
|
||||||
var timestamp = FormatTimestamp(message.Date);
|
// A system message has no sender, so a header row would be a stamp on a
|
||||||
if (message.Sender.Count > 0)
|
// 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} ");
|
DrawTimestampCell(message);
|
||||||
ImGui.SameLine(0f, 0f);
|
_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);
|
_chunkRenderer.DrawChunks(message.Sender, wrap: true, handler: _handler, lineWidth: 0f);
|
||||||
}
|
|
||||||
else
|
// Indented onto the text column so the body lines up under the name.
|
||||||
{
|
ImGui.Indent(_stampColumnWidth);
|
||||||
ImGui.TextUnformatted(timestamp);
|
|
||||||
}
|
|
||||||
_chunkRenderer.DrawChunks(message.Content, wrap: true, handler: _handler, lineWidth: 0f);
|
_chunkRenderer.DrawChunks(message.Content, wrap: true, handler: _handler, lineWidth: 0f);
|
||||||
|
ImGui.Unindent(_stampColumnWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatTimestamp(DateTimeOffset date)
|
private static string FormatTimestamp(DateTimeOffset date)
|
||||||
|
|||||||
Reference in New Issue
Block a user