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 long _lastActiveStampCheckMs = -ActiveStampPollIntervalMs;
|
||||||
private DateTime _lastActiveStamp = DateTime.MinValue;
|
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)
|
public ThemeRegistry(string? customThemesDir = null, ILogger<ThemeRegistry>? logger = null)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -87,6 +97,13 @@ public sealed class ThemeRegistry
|
|||||||
// a state where _active and Get(_active.Slug) disagree.
|
// a state where _active and Get(_active.Slug) disagree.
|
||||||
public void Switch(string slug)
|
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))
|
if (_builtIns.TryGetValue(slug, out var builtin))
|
||||||
{
|
{
|
||||||
_active = builtin;
|
_active = builtin;
|
||||||
@@ -115,6 +132,84 @@ public sealed class ThemeRegistry
|
|||||||
_activeCustomPath = null;
|
_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.
|
// 1Hz-throttled disk-stat on the currently active custom theme file.
|
||||||
// When the file's LastWriteTime moves forward (editor save), reload the
|
// When the file's LastWriteTime moves forward (editor save), reload the
|
||||||
// theme via Get() so the user sees the edit immediately without
|
// theme via Get() so the user sees the edit immediately without
|
||||||
|
|||||||
Reference in New Issue
Block a user