Files
cursor-lang/CursorLang/Services/ThemeService.cs
T
2026-08-09 20:11:14 +05:00

162 lines
4.8 KiB
C#

using System.ComponentModel;
using System.Windows;
using System.Windows.Interop;
using CursorLang.Interop;
using CursorLang.Models;
using Microsoft.Win32;
namespace CursorLang.Services;
/// <summary>
/// Keeps the palette of the chosen theme in the application resources and swaps it
/// when the setting changes — the windows are recoloured without a restart.
/// </summary>
public sealed class ThemeService : IThemeService, IDisposable
{
private const string PersonalizeKey =
@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
private readonly AppSettings _settings;
private readonly Func<AppTheme> _detectSystemTheme;
private readonly List<Window> _windows = [];
private ResourceDictionary? _palette;
private AppTheme _current;
public ThemeService(AppSettings settings)
: this(settings, DetectSystemTheme)
{
}
/// <summary>
/// Takes the source of the system theme explicitly: in tests it is not the registry
/// that provides it.
/// </summary>
internal ThemeService(AppSettings settings, Func<AppTheme> detectSystemTheme)
{
_settings = settings;
_detectSystemTheme = detectSystemTheme;
_settings.PropertyChanged += OnSettingsChanged;
SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged;
Apply();
}
/// <summary>The theme the user sees: <c>System</c> is already resolved here.</summary>
public AppTheme CurrentTheme => _current;
public void Register(Window window)
{
_windows.Add(window);
window.Closed += OnWindowClosed;
if (new WindowInteropHelper(window).Handle == IntPtr.Zero)
{
window.SourceInitialized += OnWindowSourceInitialized;
return;
}
ApplyTitleBar(window);
}
public void Dispose()
{
_settings.PropertyChanged -= OnSettingsChanged;
SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged;
foreach (Window window in _windows)
{
window.Closed -= OnWindowClosed;
window.SourceInitialized -= OnWindowSourceInitialized;
}
_windows.Clear();
}
/// <summary>The app theme from the Windows settings.</summary>
internal static AppTheme DetectSystemTheme()
{
try
{
using RegistryKey? key = Registry.CurrentUser.OpenSubKey(PersonalizeKey);
return key?.GetValue("AppsUseLightTheme") is int light && light == 0
? AppTheme.Dark
: AppTheme.Light;
}
catch (Exception e) when (e is System.Security.SecurityException or UnauthorizedAccessException)
{
return AppTheme.Light;
}
}
private void OnSettingsChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(AppSettings.Theme))
{
Apply();
}
}
// Windows reports a theme change from outside the interface thread
private void OnUserPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e) =>
Application.Current?.Dispatcher.InvokeAsync(Apply);
private void Apply()
{
AppTheme theme = _settings.Theme == AppTheme.System ? _detectSystemTheme() : _settings.Theme;
if (_palette is not null && theme == _current)
{
return;
}
_current = theme;
var next = new ResourceDictionary
{
Source = PaletteUri(theme),
};
ICollection<ResourceDictionary> dictionaries = Application.Current.Resources.MergedDictionaries;
dictionaries.Add(next);
if (_palette is not null)
{
dictionaries.Remove(_palette);
}
_palette = next;
foreach (Window window in _windows)
{
ApplyTitleBar(window);
}
}
// The assembly name in the address is there on purpose: without it the dictionary
// is looked up in the assembly the process started from, which is not always the
// application itself
internal static Uri PaletteUri(AppTheme theme) =>
new($"pack://application:,,,/CursorLang;component/Themes/{theme}.xaml", UriKind.Absolute);
private void ApplyTitleBar(Window window) =>
WindowThemeNative.SetDarkTitleBar(
new WindowInteropHelper(window).Handle,
_current == AppTheme.Dark);
private void OnWindowSourceInitialized(object? sender, EventArgs e)
{
if (sender is Window window)
{
window.SourceInitialized -= OnWindowSourceInitialized;
ApplyTitleBar(window);
}
}
private void OnWindowClosed(object? sender, EventArgs e)
{
if (sender is Window window)
{
window.Closed -= OnWindowClosed;
_windows.Remove(window);
}
}
}