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

XFESystem

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

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Globalization;

namespace XFESystem.Browser;

public readonly record struct CssColor(uint Rgb, double Alpha = 1);

public sealed class CssComputedStyle
{
    internal CssComputedStyle(Dictionary<string, string> properties, CssComputedStyle? parent, BrowserViewport viewport,
        IReadOnlyDictionary<string, string> customProperties)
    {
        values = properties;
        CustomProperties = customProperties;
        Properties = new CssPropertyMap(properties, customProperties);
        double inheritedSize = parent?.Font.Size ?? 16;
        double fontSize = CssValue.Length(Get("font-size", inheritedSize.ToString(CultureInfo.InvariantCulture) + "px"), inheritedSize, inheritedSize, viewport, 16) ?? inheritedSize;
        Font = new(Math.Clamp(fontSize, 1, 256), Get("font-family", parent?.Font.Family ?? "monospace").Trim(' ', '\'', '"'),
            Get("font-weight", parent?.Font.Weight.ToString(CultureInfo.InvariantCulture) ?? "400") switch
            { "bold" or "bolder" => 700, "normal" => 400, var weight => (int)Math.Clamp(CssValue.Number(weight, 400), 100, 900) },
            Get("font-style", parent?.Font.Italic == true ? "italic" : "normal") is "italic" or "oblique");
        // Resolve font-relative lengths before inheritance; percentages keep
        // referring to the receiving block's own inline size.
        string indent = Get("text-indent", "0");
        if (values.ContainsKey("text-indent") && !indent.Contains('%') &&
            CssValue.Length(indent, 0, Font.Size, viewport, 16) is { } indentLength)
            values["text-indent"] = CssValue.Pixels(indentLength);
        Color = CssValue.Color(Get("color", parent is null ? "black" : CssValue.SerializeColor(parent.Color)), parent?.Color ?? new(0));
        Background = CssValue.Color(Get("background-color", "transparent"), Color, new(0, 0));
        double line = CssValue.Number(Get("line-height", "normal"), double.NaN);
        LineHeight = double.IsFinite(line) ? Math.Max(1, line * Font.Size) :
            CssValue.Length(Get("line-height", "normal"), Font.Size, Font.Size, viewport, 16) ?? Font.Size * 1.2;
        Viewport = viewport;
    }
    private readonly Dictionary<string, string> values;
    private readonly Dictionary<string, string> used = new(StringComparer.OrdinalIgnoreCase);
    internal BrowserViewport Viewport { get; }
    internal IReadOnlyDictionary<string, string> OwnProperties => values;
    internal IReadOnlyDictionary<string, string> CustomProperties { get; }
    public IReadOnlyDictionary<string, string> Properties { get; }
    public BrowserFont Font { get; }
    public CssColor Color { get; }
    public CssColor Background { get; }
    public double LineHeight { get; }
    public string Display => Get("float", "none") is "left" or "right" ? Get("display", "inline") switch
        { "inline" or "inline-block" => "block", "inline-flex" => "flex", var display => display } : Get("display", "inline");
    public string Position => Get("position", "static");
    public string Visibility => Get("visibility", "visible");
    public string PointerEvents => Get("pointer-events", "auto");
    public string WhiteSpace => Get("white-space", "normal");
    public string BoxSizing => Get("box-sizing", "content-box");
    public string OverflowX => Get("overflow-x", Get("overflow", "visible"));
    public string OverflowY => Get("overflow-y", Get("overflow", "visible"));
    public double Opacity => Math.Clamp(CssValue.Number(Get("opacity", "1"), 1), 0, 1);
    public int ZIndex => (int)Math.Clamp(CssValue.Number(Get("z-index", "0"), 0), -1000000, 1000000);
    public string GetPropertyValue(string name) => used.TryGetValue(name, out string? resolved) ? resolved : Get(name, "");
    internal string Get(string name, string fallback) => Properties.TryGetValue(name, out string? value) ? value : fallback;
    internal double? Length(string name, double basis) => CssValue.Length(Get(name, "auto"), basis, Font.Size, Viewport, 16);
    internal BrowserEdges Edges(string prefix, double basis, bool border = false)
    {
        double Edge(string side)
        {
            if (border && Get("border-" + side + "-style", "none") is "none" or "hidden") return 0;
            return Math.Clamp(Length(prefix + "-" + side + (border ? "-width" : ""), basis) ?? 0,
                prefix == "margin" ? -100000 : 0, 100000);
        }
        return new(Edge("top"), Edge("right"), Edge("bottom"), Edge("left"));
    }
    internal void Resolve(BrowserLayoutBox box)
    {
        used["width"] = CssValue.Pixels(box.ContentBox.Width); used["height"] = CssValue.Pixels(box.ContentBox.Height);
        used["font-size"] = CssValue.Pixels(Font.Size); used["line-height"] = CssValue.Pixels(LineHeight);
        used["font-family"] = Font.Family; used["font-weight"] = Font.Weight.ToString(CultureInfo.InvariantCulture);
        used["display"] = Display; used["position"] = Position; used["visibility"] = Visibility; used["box-sizing"] = BoxSizing;
        used["color"] = CssValue.SerializeColor(Color); used["background-color"] = CssValue.SerializeColor(Background);
        foreach (var pair in new[] { ("margin", box.Margin), ("padding", box.Padding), ("border", box.Border) })
        {
            used[pair.Item1 + "-top" + (pair.Item1 == "border" ? "-width" : "")] = CssValue.Pixels(pair.Item2.Top);
            used[pair.Item1 + "-right" + (pair.Item1 == "border" ? "-width" : "")] = CssValue.Pixels(pair.Item2.Right);
            used[pair.Item1 + "-bottom" + (pair.Item1 == "border" ? "-width" : "")] = CssValue.Pixels(pair.Item2.Bottom);
            used[pair.Item1 + "-left" + (pair.Item1 == "border" ? "-width" : "")] = CssValue.Pixels(pair.Item2.Left);
        }
    }
}

