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; } }