Files
HellionChat/HellionChat/SelfTests/CardClipPlanStep.cs
T
JonKazama-Hellion 0ef33934a2 refactor(messages): plan compact rows like cards instead of assuming a fixed height
Compact mode ran an ImGuiListClipper with CompactRowHeight = 18f. Two things
were wrong with that.

The number: at the default 12.75pt the font is 17px, and ChunkRenderer pushes
ItemSpacing to zero for the whole chunk loop, so a single-line compact row
advances the cursor by 17, not 18. The clipper seeded the cursor one pixel too
low per row, which accumulates into a visible drift against the scrollbar.

The assumption: compact rows are not constant height at all. DrawCompactRow
renders content with wrap: true, and WrapEncodedLine submits one text item per
wrapped line. At 620px and 17px type that kicks in around 70 characters, so
most chat lines are multi-line.

Both densities now share DrawRows/DrawLinearAndMeasure over the existing
CardClipPlanner, with the row painter passed in. The fixed height and the
clipper are gone.

Also corrects the CompensatedDummy comment: the cached heights carry no
trailing ItemSpacing (every row ends inside DrawChunks, where spacing is
zero, and ImGui writes the advance at submission). The compensation is
correct because it cancels the spacing the dummy itself appends.

CardClipPlanStep drives the invalidation hook directly, which now sits behind
the settle gate, so it walks a synthetic clock past the window.
2026-08-17 23:32:04 +02:00

112 lines
4.1 KiB
C#

using System.Collections.Generic;
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin.SelfTest;
using HellionChat.Util;
namespace HellionChat.SelfTests;
// B2: behavioural check that the card path feeds CardClipPlanner AND that a
// layout change clears the height cache — not a non-null check. Pure plan math
// is pinned headless by CardClipPlanTests; this drives the live accessors.
internal sealed class CardClipPlanStep : ISelfTestStep
{
private readonly Plugin plugin;
public CardClipPlanStep(Plugin plugin) => this.plugin = plugin;
public string Name => "Hellion Chat - Card clip plan + cache invalidation";
public SelfTestStepResult RunStep()
{
var messages = plugin.MainWindow.GetMessageListForSelfTest();
if (messages is null)
{
ImGui.Text("MessageList null");
SelfTestReport.Append(Name, "FAIL", new[] { "MessageList null" });
return SelfTestStepResult.Fail;
}
// 5 rows of 20, viewport 50, scroll 45 -> skip rows 0,1 (lead 40), see 2..4.
IReadOnlyList<float> heights = [20f, 20f, 20f, 20f, 20f];
var plan = messages.PlanCardClipForSelfTest(heights, scrollY: 45f, viewportHeight: 50f);
if (
plan.FirstVisible != 2
|| plan.LastVisible != 4
|| plan.LeadDummyHeight is < 39.9f or > 40.1f
)
{
var msg =
$"Plan wrong: first={plan.FirstVisible} last={plan.LastVisible} lead={plan.LeadDummyHeight}";
ImGui.Text(msg);
SelfTestReport.Append(Name, "FAIL", new[] { msg });
return SelfTestStepResult.Fail;
}
var tab = plugin.MainWindow.ActiveTab;
if (tab is null)
{
ImGui.Text("No active tab");
SelfTestReport.Append(Name, "FAIL", new[] { "No active tab" });
return SelfTestStepResult.Fail;
}
// Flip a height-affecting mode -> fingerprint changes -> cache must drop.
// Config restored in finally (live-singleton discipline).
var savedForm = Plugin.Config.NameFormMode;
int remaining;
try
{
// v1.10.0/A1: the fingerprint gate waits for the value to settle, so
// the step walks a synthetic clock past the window instead of sleeping.
var clock = Environment.TickCount64;
messages.RunHeightCacheInvalidationForSelfTest(tab, 400f, clock);
using (var snap = tab.Messages.GetReadOnly(3))
{
if (snap.Count > 0)
snap[0].Height[tab.Identifier] = 42f;
}
Plugin.Config.NameFormMode =
savedForm == NameFormMode.Full ? NameFormMode.Initials : NameFormMode.Full;
messages.RunHeightCacheInvalidationForSelfTest(tab, 400f, clock);
clock += LayoutFingerprintGate.SettleMs;
remaining = messages.RunHeightCacheInvalidationForSelfTest(tab, 400f, clock);
}
finally
{
Plugin.Config.NameFormMode = savedForm;
var restore = Environment.TickCount64 + LayoutFingerprintGate.SettleMs * 2;
messages.RunHeightCacheInvalidationForSelfTest(tab, 400f, restore);
messages.RunHeightCacheInvalidationForSelfTest(
tab,
400f,
restore + LayoutFingerprintGate.SettleMs
);
}
if (remaining != 0)
{
var msg = $"Cache not cleared after layout change: {remaining} left";
ImGui.Text(msg);
SelfTestReport.Append(Name, "FAIL", new[] { msg });
return SelfTestStepResult.Fail;
}
SelfTestReport.Append(
Name,
"PASS",
new[]
{
$"Plan range [{plan.FirstVisible}..{plan.LastVisible}], lead={plan.LeadDummyHeight}",
"Height cache cleared after layout-fingerprint change",
}
);
ImGui.Text(
$"PASS — plan [{plan.FirstVisible}..{plan.LastVisible}], cache cleared on layout change."
);
return SelfTestStepResult.Pass;
}
public void CleanUp() { }
}