From 186f74dad22d683a08b4ce8d2f818e2d62b661a0 Mon Sep 17 00:00:00 2001 From: Matt Edmondson Date: Wed, 15 Jul 2026 18:04:22 +1000 Subject: [PATCH] [minor] Add ThemeProvider.Analysis palette audit tool A console tool that analyzes the ImGui palette each registered theme produces and writes a markdown report. Four checks with fixed WCAG-based gates: text/glyph contrast over every surface they render on, interaction- state distinctness (Oklab lightness), canonical fidelity (accent hue drift and chroma retention vs the source palette), and semantic coverage (meanings defined but unused, or required but missing). Exits non-zero when any theme fails a gate so it doubles as a regression check. Thresholds live in AnalysisThresholds; run with --theme to filter and --strict to fail on warnings too. --- ThemeProvider.Analysis/AnalysisModel.cs | 69 +++++ ThemeProvider.Analysis/AnalysisThresholds.cs | 34 +++ ThemeProvider.Analysis/MarkdownReport.cs | 115 +++++++ ThemeProvider.Analysis/PaletteAnalyzer.cs | 281 ++++++++++++++++++ ThemeProvider.Analysis/Program.cs | 82 +++++ .../ThemeProvider.Analysis.csproj | 22 ++ ThemeProvider.sln | 14 + 7 files changed, 617 insertions(+) create mode 100644 ThemeProvider.Analysis/AnalysisModel.cs create mode 100644 ThemeProvider.Analysis/AnalysisThresholds.cs create mode 100644 ThemeProvider.Analysis/MarkdownReport.cs create mode 100644 ThemeProvider.Analysis/PaletteAnalyzer.cs create mode 100644 ThemeProvider.Analysis/Program.cs create mode 100644 ThemeProvider.Analysis/ThemeProvider.Analysis.csproj diff --git a/ThemeProvider.Analysis/AnalysisModel.cs b/ThemeProvider.Analysis/AnalysisModel.cs new file mode 100644 index 0000000..4ead0cc --- /dev/null +++ b/ThemeProvider.Analysis/AnalysisModel.cs @@ -0,0 +1,69 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ThemeProvider.Analysis; + +using System.Collections.Generic; +using System.Linq; + +/// The outcome of a single check. +internal enum CheckStatus +{ + /// The check met its gate. + Pass, + + /// The check is within a soft tolerance but worth attention. + Warn, + + /// The check failed its gate. + Fail, +} + +/// A single issue surfaced by a check (only warnings and failures are recorded). +/// The check that produced the finding. +/// The severity. +/// A human-readable description with the measured value and gate. +internal sealed record Finding(string Category, CheckStatus Status, string Message); + +/// The headline numbers shown in the per-theme summary row. Arrows in the field +/// names indicate the direction that is worse (lower contrast, higher drift, etc.). +/// Lowest text-over-surface contrast ratio. +/// The foreground/background pair that produced it. +/// Lowest UI-glyph contrast ratio. +/// The foreground/background pair that produced it. +/// Smallest Oklab lightness delta between adjacent interaction states. +/// The interaction-state pair that produced it. +/// Largest accent hue drift from the source palette, in degrees. +/// Smallest accent chroma retention ratio. +/// Semantic meanings the theme defines but the ImGui mapper never consumes. +internal sealed record ThemeMetrics( + double MinTextContrast, + string MinTextPair, + double MinGlyphContrast, + string MinGlyphPair, + double MinStateDelta, + string MinStatePair, + double MaxHueDrift, + double MinChromaRetention, + IReadOnlyList UnusedMeanings); + +/// The full analysis result for one theme. +/// The theme's display name. +/// The theme family. +/// Whether the theme is dark. +/// The headline numbers. +/// The warnings and failures. +internal sealed record ThemeAnalysis( + string Name, + string Family, + bool IsDark, + ThemeMetrics Metrics, + IReadOnlyList Findings) +{ + /// The worst status across all findings. + public CheckStatus Status => + Findings.Any(f => f.Status == CheckStatus.Fail) ? CheckStatus.Fail + : Findings.Any(f => f.Status == CheckStatus.Warn) ? CheckStatus.Warn + : CheckStatus.Pass; +} diff --git a/ThemeProvider.Analysis/AnalysisThresholds.cs b/ThemeProvider.Analysis/AnalysisThresholds.cs new file mode 100644 index 0000000..38ee0ec --- /dev/null +++ b/ThemeProvider.Analysis/AnalysisThresholds.cs @@ -0,0 +1,34 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ThemeProvider.Analysis; + +/// +/// The fixed gate values every theme is measured against. Tune them here; the report and the +/// process exit code both derive from these. Contrast values are WCAG contrast ratios (1..21), +/// lightness deltas are in Oklab L (0..1), hue drift is in degrees, chroma retention is a ratio. +/// +internal static class AnalysisThresholds +{ + /// Minimum contrast for body text over the surfaces it is drawn on (WCAG AA, normal text). + public const double TextContrastAa = 4.5; + + /// Floor for intentionally-muted disabled text: below this it reads as invisible rather than dimmed. + public const double DisabledTextFloor = 2.0; + + /// Minimum contrast for UI glyphs such as the checkmark (WCAG 1.4.11 non-text contrast). + public const double GlyphContrast = 3.0; + + /// Minimum Oklab lightness difference between adjacent interaction states to be perceptible. + public const double MinStateLightnessDelta = 0.015; + + /// Maximum hue drift (degrees) an accent color may wander from the theme's source palette. + public const double MaxAccentHueDriftDegrees = 12.0; + + /// Warn when a derived accent keeps less than this fraction of the source palette's chroma. + public const double MinAccentChromaRetention = 0.40; + + /// Chroma below which a color is treated as neutral and its hue is not meaningful. + public const double MeaningfulChroma = 0.02; +} diff --git a/ThemeProvider.Analysis/MarkdownReport.cs b/ThemeProvider.Analysis/MarkdownReport.cs new file mode 100644 index 0000000..f6af2ba --- /dev/null +++ b/ThemeProvider.Analysis/MarkdownReport.cs @@ -0,0 +1,115 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ThemeProvider.Analysis; + +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +/// Renders a set of theme analyses as a single deterministic markdown document. +internal static class MarkdownReport +{ + /// Builds the markdown report. + /// The per-theme results, in the order they should appear. + /// The report text. + public static string Build(IReadOnlyList analyses) + { + StringBuilder sb = new(); + + int failed = analyses.Count(a => a.Status == CheckStatus.Fail); + int warned = analyses.Count(a => a.Status == CheckStatus.Warn); + int passed = analyses.Count - failed - warned; + + sb.AppendLine("# Theme palette analysis"); + sb.AppendLine(); + sb.AppendLine(Inv($"{analyses.Count} themes analyzed: {passed} pass, {warned} warn, {failed} fail.")); + sb.AppendLine(); + AppendLegend(sb); + + sb.AppendLine("## Summary"); + sb.AppendLine(); + sb.AppendLine("| Theme | Mode | Text | Glyph | State delta | Hue drift | Chroma | Status |"); + sb.AppendLine("| --- | --- | --- | --- | --- | --- | --- | --- |"); + foreach (ThemeAnalysis a in analyses) + { + ThemeMetrics m = a.Metrics; + string mode = a.IsDark ? "dark" : "light"; + sb.AppendLine(Inv($"| {a.Name} | {mode} | {m.MinTextContrast:0.0}:1 | {m.MinGlyphContrast:0.0}:1 | {m.MinStateDelta:0.000} | {m.MaxHueDrift:0}deg | {m.MinChromaRetention * 100.0:0}% | {Label(a.Status)} |")); + } + + sb.AppendLine(); + sb.AppendLine("## Findings"); + sb.AppendLine(); + + IReadOnlyList withFindings = [.. analyses.Where(a => a.Findings.Count > 0)]; + if (withFindings.Count == 0) + { + sb.AppendLine("No warnings or failures."); + } + else + { + foreach (ThemeAnalysis a in withFindings) + { + sb.AppendLine(Inv($"### {a.Name} — {Label(a.Status)}")); + sb.AppendLine(); + foreach (Finding f in a.Findings.OrderByDescending(f => f.Status)) + { + sb.AppendLine(Inv($"- **{Label(f.Status)}** [{f.Category}] {f.Message}")); + } + + sb.AppendLine(); + } + } + + AppendUnusedMeanings(sb, analyses); + + return sb.ToString(); + } + + private static void AppendLegend(StringBuilder sb) + { + sb.AppendLine("## What is measured"); + sb.AppendLine(); + sb.AppendLine(Inv($"- **Text** — lowest WCAG contrast of body text over any surface it renders on. Gate: {AnalysisThresholds.TextContrastAa:0.0}:1 (AA).")); + sb.AppendLine(Inv($"- **Glyph** — lowest contrast of UI glyphs (checkmark). Gate: {AnalysisThresholds.GlyphContrast:0.0}:1 (WCAG 1.4.11).")); + sb.AppendLine(Inv($"- **State delta** — smallest Oklab lightness step between adjacent interaction states (button/frame hover/active). Gate: {AnalysisThresholds.MinStateLightnessDelta:0.000}.")); + sb.AppendLine(Inv($"- **Hue drift** — largest angle an accent wanders from the theme's source palette. Gate: {AnalysisThresholds.MaxAccentHueDriftDegrees:0}deg.")); + sb.AppendLine(Inv($"- **Chroma** — smallest fraction of source accent chroma retained. Warns below {AnalysisThresholds.MinAccentChromaRetention * 100.0:0}%.")); + sb.AppendLine(); + sb.AppendLine("Disabled text is checked against an invisibility floor only, since it is meant to read as muted."); + sb.AppendLine(); + } + + private static void AppendUnusedMeanings(StringBuilder sb, IReadOnlyList analyses) + { + IReadOnlyList withUnused = [.. analyses.Where(a => a.Metrics.UnusedMeanings.Count > 0)]; + if (withUnused.Count == 0) + { + return; + } + + sb.AppendLine("## Semantic meanings defined but unused by the ImGui mapper"); + sb.AppendLine(); + sb.AppendLine("These are computed by the theme but never mapped to an ImGui slot, so status colors (success/warning/error/etc.) are unavailable to applications through the mapper."); + sb.AppendLine(); + foreach (ThemeAnalysis a in withUnused) + { + string meanings = string.Join(", ", a.Metrics.UnusedMeanings.Select(m => m.ToString())); + sb.AppendLine(Inv($"- {a.Name}: {meanings}")); + } + + sb.AppendLine(); + } + + private static string Label(CheckStatus status) => status switch + { + CheckStatus.Fail => "FAIL", + CheckStatus.Warn => "WARN", + _ => "PASS", + }; + + private static string Inv(FormattableString text) => text.ToString(CultureInfo.InvariantCulture); +} diff --git a/ThemeProvider.Analysis/PaletteAnalyzer.cs b/ThemeProvider.Analysis/PaletteAnalyzer.cs new file mode 100644 index 0000000..82e9e35 --- /dev/null +++ b/ThemeProvider.Analysis/PaletteAnalyzer.cs @@ -0,0 +1,281 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ThemeProvider.Analysis; + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Numerics; + +using Hexa.NET.ImGui; + +using ktsu.Semantics.Color; +using ktsu.ThemeProvider.ImGui; + +/// +/// Runs the four color checks against the actual ImGui palette a theme produces, plus the +/// semantic palette it is derived from. Analyzes the real shipped output of +/// so a change to the mapper is caught here too. +/// +internal static class PaletteAnalyzer +{ + // The semantic meanings ImGuiPaletteMapper actually consumes. Kept in sync with that mapper by hand; + // meanings outside this set are reported as unused, and a missing member is reported as a coverage gap. + private static readonly SemanticMeaning[] MapperUsedMeanings = + [ + SemanticMeaning.Neutral, + SemanticMeaning.Primary, + SemanticMeaning.Alternate, + ]; + + // Text-over-surface pairs. Body text is drawn on all of these; each must clear WCAG AA. + private static readonly (ImGuiCol Fg, ImGuiCol Bg)[] TextPairs = + [ + (ImGuiCol.Text, ImGuiCol.WindowBg), + (ImGuiCol.Text, ImGuiCol.ChildBg), + (ImGuiCol.Text, ImGuiCol.PopupBg), + (ImGuiCol.Text, ImGuiCol.MenuBarBg), + (ImGuiCol.Text, ImGuiCol.FrameBg), + (ImGuiCol.Text, ImGuiCol.Button), + (ImGuiCol.Text, ImGuiCol.Header), + (ImGuiCol.Text, ImGuiCol.TitleBgActive), + (ImGuiCol.Text, ImGuiCol.Tab), + (ImGuiCol.Text, ImGuiCol.TabSelected), + (ImGuiCol.Text, ImGuiCol.TableHeaderBg), + ]; + + // Disabled text is intentionally muted; it only needs to stay above the invisibility floor. + private static readonly (ImGuiCol Fg, ImGuiCol Bg)[] DisabledTextPairs = + [ + (ImGuiCol.TextDisabled, ImGuiCol.WindowBg), + (ImGuiCol.TextDisabled, ImGuiCol.FrameBg), + ]; + + // UI glyphs (the checkmark) drawn inside frames; WCAG 1.4.11 non-text contrast applies. + private static readonly (ImGuiCol Fg, ImGuiCol Bg)[] GlyphPairs = + [ + (ImGuiCol.CheckMark, ImGuiCol.FrameBg), + (ImGuiCol.CheckMark, ImGuiCol.WindowBg), + ]; + + // Adjacent interaction states that must be visibly distinct. + private static readonly (ImGuiCol A, ImGuiCol B)[] StatePairs = + [ + (ImGuiCol.Button, ImGuiCol.ButtonHovered), + (ImGuiCol.ButtonHovered, ImGuiCol.ButtonActive), + (ImGuiCol.FrameBg, ImGuiCol.FrameBgHovered), + (ImGuiCol.FrameBgHovered, ImGuiCol.FrameBgActive), + ]; + + // Accent meanings and the priorities the mapper draws from them, used for the fidelity check. + private static readonly (SemanticMeaning Meaning, Priority[] Priorities)[] AccentUsage = + [ + (SemanticMeaning.Primary, [Priority.MediumLow, Priority.Medium, Priority.High, Priority.VeryHigh]), + (SemanticMeaning.Alternate, [Priority.Medium, Priority.High]), + ]; + + /// Analyzes one theme and returns its full result. + /// The theme to analyze. + /// The analysis, including headline metrics and any findings. + public static ThemeAnalysis Analyze(ThemeRegistry.ThemeInfo info) + { + Ensure.NotNull(info); + + ISemanticTheme theme = info.CreateInstance(); + IReadOnlyDictionary imgui = new ImGuiPaletteMapper().MapTheme(theme); + IReadOnlyDictionary semantic = SemanticColorMapper.MakeCompletePalette(theme); + + List findings = []; + + (double minTextContrast, string minTextPair) = CheckContrast(imgui, TextPairs, AnalysisThresholds.TextContrastAa, "Text contrast", findings); + CheckContrast(imgui, DisabledTextPairs, AnalysisThresholds.DisabledTextFloor, "Disabled text", findings); + (double minGlyphContrast, string minGlyphPair) = CheckContrast(imgui, GlyphPairs, AnalysisThresholds.GlyphContrast, "Glyph contrast", findings); + (double minStateDelta, string minStatePair) = CheckDistinctness(imgui, findings); + (double maxHueDrift, double minChromaRetention) = CheckFidelity(theme, semantic, findings); + IReadOnlyList unused = CheckCoverage(theme, findings); + + ThemeMetrics metrics = new( + minTextContrast, minTextPair, + minGlyphContrast, minGlyphPair, + minStateDelta, minStatePair, + maxHueDrift, minChromaRetention, + unused); + + return new ThemeAnalysis(info.Name, info.Family, info.IsDark, metrics, findings); + } + + private static (double Min, string Pair) CheckContrast( + IReadOnlyDictionary imgui, + (ImGuiCol Fg, ImGuiCol Bg)[] pairs, + double gate, + string category, + List findings) + { + double min = double.PositiveInfinity; + string minPair = "n/a"; + + foreach ((ImGuiCol fg, ImGuiCol bg) in pairs) + { + if (ToColor(imgui, fg) is not Color fgColor || ToColor(imgui, bg) is not Color bgColor) + { + continue; + } + + double ratio = fgColor.ContrastRatio(bgColor); + string pair = $"{fg}/{bg}"; + if (ratio < min) + { + min = ratio; + minPair = pair; + } + + if (ratio < gate) + { + findings.Add(new Finding( + category, + CheckStatus.Fail, + $"{pair} is {Num(ratio)}:1 (needs {Num(gate)}:1)")); + } + } + + return (double.IsPositiveInfinity(min) ? 0.0 : min, minPair); + } + + private static (double MinDelta, string Pair) CheckDistinctness( + IReadOnlyDictionary imgui, + List findings) + { + double min = double.PositiveInfinity; + string minPair = "n/a"; + + foreach ((ImGuiCol a, ImGuiCol b) in StatePairs) + { + if (ToColor(imgui, a) is not Color ca || ToColor(imgui, b) is not Color cb) + { + continue; + } + + double delta = Math.Abs(ca.ToOklab().L - cb.ToOklab().L); + string pair = $"{a}->{b}"; + if (delta < min) + { + min = delta; + minPair = pair; + } + + if (delta < AnalysisThresholds.MinStateLightnessDelta) + { + findings.Add(new Finding( + "Interaction distinctness", + CheckStatus.Fail, + $"{pair} differ by only {Num(delta, "0.000")} Oklab L (needs {Num(AnalysisThresholds.MinStateLightnessDelta, "0.000")})")); + } + } + + return (double.IsPositiveInfinity(min) ? 0.0 : min, minPair); + } + + private static (double MaxHueDrift, double MinChromaRetention) CheckFidelity( + ISemanticTheme theme, + IReadOnlyDictionary semantic, + List findings) + { + double maxHueDrift = 0.0; + double minChromaRetention = double.PositiveInfinity; + + foreach ((SemanticMeaning meaning, Priority[] priorities) in AccentUsage) + { + if (!theme.SemanticMapping.TryGetValue(meaning, out Collection? sourceColors) || sourceColors.Count == 0) + { + continue; + } + + List sourceHued = [.. sourceColors + .Select(c => c.ToOklch()) + .Where(o => o.C > AnalysisThresholds.MeaningfulChroma)]; + double maxSourceChroma = sourceColors.Max(c => c.ToOklch().C); + + foreach (Priority priority in priorities) + { + if (!semantic.TryGetValue(new SemanticColorRequest(meaning, priority), out Color derived)) + { + continue; + } + + Oklch d = derived.ToOklch(); + + if (maxSourceChroma > AnalysisThresholds.MeaningfulChroma) + { + double retention = d.C / maxSourceChroma; + if (retention < minChromaRetention) + { + minChromaRetention = retention; + } + + if (retention < AnalysisThresholds.MinAccentChromaRetention) + { + findings.Add(new Finding( + "Canonical fidelity", + CheckStatus.Warn, + $"{meaning} {priority} keeps only {Num(retention * 100.0, "0")}% of source chroma")); + } + } + + if (d.C > AnalysisThresholds.MeaningfulChroma && sourceHued.Count > 0) + { + double drift = sourceHued.Min(s => HueDelta(d.H, s.H)); + if (drift > maxHueDrift) + { + maxHueDrift = drift; + } + + if (drift > AnalysisThresholds.MaxAccentHueDriftDegrees) + { + findings.Add(new Finding( + "Canonical fidelity", + CheckStatus.Warn, + $"{meaning} {priority} hue drifts {Num(drift, "0")}deg from source (max {Num(AnalysisThresholds.MaxAccentHueDriftDegrees, "0")}deg)")); + } + } + } + } + + return (maxHueDrift, double.IsPositiveInfinity(minChromaRetention) ? 1.0 : minChromaRetention); + } + + private static IReadOnlyList CheckCoverage(ISemanticTheme theme, List findings) + { + HashSet provided = [.. theme.SemanticMapping.Keys]; + + foreach (SemanticMeaning required in MapperUsedMeanings) + { + if (!provided.Contains(required)) + { + // Neutral and Primary underpin backgrounds and accents; Alternate only tints plots/selection. + CheckStatus severity = required == SemanticMeaning.Alternate ? CheckStatus.Warn : CheckStatus.Fail; + findings.Add(new Finding( + "Semantic coverage", + severity, + $"theme does not define {required}, which the ImGui mapper needs; those slots fall back to defaults")); + } + } + + return [.. provided.Where(m => !MapperUsedMeanings.Contains(m)).OrderBy(m => m.ToString(), StringComparer.Ordinal)]; + } + + private static Color? ToColor(IReadOnlyDictionary imgui, ImGuiCol slot) => + imgui.TryGetValue(slot, out Vector4 v) ? Color.FromSrgb(v.X, v.Y, v.Z, v.W) : null; + + private static double HueDelta(double a, double b) + { + double d = Math.Abs(a - b) % 360.0; + return d > 180.0 ? 360.0 - d : d; + } + + private static string Num(double value, string format = "0.0") => + value.ToString(format, CultureInfo.InvariantCulture); +} diff --git a/ThemeProvider.Analysis/Program.cs b/ThemeProvider.Analysis/Program.cs new file mode 100644 index 0000000..9e0534f --- /dev/null +++ b/ThemeProvider.Analysis/Program.cs @@ -0,0 +1,82 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ThemeProvider.Analysis; + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; + +/// +/// Command-line entry point. Analyzes the registered themes, writes a markdown report, and returns a +/// non-zero exit code when any theme fails a gate (so it doubles as a regression check). +/// +internal static class Program +{ + private static int Main(string[] args) + { + string outputPath = "theme-analysis.md"; + string? themeFilter = null; + bool strict = false; + + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "--output" or "-o" when i + 1 < args.Length: + outputPath = args[++i]; + break; + case "--theme" or "-t" when i + 1 < args.Length: + themeFilter = args[++i]; + break; + case "--strict": + strict = true; + break; + case "--help" or "-h": + PrintUsage(); + return 0; + default: + Console.Error.WriteLine(Inv($"Unknown or incomplete argument: {args[i]}")); + PrintUsage(); + return 2; + } + } + + IReadOnlyList themes = themeFilter is null + ? ThemeRegistry.AllThemes + : [.. ThemeRegistry.AllThemes.Where(t => t.Name.Contains(themeFilter, StringComparison.OrdinalIgnoreCase))]; + + if (themes.Count == 0) + { + Console.Error.WriteLine(Inv($"No themes matched \"{themeFilter}\".")); + return 2; + } + + List analyses = [.. themes.Select(PaletteAnalyzer.Analyze)]; + string report = MarkdownReport.Build(analyses); + File.WriteAllText(outputPath, report); + + int failed = analyses.Count(a => a.Status == CheckStatus.Fail); + int warned = analyses.Count(a => a.Status == CheckStatus.Warn); + int passed = analyses.Count - failed - warned; + + Console.WriteLine(Inv($"Analyzed {analyses.Count} theme(s): {passed} pass, {warned} warn, {failed} fail.")); + Console.WriteLine(Inv($"Report written to {Path.GetFullPath(outputPath)}")); + + bool regressed = failed > 0 || (strict && warned > 0); + return regressed ? 1 : 0; + } + + private static void PrintUsage() + { + Console.WriteLine("Usage: ThemeProvider.Analysis [--output ] [--theme ] [--strict]"); + Console.WriteLine(" --output, -o Markdown report path (default: theme-analysis.md)"); + Console.WriteLine(" --theme, -t Analyze only themes whose name contains this text"); + Console.WriteLine(" --strict Exit non-zero on warnings as well as failures"); + } + + private static string Inv(FormattableString text) => text.ToString(CultureInfo.InvariantCulture); +} diff --git a/ThemeProvider.Analysis/ThemeProvider.Analysis.csproj b/ThemeProvider.Analysis/ThemeProvider.Analysis.csproj new file mode 100644 index 0000000..b37b68b --- /dev/null +++ b/ThemeProvider.Analysis/ThemeProvider.Analysis.csproj @@ -0,0 +1,22 @@ + + + + + + + net10.0 + + + + + + + + + + + + + + + diff --git a/ThemeProvider.sln b/ThemeProvider.sln index 77cc420..78c1ad4 100644 --- a/ThemeProvider.sln +++ b/ThemeProvider.sln @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThemeProvider.ImGui", "Them EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThemeProvider.Test", "ThemeProvider.Test\ThemeProvider.Test.csproj", "{202F4B12-82DA-45D3-94CB-75B8C10A0659}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThemeProvider.Analysis", "ThemeProvider.Analysis\ThemeProvider.Analysis.csproj", "{3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +71,18 @@ Global {202F4B12-82DA-45D3-94CB-75B8C10A0659}.Release|x64.Build.0 = Release|Any CPU {202F4B12-82DA-45D3-94CB-75B8C10A0659}.Release|x86.ActiveCfg = Release|Any CPU {202F4B12-82DA-45D3-94CB-75B8C10A0659}.Release|x86.Build.0 = Release|Any CPU + {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Debug|x64.ActiveCfg = Debug|Any CPU + {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Debug|x64.Build.0 = Debug|Any CPU + {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Debug|x86.ActiveCfg = Debug|Any CPU + {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Debug|x86.Build.0 = Debug|Any CPU + {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Release|Any CPU.Build.0 = Release|Any CPU + {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Release|x64.ActiveCfg = Release|Any CPU + {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Release|x64.Build.0 = Release|Any CPU + {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Release|x86.ActiveCfg = Release|Any CPU + {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE