feat(style): hold hover state across frames instead of one-shot sweeps
DrawHoverSheen measured its own elapsed time against DateTime.UtcNow and gave up after 0.65s, so a row stopped reacting while the pointer was still on it. There was no held value to interpolate colours against. HoverState keeps one 0..1 intensity per element, rising at 14/s and falling at 8/s. Slower out than in is what makes the fade read as deliberate. Query and advance are separate on purpose. Several SelfTest steps call Sidebar.Draw against the live tab list, so the same element gets submitted up to three times in one frame, twice from a window the mouse is not over. If the query advanced the value, the last caller would win and the fade would run backwards. Query only ORs the hover flag; BeginFrame does all the moving and the eviction. BeginFrame sits above the HideInLoadingScreens and New Game+ early returns, so a hidden main window still lets pop-out hovers fade out instead of freezing mid-blend. FrameLerp gains Ramp: Smooth approaches asymptotically and never arrives, so a value driven by it would never reach zero and never become evictable. ReduceMotion short-circuits before the map is touched, returning a hard 0 or 1. An infinite rate would produce NaN and poison the entry for the session.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Ui.StyleEngine;
|
||||
|
||||
// Held hover intensity per element, 0..1. Replaces the one-shot sheen timer,
|
||||
// which stopped after 0.65s while the pointer was still on the row.
|
||||
//
|
||||
// Query and advance are deliberately separate. Several SelfTest steps call
|
||||
// Sidebar.Draw against the live tab list, so the same element can be submitted
|
||||
// three times in one frame -- twice from a window the mouse is not over. If the
|
||||
// query advanced the value, the last caller would win and the fade would run
|
||||
// backwards. Query only marks; BeginFrame does all the moving. Pattern anchor:
|
||||
// LightlessSync Selune.cs:106-146.
|
||||
internal static class HoverState
|
||||
{
|
||||
private sealed class Entry
|
||||
{
|
||||
internal float Value;
|
||||
internal bool Hovered;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<uint, Entry> Entries = [];
|
||||
private static readonly List<uint> Evicted = [];
|
||||
|
||||
internal static int TrackedCount => Entries.Count;
|
||||
|
||||
// Once per frame from Plugin.Draw, before any window renders.
|
||||
internal static void BeginFrame() => Advance(ImGui.GetIO().DeltaTime);
|
||||
|
||||
// SelfTest seam: the alloc step submits many elements inside a single frame,
|
||||
// so it needs to step the clock without waiting for real frames.
|
||||
internal static void AdvanceForTest(float deltaTime) => Advance(deltaTime);
|
||||
|
||||
internal static void Reset()
|
||||
{
|
||||
Entries.Clear();
|
||||
Evicted.Clear();
|
||||
}
|
||||
|
||||
internal static float Query(uint id, bool hovered)
|
||||
{
|
||||
// ReduceMotion short-circuits before touching the map: no entry, no fade,
|
||||
// nothing to evict. Returning a hard 0/1 is also NaN-safe, which setting
|
||||
// an infinite rate would not be.
|
||||
if (Plugin.Config.ReduceMotion)
|
||||
return hovered ? 1f : 0f;
|
||||
|
||||
if (!Entries.TryGetValue(id, out var entry))
|
||||
{
|
||||
entry = new Entry();
|
||||
Entries[id] = entry;
|
||||
}
|
||||
|
||||
// OR, never assign: a second caller in the same frame that is not hovered
|
||||
// must not cancel the first one that is.
|
||||
entry.Hovered |= hovered;
|
||||
return entry.Value;
|
||||
}
|
||||
|
||||
private static void Advance(float deltaTime)
|
||||
{
|
||||
if (deltaTime <= 0f)
|
||||
return;
|
||||
|
||||
Evicted.Clear();
|
||||
foreach (var (id, entry) in Entries)
|
||||
{
|
||||
entry.Value = HoverMath.Step(entry.Value, entry.Hovered, deltaTime);
|
||||
if (HoverMath.ShouldEvict(entry.Value, entry.Hovered))
|
||||
Evicted.Add(id);
|
||||
entry.Hovered = false;
|
||||
}
|
||||
|
||||
foreach (var id in Evicted)
|
||||
Entries.Remove(id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user