Index.axaml.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. using Avalonia;
  2. using Avalonia.Collections;
  3. using Avalonia.Data;
  4. using Avalonia.Markup.Xaml;
  5. using Avalonia.Media;
  6. using Avalonia.Styling;
  7. using SukiUI.Enums;
  8. using SukiUI.Extensions;
  9. using SukiUI.Models;
  10. using System;
  11. using System.Collections.Generic;
  12. using System.Linq;
  13. namespace SukiUI;
  14. public partial class SukiTheme : Styles
  15. {
  16. public static readonly StyledProperty<SukiColor> ThemeColorProperty =
  17. AvaloniaProperty.Register<SukiTheme, SukiColor>(nameof(Color), defaultBindingMode: BindingMode.OneTime,
  18. defaultValue: SukiColor.Blue);
  19. public static readonly StyledProperty<bool> IsRightToLeftProperty =
  20. AvaloniaProperty.Register<SukiTheme, bool>(nameof(IsRightToLeft), defaultBindingMode: BindingMode.OneTime,
  21. defaultValue: false);
  22. /// <summary>
  23. /// Used to assign the ColorTheme at launch,
  24. /// </summary>
  25. public SukiColor ThemeColor
  26. {
  27. get => GetValue(ThemeColorProperty);
  28. set
  29. {
  30. SetValue(ThemeColorProperty, value);
  31. SetColorThemeResourcesOnColorThemeChanged();
  32. }
  33. }
  34. public bool IsRightToLeft
  35. {
  36. get => GetValue(IsRightToLeftProperty);
  37. set => SetValue(IsRightToLeftProperty, value);
  38. }
  39. /// <summary>
  40. /// Called whenever the application's <see cref="SukiColorTheme"/> is changed.
  41. /// Useful where controls cannot use "DynamicResource"
  42. /// </summary>
  43. public Action<SukiColorTheme>? OnColorThemeChanged { get; set; }
  44. /// <summary>
  45. /// Called whenever the application's <see cref="ThemeVariant"/> is changed.
  46. /// Useful where controls need to change based on light/dark.
  47. /// </summary>
  48. public Action<ThemeVariant>? OnBaseThemeChanged { get; set; }
  49. /// <summary>
  50. /// Currently active <see cref="SukiColorTheme"/>
  51. /// If you want to change this please use <see cref="ChangeColorTheme(SukiUI.Models.SukiColorTheme)"/>
  52. /// </summary>
  53. public SukiColorTheme? ActiveColorTheme { get; private set; }
  54. /// <summary>
  55. /// All available Color Themes.
  56. /// </summary>
  57. public IAvaloniaReadOnlyList<SukiColorTheme> ColorThemes => _allThemes;
  58. /// <summary>
  59. /// Currently active <see cref="ThemeVariant"/>
  60. /// If you want to change this please use <see cref="ChangeBaseTheme"/> or <see cref="SwitchBaseTheme"/>
  61. /// </summary>
  62. public ThemeVariant ActiveBaseTheme => _app.ActualThemeVariant;
  63. private readonly Application _app;
  64. private readonly HashSet<SukiColorTheme> _colorThemeHashset = new();
  65. private readonly AvaloniaList<SukiColorTheme> _allThemes = new();
  66. public SukiTheme()
  67. {
  68. AvaloniaXamlLoader.Load(this);
  69. _app = Application.Current!;
  70. _app.ActualThemeVariantChanged += (_, e) => OnBaseThemeChanged?.Invoke(_app.ActualThemeVariant);
  71. foreach (var theme in DefaultColorThemes)
  72. AddColorTheme(theme.Value);
  73. UpdateFlowDirectionResources(IsRightToLeft);
  74. }
  75. protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
  76. {
  77. if (change.Property == IsRightToLeftProperty)
  78. {
  79. UpdateFlowDirectionResources(change.GetNewValue<bool>());
  80. }
  81. base.OnPropertyChanged(change);
  82. }
  83. /// <summary>
  84. /// Change the theme to one of the default themes.
  85. /// </summary>
  86. /// <param name="sukiColor">The <see cref="SukiColor"/> to change to.</param>
  87. public void ChangeColorTheme(SukiColor sukiColor) =>
  88. ThemeColor = sukiColor;
  89. /// <summary>
  90. /// Tries to change the theme to a specific theme, this can be either a default or a custom defined one.
  91. /// </summary>
  92. /// <param name="sukiColorTheme"></param>
  93. public void ChangeColorTheme(SukiColorTheme sukiColorTheme) =>
  94. SetColorTheme(sukiColorTheme);
  95. /// <summary>
  96. /// Blindly switches to the "next" theme available in the <see cref="ColorThemes"/> collection.
  97. /// </summary>
  98. public void SwitchColorTheme()
  99. {
  100. var index = -1;
  101. for (var i = 0; i < ColorThemes.Count; i++)
  102. {
  103. if (ColorThemes[i] != ActiveColorTheme) continue;
  104. index = i;
  105. break;
  106. }
  107. if (index == -1) return;
  108. var newIndex = (index + 1) % ColorThemes.Count;
  109. var newColorTheme = ColorThemes[newIndex];
  110. ChangeColorTheme(newColorTheme);
  111. }
  112. /// <summary>
  113. /// Add a new <see cref="SukiColorTheme"/> to the ones available, without making it active.
  114. /// </summary>
  115. /// <param name="sukiColorTheme">New <see cref="SukiColorTheme"/> to add.</param>
  116. public void AddColorTheme(SukiColorTheme sukiColorTheme)
  117. {
  118. if (_colorThemeHashset.Contains(sukiColorTheme))
  119. throw new InvalidOperationException("This color theme has already been added.");
  120. _colorThemeHashset.Add(sukiColorTheme);
  121. _allThemes.Add(sukiColorTheme);
  122. }
  123. /// <summary>
  124. /// Adds multiple new <see cref="SukiColorTheme"/> to the ones available, without making any active.
  125. /// </summary>
  126. /// <param name="sukiColorThemes">A collection of new <see cref="SukiColorTheme"/> to add.</param>
  127. public void AddColorThemes(IEnumerable<SukiColorTheme> sukiColorThemes)
  128. {
  129. foreach (var colorTheme in sukiColorThemes)
  130. AddColorTheme(colorTheme);
  131. }
  132. /// <summary>
  133. /// Tries to change the base theme to the one provided, if it is different.
  134. /// </summary>
  135. /// <param name="baseTheme"><see cref="ThemeVariant"/> to change to.</param>
  136. public void ChangeBaseTheme(ThemeVariant baseTheme)
  137. {
  138. if (_app.ActualThemeVariant == baseTheme) return;
  139. _app.RequestedThemeVariant = baseTheme;
  140. }
  141. /// <summary>
  142. /// Simply switches from Light -> Dark and visa versa.
  143. /// </summary>
  144. public void SwitchBaseTheme()
  145. {
  146. if (Application.Current is null) return;
  147. var newBase = Application.Current.ActualThemeVariant == ThemeVariant.Dark
  148. ? ThemeVariant.Light
  149. : ThemeVariant.Dark;
  150. Application.Current.RequestedThemeVariant = newBase;
  151. }
  152. private void UpdateFlowDirectionResources(bool rightToLeft)
  153. {
  154. var primary = rightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight;
  155. var opposite = rightToLeft ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
  156. Resources["FlowDirectionPrimary"] = primary;
  157. Resources["FlowDirectionOpposite"] = opposite;
  158. }
  159. /// <summary>
  160. /// Initializes the color theme resources whenever the property is changed.
  161. /// In an ideal world people wouldn't use the property
  162. /// </summary>
  163. private void SetColorThemeResourcesOnColorThemeChanged()
  164. {
  165. if (!DefaultColorThemes.TryGetValue(ThemeColor, out var colorTheme))
  166. throw new Exception($"{ThemeColor} has no defined color theme.");
  167. SetColorTheme(colorTheme);
  168. }
  169. private void SetColorTheme(SukiColorTheme colorTheme)
  170. {
  171. SetColorWithOpacities("SukiPrimaryColor", colorTheme.Primary);
  172. SetResource("SukiPrimaryDarkColor", colorTheme.PrimaryDark);
  173. SetColorWithOpacities("SukiAccentColor", colorTheme.Accent);
  174. SetResource("SukiAccentDarkColor", colorTheme.AccentDark);
  175. ActiveColorTheme = colorTheme;
  176. OnColorThemeChanged?.Invoke(ActiveColorTheme);
  177. }
  178. private void SetColorWithOpacities(string baseName, Color baseColor)
  179. {
  180. SetResource(baseName, baseColor);
  181. SetResource($"{baseName}75", baseColor.WithAlpha(0.75));
  182. SetResource($"{baseName}50", baseColor.WithAlpha(0.50));
  183. SetResource($"{baseName}25", baseColor.WithAlpha(0.25));
  184. SetResource($"{baseName}20", baseColor.WithAlpha(0.2));
  185. SetResource($"{baseName}15", baseColor.WithAlpha(0.15));
  186. SetResource($"{baseName}10", baseColor.WithAlpha(0.10));
  187. SetResource($"{baseName}7", baseColor.WithAlpha(0.07));
  188. SetResource($"{baseName}5", baseColor.WithAlpha(0.05));
  189. SetResource($"{baseName}3", baseColor.WithAlpha(0.03));
  190. SetResource($"{baseName}1", baseColor.WithAlpha(0.005));
  191. SetResource($"{baseName}0", baseColor.WithAlpha(0.00));
  192. }
  193. private void SetResource(string name, Color color) =>
  194. _app.Resources[name] = color;
  195. // Static Members...
  196. /// <summary>
  197. /// The default Color Themes included with SukiUI.
  198. /// </summary>
  199. public static readonly IReadOnlyDictionary<SukiColor, SukiColorTheme> DefaultColorThemes;
  200. static SukiTheme()
  201. {
  202. var defaultThemes = new[]
  203. {
  204. new DefaultSukiColorTheme(SukiColor.Orange, Color.Parse("#d48806"), Color.Parse("#176CE8")),
  205. new DefaultSukiColorTheme(SukiColor.Red, Color.Parse("#D03A2F"), Color.Parse("#2FC5D0")),
  206. new DefaultSukiColorTheme(SukiColor.Green, Color.Parse("#537834"), Color.Parse("#B24DB0")),
  207. new DefaultSukiColorTheme(SukiColor.Blue, Color.Parse("#0A59F7"), Color.Parse("#F7A80A")),
  208. };
  209. DefaultColorThemes = defaultThemes.ToDictionary(x => x.ThemeColor, y => (SukiColorTheme)y);
  210. }
  211. /// <summary>
  212. /// Retrieves an instance tied to a specific instance of an application.
  213. /// </summary>
  214. /// <returns>A <see cref="SukiTheme"/> instance that can be used to change themes.</returns>
  215. /// <exception cref="InvalidOperationException">Thrown if no SukiTheme has been defined in App.axaml.</exception>
  216. public static SukiTheme GetInstance(Application app)
  217. {
  218. var theme = app.Styles.FirstOrDefault(style => style is SukiTheme);
  219. if (theme is not SukiTheme sukiTheme)
  220. throw new InvalidOperationException(
  221. "No SukiTheme instance available. Ensure SukiTheme has been set in Application.Styles in App.axaml.");
  222. return sukiTheme;
  223. }
  224. /// <summary>
  225. /// Retrieves an instance tied to the currently active application.
  226. /// </summary>
  227. /// <returns>A <see cref="SukiTheme"/> instance that can be used to change themes.</returns>
  228. /// <exception cref="InvalidOperationException">Thrown if no SukiTheme has been defined in App.axaml.</exception>
  229. public static SukiTheme GetInstance() => GetInstance(Application.Current!);
  230. }