design: the on-stream graphics join the standard (OutputPalette)
The broadcast picture is part of the app, so it stops carrying its own colours. Alert cards, the ticker and the branding flash now paint from OutputPalette, which Mirrors DESIGN.md for the brand tokens and owns the alert box's six-event accent language (gold/violet/pink — content, not chrome; the standard has no purple and should not grow one, but they live in ONE file instead of 57 scattered literals). It is a separate type because the renderers run on the frame-pump thread and must not touch application resources. The duplication is made safe by OutputPaletteTests: it parses Themes/Tokens.xaml and fails if any mirrored token drifts, and fails if a renderer reintroduces a colour literal or "Segoe UI". Ticker and flash glyphs now use the bundled Inter instead of Segoe UI. Neon red is the standard live red — one red in the product, including the credit. Fixed on the way: the palette's font fields were static pack-URI initialisers, which throw TypeInitializationException in any host without a WPF Application (the test process caught it); they are lazy now with a name fallback.
This commit is contained in:
+19
-18
@@ -3,6 +3,7 @@ using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using ytLive.Models;
|
||||
using ytLive.Services.Compositor;
|
||||
|
||||
namespace ytLive.Services;
|
||||
|
||||
@@ -27,9 +28,9 @@ public sealed class AlertRenderer
|
||||
private static readonly Color White = Colors.White;
|
||||
private static readonly Color[] ConfettiPalette =
|
||||
{
|
||||
Color.FromRgb(0xf4, 0x2f, 0x5f), Color.FromRgb(0xff, 0xd1, 0x66),
|
||||
Color.FromRgb(0x38, 0xb6, 0xf8), Color.FromRgb(0xa7, 0x8b, 0xfa),
|
||||
Color.FromRgb(0x39, 0xd3, 0x53),
|
||||
OutputPalette.Crimson, OutputPalette.Gold,
|
||||
OutputPalette.Tertiary, OutputPalette.Violet,
|
||||
OutputPalette.Positive,
|
||||
};
|
||||
|
||||
public VideoFrame Render(ChatMessage e, double t01, int width, int height)
|
||||
@@ -70,10 +71,10 @@ public sealed class AlertRenderer
|
||||
var shownAmount = e.SuperChatDisplayString
|
||||
?? (e.SuperChatCurrency is { Length: > 0 } cur ? $"{cur}{e.SuperChatAmount:0.00}" : $"${e.SuperChatAmount:0.00}");
|
||||
var fontSize = cardH * 0.24;
|
||||
DrawCentered(ctx, shownAmount, cardCX, cardY + cardH * 0.24, fontSize, FontWeights.Bold, Color.FromRgb(0xff, 0xd1, 0x66), cardW);
|
||||
DrawCentered(ctx, shownAmount, cardCX, cardY + cardH * 0.24, fontSize, FontWeights.Bold, OutputPalette.Gold, cardW);
|
||||
|
||||
DrawCentered(ctx, e.AuthorName, cardCX, cardY + cardH * 0.56, cardH * 0.13, FontWeights.SemiBold, White, cardW);
|
||||
DrawCentered(ctx, e.Message, cardCX, cardY + cardH * 0.74, cardH * 0.10, FontWeights.Normal, Color.FromRgb(0xdd, 0xff, 0xf0), cardW * 0.92);
|
||||
DrawCentered(ctx, e.Message, cardCX, cardY + cardH * 0.74, cardH * 0.10, FontWeights.Normal, OutputPalette.MintText, cardW * 0.92);
|
||||
|
||||
if (t >= EnterEnd && t <= ExitStart)
|
||||
{
|
||||
@@ -122,10 +123,10 @@ public sealed class AlertRenderer
|
||||
var desc = string.IsNullOrWhiteSpace(e.SuperStickerDescription) ? "SUPER STICKER" : e.SuperStickerDescription.ToUpperInvariant();
|
||||
DrawCentered(ctx, desc, cardCX, h / 2.0 - cardH * 0.12, cardH * 0.17, FontWeights.Bold, White, cardW * 0.9);
|
||||
|
||||
DrawCentered(ctx, e.AuthorName, cardCX, h / 2.0 + cardH * 0.16, cardH * 0.12, FontWeights.SemiBold, Color.FromRgb(0xff, 0xb3, 0xd9), cardW * 0.9);
|
||||
DrawCentered(ctx, e.AuthorName, cardCX, h / 2.0 + cardH * 0.16, cardH * 0.12, FontWeights.SemiBold, OutputPalette.Pink, cardW * 0.9);
|
||||
var tier = e.SuperChatDisplayString ?? (e.SuperChatCurrency is { Length: > 0 } cur ? $"{cur}{e.SuperChatAmount:0.00}" : null);
|
||||
if (!string.IsNullOrWhiteSpace(tier))
|
||||
DrawCentered(ctx, tier, cardCX, h / 2.0 + cardH * 0.32, cardH * 0.10, FontWeights.Bold, Color.FromRgb(0xff, 0xd1, 0x66), cardW * 0.9);
|
||||
DrawCentered(ctx, tier, cardCX, h / 2.0 + cardH * 0.32, cardH * 0.10, FontWeights.Bold, OutputPalette.Gold, cardW * 0.9);
|
||||
|
||||
ctx.Pop();
|
||||
ctx.Pop();
|
||||
@@ -163,10 +164,10 @@ public sealed class AlertRenderer
|
||||
}
|
||||
|
||||
DrawCentered(ctx, e.AuthorName, cardCX, cardY + cardH * 0.36, cardH * 0.22, FontWeights.Bold, White, cardW * 0.9);
|
||||
DrawCentered(ctx, "JOINED THE MEMBERSHIP", cardCX, cardY + cardH * 0.62, cardH * 0.11, FontWeights.SemiBold, Color.FromRgb(0xf5, 0xd0, 0xfe), cardW * 0.9);
|
||||
DrawCentered(ctx, "JOINED THE MEMBERSHIP", cardCX, cardY + cardH * 0.62, cardH * 0.11, FontWeights.SemiBold, OutputPalette.Lilac, cardW * 0.9);
|
||||
|
||||
var level = string.IsNullOrWhiteSpace(e.MembershipLevel) ? "MEMBER" : e.MembershipLevel.ToUpperInvariant();
|
||||
DrawCentered(ctx, level, cardCX, cardY + cardH * 0.80, cardH * 0.10, FontWeights.Bold, Color.FromRgb(0xff, 0xd1, 0x66), cardW * 0.9);
|
||||
DrawCentered(ctx, level, cardCX, cardY + cardH * 0.80, cardH * 0.10, FontWeights.Bold, OutputPalette.Gold, cardW * 0.9);
|
||||
}
|
||||
return CreateFrame(visual, w, h, dpi);
|
||||
}
|
||||
@@ -193,7 +194,7 @@ public sealed class AlertRenderer
|
||||
var count = Math.Max(1, e.GiftNewMembersCount);
|
||||
var spread = 1.0 - exit;
|
||||
var hero = count > 1 ? $"×{count}" : "MEMBERSHIP";
|
||||
DrawCentered(ctx, hero, cardCX, margin + cardH * 0.24, cardH * 0.26, FontWeights.Bold, Color.FromRgb(0xff, 0xd1, 0x66), cardW * 0.8);
|
||||
DrawCentered(ctx, hero, cardCX, margin + cardH * 0.24, cardH * 0.26, FontWeights.Bold, OutputPalette.Gold, cardW * 0.8);
|
||||
DrawCentered(ctx, $"{e.AuthorName} is gifting", cardCX, margin + cardH * 0.52, cardH * 0.11, FontWeights.SemiBold, White, cardW * 0.9);
|
||||
|
||||
var chipCount = Math.Min(count, 6);
|
||||
@@ -204,7 +205,7 @@ public sealed class AlertRenderer
|
||||
var chipX = cardCX + (i - slot) * gap;
|
||||
var chipY = margin + cardH * 0.70 + Math.Sin((i + t * 6) * 2.4) * cardH * 0.015;
|
||||
ctx.DrawRoundedRectangle(
|
||||
new SolidColorBrush(Color.FromRgb(0xff, 0xf3, 0xc4)),
|
||||
new SolidColorBrush(OutputPalette.GoldPale),
|
||||
null,
|
||||
new Rect(chipX - cardH * 0.05, chipY, cardH * 0.10, cardH * 0.10),
|
||||
cardH * 0.03, cardH * 0.03);
|
||||
@@ -252,10 +253,10 @@ public sealed class AlertRenderer
|
||||
}
|
||||
}
|
||||
|
||||
DrawCentered(ctx, "GIFTED MEMBERSHIP", cardCX, margin + cardH * 0.22, cardH * 0.12, FontWeights.Bold, Color.FromRgb(0xff, 0xd1, 0x66), cardW * 0.9);
|
||||
DrawCentered(ctx, "GIFTED MEMBERSHIP", cardCX, margin + cardH * 0.22, cardH * 0.12, FontWeights.Bold, OutputPalette.Gold, cardW * 0.9);
|
||||
var giver = string.IsNullOrWhiteSpace(e.GifterName) ? e.AuthorName : e.GifterName;
|
||||
DrawCentered(ctx, giver, cardCX, margin + cardH * 0.42, cardH * 0.20, FontWeights.Bold, White, cardW * 0.9);
|
||||
DrawCentered(ctx, "gave you a membership — celebrate!", cardCX, margin + cardH * 0.68, cardH * 0.11, FontWeights.Normal, Color.FromRgb(0xdb, 0xff, 0xf0), cardW * 0.9);
|
||||
DrawCentered(ctx, "gave you a membership — celebrate!", cardCX, margin + cardH * 0.68, cardH * 0.11, FontWeights.Normal, OutputPalette.MintText, cardW * 0.9);
|
||||
|
||||
ctx.Pop();
|
||||
ctx.Pop();
|
||||
@@ -283,17 +284,17 @@ public sealed class AlertRenderer
|
||||
DrawBrand(ctx, margin, cardY, cardW, cardH);
|
||||
|
||||
var hero = e.MilestoneMonths > 0 ? $"{e.MilestoneMonths}" : "MILESTONE";
|
||||
DrawCentered(ctx, hero, cardCX, cardY + cardH * 0.26, cardH * 0.30, FontWeights.Bold, Color.FromRgb(0xff, 0xdd, 0x66), cardW * 0.8);
|
||||
DrawCentered(ctx, hero, cardCX, cardY + cardH * 0.26, cardH * 0.30, FontWeights.Bold, OutputPalette.GoldBright, cardW * 0.8);
|
||||
|
||||
var sub = e.MilestoneMonths > 0 ? "MONTHS STRONG" : "SUPPORTING THE STREAM";
|
||||
DrawCentered(ctx, sub, cardCX, cardY + cardH * 0.52, cardH * 0.11, FontWeights.SemiBold, White, cardW * 0.9);
|
||||
DrawCentered(ctx, "— " + e.AuthorName + " —", cardCX, cardY + cardH * 0.70, cardH * 0.10, FontWeights.Normal, Color.FromRgb(0xff, 0xe8, 0xd0), cardW * 0.9);
|
||||
DrawCentered(ctx, "— " + e.AuthorName + " —", cardCX, cardY + cardH * 0.70, cardH * 0.10, FontWeights.Normal, OutputPalette.WarmCream, cardW * 0.9);
|
||||
|
||||
// Tenure bar: grows through the first half, retracts on exit.
|
||||
var barP = Clamp01((t - 0.10) / 0.55) * (1 - exit);
|
||||
var barW = cardW * 0.6 * barP;
|
||||
ctx.DrawRoundedRectangle(
|
||||
new SolidColorBrush(Color.FromRgb(0xff, 0xd1, 0x66)),
|
||||
new SolidColorBrush(OutputPalette.Gold),
|
||||
null,
|
||||
new Rect(cardCX - barW / 2, cardY + cardH * 0.84, barW, cardH * 0.045),
|
||||
4, 4);
|
||||
@@ -313,7 +314,7 @@ public sealed class AlertRenderer
|
||||
{
|
||||
var formatted = new FormattedText(
|
||||
Brand, Invariant, FlowDirection.LeftToRight,
|
||||
new Typeface(new FontFamily("Segoe UI"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
|
||||
new Typeface(OutputPalette.Sans, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
|
||||
Math.Max(9, h * 0.055),
|
||||
new SolidColorBrush(Color.FromArgb(0x88, 0xff, 0xff, 0xff)),
|
||||
96.0);
|
||||
@@ -324,7 +325,7 @@ public sealed class AlertRenderer
|
||||
{
|
||||
var formatted = new FormattedText(
|
||||
text, Invariant, FlowDirection.LeftToRight,
|
||||
new Typeface(new FontFamily("Segoe UI"), FontStyles.Normal, weight, FontStretches.Normal),
|
||||
new Typeface(OutputPalette.Sans, FontStyles.Normal, weight, FontStretches.Normal),
|
||||
size,
|
||||
new SolidColorBrush(color),
|
||||
96.0);
|
||||
|
||||
@@ -143,7 +143,7 @@ public static class AlertTickerRenderer
|
||||
const double padX = 16;
|
||||
const double gap = 8;
|
||||
|
||||
var font = new Typeface(new FontFamily("Segoe UI"), FontStyles.Normal, FontWeights.SemiBold, FontStretches.Normal);
|
||||
var font = new Typeface(OutputPalette.Sans, FontStyles.Normal, FontWeights.SemiBold, FontStretches.Normal);
|
||||
var ft = new FormattedText(text, CultureInfo.InvariantCulture, FlowDirection.LeftToRight,
|
||||
font, 22, Brushes.White, 1.0);
|
||||
|
||||
@@ -156,7 +156,7 @@ public static class AlertTickerRenderer
|
||||
{
|
||||
var pill = new Rect(0, top, pillW, pillHeight);
|
||||
dc.DrawRoundedRectangle(new SolidColorBrush(Color.FromArgb(0xE6, 0x10, 0x13, 0x24)), null, pill, pillHeight / 2, pillHeight / 2);
|
||||
dc.DrawRoundedRectangle(new SolidColorBrush(Color.FromRgb(0xe9, 0x45, 0x60)), null,
|
||||
dc.DrawRoundedRectangle(new SolidColorBrush(OutputPalette.Live), null,
|
||||
new Rect(padX, top + (pillHeight - 12) / 2.0, accentW, 12), 2, 2);
|
||||
dc.DrawText(ft, new Point(padX + accentW + gap, textTop));
|
||||
}
|
||||
|
||||
@@ -53,8 +53,8 @@ public sealed class BrandFlashPresenter : IDisposable
|
||||
/// red and blue channels pushed in OPPOSITE directions — Maxon's "Chromatic Glow"
|
||||
/// notes the green channel is all but invisible behind white text once R and B are
|
||||
/// split, which is exactly the look.</summary>
|
||||
private static readonly Color NeonRed = Color.FromRgb(0xe9, 0x45, 0x60); // brand red, exact
|
||||
private static readonly Color NeonBlue = Color.FromRgb(0x4d, 0x8b, 0xff); // brand blue #0f3460 lifted for luminance
|
||||
private static readonly Color NeonRed = OutputPalette.NeonRed; // one red: the standard live red
|
||||
private static readonly Color NeonBlue = OutputPalette.NeonBlue; // lifted for luminance against video
|
||||
private static readonly Color CoreColor = Colors.White;
|
||||
|
||||
/// <summary>Font size chosen so the credit spans roughly half the master width:
|
||||
@@ -354,7 +354,7 @@ public sealed class BrandFlashPresenter : IDisposable
|
||||
return result;
|
||||
}
|
||||
|
||||
var font = new Typeface(new FontFamily("Segoe UI"), FontStyles.Normal,
|
||||
var font = new Typeface(OutputPalette.Sans, FontStyles.Normal,
|
||||
FontWeights.Bold, FontStretches.Normal);
|
||||
var probe = new FormattedText(text, CultureInfo.InvariantCulture,
|
||||
FlowDirection.LeftToRight, font, FontSize, Brushes.White, 1.0);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace ytLive.Services.Compositor;
|
||||
|
||||
/// <summary>The colours and typefaces the ON-STREAM graphics paint with.
|
||||
///
|
||||
/// <para>Why this exists rather than reading <c>Themes/Tokens.xaml</c>: the renderers run on the
|
||||
/// frame-pump thread and must never touch application resources or a dispatcher. They need frozen
|
||||
/// values they can read from any thread.</para>
|
||||
///
|
||||
/// <para>Two groups live here, deliberately separated:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item><b>Brand tokens</b> — a mirror of DESIGN.md. <c>OutputPaletteTests</c> parses the design
|
||||
/// document and fails if any of these drifts from the theme, so the duplication cannot rot.</item>
|
||||
/// <item><b>Event accents</b> — the alert box's own six-event colour language (gold for Super
|
||||
/// Chats, violet for members, …). These are content, not chrome: the design standard has no
|
||||
/// purple and should not grow one, but they still belong in ONE place instead of 57 scattered
|
||||
/// literals.</item>
|
||||
/// </list></summary>
|
||||
public static class OutputPalette
|
||||
{
|
||||
// ─── Brand tokens (must equal Themes/Tokens.xaml; guarded) ────────────────
|
||||
public static readonly Color Live = Color.FromRgb(0xff, 0x54, 0x51); // primary
|
||||
public static readonly Color LiveDark = Color.FromRgb(0xb9, 0x1a, 0x24); // primary-container
|
||||
public static readonly Color Amber = Color.FromRgb(0xff, 0xb9, 0x5f); // secondary
|
||||
public static readonly Color Attention = Color.FromRgb(0xff, 0x8c, 0x3a);
|
||||
public static readonly Color Positive = Color.FromRgb(0x22, 0xc5, 0x5e);
|
||||
public static readonly Color Error = Color.FromRgb(0xef, 0x44, 0x44);
|
||||
public static readonly Color ErrorSurface = Color.FromRgb(0x93, 0x00, 0x0a);
|
||||
public static readonly Color Tertiary = Color.FromRgb(0x38, 0xbd, 0xf8);
|
||||
public static readonly Color TextPrimary = Color.FromRgb(0xda, 0xe2, 0xfd);
|
||||
public static readonly Color TextMuted = Color.FromRgb(0x94, 0xa3, 0xb8);
|
||||
public static readonly Color Background = Color.FromRgb(0x0b, 0x13, 0x26);
|
||||
public static readonly Color Surface = Color.FromRgb(0x17, 0x1f, 0x33);
|
||||
public static readonly Color SurfaceHigh = Color.FromRgb(0x22, 0x2a, 0x3d);
|
||||
public static readonly Color Outline = Color.FromRgb(0x33, 0x41, 0x55);
|
||||
|
||||
// ─── Alert event accents (content language, not chrome) ───────────────────
|
||||
public static readonly Color Gold = Color.FromRgb(0xff, 0xd1, 0x66);
|
||||
public static readonly Color GoldBright = Color.FromRgb(0xff, 0xdd, 0x66);
|
||||
public static readonly Color GoldPale = Color.FromRgb(0xff, 0xf3, 0xc4);
|
||||
public static readonly Color WarmCream = Color.FromRgb(0xff, 0xe8, 0xd0);
|
||||
public static readonly Color Pink = Color.FromRgb(0xff, 0xb3, 0xd9);
|
||||
public static readonly Color Lilac = Color.FromRgb(0xf5, 0xd0, 0xfe);
|
||||
public static readonly Color Crimson = Color.FromRgb(0xf4, 0x2f, 0x5f);
|
||||
public static readonly Color MintText = Color.FromRgb(0xdb, 0xff, 0xf0);
|
||||
public static readonly Color Violet = Color.FromRgb(0xa7, 0x8b, 0xfa);
|
||||
|
||||
/// <summary>The branding flash's halo channels. Red is the standard live red — one red in the
|
||||
/// product, including the credit — and the blue is lifted for luminance against a video feed.</summary>
|
||||
public static readonly Color NeonRed = Live;
|
||||
public static readonly Color NeonBlue = Color.FromRgb(0x4d, 0x8b, 0xff);
|
||||
|
||||
// ─── Typefaces ───────────────────────────────────────────────────────────
|
||||
/// <summary>Bundled families (OFL). Resolved from the pack folder so on-stream graphics wear
|
||||
/// the same faces as the UI; a failure falls back to the system face rather than throwing
|
||||
/// inside a render tick.</summary>
|
||||
private static readonly System.Lazy<FontFamily> SansFamily = new(() => Family("Inter"));
|
||||
private static readonly System.Lazy<FontFamily> MonoFamily = new(() => Family("JetBrains Mono"));
|
||||
|
||||
public static FontFamily Sans => SansFamily.Value;
|
||||
public static FontFamily Mono => MonoFamily.Value;
|
||||
|
||||
/// <summary>The pack scheme only exists once WPF has registered it, so a static initializer
|
||||
/// must not build a pack URI: it throws a TypeInitializationException in any host without an
|
||||
/// Application (exactly what the first version did in the test process). Fall back to the
|
||||
/// family name, which resolves from the bundled resource or the system face.</summary>
|
||||
private static FontFamily Family(string name)
|
||||
{
|
||||
try { return new FontFamily(new System.Uri("pack://application:,,,/"), "/Assets/Fonts/#" + name); }
|
||||
catch (System.Exception) { return new FontFamily(name); }
|
||||
}
|
||||
|
||||
public static SolidColorBrush Brush(Color colour)
|
||||
{
|
||||
var brush = new SolidColorBrush(colour);
|
||||
brush.Freeze();
|
||||
return brush;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Media;
|
||||
using Xunit;
|
||||
using ytLive.Services.Compositor;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>On-stream graphics cannot read application resources (the frame pump is not the UI
|
||||
/// thread), so their colours live in <see cref="OutputPalette"/>. That duplication is safe only
|
||||
/// if it is checked: this test proves every brand token there equals the brush in
|
||||
/// <c>Themes/Tokens.xaml</c>, and that the renderers carry no colour literals of their own.
|
||||
///
|
||||
/// <para>Without it, the broadcast picture would drift away from the UI one "quick fix" at a
|
||||
/// time — the exact failure mode DESIGN.md exists to stop.</para></summary>
|
||||
public class OutputPaletteTests
|
||||
{
|
||||
[Fact]
|
||||
public void The_on_stream_palette_matches_the_theme_and_the_renderers_carry_no_literals()
|
||||
{
|
||||
var brushes = Tokens();
|
||||
|
||||
var mirrored = new (string Brush, Color Colour)[]
|
||||
{
|
||||
("Brush.Primary", OutputPalette.Live),
|
||||
("Brush.PrimaryContainer", OutputPalette.LiveDark),
|
||||
("Brush.Secondary", OutputPalette.Amber),
|
||||
("Brush.Attention", OutputPalette.Attention),
|
||||
("Brush.Positive", OutputPalette.Positive),
|
||||
("Brush.Error", OutputPalette.Error),
|
||||
("Brush.ErrorContainer", OutputPalette.ErrorSurface),
|
||||
("Brush.Tertiary", OutputPalette.Tertiary),
|
||||
("Brush.OnSurface", OutputPalette.TextPrimary),
|
||||
("Brush.OnSurfaceVariant", OutputPalette.TextMuted),
|
||||
("Brush.Background", OutputPalette.Background),
|
||||
("Brush.SurfaceContainer", OutputPalette.Surface),
|
||||
("Brush.SurfaceContainerHigh", OutputPalette.SurfaceHigh),
|
||||
("Brush.Outline", OutputPalette.Outline),
|
||||
};
|
||||
|
||||
foreach (var (brushKey, colour) in mirrored)
|
||||
{
|
||||
Assert.True(brushes.ContainsKey(brushKey), $"{brushKey} missing from Themes/Tokens.xaml");
|
||||
Assert.True(brushes[brushKey] == colour,
|
||||
$"{brushKey} is {brushes[brushKey]} in the theme but {colour} in OutputPalette — the " +
|
||||
"broadcast picture and the UI have drifted apart");
|
||||
}
|
||||
|
||||
// The renderers must not introduce colours of their own; the alert accents belong to
|
||||
// OutputPalette too, so one file owns every on-stream value.
|
||||
foreach (var file in new[]
|
||||
{
|
||||
"Services/AlertRenderer.cs", "Services/Compositor/AlertTickerRenderer.cs",
|
||||
"Services/Compositor/BrandFlashPresenter.cs",
|
||||
})
|
||||
{
|
||||
var text = SourceAssert.Text(file.Split('/'));
|
||||
Assert.DoesNotContain("Color.FromRgb(0x", text);
|
||||
Assert.DoesNotContain("\"Segoe UI\"", text);
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, Color> Tokens()
|
||||
{
|
||||
var text = SourceAssert.Text("Themes", "Tokens.xaml");
|
||||
var result = new Dictionary<string, Color>(StringComparer.Ordinal);
|
||||
foreach (Match m in Regex.Matches(text, "x:Key=\"(Brush\\.[^\"]+)\"\\s+Color=\"#([0-9a-fA-F]{6,8})\""))
|
||||
{
|
||||
var hex = m.Groups[2].Value;
|
||||
if (hex.Length == 8) hex = hex[2..]; // drop the alpha the palette does not mirror
|
||||
var r = int.Parse(hex[..2], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
||||
var g = int.Parse(hex[2..4], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
||||
var b = int.Parse(hex[4..6], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
||||
result[m.Groups[1].Value] = Color.FromRgb((byte)r, (byte)g, (byte)b);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user