WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 8814d9d

Browse files
committed
Add theme management and value converters for Avalonia UI
Introduces ApplicationThemeManager and SystemThemeManager for handling application and system themes, including accent color and high contrast detection. Adds several value converters for Avalonia UI, such as BoolToInvertedBoolConverter, BrushToColorConverter, ColorToBrushConverter, ColorToHexConverter, EnumToBoolConverter, MinConverter, and TextToAsteriskConverter, to facilitate common UI data transformations. Also includes a Transition enum for animation types and a ThemeChangedEvent delegate.
1 parent decafcd commit 8814d9d

File tree

11 files changed

+625
-0
lines changed

11 files changed

+625
-0
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved.
2+
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
namespace CrissCross.Avalonia.UI.Animations;
6+
7+
/// <summary>
8+
/// Available types of transitions.
9+
/// </summary>
10+
public enum Transition
11+
{
12+
/// <summary>
13+
/// None.
14+
/// </summary>
15+
None,
16+
17+
/// <summary>
18+
/// Change opacity.
19+
/// </summary>
20+
FadeIn,
21+
22+
/// <summary>
23+
/// Change opacity and slide from bottom.
24+
/// </summary>
25+
FadeInWithSlide,
26+
27+
/// <summary>
28+
/// Slide from bottom.
29+
/// </summary>
30+
SlideBottom,
31+
32+
/// <summary>
33+
/// Slide from the right side.
34+
/// </summary>
35+
SlideRight,
36+
37+
/// <summary>
38+
/// Slide from the left side.
39+
/// </summary>
40+
SlideLeft,
41+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved.
2+
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using Avalonia;
6+
using Avalonia.Media;
7+
using Avalonia.Styling;
8+
9+
namespace CrissCross.Avalonia.UI.Appearance;
10+
11+
/// <summary>
12+
/// Allows to manage the application theme.
13+
/// </summary>
14+
public static class ApplicationThemeManager
15+
{
16+
private static ApplicationTheme _cachedApplicationTheme = ApplicationTheme.Unknown;
17+
private static Color _accentColor = Colors.CornflowerBlue;
18+
19+
/// <summary>
20+
/// Event triggered when the application's theme is changed.
21+
/// </summary>
22+
public static event ThemeChangedEvent? Changed;
23+
24+
/// <summary>
25+
/// Gets a value that indicates whether the application is currently using the high contrast theme.
26+
/// </summary>
27+
/// <returns><see langword="true"/> if application uses high contrast theme.</returns>
28+
public static bool IsHighContrast() => _cachedApplicationTheme == ApplicationTheme.HighContrast;
29+
30+
/// <summary>
31+
/// Gets the current application theme.
32+
/// </summary>
33+
/// <returns>The current <see cref="ApplicationTheme"/>.</returns>
34+
public static ApplicationTheme GetAppTheme()
35+
{
36+
if (_cachedApplicationTheme == ApplicationTheme.Unknown)
37+
{
38+
FetchApplicationTheme();
39+
}
40+
41+
return _cachedApplicationTheme;
42+
}
43+
44+
/// <summary>
45+
/// Gets the current accent color.
46+
/// </summary>
47+
/// <returns>The current accent <see cref="Color"/>.</returns>
48+
public static Color GetAccentColor() => _accentColor;
49+
50+
/// <summary>
51+
/// Changes the current application theme.
52+
/// </summary>
53+
/// <param name="applicationTheme">Theme to set.</param>
54+
/// <param name="accentColor">Optional accent color.</param>
55+
public static void Apply(ApplicationTheme applicationTheme, Color? accentColor = null)
56+
{
57+
if (applicationTheme == ApplicationTheme.Unknown)
58+
{
59+
return;
60+
}
61+
62+
_cachedApplicationTheme = applicationTheme;
63+
64+
if (accentColor.HasValue)
65+
{
66+
_accentColor = accentColor.Value;
67+
}
68+
69+
// Set the theme variant in Avalonia
70+
if (Application.Current is not null)
71+
{
72+
Application.Current.RequestedThemeVariant = applicationTheme switch
73+
{
74+
ApplicationTheme.Dark => ThemeVariant.Dark,
75+
ApplicationTheme.Light => ThemeVariant.Light,
76+
_ => ThemeVariant.Default
77+
};
78+
}
79+
80+
Changed?.Invoke(_cachedApplicationTheme, _accentColor);
81+
}
82+
83+
/// <summary>
84+
/// Applies the system theme to the application.
85+
/// </summary>
86+
public static void ApplySystemTheme()
87+
{
88+
var systemTheme = SystemThemeManager.GetSystemTheme();
89+
90+
var themeToSet = systemTheme switch
91+
{
92+
SystemTheme.Dark or SystemTheme.CapturedMotion or SystemTheme.Glow => ApplicationTheme.Dark,
93+
SystemTheme.HC1 or SystemTheme.HC2 or SystemTheme.HCBlack or SystemTheme.HCWhite => ApplicationTheme.HighContrast,
94+
_ => ApplicationTheme.Light
95+
};
96+
97+
Apply(themeToSet);
98+
}
99+
100+
/// <summary>
101+
/// Gets a value that indicates whether the application is matching the system theme.
102+
/// </summary>
103+
/// <returns><see langword="true"/> if the application has the same theme as the system.</returns>
104+
public static bool IsAppMatchesSystem()
105+
{
106+
var appTheme = GetAppTheme();
107+
var sysTheme = SystemThemeManager.GetSystemTheme();
108+
109+
return appTheme switch
110+
{
111+
ApplicationTheme.Dark => sysTheme is SystemTheme.Dark or SystemTheme.CapturedMotion or SystemTheme.Glow,
112+
ApplicationTheme.Light => sysTheme is SystemTheme.Light or SystemTheme.Flow or SystemTheme.Sunrise,
113+
ApplicationTheme.HighContrast => sysTheme is SystemTheme.HC1 or SystemTheme.HC2 or SystemTheme.HCBlack or SystemTheme.HCWhite,
114+
_ => false
115+
};
116+
}
117+
118+
/// <summary>
119+
/// Checks if the application is currently using a dark theme.
120+
/// </summary>
121+
/// <returns><see langword="true"/> if using dark theme.</returns>
122+
public static bool IsDark() => _cachedApplicationTheme == ApplicationTheme.Dark;
123+
124+
/// <summary>
125+
/// Checks if the application is currently using a light theme.
126+
/// </summary>
127+
/// <returns><see langword="true"/> if using light theme.</returns>
128+
public static bool IsLight() => _cachedApplicationTheme == ApplicationTheme.Light;
129+
130+
/// <summary>
131+
/// Sets the accent color.
132+
/// </summary>
133+
/// <param name="color">The accent color to set.</param>
134+
public static void SetAccentColor(Color color)
135+
{
136+
_accentColor = color;
137+
Changed?.Invoke(_cachedApplicationTheme, _accentColor);
138+
}
139+
140+
private static void FetchApplicationTheme()
141+
{
142+
if (Application.Current is null)
143+
{
144+
_cachedApplicationTheme = ApplicationTheme.Light;
145+
return;
146+
}
147+
148+
var actualTheme = Application.Current.ActualThemeVariant;
149+
_cachedApplicationTheme = actualTheme == ThemeVariant.Dark
150+
? ApplicationTheme.Dark
151+
: ApplicationTheme.Light;
152+
}
153+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved.
2+
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using System.Runtime.InteropServices;
6+
7+
namespace CrissCross.Avalonia.UI.Appearance;
8+
9+
/// <summary>
10+
/// Provides methods for detecting the system theme.
11+
/// </summary>
12+
public static class SystemThemeManager
13+
{
14+
private static SystemTheme _cachedSystemTheme = SystemTheme.Unknown;
15+
16+
/// <summary>
17+
/// Gets the current system theme.
18+
/// </summary>
19+
/// <returns>The current <see cref="SystemTheme"/>.</returns>
20+
public static SystemTheme GetSystemTheme()
21+
{
22+
if (_cachedSystemTheme == SystemTheme.Unknown)
23+
{
24+
UpdateSystemThemeCache();
25+
}
26+
27+
return _cachedSystemTheme;
28+
}
29+
30+
/// <summary>
31+
/// Updates the cached system theme.
32+
/// </summary>
33+
public static void UpdateSystemThemeCache()
34+
{
35+
_cachedSystemTheme = DetectSystemTheme();
36+
}
37+
38+
/// <summary>
39+
/// Gets a value indicating whether the system is using high contrast.
40+
/// </summary>
41+
/// <returns><see langword="true"/> if high contrast is enabled.</returns>
42+
public static bool IsHighContrast()
43+
{
44+
var theme = GetSystemTheme();
45+
return theme is SystemTheme.HC1 or SystemTheme.HC2 or SystemTheme.HCBlack or SystemTheme.HCWhite;
46+
}
47+
48+
/// <summary>
49+
/// Gets a value indicating whether the system is using a dark theme.
50+
/// </summary>
51+
/// <returns><see langword="true"/> if using dark theme.</returns>
52+
public static bool IsDark()
53+
{
54+
var theme = GetSystemTheme();
55+
return theme is SystemTheme.Dark or SystemTheme.CapturedMotion or SystemTheme.Glow;
56+
}
57+
58+
/// <summary>
59+
/// Gets a value indicating whether the system is using a light theme.
60+
/// </summary>
61+
/// <returns><see langword="true"/> if using light theme.</returns>
62+
public static bool IsLight()
63+
{
64+
var theme = GetSystemTheme();
65+
return theme is SystemTheme.Light or SystemTheme.Flow or SystemTheme.Sunrise;
66+
}
67+
68+
private static SystemTheme DetectSystemTheme()
69+
{
70+
// Cross-platform detection
71+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
72+
{
73+
return DetectWindowsTheme();
74+
}
75+
76+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
77+
{
78+
return DetectMacOSTheme();
79+
}
80+
81+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
82+
{
83+
return DetectLinuxTheme();
84+
}
85+
86+
return SystemTheme.Light;
87+
}
88+
89+
private static SystemTheme DetectWindowsTheme()
90+
{
91+
try
92+
{
93+
using var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
94+
if (key?.GetValue("AppsUseLightTheme") is int useLightTheme)
95+
{
96+
return useLightTheme == 0 ? SystemTheme.Dark : SystemTheme.Light;
97+
}
98+
}
99+
catch
100+
{
101+
// Registry access might fail on some systems
102+
}
103+
104+
return SystemTheme.Light;
105+
}
106+
107+
private static SystemTheme DetectMacOSTheme()
108+
{
109+
// On macOS, we'd typically use NSAppearance
110+
// For now, default to Light
111+
return SystemTheme.Light;
112+
}
113+
114+
private static SystemTheme DetectLinuxTheme()
115+
{
116+
// On Linux, theme detection varies by desktop environment
117+
// For now, default to Light
118+
try
119+
{
120+
var gtkTheme = Environment.GetEnvironmentVariable("GTK_THEME");
121+
if (!string.IsNullOrEmpty(gtkTheme) && gtkTheme.Contains("dark", StringComparison.OrdinalIgnoreCase))
122+
{
123+
return SystemTheme.Dark;
124+
}
125+
}
126+
catch
127+
{
128+
// Environment variable access might fail
129+
}
130+
131+
return SystemTheme.Light;
132+
}
133+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved.
2+
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using Avalonia.Media;
6+
7+
namespace CrissCross.Avalonia.UI.Appearance;
8+
9+
/// <summary>
10+
/// Event delegate for theme changes.
11+
/// </summary>
12+
/// <param name="currentTheme">The current application theme.</param>
13+
/// <param name="accentColor">The current accent color.</param>
14+
public delegate void ThemeChangedEvent(ApplicationTheme currentTheme, Color accentColor);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved.
2+
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using System.Globalization;
6+
using Avalonia.Data.Converters;
7+
8+
namespace CrissCross.Avalonia.UI.Converters;
9+
10+
/// <summary>
11+
/// Converts a boolean value to its inverted value.
12+
/// </summary>
13+
public class BoolToInvertedBoolConverter : IValueConverter
14+
{
15+
/// <summary>
16+
/// Gets the default instance of this converter.
17+
/// </summary>
18+
public static BoolToInvertedBoolConverter Instance { get; } = new();
19+
20+
/// <inheritdoc/>
21+
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
22+
{
23+
if (value is bool boolValue)
24+
{
25+
return !boolValue;
26+
}
27+
28+
return false;
29+
}
30+
31+
/// <inheritdoc/>
32+
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
33+
{
34+
if (value is bool boolValue)
35+
{
36+
return !boolValue;
37+
}
38+
39+
return false;
40+
}
41+
}

0 commit comments

Comments
 (0)