using System.Globalization; namespace XFESystem.Browser; public readonly record struct CssColor(uint Rgb, double Alpha = 1); public sealed class CssComputedStyle { internal CssComputedStyle(Dictionary properties, CssComputedStyle? parent, BrowserViewport viewport) { values = properties; 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"); 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 values; private readonly Dictionary used = new(StringComparer.OrdinalIgnoreCase); internal BrowserViewport Viewport { get; } public IReadOnlyDictionary Properties => values; public BrowserFont Font { get; } public CssColor Color { get; } public CssColor Background { get; } public double LineHeight { get; } public string 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) => values.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 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) + ")"; }