The arithmetic had tests from the first commit; the table in front of it did not. Factors is indexed by the enum, so a role inserted in the middle shifts every factor below it -- and each one still resolves to a plausible size, which is exactly why nothing would have failed. Three files were missing their TEST-MIRROR marker despite having mirrors. The marker is how the drift check finds them.
128 lines
3.8 KiB
C#
128 lines
3.8 KiB
C#
namespace HellionChat.Util;
|
|
|
|
// TEST-MIRROR: Util/LayoutFingerprintGateTests.cs
|
|
//
|
|
// Layout inputs that make a tab's cached row heights stale. Kept as a plain
|
|
// value type so the build suite can pin the gate without an ImGui frame.
|
|
internal readonly record struct LayoutFingerprint(
|
|
float FontGlobal,
|
|
float FontSymbols,
|
|
float FontSender,
|
|
float FontMeta,
|
|
float FontItalic,
|
|
bool Compact,
|
|
bool FontsEnabled,
|
|
bool UseHellionFont,
|
|
bool ItalicEnabled,
|
|
bool Use24Hour,
|
|
bool ShowTimestamp,
|
|
int NameForm,
|
|
int WorldSuffix,
|
|
float Width,
|
|
float UiScale
|
|
)
|
|
{
|
|
// Toggles: they land on a new value in one frame and stay there. Waiting on
|
|
// them would leave the planner running against the previous density's
|
|
// heights while the rows are already painted the new way.
|
|
internal (bool, bool, bool, bool, bool, bool, int, int) Discrete =>
|
|
(
|
|
Compact,
|
|
FontsEnabled,
|
|
UseHellionFont,
|
|
ItalicEnabled,
|
|
Use24Hour,
|
|
ShowTimestamp,
|
|
NameForm,
|
|
WorldSuffix
|
|
);
|
|
}
|
|
|
|
// Dragging a window edge or the Dalamud UI-scale slider moves the continuous
|
|
// half of the fingerprint on every frame. Acting on each one drops the height
|
|
// cache and sends the whole tab through the linear measure path (up to
|
|
// MessageManager.MessageDisplayLimit rows). Waiting for those to settle turns a
|
|
// drag into one rebuild. Discrete changes bypass the wait entirely.
|
|
internal sealed class LayoutFingerprintGate
|
|
{
|
|
internal const long SettleMs = 200;
|
|
|
|
// A value that never stops moving would otherwise hold the gate shut
|
|
// forever while the applied fingerprint stays wrong.
|
|
internal const long MaxWaitMs = 1000;
|
|
|
|
private LayoutFingerprint? _applied;
|
|
private LayoutFingerprint _pending;
|
|
private long _pendingSinceMs;
|
|
private long _divergedSinceMs;
|
|
|
|
// True while a continuous change is being waited out. The caller must not
|
|
// write fresh measurements into the cache during this window, or the cache
|
|
// becomes a mix of the old and the in-flight geometry.
|
|
internal bool IsPending { get; private set; }
|
|
|
|
internal bool ShouldInvalidate(LayoutFingerprint current, long nowMs)
|
|
{
|
|
if (_applied is null)
|
|
{
|
|
Settle(current, nowMs);
|
|
return false;
|
|
}
|
|
|
|
var applied = _applied.Value;
|
|
if (current.Equals(applied))
|
|
{
|
|
Settle(current, nowMs);
|
|
return false;
|
|
}
|
|
|
|
// Density and the two name modes are switches, not sliders: apply now.
|
|
if (!current.Discrete.Equals(applied.Discrete))
|
|
{
|
|
Settle(current, nowMs);
|
|
return true;
|
|
}
|
|
|
|
// First frame of a divergence starts both clocks: the settle window
|
|
// restarts on every further move, the deadline does not.
|
|
if (!IsPending)
|
|
{
|
|
_divergedSinceMs = nowMs;
|
|
_pending = current;
|
|
_pendingSinceMs = nowMs;
|
|
IsPending = true;
|
|
return false;
|
|
}
|
|
|
|
// Checked before the still-moving branch below: a value that changes on
|
|
// every frame would otherwise never reach it.
|
|
if (nowMs - _divergedSinceMs >= MaxWaitMs)
|
|
{
|
|
Settle(current, nowMs);
|
|
return true;
|
|
}
|
|
|
|
if (!current.Equals(_pending))
|
|
{
|
|
_pending = current;
|
|
_pendingSinceMs = nowMs;
|
|
return false;
|
|
}
|
|
|
|
if (nowMs - _pendingSinceMs < SettleMs)
|
|
return false;
|
|
|
|
Settle(current, nowMs);
|
|
return true;
|
|
}
|
|
|
|
private void Settle(LayoutFingerprint current, long nowMs)
|
|
{
|
|
_applied = current;
|
|
_pending = current;
|
|
_pendingSinceMs = nowMs;
|
|
_divergedSinceMs = nowMs;
|
|
IsPending = false;
|
|
}
|
|
}
|