XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
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;
    }
}