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.
112 lines
4.1 KiB
C#
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() { }
|
|
}
|