feat(themes): arm crossfade state in ThemeRegistry.Switch
Three new private fields plus TryGetActiveCrossfade entry-point, plus SwitchSilent variant for the plugin-load init path. ArmCrossfade captures a value-copy of the active AbgrCache and stamps TickCount64; mid-crossfade Switch composes the current lerped state as the next fade origin so back-to-back theme switches stay smooth. Same-slug Switch is a no-op (no identity-crossfade).
This commit is contained in:
@@ -32,6 +32,16 @@ public sealed class ThemeRegistry
|
||||
private long _lastActiveStampCheckMs = -ActiveStampPollIntervalMs;
|
||||
private DateTime _lastActiveStamp = DateTime.MinValue;
|
||||
|
||||
// PM-1 crossfade state. Switch() captures the previous AbgrCache as a
|
||||
// VALUE-COPY (not a Theme reference) -- the built-in singletons share
|
||||
// their RecomputeAbgrCache identity, so a reference would mutate
|
||||
// alongside the new active. _crossfadeStartTickMs == long.MinValue
|
||||
// means "no crossfade armed yet"; the field stays MinValue after
|
||||
// SwitchSilent so the plugin-load init-path does not trigger a fade.
|
||||
private ThemeAbgrCache? _previousAbgrSnapshot;
|
||||
private long _crossfadeStartTickMs = long.MinValue;
|
||||
private const int CrossfadeDurationMs = 300;
|
||||
|
||||
public ThemeRegistry(string? customThemesDir = null, ILogger<ThemeRegistry>? logger = null)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -87,6 +97,13 @@ public sealed class ThemeRegistry
|
||||
// a state where _active and Get(_active.Slug) disagree.
|
||||
public void Switch(string slug)
|
||||
{
|
||||
// Same-slug switch is a no-op -- avoids a 300ms identity-crossfade
|
||||
// when the user re-selects the active theme in the picker.
|
||||
if (string.Equals(_active.Slug, slug, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
ArmCrossfade();
|
||||
|
||||
if (_builtIns.TryGetValue(slug, out var builtin))
|
||||
{
|
||||
_active = builtin;
|
||||
@@ -115,6 +132,84 @@ public sealed class ThemeRegistry
|
||||
_activeCustomPath = null;
|
||||
}
|
||||
|
||||
// SwitchSilent is the plugin-load init path -- identical to Switch
|
||||
// but does NOT arm the crossfade state. Called from
|
||||
// ThemeRegistryInitHostedService.StartAsync so opening the plugin
|
||||
// does not produce a 300ms fade from the default theme to the user's
|
||||
// saved theme.
|
||||
public void SwitchSilent(string slug)
|
||||
{
|
||||
if (string.Equals(_active.Slug, slug, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
if (_builtIns.TryGetValue(slug, out var builtin))
|
||||
{
|
||||
_active = builtin;
|
||||
_active.RecomputeAbgrCache();
|
||||
_activeCustomPath = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var customTheme = LoadCustomBySlug(slug, out var customPath);
|
||||
if (customTheme is not null)
|
||||
{
|
||||
_active = customTheme;
|
||||
_active.RecomputeAbgrCache();
|
||||
_activeCustomPath = customPath;
|
||||
_lastActiveStamp = DateTime.MinValue;
|
||||
return;
|
||||
}
|
||||
|
||||
_active = _builtIns[DefaultSlug];
|
||||
_active.RecomputeAbgrCache();
|
||||
_activeCustomPath = null;
|
||||
}
|
||||
|
||||
// Captures the AbgrCache snapshot that PushGlobal should fade FROM.
|
||||
// If a crossfade is already mid-flight (second Switch within 300ms),
|
||||
// the current lerped state replaces the snapshot -- the next fade
|
||||
// starts from where we currently are, not from the original "from".
|
||||
private void ArmCrossfade()
|
||||
{
|
||||
var now = Environment.TickCount64;
|
||||
ThemeAbgrCache snapshot;
|
||||
if (
|
||||
_previousAbgrSnapshot.HasValue
|
||||
&& _crossfadeStartTickMs != long.MinValue
|
||||
&& now - _crossfadeStartTickMs < CrossfadeDurationMs
|
||||
)
|
||||
{
|
||||
var t = (float)(now - _crossfadeStartTickMs) / CrossfadeDurationMs;
|
||||
snapshot = ThemeAbgrCacheLerp.Lerp(_previousAbgrSnapshot.Value, _active.AbgrCache, t);
|
||||
}
|
||||
else
|
||||
{
|
||||
snapshot = _active.AbgrCache;
|
||||
}
|
||||
|
||||
_previousAbgrSnapshot = snapshot;
|
||||
_crossfadeStartTickMs = now;
|
||||
}
|
||||
|
||||
// Returns the lerped AbgrCache while the crossfade is active.
|
||||
// PushGlobal reads this once per frame; outside the 300ms window
|
||||
// it short-circuits via the TickCount64 delta so the per-frame
|
||||
// overhead is a couple of integer comparisons.
|
||||
public bool TryGetActiveCrossfade(out ThemeAbgrCache lerped)
|
||||
{
|
||||
lerped = default;
|
||||
if (_crossfadeStartTickMs == long.MinValue || !_previousAbgrSnapshot.HasValue)
|
||||
return false;
|
||||
|
||||
var elapsed = Environment.TickCount64 - _crossfadeStartTickMs;
|
||||
if (elapsed >= CrossfadeDurationMs)
|
||||
return false;
|
||||
|
||||
var t = (float)elapsed / CrossfadeDurationMs;
|
||||
lerped = ThemeAbgrCacheLerp.Lerp(_previousAbgrSnapshot.Value, _active.AbgrCache, t);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 1Hz-throttled disk-stat on the currently active custom theme file.
|
||||
// When the file's LastWriteTime moves forward (editor save), reload the
|
||||
// theme via Get() so the user sees the edit immediately without
|
||||
|
||||
Reference in New Issue
Block a user