| | | 1 | | namespace Allyaria.Theming.Services; |
| | | 2 | | |
| | | 3 | | /// <summary> |
| | | 4 | | /// Provides the core implementation of the <see cref="IThemingService" /> interface, managing the currently active and |
| | | 5 | | /// stored theme types, as well as CSS generation for Allyaria-themed components and documents. |
| | | 6 | | /// </summary> |
| | | 7 | | /// <remarks> |
| | | 8 | | /// <para> |
| | | 9 | | /// This service maintains both the <see cref="EffectiveType" /> (the theme currently applied in the UI) and the |
| | | 10 | | /// <see cref="StoredType" /> (the user’s persisted theme preference). |
| | | 11 | | /// </para> |
| | | 12 | | /// <para> |
| | | 13 | | /// When <see cref="StoredType" /> changes, the service raises the <see cref="ThemeChanged" /> event so that UI |
| | | 14 | | /// components can reactively update their styling. |
| | | 15 | | /// </para> |
| | | 16 | | /// </remarks> |
| | | 17 | | public sealed class ThemingService : IThemingService |
| | | 18 | | { |
| | | 19 | | /// <summary>The underlying theme definition used to compute and generate CSS styles.</summary> |
| | | 20 | | private readonly Theme _theme; |
| | | 21 | | |
| | | 22 | | /// <summary> |
| | | 23 | | /// Initializes a new instance of the <see cref="ThemingService" /> class with the specified theme and initial type. |
| | | 24 | | /// </summary> |
| | | 25 | | /// <param name="theme">The <see cref="Theme" /> instance used to generate CSS and map component styles.</param> |
| | | 26 | | /// <param name="themeType">The initial <see cref="ThemeType" /> to use. Defaults to <see cref="ThemeType.System" /> |
| | 16 | 27 | | internal ThemingService(Theme theme, ThemeType themeType = ThemeType.System) |
| | | 28 | | { |
| | 16 | 29 | | _theme = theme; |
| | 16 | 30 | | StoredType = themeType; |
| | | 31 | | |
| | 16 | 32 | | EffectiveType = StoredType is ThemeType.System |
| | 16 | 33 | | ? ThemeType.Light |
| | 16 | 34 | | : StoredType; |
| | 16 | 35 | | } |
| | | 36 | | |
| | | 37 | | /// <summary>Occurs when the currently active (effective) theme changes.</summary> |
| | | 38 | | public event EventHandler? ThemeChanged; |
| | | 39 | | |
| | | 40 | | /// <summary>Gets the currently effective <see cref="ThemeType" /> applied to the UI.</summary> |
| | 41 | 41 | | public ThemeType EffectiveType { get; private set; } |
| | | 42 | | |
| | | 43 | | /// <summary> |
| | | 44 | | /// Gets the persisted or stored <see cref="ThemeType" />, representing user preference or saved configuration. |
| | | 45 | | /// </summary> |
| | 56 | 46 | | public ThemeType StoredType { get; private set; } |
| | | 47 | | |
| | | 48 | | /// <summary> |
| | | 49 | | /// Generates component-level CSS for a given prefix, component type, and state, based on the current effective them |
| | | 50 | | /// </summary> |
| | | 51 | | /// <param name="prefix">The CSS class prefix for the component.</param> |
| | | 52 | | /// <param name="componentType">The <see cref="ComponentType" /> representing the themed component.</param> |
| | | 53 | | /// <param name="componentState">The <see cref="ComponentState" /> representing the current visual state.</param> |
| | | 54 | | /// <returns>A string containing CSS rules scoped to the specified component and theme state.</returns> |
| | | 55 | | public string GetComponentCss(string prefix, ComponentType componentType, ComponentState componentState) |
| | 4 | 56 | | => _theme.GetComponentCss( |
| | 4 | 57 | | prefix: prefix, |
| | 4 | 58 | | componentType: componentType, |
| | 4 | 59 | | themeType: EffectiveType, |
| | 4 | 60 | | componentState: componentState |
| | 4 | 61 | | ); |
| | | 62 | | |
| | | 63 | | /// <summary> |
| | | 64 | | /// Generates CSS variable declarations for a specific <see cref="ThemeType" />, <see cref="ComponentType" />, and |
| | | 65 | | /// <see cref="ComponentState" /> by transforming computed component CSS into corresponding <c>var(--prefix-property |
| | | 66 | | /// references. |
| | | 67 | | /// </summary> |
| | | 68 | | /// <param name="themeType"> |
| | | 69 | | /// The theme from which variables should be generated. If <see cref="ThemeType.System" /> is specified, no variable |
| | | 70 | | /// produced. |
| | | 71 | | /// </param> |
| | | 72 | | /// <param name="componentType">The component type whose themed styles are being converted into variables.</param> |
| | | 73 | | /// <param name="componentState">The visual state of the component whose styles should be mapped.</param> |
| | | 74 | | /// <returns> |
| | | 75 | | /// A string containing CSS variable references derived from the component’s themed CSS, or an empty string when no |
| | | 76 | | /// variables can be generated. |
| | | 77 | | /// </returns> |
| | | 78 | | public string GetComponentCssVars(ThemeType themeType, ComponentType componentType, ComponentState componentState) |
| | | 79 | | { |
| | 3 | 80 | | if (themeType is ThemeType.System) |
| | | 81 | | { |
| | 1 | 82 | | return string.Empty; |
| | | 83 | | } |
| | | 84 | | |
| | 2 | 85 | | var cssVars = GetComponentCss( |
| | 2 | 86 | | prefix: StyleDefaults.VarPrefix, componentType: componentType, componentState: componentState |
| | 2 | 87 | | ); |
| | | 88 | | |
| | 2 | 89 | | if (string.IsNullOrWhiteSpace(value: cssVars)) |
| | | 90 | | { |
| | | 91 | | // Code Coverage: Unreachable code path |
| | 0 | 92 | | return string.Empty; |
| | | 93 | | } |
| | | 94 | | |
| | 2 | 95 | | var builder = new StringBuilder(); |
| | 2 | 96 | | var prefix = $"{StyleDefaults.VarPrefix}-{componentType}-{themeType}-{componentState}".ToCssName(); |
| | 2 | 97 | | var split = cssVars.Split(separator: ';', options: StringSplitOptions.RemoveEmptyEntries); |
| | | 98 | | |
| | 10 | 99 | | foreach (var item in split) |
| | | 100 | | { |
| | 3 | 101 | | var pair = item.Split(separator: ':', options: StringSplitOptions.RemoveEmptyEntries); |
| | | 102 | | |
| | 3 | 103 | | if (pair.Length < 2) |
| | | 104 | | { |
| | | 105 | | continue; |
| | | 106 | | } |
| | | 107 | | |
| | 1 | 108 | | var property = pair[0].ToCssName(); |
| | | 109 | | |
| | 1 | 110 | | if (string.IsNullOrWhiteSpace(value: property)) |
| | | 111 | | { |
| | | 112 | | // Code Coverage: Unreachable code path |
| | | 113 | | continue; |
| | | 114 | | } |
| | | 115 | | |
| | 1 | 116 | | builder.Append(handler: $"{property}:var(--{prefix}-{property});"); |
| | | 117 | | } |
| | | 118 | | |
| | 2 | 119 | | return builder.ToString(); |
| | | 120 | | } |
| | | 121 | | |
| | | 122 | | /// <summary>Generates global document-level CSS reflecting the currently active theme.</summary> |
| | | 123 | | /// <returns>A string containing CSS rules applicable at the document scope.</returns> |
| | 2 | 124 | | public string GetDocumentCss() => _theme.GetDocumentCss(themeType: EffectiveType); |
| | | 125 | | |
| | | 126 | | /// <summary>Raises the <see cref="ThemeChanged" /> event to notify subscribers of theme changes.</summary> |
| | 3 | 127 | | private void OnThemeChanged() => ThemeChanged?.Invoke(sender: this, e: EventArgs.Empty); |
| | | 128 | | |
| | | 129 | | /// <summary>Sets the currently effective <see cref="ThemeType" /> and triggers a theme update if it changes.</summa |
| | | 130 | | /// <param name="themeType">The new <see cref="ThemeType" /> to apply.</param> |
| | | 131 | | public void SetEffectiveType(ThemeType themeType) |
| | | 132 | | { |
| | 5 | 133 | | if (themeType == ThemeType.System || EffectiveType == themeType) |
| | | 134 | | { |
| | 2 | 135 | | return; |
| | | 136 | | } |
| | | 137 | | |
| | 3 | 138 | | EffectiveType = themeType; |
| | 3 | 139 | | OnThemeChanged(); |
| | 3 | 140 | | } |
| | | 141 | | |
| | | 142 | | /// <summary>Sets the stored <see cref="ThemeType" /> preference and updates the effective type accordingly.</summar |
| | | 143 | | /// <param name="themeType">The <see cref="ThemeType" /> to store and potentially activate.</param> |
| | | 144 | | public void SetStoredType(ThemeType themeType) |
| | | 145 | | { |
| | 3 | 146 | | if (StoredType == themeType) |
| | | 147 | | { |
| | 1 | 148 | | return; |
| | | 149 | | } |
| | | 150 | | |
| | 2 | 151 | | StoredType = themeType; |
| | | 152 | | |
| | 2 | 153 | | if (themeType != ThemeType.System) |
| | | 154 | | { |
| | 1 | 155 | | SetEffectiveType(themeType: themeType); |
| | | 156 | | } |
| | | 157 | | else |
| | | 158 | | { |
| | 1 | 159 | | ThemeChanged?.Invoke(sender: this, e: EventArgs.Empty); |
| | | 160 | | } |
| | 1 | 161 | | } |
| | | 162 | | } |