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) + ")";
}
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) + ")";
}