internal static class CssValue
{
    public static double Number(string value, double fallback) => double.TryParse(value, NumberStyles.Float,
        CultureInfo.InvariantCulture, out double number) && double.IsFinite(number) ? number : fallback;
    public static string Pixels(double value) => value.ToString("0.###", CultureInfo.InvariantCulture) + "px";
    public static double? Length(string value, double basis, double font, BrowserViewport viewport, double rootFont)
        => Length(value, basis, font, viewport, rootFont, 0);
    private static double? Length(string value, double basis, double font, BrowserViewport viewport, double rootFont, int depth)
    {
        if (depth > 32) return null;
        value = value.Trim().ToLowerInvariant();
        if (value is "auto" or "none" or "normal" or "" || value.StartsWith("var(")) return null;
        if ((value.StartsWith("min(") || value.StartsWith("max(") || value.StartsWith("clamp(")) && value.EndsWith(')'))
        {
            int open = value.IndexOf('(');
            var pieces = Split(value[(open + 1)..^1], ',').Select(part => Length(part, basis, font, viewport, rootFont, depth + 1)).ToArray();
            if (pieces.Length == 0 || pieces.Any(part => part is null)) return null;
            return value.StartsWith("min(") ? pieces.Min() : value.StartsWith("max(") ? pieces.Max() :
                pieces.Length == 3 ? Math.Max(pieces[0]!.Value, Math.Min(pieces[1]!.Value, pieces[2]!.Value)) : null;
        }
        if (value.StartsWith("calc(") && value.EndsWith(')'))
        {
            string expression = value[5..^1];
            int sign = 1; double sum = 0; int start = 0;
            for (int index = 0; index <= expression.Length; index++)
                if (index == expression.Length || (index > start && expression[index] is '+' or '-' && char.IsWhiteSpace(expression[index - 1])))
                {
                    double? term = Length(expression[start..index], basis, font, viewport, rootFont, depth + 1);
                    if (term is null) return null;
                    sum += sign * term.Value;
                    if (index < expression.Length) { sign = expression[index] == '-' ? -1 : 1; start = index + 1; }
                }
            return Math.Clamp(sum, -1000000, 1000000);
        }
        string[] units = ["rem", "px", "em", "vh", "vw", "%", "pt"];
        foreach (string unit in units)
            if (value.EndsWith(unit, StringComparison.Ordinal))
            {
                double amount = Number(value[..^unit.Length], double.NaN);
                if (!double.IsFinite(amount)) return null;
                return Math.Clamp(amount * (unit switch { "rem" => rootFont, "em" => font, "vh" => viewport.Height / 100,
                    "vw" => viewport.Width / 100, "%" => basis / 100, "pt" => 4d / 3, _ => 1 }), -1000000, 1000000);
            }
        double plain = Number(value, double.NaN);
        return plain == 0 ? 0 : null;
    }
    public static CssColor Color(string value, CssColor current, CssColor? fallback = null)
        => Color(value, current, fallback, 0);
    private static CssColor Color(string value, CssColor current, CssColor? fallback, int depth)
    {
        if (depth > 32) return fallback ?? current;
        value = value.Trim().ToLowerInvariant();
        if (value == "currentcolor") return current;
        if (value == "transparent") return new(0, 0);
        if (value.StartsWith("color-mix(") && value.EndsWith(')'))
        {
            var pieces = Split(value[10..^1], ',').ToArray();
            if (pieces.Length != 3 || pieces[0].Trim() != "in srgb") return fallback ?? current;
            (string Text, double? Weight) Part(string part)
            {
                int space = part.LastIndexOf(' '); string last = space < 0 ? "" : part[(space + 1)..].Trim();
                return last.EndsWith('%') && double.TryParse(last[..^1], NumberStyles.Float, CultureInfo.InvariantCulture, out double weight)
                    ? (part[..space].Trim(), Math.Clamp(weight / 100, 0, 1)) : (part.Trim(), null);
            }
            var a = Part(pieces[1].Trim()); var b = Part(pieces[2].Trim());
            double aw = a.Weight ?? (b.Weight is { } bw0 ? 1 - bw0 : .5), bw = b.Weight ?? 1 - aw, sum = aw + bw;
            if (sum <= 0) return new(0, 0);
            return Mix(Color(a.Text, current, fallback, depth + 1), Color(b.Text, current, fallback, depth + 1), aw / sum, Math.Min(1, sum));
        }
        if (value.StartsWith('#'))
        {
            string hex = value[1..];
            if (hex.Length is 3 or 4) hex = string.Concat(hex.Select(character => new string(character, 2)));
            if (hex.Length is 6 or 8 && uint.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint rgba))
                return hex.Length == 8 ? new(rgba >> 8, (rgba & 255) / 255d) : new(rgba);
        }
        if (value.StartsWith("rgb(") || value.StartsWith("rgba("))
        {
            int open = value.IndexOf('('); var pieces = value[(open + 1)..].TrimEnd(')').Replace('/', ',').Split([',', ' '], StringSplitOptions.RemoveEmptyEntries);
            if (pieces.Length is 3 or 4)
            {
                uint Channel(string item) => (uint)Math.Clamp(Math.Round(Number(item.TrimEnd('%'), 0) * (item.EndsWith('%') ? 2.55 : 1)), 0, 255);
                return new(Channel(pieces[0]) << 16 | Channel(pieces[1]) << 8 | Channel(pieces[2]),
                    pieces.Length == 4 ? Math.Clamp(Number(pieces[3].TrimEnd('%'), 1) / (pieces[3].EndsWith('%') ? 100 : 1), 0, 1) : 1);
            }
        }
        uint? named = value switch { "white" => 0xFFFFFF, "black" => 0, "red" => 0xFF0000, "green" => 0x008000,
            "blue" => 0x0000FF, "yellow" => 0xFFFF00, "gray" or "grey" => 0x808080, "silver" => 0xC0C0C0,
            "orange" => 0xFFA500, "purple" => 0x800080, "pink" => 0xFFC0CB, "cyan" or "aqua" => 0x00FFFF,
            "magenta" or "fuchsia" => 0xFF00FF, "navy" => 0x000080, "teal" => 0x008080, "lime" => 0x00FF00,
            "gold" => 0xFFD700, "rebeccapurple" => 0x663399, _ => null };
        return named is { } rgb ? new(rgb) : fallback ?? current;
    }
    internal static CssColor Mix(CssColor a, CssColor b, double aWeight, double alphaMultiplier = 1)
    {
        double aa = a.Alpha * aWeight, ba = b.Alpha * (1 - aWeight), alpha = aa + ba;
        if (alpha <= 0) return new(0, 0);
        uint Channel(int shift) => (uint)Math.Clamp(Math.Round((((a.Rgb >> shift) & 255) * aa + ((b.Rgb >> shift) & 255) * ba) / alpha), 0, 255);
        return new(Channel(16) << 16 | Channel(8) << 8 | Channel(0), alpha * alphaMultiplier);
    }
    internal static IEnumerable<string> Split(string text, char delimiter)
    {
        int depth = 0, start = 0; char quote = '\0';
        for (int i = 0; i < text.Length; i++)
        {
            char c = text[i];
            if (quote != '\0') { if (c == quote && (i == 0 || text[i - 1] != '\\')) quote = '\0'; continue; }
            if (c is '\'' or '"') { quote = c; continue; }
            if (c == '(') depth++; else if (c == ')') depth--;
            else if (c == delimiter && depth == 0) { if (i > start) yield return text[start..i]; start = i + 1; }
        }
        if (start < text.Length) yield return text[start..];
    }
    public static string SerializeColor(CssColor color) => color.Alpha == 1 ? "rgb(" + (color.Rgb >> 16 & 255) + ", " +
        (color.Rgb >> 8 & 255) + ", " + (color.Rgb & 255) + ")" : "rgba(" + (color.Rgb >> 16 & 255) + ", " +
        (color.Rgb >> 8 & 255) + ", " + (color.Rgb & 255) + ", " + color.Alpha.ToString("0.###", CultureInfo.InvariantCulture) + ")";
}