| | 1 | | using Allyaria.Theming.Constants; |
| | 2 | | using Allyaria.Theming.Helpers; |
| | 3 | | using Allyaria.Theming.Values; |
| | 4 | | using System.Text; |
| | 5 | | using System.Text.RegularExpressions; |
| | 6 | |
|
| | 7 | | namespace Allyaria.Theming.Styles; |
| | 8 | |
|
| | 9 | | /// <summary> |
| | 10 | | /// Represents an immutable, strongly typed palette used by the Allyaria theme engine to compute effective foreground, |
| | 11 | | /// background, and border styles (including hover variants) with the documented precedence rules. |
| | 12 | | /// </summary> |
| | 13 | | /// <remarks> |
| | 14 | | /// This type is designed for inline style generation and CSS custom property emission. It follows Allyaria theming |
| | 15 | | /// precedence—background images override region colors, explicit overrides beat defaults, and borders are opt-in. See |
| | 16 | | /// <see cref="ToCss" /> and <see cref="ToCssVars" />. |
| | 17 | | /// </remarks> |
| | 18 | | public readonly record struct AllyariaPalette |
| | 19 | | { |
| | 20 | | /// <summary>The base background color as provided, before any precedence (e.g., border presence) is applied.</summa |
| | 21 | | private readonly AllyariaColorValue? _backgroundColor; |
| | 22 | |
|
| | 23 | | /// <summary>Optional background image or <see langword="null" /> when no image was provided.</summary> |
| | 24 | | private readonly AllyariaImageValue? _backgroundImage; |
| | 25 | |
|
| | 26 | | /// <summary>Whether or not the background image is stretched.</summary> |
| | 27 | | private readonly bool _backgroundImageStretch; |
| | 28 | |
|
| | 29 | | /// <summary> |
| | 30 | | /// Optional explicit border color as provided by the caller; may be <see langword="null" /> to signal defaulting to |
| | 31 | | /// <see cref="BackgroundColor" /> when a border is present. |
| | 32 | | /// </summary> |
| | 33 | | private readonly AllyariaColorValue? _borderColor; |
| | 34 | |
|
| | 35 | | /// <summary>Optional border radius value (e.g., <c>4px</c>) or <see langword="null" /> to omit the declaration.</su |
| | 36 | | private readonly AllyariaStringValue? _borderRadius; |
| | 37 | |
|
| | 38 | | /// <summary>Border style token (e.g., <c>solid</c>). Defaults to <c>solid</c> when not supplied.</summary> |
| | 39 | | private readonly AllyariaStringValue? _borderStyle; |
| | 40 | |
|
| | 41 | | /// <summary> |
| | 42 | | /// Optional border width (e.g., <c>1px</c>) or <see langword="null" /> when no border should be rendered. |
| | 43 | | /// </summary> |
| | 44 | | private readonly int? _borderWidth; |
| | 45 | |
|
| | 46 | | /// <summary> |
| | 47 | | /// Optional explicit foreground color as provided by the caller; may be <see langword="null" /> to signal contrast- |
| | 48 | | /// defaulting from <see cref="BackgroundColor" />. |
| | 49 | | /// </summary> |
| | 50 | | private readonly AllyariaColorValue? _foregroundColor; |
| | 51 | |
|
| | 52 | | /// <summary>Initializes a new immutable <see cref="AllyariaPalette" />.</summary> |
| | 53 | | /// <param name="backgroundColor"> |
| | 54 | | /// Optional base background color; defaults to <see cref="Colors.White" /> when not |
| | 55 | | /// provided. |
| | 56 | | /// </param> |
| | 57 | | /// <param name="foregroundColor"> |
| | 58 | | /// Optional explicit foreground color; when not provided, it is computed from <see cref="BackgroundColor" /> lightn |
| | 59 | | /// contrast. |
| | 60 | | /// </param> |
| | 61 | | /// <param name="backgroundImage"> |
| | 62 | | /// An optional background image URL. When non-empty, this is transformed into a composite |
| | 63 | | /// <c>linear-gradient(...), url("...")</c> value to ensure readability. Whitespace is trimmed and the URL is lower- |
| | 64 | | /// for stability. When empty or whitespace, no image is used. |
| | 65 | | /// </param> |
| | 66 | | /// <param name="backgroundImageStretch"> |
| | 67 | | /// An optional boolean for determining if the background image is stretched or tiled. Defaults to stretched. |
| | 68 | | /// </param> |
| | 69 | | /// <param name="borderWidth"> |
| | 70 | | /// Border width in CSS pixels. Values <= 0 omit the border entirely; values > 0 render as <c><width>px< |
| | 71 | | /// </param> |
| | 72 | | /// <param name="borderColor"> |
| | 73 | | /// Optional explicit border color. When not provided but a border is present, the border color defaults to |
| | 74 | | /// <see cref="BackgroundColor" /> to maintain visual cohesion. |
| | 75 | | /// </param> |
| | 76 | | /// <param name="borderStyle"> |
| | 77 | | /// Optional border style token (e.g., <c>solid</c>, <c>dashed</c>). Defaults to <c>solid</c> when not supplied. |
| | 78 | | /// </param> |
| | 79 | | /// <param name="borderRadius"> |
| | 80 | | /// Optional border radius (e.g., <c>4px</c>). When provided and a border is present, a corresponding <c>border-radi |
| | 81 | | /// declaration is emitted. |
| | 82 | | /// </param> |
| | 83 | | public AllyariaPalette(AllyariaColorValue? backgroundColor = null, |
| | 84 | | AllyariaColorValue? foregroundColor = null, |
| | 85 | | AllyariaImageValue? backgroundImage = null, |
| | 86 | | bool backgroundImageStretch = true, |
| | 87 | | int? borderWidth = 0, |
| | 88 | | AllyariaColorValue? borderColor = null, |
| | 89 | | AllyariaStringValue? borderStyle = null, |
| | 90 | | AllyariaStringValue? borderRadius = null) |
| | 91 | | { |
| 174 | 92 | | _backgroundColor = backgroundColor; |
| 174 | 93 | | _backgroundImage = backgroundImage; |
| 174 | 94 | | _backgroundImageStretch = backgroundImageStretch; |
| 174 | 95 | | _borderColor = borderColor; |
| 174 | 96 | | _borderRadius = borderRadius; |
| 174 | 97 | | _borderStyle = borderStyle; |
| 174 | 98 | | _borderWidth = borderWidth; |
| 174 | 99 | | _foregroundColor = foregroundColor; |
| 174 | 100 | | } |
| | 101 | |
|
| | 102 | | /// <summary>Gets the effective background color after precedence is applied.</summary> |
| 368 | 103 | | public AllyariaColorValue BackgroundColor => _backgroundColor ?? Colors.White; |
| | 104 | |
|
| | 105 | | /// <summary> |
| | 106 | | /// Gets the effective background image declaration value, or <see langword="null" /> when no image is set. |
| | 107 | | /// </summary> |
| 124 | 108 | | public AllyariaImageValue? BackgroundImage => _backgroundImage; |
| | 109 | |
|
| | 110 | | /// <summary> |
| | 111 | | /// Gets the effective border color. If there is no border, the color is <see cref="Colors.Transparent" />. |
| | 112 | | /// </summary> |
| 56 | 113 | | public AllyariaColorValue BorderColor => _borderColor ?? BackgroundColor; |
| | 114 | |
|
| | 115 | | /// <summary>Gets the effective border radius declaration value, or <see langword="null" /> when not set.</summary> |
| 122 | 116 | | public AllyariaStringValue? BorderRadius => _borderRadius; |
| | 117 | |
|
| | 118 | | /// <summary>Gets the border style token (e.g., <c>solid</c>).</summary> |
| 36 | 119 | | public AllyariaStringValue BorderStyle => _borderStyle ?? new AllyariaStringValue("solid"); |
| | 120 | |
|
| | 121 | | /// <summary> |
| | 122 | | /// Gets the effective border width declaration value, or <see langword="null" /> when no border should be rendered. |
| | 123 | | /// </summary> |
| | 124 | | public AllyariaStringValue? BorderWidth |
| 172 | 125 | | => _borderWidth > 0 |
| 172 | 126 | | ? new AllyariaStringValue($"{_borderWidth}px") |
| 172 | 127 | | : null; |
| | 128 | |
|
| | 129 | | /// <summary> |
| | 130 | | /// Gets the resolved foreground color to render against <see cref="BackgroundColor" />: |
| | 131 | | /// <list type="bullet"> |
| | 132 | | /// <item> |
| | 133 | | /// <description> |
| | 134 | | /// If no explicit foreground is set (<c>_foregroundColor</c> is <see langword="null" />), the getter choose |
| | 135 | | /// whichever of <see cref="Colors.White" /> or <see cref="Colors.Black" /> produces the higher WCAG contras |
| | 136 | | /// against <see cref="BackgroundColor" />. |
| | 137 | | /// </description> |
| | 138 | | /// </item> |
| | 139 | | /// <item> |
| | 140 | | /// <description> |
| | 141 | | /// If an explicit foreground is provided, the getter ensures it meets at least the minimum accessibility co |
| | 142 | | /// ratio (default 4.5:1 for normal text) using <see cref="ColorHelper" />; it will preserve the original hu |
| | 143 | | /// possible and, if needed, adjust lightness or mix toward black/white to reach the target. |
| | 144 | | /// </description> |
| | 145 | | /// </item> |
| | 146 | | /// </list> |
| | 147 | | /// </summary> |
| | 148 | | /// <remarks> |
| | 149 | | /// This property uses real WCAG contrast calculations rather than brightness heuristics (e.g. V or H from HSV). It |
| | 150 | | /// text remains readable and accessible on the current background color while honoring any explicitly provided fore |
| | 151 | | /// when possible. |
| | 152 | | /// </remarks> |
| | 153 | | public AllyariaColorValue ForegroundColor |
| | 154 | | { |
| | 155 | | get |
| | 156 | | { |
| 190 | 157 | | if (_foregroundColor is null) |
| | 158 | | { |
| 12 | 159 | | var rWhite = ColorHelper.ContrastRatio(Colors.White, BackgroundColor); |
| 12 | 160 | | var rBlack = ColorHelper.ContrastRatio(Colors.Black, BackgroundColor); |
| | 161 | |
|
| 12 | 162 | | return rWhite >= rBlack |
| 12 | 163 | | ? Colors.White |
| 12 | 164 | | : Colors.Black; |
| | 165 | | } |
| | 166 | |
|
| | 167 | | // Ensure the explicit color meets contrast over the *effective* BackgroundColor |
| 178 | 168 | | var result = ColorHelper.EnsureMinimumContrast(_foregroundColor, BackgroundColor, 4.5); |
| | 169 | |
|
| 178 | 170 | | return result.ForegroundColor; |
| | 171 | | } |
| | 172 | | } |
| | 173 | |
|
| | 174 | | /// <summary> |
| | 175 | | /// Cascades the palette by applying overrides, falling back to the original base values of this instance when not p |
| | 176 | | /// (no reapplication of effective/derived precedence). |
| | 177 | | /// </summary> |
| | 178 | | /// <param name="backgroundColor">Optional new background color (base).</param> |
| | 179 | | /// <param name="foregroundColor">Optional new foreground color (base).</param> |
| | 180 | | /// <param name="backgroundImage"> |
| | 181 | | /// Optional new background image URL (unwrapped; overlaying is handled by <see cref="BackgroundImage" />). |
| | 182 | | /// </param> |
| | 183 | | /// <param name="backgroundImageStretch">An optional boolean for determining if the background image is stretched or |
| | 184 | | /// <param name="borderWidth"> |
| | 185 | | /// Optional new border width in CSS pixels; values < 0 are treated as <c>null</c> (no |
| | 186 | | /// border). |
| | 187 | | /// </param> |
| | 188 | | /// <param name="borderColor">Optional new border color (base).</param> |
| | 189 | | /// <param name="borderStyle">Optional new border style token.</param> |
| | 190 | | /// <param name="borderRadius">Optional new border radius token.</param> |
| | 191 | | /// <returns> |
| | 192 | | /// A new <see cref="AllyariaPalette" /> with the specified overrides applied atop this instance’s base values. |
| | 193 | | /// </returns> |
| | 194 | | public AllyariaPalette Cascade(AllyariaColorValue? backgroundColor = null, |
| | 195 | | AllyariaColorValue? foregroundColor = null, |
| | 196 | | AllyariaImageValue? backgroundImage = null, |
| | 197 | | bool? backgroundImageStretch = null, |
| | 198 | | int? borderWidth = null, |
| | 199 | | AllyariaColorValue? borderColor = null, |
| | 200 | | AllyariaStringValue? borderStyle = null, |
| | 201 | | AllyariaStringValue? borderRadius = null) |
| | 202 | | { |
| 68 | 203 | | var sanitizedBorderWidth = borderWidth is < 0 |
| 68 | 204 | | ? null |
| 68 | 205 | | : borderWidth; |
| | 206 | |
|
| 68 | 207 | | var newBackgroundColor = backgroundColor ?? _backgroundColor; |
| 68 | 208 | | var newBackgroundImage = backgroundImage ?? _backgroundImage; |
| 68 | 209 | | var newBackgroundImageStretch = backgroundImageStretch ?? _backgroundImageStretch; |
| 68 | 210 | | var newBorderColor = borderColor ?? _borderColor; |
| 68 | 211 | | var newBorderStyle = borderStyle ?? _borderStyle; |
| 68 | 212 | | var newBorderRadius = borderRadius ?? _borderRadius; |
| 68 | 213 | | var newBorderWidth = sanitizedBorderWidth ?? _borderWidth; |
| 68 | 214 | | var newForegroundColor = foregroundColor ?? _foregroundColor; |
| | 215 | |
|
| 68 | 216 | | return new AllyariaPalette( |
| 68 | 217 | | newBackgroundColor, |
| 68 | 218 | | newForegroundColor, |
| 68 | 219 | | newBackgroundImage, |
| 68 | 220 | | newBackgroundImageStretch, |
| 68 | 221 | | newBorderWidth, |
| 68 | 222 | | newBorderColor, |
| 68 | 223 | | newBorderStyle, |
| 68 | 224 | | newBorderRadius |
| 68 | 225 | | ); |
| | 226 | | } |
| | 227 | |
|
| | 228 | | /// <summary> |
| | 229 | | /// Builds a string of inline CSS declarations (e.g., <c>color: #fff; background-color: #000;</c>) that applies the |
| | 230 | | /// palette with the documented precedence (background image > background color; explicit overrides > defaults |
| | 231 | | /// border rendered only when width > 0). |
| | 232 | | /// </summary> |
| | 233 | | /// <returns>A CSS declaration string suitable for inline <c>style</c> attributes.</returns> |
| | 234 | | public string ToCss() |
| | 235 | | { |
| 58 | 236 | | var builder = new StringBuilder(); |
| 58 | 237 | | builder.Append(BackgroundColor.ToCss("background-color")); |
| 58 | 238 | | builder.Append(ForegroundColor.ToCss("color")); |
| | 239 | |
|
| 58 | 240 | | if (BackgroundImage is not null) |
| | 241 | | { |
| 8 | 242 | | builder.Append(BackgroundImage.ToCssBackground(BackgroundColor, _backgroundImageStretch)); |
| | 243 | | } |
| | 244 | |
|
| 58 | 245 | | if (BorderWidth is not null) |
| | 246 | | { |
| 16 | 247 | | builder.Append(BorderColor.ToCss("border-color")); |
| 16 | 248 | | builder.Append(BorderStyle.ToCss("border-style")); |
| 16 | 249 | | builder.Append(BorderWidth.ToCss("border-width")); |
| | 250 | | } |
| | 251 | |
|
| 58 | 252 | | if (BorderRadius is not null) |
| | 253 | | { |
| 8 | 254 | | builder.Append(BorderRadius.ToCss("border-radius")); |
| | 255 | | } |
| | 256 | |
|
| 58 | 257 | | return builder.ToString(); |
| | 258 | | } |
| | 259 | |
|
| | 260 | | /// <summary> |
| | 261 | | /// Builds a string of CSS custom property declarations for theming. The method normalizes the optional |
| | 262 | | /// <paramref name="prefix" /> by trimming whitespace and dashes, converting to lowercase, and replacing spaces with |
| | 263 | | /// hyphens. If no usable prefix remains, variables are emitted with the default <c>--aa-</c> prefix; otherwise, the |
| | 264 | | /// computed prefix is applied (e.g., <c>--mytheme-color</c>, <c>--mytheme-background-color</c>). |
| | 265 | | /// </summary> |
| | 266 | | /// <param name="prefix"> |
| | 267 | | /// An optional string used to namespace the CSS variables. May contain spaces or leading/trailing dashes, which are |
| | 268 | | /// normalized before use. If empty or whitespace, defaults to <c>--aa-</c>. |
| | 269 | | /// </param> |
| | 270 | | /// <returns> |
| | 271 | | /// A CSS declaration string defining theme variables (foreground, background, border, and radius) that can be consu |
| | 272 | | /// component CSS. If a background image is present, a <c>--{prefix}-background-image</c> variable is emitted and |
| | 273 | | /// background color variables are omitted. |
| | 274 | | /// </returns> |
| | 275 | | /// <remarks> |
| | 276 | | /// Border and radius variables are included only when explicitly set. This ensures that only relevant theming prope |
| | 277 | | /// are emitted, keeping CSS concise. |
| | 278 | | /// </remarks> |
| | 279 | | public string ToCssVars(string prefix = "") |
| | 280 | | { |
| 38 | 281 | | var basePrefix = Regex.Replace(prefix, @"[\s-]+", "-").Trim('-').ToLowerInvariant(); |
| | 282 | |
|
| 38 | 283 | | basePrefix = string.IsNullOrWhiteSpace(prefix) |
| 38 | 284 | | ? "--aa-" |
| 38 | 285 | | : $"--{basePrefix}-"; |
| | 286 | |
|
| 38 | 287 | | var builder = new StringBuilder(); |
| 38 | 288 | | builder.Append(ForegroundColor.ToCss($"{basePrefix}color")); |
| 38 | 289 | | builder.Append(ForegroundColor.ToCss($"{basePrefix}background-color")); |
| | 290 | |
|
| 38 | 291 | | if (BackgroundImage is not null) |
| | 292 | | { |
| 4 | 293 | | builder.Append(BackgroundImage.ToCssVarsBackground(basePrefix, BackgroundColor, _backgroundImageStretch)); |
| | 294 | | } |
| | 295 | |
|
| 38 | 296 | | if (BorderWidth is not null) |
| | 297 | | { |
| 4 | 298 | | builder.Append(BorderColor.ToCss($"{basePrefix}border-color")); |
| 4 | 299 | | builder.Append(BorderStyle.ToCss($"{basePrefix}border-style")); |
| 4 | 300 | | builder.Append(BorderWidth.ToCss($"{basePrefix}border-width")); |
| | 301 | | } |
| | 302 | |
|
| 38 | 303 | | if (BorderRadius is not null) |
| | 304 | | { |
| 2 | 305 | | builder.Append(BorderRadius.ToCss($"{prefix}border-radius")); |
| | 306 | | } |
| | 307 | |
|
| 38 | 308 | | return builder.ToString(); |
| | 309 | | } |
| | 310 | |
|
| | 311 | | /// <summary> |
| | 312 | | /// Produces a derived palette for the <c>[disabled]</c> state by desaturating the background and gently compressing |
| | 313 | | /// Value (V) toward the mid range to reduce emphasis. The foreground is then contrast-corrected to a relaxed minimu |
| | 314 | | /// suitable for UI affordances (3.0:1). |
| | 315 | | /// </summary> |
| | 316 | | /// <param name="desaturateBy"> |
| | 317 | | /// Percentage points to subtract from the background Saturation (S). Default is <c>60</c>, yielding a muted surface |
| | 318 | | /// </param> |
| | 319 | | /// <param name="valueBlendTowardMid"> |
| | 320 | | /// Blend factor in [0..1] that moves background Value (V) toward mid (50). Default is <c>0.15</c> (15% toward mid). |
| | 321 | | /// </param> |
| | 322 | | /// <param name="minimumContrast"> |
| | 323 | | /// Minimum required contrast ratio for disabled foreground over the derived background; defaults to <c>3.0</c>. |
| | 324 | | /// </param> |
| | 325 | | /// <returns>A new <see cref="AllyariaPalette" /> suitable for the disabled state.</returns> |
| | 326 | | /// <remarks> |
| | 327 | | /// *Hue is preserved* for the background; only S/V are adjusted. If a border is present, its hue is preserved and i |
| | 328 | | /// desaturated and value-compressed in tandem with the background to avoid visual dominance. Background images are |
| | 329 | | /// unchanged (image precedence still applies). |
| | 330 | | /// </remarks> |
| | 331 | | public AllyariaPalette ToDisabledPalette(double desaturateBy = 60.0, |
| | 332 | | double valueBlendTowardMid = 0.15, |
| | 333 | | double minimumContrast = 3.0) |
| | 334 | | { |
| 22 | 335 | | var baseBg = BackgroundColor; |
| | 336 | |
|
| 22 | 337 | | var disabledBg = AllyariaColorValue.FromHsva( |
| 22 | 338 | | baseBg.H, |
| 22 | 339 | | Math.Max(0.0, baseBg.S - desaturateBy), |
| 22 | 340 | | ColorHelper.Blend(baseBg.V, 50.0, valueBlendTowardMid) |
| 22 | 341 | | ); |
| | 342 | |
|
| 22 | 343 | | AllyariaColorValue? disabledBorder = null; |
| | 344 | |
|
| 22 | 345 | | if (BorderWidth is not null) |
| | 346 | | { |
| 2 | 347 | | var baseBorder = BorderColor; |
| | 348 | |
|
| 2 | 349 | | disabledBorder = AllyariaColorValue.FromHsva( |
| 2 | 350 | | baseBorder.H, |
| 2 | 351 | | Math.Max(0.0, baseBorder.S - desaturateBy), |
| 2 | 352 | | ColorHelper.Blend(baseBorder.V, 50.0, valueBlendTowardMid) |
| 2 | 353 | | ); |
| | 354 | | } |
| | 355 | |
|
| | 356 | | // Start from existing foreground (effective), then ensure a relaxed contrast target against the disabled backgr |
| 22 | 357 | | var candidateFg = ForegroundColor; |
| 22 | 358 | | var disabledFg = ColorHelper.EnsureMinimumContrast(candidateFg, disabledBg, minimumContrast).ForegroundColor; |
| | 359 | |
|
| 22 | 360 | | return Cascade( |
| 22 | 361 | | disabledBg, |
| 22 | 362 | | disabledFg, |
| 22 | 363 | | borderColor: disabledBorder |
| 22 | 364 | | ); |
| | 365 | | } |
| | 366 | |
|
| | 367 | | /// <summary> |
| | 368 | | /// Produces a derived palette intended for the <c>:hover</c> state by nudging the background (and border, if presen |
| | 369 | | /// along the HSV Value rail to increase perceived affordance while preserving hue. The foreground is then |
| | 370 | | /// contrast-corrected against the new background to meet WCAG AA for body text (4.5:1). |
| | 371 | | /// </summary> |
| | 372 | | /// <param name="backgroundDeltaV"> |
| | 373 | | /// The absolute Value (V) change in percentage points to apply to <see cref="BackgroundColor" />. On light backgrou |
| | 374 | | /// ≥ 50) the value is decreased; on dark backgrounds it is increased. Default is <c>6</c>. |
| | 375 | | /// </param> |
| | 376 | | /// <param name="borderDeltaV"> |
| | 377 | | /// The absolute Value (V) change in percentage points to apply to <see cref="BorderColor" /> when a border is prese |
| | 378 | | /// Mirrors the direction used for the background. Default is <c>8</c>. |
| | 379 | | /// </param> |
| | 380 | | /// <param name="minimumContrast"> |
| | 381 | | /// Minimum required contrast ratio for the foreground over the derived background; defaults to <c>4.5</c> (WCAG AA) |
| | 382 | | /// </param> |
| | 383 | | /// <returns>A new <see cref="AllyariaPalette" /> suitable for the hover state.</returns> |
| | 384 | | /// <remarks> |
| | 385 | | /// This method does not alter the background image. If a background image is active, it remains in effect (image |
| | 386 | | /// precedence still applies), but the derived background color—while not painted—still participates in computing a |
| | 387 | | /// readable foreground as a fallback. |
| | 388 | | /// </remarks> |
| | 389 | | public AllyariaPalette ToHoverPalette(double backgroundDeltaV = 6.0, |
| | 390 | | double borderDeltaV = 8.0, |
| | 391 | | double minimumContrast = 4.5) |
| | 392 | | { |
| 18 | 393 | | var baseBg = BackgroundColor; |
| | 394 | |
|
| | 395 | | // Direction: lighten for dark surfaces, darken for light surfaces. |
| 18 | 396 | | var direction = baseBg.V >= 50.0 |
| 18 | 397 | | ? -1.0 |
| 18 | 398 | | : +1.0; |
| | 399 | |
|
| 18 | 400 | | var hoverBg = AllyariaColorValue.FromHsva(baseBg.H, baseBg.S, baseBg.V + direction * backgroundDeltaV); |
| | 401 | |
|
| 18 | 402 | | AllyariaColorValue? hoverBorder = null; |
| | 403 | |
|
| 18 | 404 | | if (BorderWidth is not null) // only compute if a border is rendered |
| | 405 | | { |
| 4 | 406 | | var baseBorder = BorderColor; |
| | 407 | |
|
| 4 | 408 | | hoverBorder = AllyariaColorValue.FromHsva( |
| 4 | 409 | | baseBorder.H, baseBorder.S, baseBorder.V + direction * borderDeltaV |
| 4 | 410 | | ); |
| | 411 | | } |
| | 412 | |
|
| | 413 | | // Ensure readable foreground against the derived background while preserving hue where possible. |
| 18 | 414 | | var fgResolved = ColorHelper.EnsureMinimumContrast(ForegroundColor, hoverBg, minimumContrast) |
| 18 | 415 | | .ForegroundColor; |
| | 416 | |
|
| 18 | 417 | | return Cascade( |
| 18 | 418 | | hoverBg, |
| 18 | 419 | | fgResolved, |
| 18 | 420 | | borderColor: hoverBorder |
| 18 | 421 | | ); |
| | 422 | | } |
| | 423 | | } |