using System.Text.Json;
using XFESystem.Core;
namespace XFESystem.Desktop;
public enum DesktopThemeMode { Light, Dark }
/// <summary>Immutable guest palette. Custom accent ink is adjusted for legibility; its stored RGB is preserved.</summary>
public sealed class DesktopTheme
{
public const uint DefaultAccent = 0x657DE2;
public const string SettingsDirectory = "X:\\System\\Settings";
public const string SettingsPath = SettingsDirectory + "\\Appearance.json";
public DesktopThemeMode Mode { get; }
public uint Accent { get; }
public string AccentHex => "#" + Accent.ToString("X6");
public bool IsDark => Mode == DesktopThemeMode.Dark;
public uint Background => IsDark ? 0x09121Bu : 0xEEEFF8u;
public uint Panel => IsDark ? 0x102538u : 0xFAFAFEu;
public uint Surface => IsDark ? 0x09121Bu : 0xFFFFFFu;
public uint Text => IsDark ? 0xE7F3FCu : 0x343C56u;
public uint Muted => IsDark ? 0x8EA5BAu : 0x606B82u;
public uint Shadow => IsDark ? 0x050B10u : 0xD9DEEDu;
public uint Border => IsDark ? 0x285171u : 0xD9DDEEu;
public uint AccentInk { get; }
public uint AccentStroke { get; }
public uint Selection { get; }
public uint SoftAccent { get; }
public uint ActiveBorder { get; }
public uint ActiveTitle { get; }
public uint AccentText => IsDark && Accent == DefaultAccent ? 0x75C9F5u : AccentInk;
public DesktopTheme(DesktopThemeMode mode = DesktopThemeMode.Dark, uint accent = DefaultAccent)
{
if (!Enum.IsDefined(mode)) throw new ArgumentOutOfRangeException(nameof(mode));
if (accent > 0xFFFFFF) throw new ArgumentOutOfRangeException(nameof(accent));
Mode = mode; Accent = accent;
uint displayedAccent = IsDark && accent == DefaultAccent ? 0x1CC0E8u : accent;
AccentInk = Legible(displayedAccent, IsDark ? Panel : Background, 4.5);
AccentStroke = Legible(displayedAccent, Surface, 3);
Selection = Mix(Panel, accent, IsDark ? 25 : 14);
SoftAccent = Mix(Panel, accent, IsDark ? 12 : 8);
ActiveBorder = IsDark && accent == DefaultAccent ? 0x285171u : Mix(Border, accent, 38);
ActiveTitle = IsDark ? (accent == DefaultAccent ? 0x285171u : Mix(Panel, accent, 30)) : Panel;
}
public uint SyntaxColor(uint original) => IsDark ? original : original switch
{
0xDCEAF4 => Text, 0x739D81 => 0x477759, 0xD6BB80 => 0x906119,
0xB99AEE => 0x6D4CAB, 0x77D7CE => 0x237A81, _ => original
};
public static int PresetCount => 5;
public static (string Name, uint Color) Preset(int index) => index switch
{
0 => ("Blue", DefaultAccent), 1 => ("Purple", 0x9898E7), 2 => ("Teal", 0x239F99),
3 => ("Rose", 0xCE6A93), 4 => ("Amber", 0xC38A2C), _ => throw new ArgumentOutOfRangeException(nameof(index))
};
public static bool TryParseAccent(string value, out uint accent)
{
accent = 0;
if (value.Length != 7 || value[0] != '#') return false;
for (int i = 1; i < value.Length; i++)
{
char c = value[i]; int digit = c is >= '0' and <= '9' ? c - '0' : c is >= 'A' and <= 'F' ? c - 'A' + 10 : c is >= 'a' and <= 'f' ? c - 'a' + 10 : -1;
if (digit < 0) return false;
accent = accent * 16 + (uint)digit;
}
return true;
}
public string ToSettingsJson() => "{\"Version\":1,\"Theme\":\"" + Mode.ToString() + "\",\"Accent\":\"" + AccentHex + "\"}";
public static DesktopTheme ParseSettings(string text)
{
if (text.Length > 1024) throw new FormatException("Appearance settings exceed the supported size.");
using var document = JsonDocument.Parse(text, new JsonDocumentOptions { MaxDepth = 4 });
if (document.RootElement.ValueKind != JsonValueKind.Object) throw new FormatException("Appearance settings must be an object.");
int fields = 0; DesktopThemeMode mode = DesktopThemeMode.Light; uint accent = DefaultAccent;
foreach (var property in document.RootElement.EnumerateObject())
{
int flag = property.Name switch { "Version" => 1, "Theme" => 2, "Accent" => 4, _ => throw new FormatException("Unknown appearance setting.") };
if ((fields & flag) != 0) throw new FormatException("Duplicate appearance setting.");
fields |= flag;
if (flag == 1 && (property.Value.ValueKind != JsonValueKind.Number || !property.Value.TryGetInt32(out int version) || version != 1)) throw new FormatException("Unsupported appearance settings version.");
if (flag == 2)
{
string? value = property.Value.ValueKind == JsonValueKind.String ? property.Value.GetString() : null;
mode = value switch { "Light" => DesktopThemeMode.Light, "Dark" => DesktopThemeMode.Dark, _ => throw new FormatException("Unsupported theme.") };
}
if (flag == 4 && (property.Value.ValueKind != JsonValueKind.String || !TryParseAccent(property.Value.GetString()!, out accent))) throw new FormatException("Invalid appearance accent.");
}
if (fields != 7) throw new FormatException("Incomplete appearance settings.");
return new(mode, accent);
}
public static DesktopTheme Load(IWorkspaceStorage storage, out string? error)
{
error = null;
try { return storage.Exists(SettingsPath) ? ParseSettings(storage.ReadText(SettingsPath)) : new(); }
catch (Exception failure) when (failure is IOException or UnauthorizedAccessException or FormatException or JsonException or InvalidOperationException)
{ error = "Appearance settings could not be loaded: " + failure.Message; return new(); }
}
public void Save(IWorkspaceStorage storage)
{
storage.CreateDirectory(SettingsDirectory);
storage.WriteText(SettingsPath, ToSettingsJson());
}
public static uint Mix(uint background, uint foreground, int percent)
{
int inverse = 100 - percent;
uint red = (uint)(((background >> 16 & 255) * inverse + (foreground >> 16 & 255) * percent + 50) / 100);
uint green = (uint)(((background >> 8 & 255) * inverse + (foreground >> 8 & 255) * percent + 50) / 100);
uint blue = (uint)(((background & 255) * inverse + (foreground & 255) * percent + 50) / 100);
return red << 16 | green << 8 | blue;
}
public static double Contrast(uint first, uint second)
{
double a = Luminance(first), b = Luminance(second);
return (Math.Max(a, b) + 0.05) / (Math.Min(a, b) + 0.05);
}
private static double Luminance(uint color)
{
static double Channel(uint value) { double c = value / 255.0; return c <= 0.04045 ? c / 12.92 : Math.Pow((c + 0.055) / 1.055, 2.4); }
return Channel(color >> 16 & 255) * 0.2126 + Channel(color >> 8 & 255) * 0.7152 + Channel(color & 255) * 0.0722;
}
private static uint Legible(uint color, uint background, double minimum)
{
if (Contrast(color, background) >= minimum) return color;
uint endpoint = Contrast(0, background) >= Contrast(0xFFFFFF, background) ? 0u : 0xFFFFFFu;
for (int percent = 5; percent <= 100; percent += 5)
{
uint candidate = Mix(color, endpoint, percent);
if (Contrast(candidate, background) >= minimum) return candidate;
}
return endpoint;
}
}
using System.Text.Json;
using XFESystem.Core;
namespace XFESystem.Desktop;
public enum DesktopThemeMode { Light, Dark }
/// <summary>Immutable guest palette. Custom accent ink is adjusted for legibility; its stored RGB is preserved.</summary>
public sealed class DesktopTheme
{
public const uint DefaultAccent = 0x657DE2;
public const string SettingsDirectory = "X:\\System\\Settings";
public const string SettingsPath = SettingsDirectory + "\\Appearance.json";
public DesktopThemeMode Mode { get; }
public uint Accent { get; }
public string AccentHex => "#" + Accent.ToString("X6");
public bool IsDark => Mode == DesktopThemeMode.Dark;
public uint Background => IsDark ? 0x09121Bu : 0xEEEFF8u;
public uint Panel => IsDark ? 0x102538u : 0xFAFAFEu;
public uint Surface => IsDark ? 0x09121Bu : 0xFFFFFFu;
public uint Text => IsDark ? 0xE7F3FCu : 0x343C56u;
public uint Muted => IsDark ? 0x8EA5BAu : 0x606B82u;
public uint Shadow => IsDark ? 0x050B10u : 0xD9DEEDu;
public uint Border => IsDark ? 0x285171u : 0xD9DDEEu;
public uint AccentInk { get; }
public uint AccentStroke { get; }
public uint Selection { get; }
public uint SoftAccent { get; }
public uint ActiveBorder { get; }
public uint ActiveTitle { get; }
public uint AccentText => IsDark && Accent == DefaultAccent ? 0x75C9F5u : AccentInk;
public DesktopTheme(DesktopThemeMode mode = DesktopThemeMode.Dark, uint accent = DefaultAccent)
{
if (!Enum.IsDefined(mode)) throw new ArgumentOutOfRangeException(nameof(mode));
if (accent > 0xFFFFFF) throw new ArgumentOutOfRangeException(nameof(accent));
Mode = mode; Accent = accent;
uint displayedAccent = IsDark && accent == DefaultAccent ? 0x1CC0E8u : accent;
AccentInk = Legible(displayedAccent, IsDark ? Panel : Background, 4.5);
AccentStroke = Legible(displayedAccent, Surface, 3);
Selection = Mix(Panel, accent, IsDark ? 25 : 14);
SoftAccent = Mix(Panel, accent, IsDark ? 12 : 8);
ActiveBorder = IsDark && accent == DefaultAccent ? 0x285171u : Mix(Border, accent, 38);
ActiveTitle = IsDark ? (accent == DefaultAccent ? 0x285171u : Mix(Panel, accent, 30)) : Panel;
}
public uint SyntaxColor(uint original) => IsDark ? original : original switch
{
0xDCEAF4 => Text, 0x739D81 => 0x477759, 0xD6BB80 => 0x906119,
0xB99AEE => 0x6D4CAB, 0x77D7CE => 0x237A81, _ => original
};
public static int PresetCount => 5;
public static (string Name, uint Color) Preset(int index) => index switch
{
0 => ("Blue", DefaultAccent), 1 => ("Purple", 0x9898E7), 2 => ("Teal", 0x239F99),
3 => ("Rose", 0xCE6A93), 4 => ("Amber", 0xC38A2C), _ => throw new ArgumentOutOfRangeException(nameof(index))
};
public static bool TryParseAccent(string value, out uint accent)
{
accent = 0;
if (value.Length != 7 || value[0] != '#') return false;
for (int i = 1; i < value.Length; i++)
{
char c = value[i]; int digit = c is >= '0' and <= '9' ? c - '0' : c is >= 'A' and <= 'F' ? c - 'A' + 10 : c is >= 'a' and <= 'f' ? c - 'a' + 10 : -1;
if (digit < 0) return false;
accent = accent * 16 + (uint)digit;
}
return true;
}
public string ToSettingsJson() => "{\"Version\":1,\"Theme\":\"" + Mode.ToString() + "\",\"Accent\":\"" + AccentHex + "\"}";
public static DesktopTheme ParseSettings(string text)
{
if (text.Length > 1024) throw new FormatException("Appearance settings exceed the supported size.");
using var document = JsonDocument.Parse(text, new JsonDocumentOptions { MaxDepth = 4 });
if (document.RootElement.ValueKind != JsonValueKind.Object) throw new FormatException("Appearance settings must be an object.");
int fields = 0; DesktopThemeMode mode = DesktopThemeMode.Light; uint accent = DefaultAccent;
foreach (var property in document.RootElement.EnumerateObject())
{
int flag = property.Name switch { "Version" => 1, "Theme" => 2, "Accent" => 4, _ => throw new FormatException("Unknown appearance setting.") };
if ((fields & flag) != 0) throw new FormatException("Duplicate appearance setting.");
fields |= flag;
if (flag == 1 && (property.Value.ValueKind != JsonValueKind.Number || !property.Value.TryGetInt32(out int version) || version != 1)) throw new FormatException("Unsupported appearance settings version.");
if (flag == 2)
{
string? value = property.Value.ValueKind == JsonValueKind.String ? property.Value.GetString() : null;
mode = value switch { "Light" => DesktopThemeMode.Light, "Dark" => DesktopThemeMode.Dark, _ => throw new FormatException("Unsupported theme.") };
}
if (flag == 4 && (property.Value.ValueKind != JsonValueKind.String || !TryParseAccent(property.Value.GetString()!, out accent))) throw new FormatException("Invalid appearance accent.");
}
if (fields != 7) throw new FormatException("Incomplete appearance settings.");
return new(mode, accent);
}
public static DesktopTheme Load(IWorkspaceStorage storage, out string? error)
{
error = null;
try { return storage.Exists(SettingsPath) ? ParseSettings(storage.ReadText(SettingsPath)) : new(); }
catch (Exception failure) when (failure is IOException or UnauthorizedAccessException or FormatException or JsonException or InvalidOperationException)
{ error = "Appearance settings could not be loaded: " + failure.Message; return new(); }
}
public void Save(IWorkspaceStorage storage)
{
storage.CreateDirectory(SettingsDirectory);
storage.WriteText(SettingsPath, ToSettingsJson());
}
public static uint Mix(uint background, uint foreground, int percent)
{
int inverse = 100 - percent;
uint red = (uint)(((background >> 16 & 255) * inverse + (foreground >> 16 & 255) * percent + 50) / 100);
uint green = (uint)(((background >> 8 & 255) * inverse + (foreground >> 8 & 255) * percent + 50) / 100);
uint blue = (uint)(((background & 255) * inverse + (foreground & 255) * percent + 50) / 100);
return red << 16 | green << 8 | blue;
}
public static double Contrast(uint first, uint second)
{
double a = Luminance(first), b = Luminance(second);
return (Math.Max(a, b) + 0.05) / (Math.Min(a, b) + 0.05);
}
private static double Luminance(uint color)
{
static double Channel(uint value) { double c = value / 255.0; return c <= 0.04045 ? c / 12.92 : Math.Pow((c + 0.055) / 1.055, 2.4); }
return Channel(color >> 16 & 255) * 0.2126 + Channel(color >> 8 & 255) * 0.7152 + Channel(color & 255) * 0.0722;
}
private static uint Legible(uint color, uint background, double minimum)
{
if (Contrast(color, background) >= minimum) return color;
uint endpoint = Contrast(0, background) >= Contrast(0xFFFFFF, background) ? 0u : 0xFFFFFFu;
for (int percent = 5; percent <= 100; percent += 5)
{
uint candidate = Mix(color, endpoint, percent);
if (Contrast(candidate, background) >= minimum) return candidate;
}
return endpoint;
}
}