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

XFESystem

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

公开
关注 0 Fork 0 Star 0
UTF-8
using AngleSharp.Dom;
using AngleSharp.Html.Dom;

namespace XFESystem.Browser;

/// <summary>
/// Original bounded CSS layout subset. Coordinates and text metrics are real
/// calculations over the supplied DOM; no page-, editor- or Monaco-specific sizes.
/// Not a complete CSS formatting/stacking implementation (see Diagnostics).
/// </summary>
public sealed class BrowserLayoutEngine(BrowserCssLimits? limits = null)
{
    private readonly BrowserCssLimits limits = limits ?? new();
    private sealed class Flow(double x, double y, double width, double lineHeight, bool measureOnly = false, double textIndent = 0)
    {
        public double X = x + Math.Clamp(textIndent, -100000, 100000), Y = y, StartX = x, Width = width, LineHeight = lineHeight;
        private readonly double indent = Math.Clamp(textIndent, -100000, 100000);
        private bool firstLine = true;
        public readonly double BaseX = x, BaseWidth = width;
        public readonly List<(BrowserRect Bounds, string Side)> Floats = [];
        public readonly bool MeasureOnly=measureOnly;
        public bool HasContent, PendingSpace;
        public (double Left, double Right) FloatEdges(double top)
        {
            double left = BaseX, right = BaseX + BaseWidth;
            foreach (var item in Floats)
                if (item.Bounds.Top <= top && item.Bounds.Bottom > top)
                { if (item.Side == "left") left = Math.Max(left, item.Bounds.Right); else right = Math.Min(right, item.Bounds.Left); }
            return (left, right);
        }
        public void RefreshEdges()
        {
            var edges = FloatEdges(Y); StartX = edges.Left; Width = Math.Max(0, edges.Right - edges.Left);
            for (int attempt = 0; Width == 0 && !HasContent && attempt < Floats.Count; attempt++)
            {
                double next = Floats.Where(item => item.Bounds.Top <= Y && item.Bounds.Bottom > Y).Select(item => item.Bounds.Bottom).DefaultIfEmpty(Y).Min();
                if (next <= Y) break; Y = next; edges = FloatEdges(Y); StartX = edges.Left; Width = Math.Max(0, edges.Right - edges.Left);
            }
            X = HasContent ? Math.Max(X, StartX) : StartX + (firstLine ? indent : 0);
        }
        public void NewLine(double nextHeight)
        { Y += LineHeight; LineHeight = nextHeight; HasContent = false; PendingSpace = false; firstLine = false; RefreshEdges(); }
    }
    private sealed record PendingAbsolute(BrowserLayoutBox Box, BrowserLayoutBox Parent, BrowserLayoutBox? Anchor);
    private readonly List<BrowserLayoutBox> all = [], painted = [];
    private readonly List<BrowserDrawCommand> commands = [];
    private readonly List<PendingAbsolute> pending = [];
    private readonly List<string> diagnostics = [];
    private readonly Dictionary<INode, int> identities = new(ReferenceEqualityComparer.Instance);
    private readonly HashSet<INode> currentIdentityNodes = new(ReferenceEqualityComparer.Instance);
    private readonly List<INode> detachedIdentityNodes = [];
    private IDocument? identityDocument;
    private int nextId, nodeCount;
    private BrowserViewport viewport;
    private IBrowserTextMetrics metrics = new BrowserFallbackTextMetrics();
    private Func<INode, int>? identity;
    private BrowserCssCascade cascade = null!;
    private BrowserInteractionState interaction = new();
    private BrowserLayoutBox? canvasBackgroundOwner;
    private Func<string, BrowserCanvasSurface?>? imageLookup;
    private bool attributeGeneratedContent;
    private static readonly HashSet<string> intrinsicAttributes = new(StringComparer.OrdinalIgnoreCase)
    {
        "style", "class", "id", "hidden", "width", "height", "size", "rows", "cols", "value", "checked", "selected", "disabled", "readonly", "type",
        "src", "srcset", "href", "dir", "lang", "contenteditable", "placeholder", "start", "reversed", "span", "colspan", "rowspan", "align", "valign",
        "bgcolor", "color", "border", "cellpadding", "cellspacing", "nowrap", "open", "d", "viewbox", "points", "transform", "fill", "fill-rule",
        "fill-opacity", "stroke", "stroke-width", "stroke-opacity", "opacity", "x", "y", "x1", "x2", "y1", "y2", "rx", "ry", "r", "cx", "cy"
    };
    internal bool AttributeAffectsLayout(string name) => intrinsicAttributes.Contains(name) || attributeGeneratedContent || cascade is null || cascade.DependsOnAttribute(name);
    public long CssParseGeneration => cascade?.ParseGeneration ?? 0;
    public long CssReuseCount => cascade?.ReuseCount ?? 0;
    public int CssSelectorEvaluations => cascade?.SelectorEvaluations ?? 0;
    public long LastStylesheetParseTicks { get; private set; }
    internal bool InteractionChangesStyles(BrowserInteractionState before, BrowserInteractionState after) =>
        cascade is null || cascade.InteractionChangesStyles(before, after, all.Where(box => box.Node is IElement).Select(box => (IElement)box.Node).Distinct());
    /// <summary>Identity references retained for the most recent bounded layout; snapshots own their separate boxes.</summary>
    public int RetainedNodeIdentityCount => identities.Count;
    internal void ReleaseDocument()
    {
        all.Clear();painted.Clear();commands.Clear();pending.Clear();diagnostics.Clear();identities.Clear();
        currentIdentityNodes.Clear();detachedIdentityNodes.Clear();identityDocument=null;identity=null;canvasBackgroundOwner=null;
        metrics=new BrowserFallbackTextMetrics();interaction=new();cascade?.ResetDocument();
    }

    public void SetInteractionState(INode? hovered, INode? active, INode? focus) => interaction = new(hovered, active, focus);

    public BrowserDisplayList Layout(IDocument document, BrowserViewport viewport,
        IReadOnlyList<BrowserStyleSheet>? linkedSheets = null, IBrowserTextMetrics? textMetrics = null,
        Func<INode, int>? nodeIdentity = null, bool blockAuthorStyles = false, BrowserInteractionState? interaction = null,
        Func<string, BrowserCanvasSurface?>? imageLookup = null)
    {
        ArgumentNullException.ThrowIfNull(document);
        if (!double.IsFinite(viewport.Width) || !double.IsFinite(viewport.Height) || viewport.Width <= 0 || viewport.Height <= 0 ||
            viewport.Width > 100000 || viewport.Height > 100000 || !double.IsFinite(viewport.ScrollX) || !double.IsFinite(viewport.ScrollY))
            throw new ArgumentOutOfRangeException(nameof(viewport));
        if (!ReferenceEquals(document, identityDocument))
        {
            if (identityDocument is not null) { cascade?.ResetDocument(); this.interaction = new(); }
            identities.Clear(); nextId = 0; identityDocument = document;
        }
        all.Clear(); painted.Clear(); commands.Clear(); pending.Clear(); diagnostics.Clear(); currentIdentityNodes.Clear(); nodeCount = 0;
        this.viewport = viewport; metrics = textMetrics ?? new BrowserFallbackTextMetrics(); identity = nodeIdentity;
        this.imageLookup = imageLookup;
        if (textMetrics is null) diagnostics.Add("Host fallback text metrics are proportional estimates; native rendering must supply its real metrics.");
        if (interaction is not null) this.interaction = interaction;
        cascade ??= new(limits, viewport, diagnostics, blockAuthorStyles, this.interaction);
        long stylesheetStarted = System.Diagnostics.Stopwatch.GetTimestamp();
        cascade.BeginLayout(viewport, diagnostics, blockAuthorStyles, this.interaction); cascade.Load(document, linkedSheets);
        LastStylesheetParseTicks = System.Diagnostics.Stopwatch.GetTimestamp() - stylesheetStarted;
        diagnostics.Add("CSS subset: no grid, complex table spans, nested float exclusions, parent-child margin collapse, 3D/perspective transforms, arbitrary webfonts, generated counters/quotes/URLs, bidi/shaping, full flex wrapping or full stacking contexts.");
        var element = document.DocumentElement ?? document.Body;
        if (element is null) { identities.Clear(); return new(viewport, [], [], diagnostics.ToArray(), viewport.Width, viewport.Height); }
        BrowserLayoutBox root;
        try { root = Build(element, null, 0); }
        // Keep explicit cleanup for the optional Legacy runtime as well as
        // the independent Microsoft runtime's ordinary exception unwinding.
        catch (Exception) { PruneNodeIdentities(); throw; }
        attributeGeneratedContent = all.Any(box => box.Style.Get("content", "").Contains("attr(", StringComparison.OrdinalIgnoreCase));
        PruneNodeIdentities();
        LayoutBlock(root, 0, 0, viewport.Width, viewport.Height, null, null);
        for (int index = 0; index < pending.Count; index++) LayoutAbsolute(pending[index]);
        ApplyTranslations(root);
        PaintCanvasBackground(root);
        Paint(root, null, 1);
        double width = viewport.Width, height = Math.Max(viewport.Height, root.MarginBox.Bottom);
        foreach (var box in painted)
        {
            if (box.Clip is null) { var bounds = box.Transform.Bounds(box.BorderBox); width = Math.Max(width, bounds.Right); height = Math.Max(height, bounds.Bottom); }
        }
        foreach (var box in all) if (box.Node is IElement) box.Style.Resolve(box);
        return new(viewport, commands.ToArray(), painted.ToArray(), diagnostics.ToArray(), width, height, all.ToArray(), metrics);
    }

    private BrowserLayoutBox Build(INode node, BrowserLayoutBox? parent, int depth)
    {
        if (++nodeCount > limits.MaximumNodes || depth > limits.MaximumDepth) throw new BrowserLimitException("CSS layout DOM bound reached.");
        var style = node is IElement element ? cascade.Compute(element, parent?.Style) : parent!.Style;
        if (!identities.TryGetValue(node, out int id)) { id = identity?.Invoke(node) ?? ++nextId; identities.Add(node, id); }
        currentIdentityNodes.Add(node);
        var box = new BrowserLayoutBox { Node = node, NodeId = id, Style = style, Parent = parent };
        if (node is IElement item && style.Display == "list-item") box.MarkerStyle = cascade.ComputeMarker(item, style);
        all.Add(box);
        if(node is IElement origin&&node is not(IHtmlTextAreaElement or IHtmlCanvasElement))AddGenerated(box,origin,"before");
        if (node is not (IHtmlTextAreaElement or IHtmlCanvasElement))
            foreach (var child in node.ChildNodes)
                if (child.NodeType == NodeType.Text || child is IElement) box.children.Add(Build(child, box, depth + 1));
        if(node is IElement after&&node is not(IHtmlTextAreaElement or IHtmlCanvasElement))AddGenerated(box,after,"after");
        return box;
    }
    private void AddGenerated(BrowserLayoutBox parent,IElement origin,string pseudo)
    {
        var style=cascade.ComputePseudo(origin,pseudo,parent.Style);
        if(style is null||style.Display=="none")return;
        string? text=GeneratedContent(style.Get("content","normal"),origin);
        if(text is null)return;
        if(++nodeCount>limits.MaximumNodes)throw new BrowserLimitException("CSS generated box limit reached.");
        var box=new BrowserLayoutBox{Node=origin,NodeId=parent.NodeId,Style=style,Parent=parent,GeneratedPseudo=pseudo,GeneratedText=text};
        all.Add(box);parent.children.Add(box);
    }
    private string? GeneratedContent(string content,IElement origin)
    {
        var result=new System.Text.StringBuilder();
        for(int offset=0;offset<content.Length;)
        {
            while(offset<content.Length&&char.IsWhiteSpace(content[offset]))offset++;
            if(offset>=content.Length)break;
            char quote=content[offset];
            if(quote is '\'' or '"')
            {
                offset++;
                while(offset<content.Length&&content[offset]!=quote)
                {
                    char ch=content[offset++];
                    if(ch=='\\'&&offset<content.Length)
                    {
                        if(content[offset] is '\r' or '\n' or '\f'){if(content[offset++]=='\r'&&offset<content.Length&&content[offset]=='\n')offset++;continue;}
                        int begin=offset;while(offset<content.Length&&offset-begin<6&&Uri.IsHexDigit(content[offset]))offset++;
                        if(offset>begin)
                        {int scalar=int.Parse(content[begin..offset],System.Globalization.NumberStyles.HexNumber,System.Globalization.CultureInfo.InvariantCulture);result.Append(scalar is >0 and <=0x10FFFF&&scalar is not(>=0xD800 and <=0xDFFF)?char.ConvertFromUtf32(scalar):"\uFFFD");if(offset<content.Length&&char.IsWhiteSpace(content[offset]))offset++;continue;}
                        ch=content[offset++];
                    }
                    result.Append(ch);if(result.Length>4096)throw new BrowserLimitException("CSS generated text limit reached.");
                }
                if(offset>=content.Length)return null;offset++;
            }
            else if(content.AsSpan(offset).StartsWith("attr(",StringComparison.Ordinal))
            {int end=content.IndexOf(')',offset+5);if(end<0)return null;string name=content[(offset+5)..end].Trim();result.Append(origin.GetAttribute(name)??"");offset=end+1;}
            else{Note("Unsupported CSS generated content expression.");return null;}
            if(result.Length>4096)throw new BrowserLimitException("CSS generated text limit reached.");
        }
        return result.ToString();
    }

    private void PruneNodeIdentities()
    {
        // An engine survives mutations and navigation. Keep only the nodes
        // built this time, even when a DOM or selector budget aborts Build.
        // Older display lists retain their own immutable geometry snapshots.
        detachedIdentityNodes.Clear();
        foreach (var node in identities.Keys)
            if (!currentIdentityNodes.Contains(node)) detachedIdentityNodes.Add(node);
        foreach (var node in detachedIdentityNodes) identities.Remove(node);
        detachedIdentityNodes.Clear(); currentIdentityNodes.Clear();
    }

    private void LayoutBlock(BrowserLayoutBox box, double x, double y, double availableWidth, double availableHeight,
        double? forcedWidth, double? forcedHeight)
    {
        if (box.Style.Display == "none") return;
        var style = box.Style;
        box.Margin = style.Edges("margin", availableWidth);
        box.Padding = style.Edges("padding", availableWidth);
        box.Border = style.Edges("border", availableWidth, true);
        double extras = box.Padding.Horizontal + box.Border.Horizontal;
        double? declared = style.Length("width", availableWidth);
        if (box.Element?.LocalName == "svg" && declared is null)
            declared = SvgDimensions(box.Element).Width;
        var image = box.Element?.LocalName == "img" ? imageLookup?.Invoke(box.Element.GetAttribute("src") ?? "") : null;
        if (box.Element?.LocalName == "img" && declared is null)
            declared = double.TryParse(box.Element.GetAttribute("width"), out double attrWidth) ? attrWidth : image?.Width ?? 0;
        double width = forcedWidth ?? (declared is { } w ? w - (style.BoxSizing == "border-box" ? extras : 0) : availableWidth - extras - box.Margin.Horizontal);
        width = Constrain(style, width, "width", availableWidth, extras);
        double spare = availableWidth - width - extras - box.Margin.Horizontal;
        if (spare > 0 && declared is not null && style.Display is not ("inline-block" or "inline-flex"))
        {
            bool leftAuto = style.Get("margin-left", "0") == "auto", rightAuto = style.Get("margin-right", "0") == "auto";
            if (leftAuto || rightAuto) box.Margin = box.Margin with { Left = leftAuto ? spare / (rightAuto ? 2 : 1) : box.Margin.Left,
                Right = rightAuto ? spare / (leftAuto ? 2 : 1) : box.Margin.Right };
        }
        double verticalExtras = box.Padding.Vertical + box.Border.Vertical;
        double? declaredHeight = style.Length("height", availableHeight);
        if (box.Element?.LocalName == "svg" && availableHeight == 0 && style.Get("height", "auto").Contains('%') && box.Parent?.Style.Length("height", 0) is null)
            declaredHeight = null;
        if (box.Element?.LocalName == "svg" && declaredHeight is null)
            declaredHeight = SvgDimensions(box.Element).Height;
        if (box.Element?.LocalName == "img" && declaredHeight is null)
            declaredHeight = double.TryParse(box.Element.GetAttribute("height"), out double attrHeight) ? attrHeight : image is { Width: > 0 } ? width * image.Height / image.Width : 0;
        double? height = forcedHeight ?? (declaredHeight is { } h ? Math.Max(0, h - (style.BoxSizing == "border-box" ? verticalExtras : 0)) : null);
        // Descendants resolve percentage heights against the constrained used
        // height, rather than an unconstrained 100% subsequently capped below.
        if (height is { } definite) height = Constrain(style, definite, "height", availableHeight, verticalExtras);
        double contentX = x + box.Margin.Left + box.Border.Left + box.Padding.Left;
        double contentY = y + box.Margin.Top + box.Border.Top + box.Padding.Top;
        box.ContentBox = new(contentX, contentY, width, height ?? 0);
        double measured = box.Element?.LocalName == "svg" ? 0 : box.GeneratedText is not null ? LayoutGenerated(box) : ControlValue(box.Node) is { } controlValue ? LayoutControl(box, controlValue) :
            box.Element?.LocalName == "table" || style.Display == "table" ? LayoutTable(box) :
            style.Display is "flex" or "inline-flex" ? LayoutFlex(box, height) : LayoutFlow(box, height);
        double resolvedHeight = Constrain(style, height ?? measured, "height", availableHeight, verticalExtras);
        UpdateRectangles(box, new(contentX, contentY, box.ContentBox.Width, resolvedHeight));
        if (style.Position == "relative") Translate(box,
            style.Length("left", availableWidth) ?? -(style.Length("right", availableWidth) ?? 0),
            style.Length("top", availableHeight) ?? -(style.Length("bottom", availableHeight) ?? 0));
    }

    private static double Constrain(CssComputedStyle style, double value, string axis, double basis, double extras)
    {
        double Adjustment(double length) => Math.Max(0, length - (style.BoxSizing == "border-box" ? extras : 0));
        double minimum = Adjustment(style.Length("min-" + axis, basis) ?? 0);
        double maximum = style.Length("max-" + axis, basis) is { } maximumValue ? Adjustment(maximumValue) : 1000000;
        return Math.Clamp(Math.Max(minimum, Math.Min(value, maximum)), 0, 1000000);
    }
    private static void UpdateRectangles(BrowserLayoutBox box, BrowserRect content)
    {
        box.ContentBox = content;
        box.PaddingBox = Inflate(content, box.Padding);
        box.BorderBox = Inflate(box.PaddingBox, box.Border);
        box.MarginBox = Inflate(box.BorderBox, box.Margin);
        if (box.Node is IElement) { box.fragments.Clear(); box.fragments.Add(box.BorderBox); }
    }
    private static BrowserRect Inflate(BrowserRect rectangle, BrowserEdges edges) => new(rectangle.X - edges.Left,
        rectangle.Y - edges.Top, Math.Max(0, rectangle.Width + edges.Horizontal), Math.Max(0, rectangle.Height + edges.Vertical));

    private double LayoutFlow(BrowserLayoutBox parent, double? fixedHeight)
    {
        var content = parent.ContentBox;
        var flow = new Flow(content.X, content.Y, content.Width, parent.Style.LineHeight, textIndent: parent.Style.Length("text-indent", content.Width) ?? 0);
        double? previousBottomMargin = null;
        foreach (var child in parent.children)
        {
            if (child.Style.Display == "none") continue;
            if (QueueAbsolute(child, parent)) continue;
            if (TryFloat(child, flow, fixedHeight ?? 0)) { previousBottomMargin = null; continue; }
            string clear = child.Node is IElement ? child.Style.Get("clear", "none") : "none";
            if (clear is "left" or "right" or "both")
            {
                double bottom = flow.Floats.Where(item => clear == "both" || item.Side == clear).Select(item => item.Bounds.Bottom).DefaultIfEmpty(flow.Y).Max();
                if (bottom > flow.Y) { if (flow.HasContent) flow.NewLine(parent.Style.LineHeight); flow.Y = Math.Max(flow.Y, bottom); flow.RefreshEdges(); }
            }
            if (child.Node.NodeType == NodeType.Text) { FlowText(child, flow); if(flow.HasContent)previousBottomMargin=null; continue; }
            if (child.Element?.LocalName == "br") { flow.NewLine(parent.Style.LineHeight); continue; }
            if (child.Style.Display == "inline") { FlowInline(child, flow); if(flow.HasContent)previousBottomMargin=null; continue; }
            if (child.Style.Display is "inline-block" or "inline-flex")
            {
                double outer = IntrinsicWidth(child, content.Width);
                if (flow.HasContent && flow.X + outer > flow.StartX + flow.Width + .000001) flow.NewLine(parent.Style.LineHeight);
                LayoutAtomicInline(child, flow.X, flow.Y, content.Width, fixedHeight ?? 0, outer);
                ApplyInlineVerticalAlign(child,flow.LineHeight);
                flow.X += child.MarginBox.Width; flow.LineHeight = Math.Max(flow.LineHeight, child.MarginBox.Height); flow.HasContent = true;
                previousBottomMargin=null;
                continue;
            }
            if (flow.HasContent) flow.NewLine(parent.Style.LineHeight);
            if(previousBottomMargin is { } previous)
            {
                double next=child.Style.Edges("margin",content.Width).Top;
                double collapsed=Math.Max(Math.Max(previous,0),Math.Max(next,0))+Math.Min(Math.Min(previous,0),Math.Min(next,0));
                flow.Y-=previous+next-collapsed;
            }
            LayoutBlock(child, content.X, flow.Y, content.Width, fixedHeight ?? 0, null, null);
            flow.Y += child.MarginBox.Height; flow.HasContent = false; flow.RefreshEdges(); flow.LineHeight = parent.Style.LineHeight;
            previousBottomMargin=child.Margin.Bottom;
        }
        AlignInlineLines(parent);
        double measured = Math.Max(0, flow.Y - content.Y + (flow.HasContent ? flow.LineHeight : 0));
        if (parent.Parent is null || parent.Style.Display is "inline-block" or "inline-flex" ||
            parent.Style.Get("float", "none") is "left" or "right" || parent.Style.Position is "absolute" or "fixed" ||
            parent.Style.OverflowX != "visible" || parent.Style.OverflowY != "visible")
            measured = Math.Max(measured, flow.Floats.Select(item => item.Bounds.Bottom - content.Y).DefaultIfEmpty(0).Max());
        return measured;
    }

    private bool TryFloat(BrowserLayoutBox box, Flow flow, double availableHeight)
    {
        if (box.Node is not IElement || box.Style.Get("float", "none") is not ("left" or "right") || box.Style.Position is "absolute" or "fixed") return false;
        string side = box.Style.Get("float", "none");
        if (side == "left" && flow.HasContent) flow.NewLine(box.Style.LineHeight);
        double outer = IntrinsicWidth(box, flow.BaseWidth, true);
        if (box.Style.Length("width", flow.BaseWidth) is null) outer = Math.Min(flow.BaseWidth, outer);
        double y = Math.Max(flow.Y, flow.Floats.Select(item => item.Bounds.Top).DefaultIfEmpty(flow.Y).Max());
        string clear = box.Style.Get("clear", "none");
        if (clear is "left" or "right" or "both")
            y = Math.Max(y, flow.Floats.Where(item => clear == "both" || item.Side == clear).Select(item => item.Bounds.Bottom).DefaultIfEmpty(y).Max());
        var edges = flow.FloatEdges(y);
        if (flow.HasContent && outer > edges.Right - Math.Max(flow.X, edges.Left)) y += flow.LineHeight;
        for (int attempt = 0; attempt <= flow.Floats.Count; attempt++)
        {
            edges = flow.FloatEdges(y);
            if (outer <= edges.Right - edges.Left) break;
            double next = flow.Floats.Where(item => item.Bounds.Top <= y && item.Bounds.Bottom > y).Select(item => item.Bounds.Bottom).DefaultIfEmpty(y).Min();
            if (next <= y) break; y = next;
        }
        double x = side == "right" ? edges.Right - outer : edges.Left;
        box.ShrinkToFitWidth = box.Style.Length("width", flow.BaseWidth) is null;
        LayoutBlock(box, x, y, outer, availableHeight, null, null);
        if (side == "right") { double correction = outer - box.MarginBox.Width; Translate(box, correction, 0); x += correction; }
        // Relative offsets affect painting, not the exclusion area.
        flow.Floats.Add((new(x, y, box.MarginBox.Width, box.MarginBox.Height), side)); flow.RefreshEdges();
        return true;
    }

    private void AlignInlineLines(BrowserLayoutBox parent)
    {
        string alignment = parent.Style.Get("text-align", "left");
        if (alignment is not ("center" or "right" or "end"))
        { if (alignment == "justify") Note("Justified text is not implemented; left alignment is used."); return; }
        var leaves = new List<BrowserLayoutBox>();
        void Collect(BrowserLayoutBox box)
        {
            if (box.Style.Display == "none" || box.Style.Position is "absolute" or "fixed") return;
            if (box.Node.NodeType == NodeType.Text || box.Style.Display is "inline-block" or "inline-flex") leaves.Add(box);
            else if (box.Style.Display == "inline") foreach (var child in box.children) Collect(child);
        }
        foreach (var child in parent.children) Collect(child);
        var ends = new Dictionary<double, double>();
        foreach (var leaf in leaves)
            foreach (var rect in leaf.Node.NodeType == NodeType.Text ? (IEnumerable<BrowserRect>)leaf.fragments : [leaf.MarginBox])
                ends[rect.Y] = Math.Max(ends.GetValueOrDefault(rect.Y), rect.Right);
        double Shift(double y) => ends.TryGetValue(y, out double right) ? Math.Max(0, parent.ContentBox.Right - right) / (alignment == "center" ? 2 : 1) : 0;
        foreach (var leaf in leaves)
        {
            if (leaf.Node.NodeType != NodeType.Text) { Translate(leaf, Shift(leaf.MarginBox.Y), 0); continue; }
            for (int index = 0; index < leaf.fragments.Count; index++) leaf.fragments[index] = leaf.fragments[index].Translate(Shift(leaf.fragments[index].Y), 0);
            for (int index = 0; index < leaf.textCommands.Count; index++)
            { var command = leaf.textCommands[index]; leaf.textCommands[index] = command with { Bounds = command.Bounds.Translate(Shift(command.Bounds.Y), 0) }; }
            BrowserRect union = default; foreach (var rect in leaf.fragments) union = union.Union(rect);
            leaf.ContentBox = leaf.PaddingBox = leaf.BorderBox = leaf.MarginBox = union;
        }
        void Refresh(BrowserLayoutBox box)
        {
            if (box.Node.NodeType == NodeType.Text || box.Style.Display != "inline") return;
            foreach (var child in box.children) Refresh(child);
            box.fragments.Clear();
            foreach (var group in box.children.SelectMany(child => child.fragments).GroupBy(rect => rect.Y))
            { BrowserRect line = default; foreach (var rect in group) line = line.Union(rect); box.fragments.Add(line); }
            BrowserRect union = default; foreach (var rect in box.fragments) union = union.Union(rect);
            box.ContentBox = union; box.PaddingBox = Inflate(union, box.Padding); box.BorderBox = Inflate(box.PaddingBox, box.Border); box.MarginBox = Inflate(box.BorderBox, box.Margin);
        }
        foreach (var child in parent.children) Refresh(child);
    }

    private void FlowInline(BrowserLayoutBox box, Flow flow)
    {
        if(box.GeneratedText is not null)
        {
            var margin=box.Style.Edges("margin",flow.Width);var padding=box.Style.Edges("padding",flow.Width);var border=box.Style.Edges("border",flow.Width,true);
            flow.X+=margin.Left+padding.Left+border.Left;FlowText(box,flow);
            if(!flow.MeasureOnly)
            {box.Margin=margin;box.Padding=padding;box.Border=border;box.PaddingBox=Inflate(box.ContentBox,padding);box.BorderBox=Inflate(box.PaddingBox,border);box.MarginBox=Inflate(box.BorderBox,margin);ApplyInlineVerticalAlign(box,flow.LineHeight);}
            flow.X+=padding.Right+border.Right+margin.Right;return;
        }
        if(flow.MeasureOnly)
        {
            var margin=box.Style.Edges("margin",flow.Width);var padding=box.Style.Edges("padding",flow.Width);var border=box.Style.Edges("border",flow.Width,true);
            flow.X+=margin.Left+padding.Left+border.Left;
            foreach(var child in box.children)
            {
                if(child.Style.Display=="none"||child.Style.Position is "absolute" or "fixed")continue;
                if(child.Node.NodeType==NodeType.Text)FlowText(child,flow);
                else if(child.Element?.LocalName=="br")flow.NewLine(box.Style.LineHeight);
                else if(child.Style.Display=="inline")FlowInline(child,flow);
                else if (child.Style.Display is "inline-block" or "inline-flex") MeasureAtomicInline(child,flow);
                else
                {
                    if (flow.HasContent) flow.NewLine(box.Style.LineHeight);
                    flow.Y += IntrinsicHeight(child, flow.Width); flow.X = flow.StartX; flow.HasContent = false; flow.LineHeight = box.Style.LineHeight;
                }
            }
            flow.X+=padding.Right+border.Right+margin.Right;return;
        }
        box.Margin = box.Style.Edges("margin", flow.Width); box.Padding = box.Style.Edges("padding", flow.Width);
        box.Border = box.Style.Edges("border", flow.Width, true);
        flow.X += box.Margin.Left + box.Padding.Left + box.Border.Left;
        foreach (var child in box.children)
        {
            if (child.Style.Display == "none" || QueueAbsolute(child, box)) continue;
            if (TryFloat(child, flow, 0)) continue;
            if (child.Node.NodeType == NodeType.Text) FlowText(child, flow);
            else if (child.Element?.LocalName == "br") flow.NewLine(box.Style.LineHeight);
            else if (child.Style.Display == "inline") FlowInline(child, flow);
            else
            {
                if (child.Style.Display is not ("inline-block" or "inline-flex"))
                {
                    if (flow.HasContent) flow.NewLine(box.Style.LineHeight);
                    LayoutBlock(child, flow.StartX, flow.Y, flow.Width, 0, null, null);
                    flow.Y += child.MarginBox.Height; flow.X = flow.StartX; flow.LineHeight = box.Style.LineHeight; flow.HasContent = false;
                    continue;
                }
                double width = IntrinsicWidth(child, flow.Width);
                if (flow.HasContent && flow.X + width > flow.StartX + flow.Width + .000001) flow.NewLine(box.Style.LineHeight);
                LayoutAtomicInline(child, flow.X, flow.Y, flow.Width, 0, width);
                ApplyInlineVerticalAlign(child,flow.LineHeight);
                flow.X += child.MarginBox.Width; flow.LineHeight = Math.Max(flow.LineHeight, child.MarginBox.Height); flow.HasContent = true;
            }
        }
        foreach (var group in box.children.SelectMany(child => child.fragments).GroupBy(rect => rect.Y))
        {
            BrowserRect line = default; foreach (var rect in group) line = line.Union(rect);
            box.fragments.Add(line);
        }
        BrowserRect content = default; foreach (var fragment in box.fragments) content = content.Union(fragment);
        if (box.fragments.Count == 0) content = new(flow.X, flow.Y, 0, box.Style.LineHeight);
        box.ContentBox = content; box.PaddingBox = Inflate(content, box.Padding); box.BorderBox = Inflate(box.PaddingBox, box.Border);
        box.MarginBox = Inflate(box.BorderBox, box.Margin);
        flow.X += box.Padding.Right + box.Border.Right + box.Margin.Right;
        if (box.Style.Position == "relative") Translate(box, box.Style.Length("left", flow.Width) ?? 0, box.Style.Length("top", 0) ?? 0);
    }

    private void FlowText(BrowserLayoutBox box, Flow flow)
    {
        string text = box.GeneratedText??box.Node.TextContent;
        double initialX = flow.X, initialY = flow.Y;
        bool preserve = box.Style.WhiteSpace is "pre" or "pre-wrap" or "break-spaces";
        bool noWrap = box.Style.WhiteSpace is "pre" or "nowrap";
        if (preserve)
        {
            var rendered = new System.Text.StringBuilder(); var offsets = new List<int> { 0 };
            void Flush()
            {
                if (rendered.Length > 0) AddText(box, flow, rendered.ToString(), !noWrap, sourceOffsets: offsets.ToArray());
                rendered.Clear(); offsets.Clear();
            }
            for (int index = 0; index < text.Length; index++)
            {
                if (text[index] == '\r') { if (offsets.Count > 0) offsets[^1] = index + 1; continue; }
                if (text[index] == '\n') { Flush(); flow.NewLine(box.Style.LineHeight); offsets.Add(index + 1); continue; }
                int count = text[index] == '\t' ? 4 : 1;
                for (int part = 0; part < count; part++) { rendered.Append(text[index] == '\t' ? ' ' : text[index]); offsets.Add(part == count - 1 ? index + 1 : index); }
            }
            Flush();
        }
        else
        {
            int start = 0, spaceStart = -1, spaceEnd = -1;
            for (int index = 0; index <= text.Length; index++)
            {
                if (index < text.Length && !Collapsible(text[index])) continue;
                if (index > start)
                {
                    string word = text[start..index];
                    if (flow.PendingSpace && flow.HasContent) AddText(box, flow, " ", !noWrap,
                        sourceOffsets: [Math.Max(0, spaceStart), Math.Max(0, spaceEnd)]);
                    flow.PendingSpace = false; spaceStart = spaceEnd = -1;
                    AddText(box, flow, word, !noWrap, start, index - start);
                }
                if (index < text.Length) { flow.PendingSpace = true; if (spaceStart < 0) spaceStart = index; spaceEnd = index + 1; }
                start = index + 1;
            }
        }
        if(flow.MeasureOnly)return;
        BrowserRect union = default; foreach (var rect in box.fragments) union = union.Union(rect);
        if (box.fragments.Count == 0) union = new(initialX, initialY, 0, box.Style.LineHeight);
        box.ContentBox = box.PaddingBox = box.BorderBox = box.MarginBox = union;
    }
    private static bool Collapsible(char value) => value is ' ' or '\r' or '\n' or '\t' or '\f';
    private static string? ControlValue(INode node) => node switch
    {
        IHtmlTextAreaElement area => area.Value,
        IHtmlInputElement input when input.Type is "checkbox" or "radio" => input.IsChecked ? "x" : "",
        IHtmlInputElement input when input.Type == "password" => new string('*', input.Value.Length),
        IHtmlInputElement input => input.Value.Length == 0 ? input.GetAttribute("placeholder") ?? "" : input.Value,
        _ => null
    };
    private double LayoutControl(BrowserLayoutBox box, string value)
    {
        var flow = new Flow(box.ContentBox.X, box.ContentBox.Y, box.ContentBox.Width, box.Style.LineHeight, textIndent: box.Style.Length("text-indent", box.ContentBox.Width) ?? 0);
        var lines = value.Replace("\r", "").Replace("\t", "    ").Split('\n');
        for (int line = 0; line < lines.Length; line++)
        {
            if (line > 0) flow.NewLine(box.Style.LineHeight);
            if (lines[line].Length > 0) AddText(box, flow, lines[line], box.Node is IHtmlTextAreaElement && box.Style.WhiteSpace != "pre");
        }
        return Math.Max(box.Style.LineHeight, flow.Y - box.ContentBox.Y + flow.LineHeight);
    }
    private double LayoutGenerated(BrowserLayoutBox box)
    {var content=box.ContentBox;var flow=new Flow(content.X,content.Y,content.Width,box.Style.LineHeight);FlowText(box,flow);box.ContentBox=content;return Math.Max(0,flow.Y-content.Y+(flow.HasContent?flow.LineHeight:0));}
    private static void ApplyInlineVerticalAlign(BrowserLayoutBox box,double lineHeight)
    {if(box.Style.Length("vertical-align",lineHeight) is { } shift&&shift!=0)Translate(box,0,-shift);}
    private void AddText(BrowserLayoutBox box, Flow flow, string text, bool wrap, int sourceStart = -1, int sourceLength = -1, int[]? sourceOffsets = null)
    {
        double width = metrics.Measure(text, box.Style.Font);
        if (!double.IsFinite(width) || width < 0 || width > 1000000) throw new BrowserLimitException("Text metrics returned an invalid or excessive width.");
        bool breakWord=box.Style.Get("overflow-wrap","normal") is "anywhere" or "break-word" || box.Style.Get("word-break","normal") == "break-all" || text.Any(character=>character is >= '\u2E80' and <= '\uFFFF');
        if(wrap&&breakWord&&text.Length>1&&width>flow.Width&&flow.Width>0)
        {
            int start=0;
            while(start<text.Length)
            {
                int end=start,fit=start;
                double room=flow.StartX+flow.Width-flow.X;
                while(end<text.Length)
                {
                    int next=end+1; if(char.IsHighSurrogate(text[end])&&next<text.Length&&char.IsLowSurrogate(text[next]))next++;
                    double measured=metrics.Measure(text[start..next],box.Style.Font);
                    if(measured>room)break;
                    fit=next;end=next;
                }
                if(fit==start&&flow.HasContent){flow.NewLine(box.Style.LineHeight);continue;}
                if(fit==start)fit=start+1+(char.IsHighSurrogate(text[start])&&start+1<text.Length&&char.IsLowSurrogate(text[start+1])?1:0);
                AddText(box,flow,text[start..fit],false,sourceStart<0?-1:sourceStart+start,fit-start,
                    sourceOffsets is null?null:sourceOffsets[start..(fit+1)]);
                start=fit;
                if(start<text.Length)flow.NewLine(box.Style.LineHeight);
            }
            return;
        }
        if (wrap && flow.HasContent && flow.X + width > flow.StartX + flow.Width + .000001) flow.NewLine(box.Style.LineHeight);
        if (text == " " && !flow.HasContent) return;
        var rect = new BrowserRect(flow.X, flow.Y, width, box.Style.LineHeight);
        if(!flow.MeasureOnly)
        {
            box.fragments.Add(rect); box.textCommands.Add(new(BrowserDrawKind.Text, rect, box.NodeId, box.Node,
                box.Style.Color.Rgb, box.Style.Color.Alpha, text, box.Style.Font));
            if (box.GeneratedPseudo is null&&sourceOffsets is not null) box.sourceRuns.Add(new(box.textCommands.Count - 1, sourceOffsets));
            else if (box.GeneratedPseudo is null&&sourceStart >= 0) box.sourceRuns.Add(new(box.textCommands.Count - 1, Enumerable.Range(sourceStart, (sourceLength < 0 ? text.Length : sourceLength) + 1).ToArray()));
        }
        if (box.textCommands.Count > limits.MaximumDisplayCommands) throw new BrowserLimitException("Text fragment limit reached.");
        flow.X += width; flow.LineHeight = Math.Max(flow.LineHeight, box.Style.LineHeight); flow.HasContent = true;
    }

    private double LayoutFlex(BrowserLayoutBox parent, double? fixedHeight)
    {
        bool column = parent.Style.Get("flex-direction", "row").StartsWith("column", StringComparison.Ordinal);
        var items = parent.children.Where(child => child.Style.Display != "none" &&
            !(child.Node.NodeType == NodeType.Text && string.IsNullOrWhiteSpace(child.Node.TextContent)) && !QueueAbsolute(child, parent)).ToList();
        if (parent.Style.Get("flex-direction", "row").EndsWith("reverse", StringComparison.Ordinal)) items.Reverse();
        items = items.OrderBy(child => CssValue.Number(child.Style.Get("order", "0"), 0)).ToList();
        if (column && parent.Style.Get("flex-wrap", "nowrap") != "nowrap") Note("Column flex wrapping is not implemented; one flex column is used.");
        double available = column ? fixedHeight ?? 0 : parent.ContentBox.Width;
        double gap = parent.Style.Length(column ? "row-gap" : "column-gap", available) ?? parent.Style.Length("gap", available) ?? 0;
        var bases = new double[items.Count]; double total = gap * Math.Max(0, items.Count - 1), grow = 0, shrink = 0;
        for (int i = 0; i < items.Count; i++)
        {
            var child = items[i];
            var margin = child.Style.Edges("margin", parent.ContentBox.Width); var padding = child.Style.Edges("padding", parent.ContentBox.Width);
            var border = child.Style.Edges("border", parent.ContentBox.Width, true);
            double extra = column ? padding.Vertical + border.Vertical : padding.Horizontal + border.Horizontal;
            bases[i] = child.Node.NodeType != NodeType.Text && child.Style.Length("flex-basis", available) is { } basis
                ? Math.Max(0, basis + (column ? margin.Vertical : margin.Horizontal) + (child.Style.BoxSizing == "border-box" ? 0 : extra))
                : column ? IntrinsicHeight(child,parent.ContentBox.Width) : IntrinsicWidth(child, parent.ContentBox.Width, parent.ShrinkToFitWidth);
            total += bases[i]; grow += CssValue.Number(child.Style.Get("flex-grow", "0"), 0);
            shrink += CssValue.Number(child.Style.Get("flex-shrink", "1"), 1) * bases[i];
        }
        if (column && fixedHeight is null) available = total;
        if (!column && available > 0 && items.Count > 1 && total > available + .000001 && parent.Style.Get("flex-wrap", "nowrap") != "nowrap")
        {
            var lines = new List<List<BrowserLayoutBox>>(); List<BrowserLayoutBox> line = []; double usedLine = 0;
            for (int index = 0; index < items.Count; index++)
            {
                if (line.Count != 0 && usedLine + gap + bases[index] > available + .000001) { lines.Add(line); line = []; usedLine = 0; }
                usedLine += (line.Count == 0 ? 0 : gap) + bases[index]; line.Add(items[index]);
            }
            if (line.Count != 0) lines.Add(line);
            if (lines.Count > 1)
            {
                if (parent.Style.Get("flex-wrap", "nowrap") == "wrap-reverse") lines.Reverse();
                var originalChildren = parent.children.ToArray(); var originalContent = parent.ContentBox;
                double crossUsed = 0, rowGap = parent.Style.Length("row-gap", parent.ContentBox.Width) ?? parent.Style.Length("gap", parent.ContentBox.Width) ?? 0;
                try
                {
                    foreach (var row in lines)
                    {
                        parent.children.Clear(); parent.children.AddRange(row);
                        parent.ContentBox = originalContent with { Y = originalContent.Y + crossUsed };
                        crossUsed += LayoutFlex(parent, null) + rowGap;
                    }
                }
                catch { parent.children.Clear(); parent.children.AddRange(originalChildren); parent.ContentBox = originalContent; throw; }
                parent.children.Clear(); parent.children.AddRange(originalChildren); parent.ContentBox = originalContent;
                if (fixedHeight is not null) Note("Wrapped row align-content distribution is not implemented.");
                return fixedHeight ?? Math.Max(0, crossUsed - rowGap);
            }
        }
        double free = available - total, used = 0, cross = 0;
        for (int i = 0; i < items.Count; i++)
        {
            var child = items[i]; double target = bases[i];
            if (free > 0 && grow > 0) target += free * CssValue.Number(child.Style.Get("flex-grow", "0"), 0) / grow;
            else if (free < 0 && shrink > 0) target += free * CssValue.Number(child.Style.Get("flex-shrink", "1"), 1) * bases[i] / shrink;
            target = Math.Max(0, target);
            var margin = child.Style.Edges("margin", parent.ContentBox.Width); var padding = child.Style.Edges("padding", parent.ContentBox.Width);
            var border = child.Style.Edges("border", parent.ContentBox.Width, true);
            double? width = column ? null : Math.Max(0, target - margin.Horizontal - padding.Horizontal - border.Horizontal);
            double? height = column ? Math.Max(0, target - margin.Vertical - padding.Vertical - border.Vertical) : null;
            string align = child.Style.Get("align-self", parent.Style.Get("align-items", "stretch"));
            if (!column && align == "stretch" && fixedHeight is { } h && child.Style.Length("height", h) is null)
                height = Math.Max(0, h - margin.Vertical - padding.Vertical - border.Vertical);
            if (child.Node.NodeType == NodeType.Text)
            {
                var flow = new Flow(parent.ContentBox.X + (column ? 0 : used), parent.ContentBox.Y + (column ? used : 0), width ?? parent.ContentBox.Width, child.Style.LineHeight);
                FlowText(child, flow);
            }
            else LayoutBlock(child, parent.ContentBox.X + (column ? 0 : used), parent.ContentBox.Y + (column ? used : 0),
                parent.ContentBox.Width, fixedHeight ?? 0, width, height);
            used += (column ? child.MarginBox.Height : child.MarginBox.Width) + gap;
            cross = Math.Max(cross, column ? child.MarginBox.Width : child.MarginBox.Height);
        }
        used -= items.Count > 0 ? gap : 0;
        double remaining = Math.Max(0, available - used);
        string justify = parent.Style.Get("justify-content", "flex-start");
        double offset = justify == "center" ? remaining / 2 : justify == "flex-end" ? remaining : justify == "space-around" && items.Count > 0 ? remaining / items.Count / 2 :
            justify == "space-evenly" ? remaining / (items.Count + 1) : 0;
        double extraGap = justify == "space-between" && items.Count > 1 ? remaining / (items.Count - 1) :
            justify == "space-around" && items.Count > 0 ? remaining / items.Count : justify == "space-evenly" ? remaining / (items.Count + 1) : 0;
        for (int i = 0; i < items.Count; i++)
        {
            var child = items[i]; string align = child.Style.Get("align-self", parent.Style.Get("align-items", "stretch"));
            double crossSpace = Math.Max(0, (column ? parent.ContentBox.Width : fixedHeight ?? cross) - (column ? child.MarginBox.Width : child.MarginBox.Height));
            double crossOffset = align == "center" ? crossSpace / 2 : align is "flex-end" or "end" ? crossSpace : 0;
            if (!column && align == "stretch" && fixedHeight is null && child.Node is IElement && child.Style.Length("height", cross) is null)
                UpdateRectangles(child, child.ContentBox with { Height = Math.Max(0, cross - child.Margin.Vertical - child.Border.Vertical - child.Padding.Vertical) });
            Translate(child, column ? crossOffset : offset + i * extraGap, column ? offset + i * extraGap : crossOffset);
        }
        return column ? used + remaining : fixedHeight ?? cross;
    }

    private static (double Width, double Height) SvgDimensions(IElement element)
    {
        var values = (element.GetAttribute("viewBox") ?? "").Split([' ', ',', '\t', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
        double width = values.Length == 4 ? CssValue.Number(values[2], 300) : 300, height = values.Length == 4 ? CssValue.Number(values[3], 150) : 150;
        return (Math.Clamp(CssValue.Number(element.GetAttribute("width") ?? "", width), 0, 4096), Math.Clamp(CssValue.Number(element.GetAttribute("height") ?? "", height), 0, 4096));
    }
    private double IntrinsicWidth(BrowserLayoutBox box, double available, bool indefinitePercentages = false)
    {
        if (box.Node.NodeType == NodeType.Text) return MeasureIntrinsicText(box);
        double extras = box.Style.Edges("padding", available).Horizontal + box.Style.Edges("border", available, true).Horizontal;
        double margin = box.Style.Edges("margin", available).Horizontal;
        if ((!indefinitePercentages || !box.Style.Get("width", "auto").Contains('%')) && box.Style.Length("width", available) is { } width) return Math.Max(0, width + margin + (box.Style.BoxSizing == "border-box" ? 0 : extras));
        if (box.Element?.LocalName == "svg") return SvgDimensions(box.Element).Width + extras + margin;
        if (box.Element?.LocalName == "img") return (imageLookup?.Invoke(box.Element.GetAttribute("src") ?? "")?.Width ?? CssValue.Number(box.Element.GetAttribute("width") ?? "", 0)) + extras + margin;
        if(box.GeneratedText is not null)return Math.Min(1000000,metrics.Measure(box.GeneratedText,box.Style.Font)+extras+margin);
        if (ControlValue(box.Node) is not null) return (box.Node is IHtmlInputElement ? 160 : 320) + extras + margin;
        double measured = 0, line = 0;
        bool flex = box.Style.Display is "flex" or "inline-flex";
        bool rowFlex = flex && !box.Style.Get("flex-direction", "row").StartsWith("column", StringComparison.Ordinal);
        int count = 0;
        foreach (var child in box.children.Where(child => child.Style.Display != "none" && child.Style.Position is not ("absolute" or "fixed")))
        {
            if (rowFlex && child.Node.NodeType == NodeType.Text && string.IsNullOrWhiteSpace(child.Node.TextContent)) continue;
            // An auto intrinsic size cannot supply a definite percentage basis
            // to descendants (e.g. a 100% underline inside an inline tab).
            // This box's own declared width was resolved above when definite.
            double childWidth = IntrinsicWidth(child, available, true);
            if (rowFlex) measured += childWidth;
            else if (flex || child.Node is IElement && child.Style.Display is not ("inline" or "inline-block" or "inline-flex"))
            { measured = Math.Max(measured, Math.Max(line, childWidth)); line = 0; }
            else line += childWidth;
            count++;
        }
        if (rowFlex) measured += Math.Max(0, count - 1) * (box.Style.Length("column-gap", available) ?? box.Style.Length("gap", available) ?? 0);
        return Math.Min(1000000, Math.Max(measured, line) + extras + margin);
    }
    private double MeasureIntrinsicText(BrowserLayoutBox box)
    {
        string text = box.Node.TextContent;
        if (box.Style.WhiteSpace is "pre" or "pre-wrap" or "break-spaces")
            return Math.Min(1000000, text.Replace("\r", "").Replace("\t", "    ").Split('\n').Select(line => metrics.Measure(line, box.Style.Font)).DefaultIfEmpty().Max());
        // FlowText paints words and collapsed spaces as separate glyph runs.
        // Shaping/kerning means measuring the unsplit source is not additive.
        double width = 0; int start = 0; bool space = false;
        for (int index = 0; index <= text.Length; index++)
        {
            if (index < text.Length && !Collapsible(text[index])) continue;
            if (index > start)
            {
                if (space) width += metrics.Measure(" ", box.Style.Font);
                width += metrics.Measure(text[start..index], box.Style.Font); space = false;
            }
            if (index < text.Length) space = true;
            start = index + 1;
        }
        if (space) width += metrics.Measure(" ", box.Style.Font);
        return Math.Min(1000000, width);
    }
    private double IntrinsicHeight(BrowserLayoutBox box,double? availableWidth=null)
    {
        if (box.Node.NodeType == NodeType.Text) return box.Style.LineHeight;
        double basis=availableWidth??viewport.Width;
        var padding=box.Style.Edges("padding",basis);var border=box.Style.Edges("border",basis,true);var margin=box.Style.Edges("margin",basis);
        double extras = padding.Vertical+border.Vertical;
        double width=box.Style.Length("width",basis) is { } w?w-(box.Style.BoxSizing=="border-box"?padding.Horizontal+border.Horizontal:0):Math.Max(0,basis-padding.Horizontal-border.Horizontal-margin.Horizontal);
        double? declared = box.Style.Length("height", viewport.Height);
        if (box.Element?.LocalName == "svg" && box.Style.Get("height", "auto").Contains('%') && box.Parent?.Style.Length("height", 0) is null) declared = null;
        if (declared is null && box.Element?.LocalName == "svg") declared = SvgDimensions(box.Element).Height;
        if (declared is null && box.Element?.LocalName == "img")
        {
            var image = imageLookup?.Invoke(box.Element.GetAttribute("src") ?? "");
            double imageWidth = box.Style.Length("width", basis) ?? CssValue.Number(box.Element.GetAttribute("width") ?? "", image?.Width ?? 0);
            declared = CssValue.Number(box.Element.GetAttribute("height") ?? "", image is { Width: > 0 } ? imageWidth * image.Height / image.Width : 0);
        }
        double height = declared is { } h ? Math.Max(0, h - (box.Style.BoxSizing == "border-box" ? extras : 0)) :
            MeasureIntrinsicContent(box,width);
        return Constrain(box.Style,height,"height",viewport.Height,extras)+margin.Vertical+extras;
    }
    private double MeasureIntrinsicContent(BrowserLayoutBox box,double width)
    {
        if(box.GeneratedText is not null){var generated=new Flow(0,0,width,box.Style.LineHeight,true);FlowText(box,generated);return generated.Y+(generated.HasContent?generated.LineHeight:0);}
        var children=box.children.Where(child=>child.Style.Display!="none"&&child.Style.Position is not ("absolute" or "fixed")).ToArray();
        if(box.Style.Display is "flex" or "inline-flex")
        {
            bool column=box.Style.Get("flex-direction","row").StartsWith("column",StringComparison.Ordinal);
            var heights=children.Where(child=>child.Node.NodeType!=NodeType.Text||!string.IsNullOrWhiteSpace(child.Node.TextContent)).Select(child=>IntrinsicHeight(child,width)).ToArray();
            return column?heights.Sum()+Math.Max(0,heights.Length-1)*(box.Style.Length("row-gap",width)??0):heights.DefaultIfEmpty().Max();
        }
        var flow=new Flow(0,0,width,box.Style.LineHeight,true,textIndent:box.Style.Length("text-indent",width)??0);double? previousBottom=null;
        if(ControlValue(box.Node) is { } value)
        {foreach(string line in value.Replace("\r","").Split('\n')){if(flow.HasContent)flow.NewLine(box.Style.LineHeight);if(line.Length>0)AddText(box,flow,line,box.Style.WhiteSpace!="pre");}return Math.Max(box.Style.LineHeight,flow.Y+flow.LineHeight);}
        foreach(var child in children)
        {
            if(child.Node.NodeType==NodeType.Text){FlowText(child,flow);if(flow.HasContent)previousBottom=null;continue;}
            if(child.Element?.LocalName=="br"){flow.NewLine(box.Style.LineHeight);previousBottom=null;continue;}
            if(child.Style.Display=="inline"){FlowInline(child,flow);if(flow.HasContent)previousBottom=null;continue;}
            if(child.Style.Display is "inline-block" or "inline-flex"){MeasureAtomicInline(child,flow);previousBottom=null;continue;}
            if(flow.HasContent)flow.NewLine(box.Style.LineHeight);
            var margin=child.Style.Edges("margin",width);
            if(previousBottom is { } previous){double collapsed=Math.Max(Math.Max(previous,0),Math.Max(margin.Top,0))+Math.Min(Math.Min(previous,0),Math.Min(margin.Top,0));flow.Y-=previous+margin.Top-collapsed;}
            flow.Y+=IntrinsicHeight(child,width);flow.X=flow.StartX;flow.LineHeight=box.Style.LineHeight;previousBottom=margin.Bottom;
        }
        return Math.Max(0,flow.Y+(flow.HasContent?flow.LineHeight:0));
    }
    private void LayoutAtomicInline(BrowserLayoutBox box, double x, double y, double containingWidth, double containingHeight, double outer)
    {
        double extras = box.Style.Edges("margin", containingWidth).Horizontal + box.Style.Edges("padding", containingWidth).Horizontal + box.Style.Edges("border", containingWidth, true).Horizontal;
        box.ShrinkToFitWidth = box.Style.Length("width", containingWidth) is null;
        LayoutBlock(box, x, y, containingWidth, containingHeight, Math.Max(0, outer - extras), null);
    }
    private void MeasureAtomicInline(BrowserLayoutBox box,Flow flow)
    {
        double width=IntrinsicWidth(box,flow.Width),height=IntrinsicHeight(box,box.Style.Length("width",flow.Width) is null?width:flow.Width);
        if(flow.HasContent&&flow.X+width>flow.StartX+flow.Width)flow.NewLine(box.Style.LineHeight);
        flow.X+=width;flow.LineHeight=Math.Max(flow.LineHeight,height);flow.HasContent=true;
    }
    private double LayoutTable(BrowserLayoutBox table)
    {
        var rows = new List<BrowserLayoutBox>();
        foreach (var child in table.children)
        {
            if (child.Style.Display == "none") continue;
            if (child.Style.Display == "table-row") rows.Add(child);
            else if (child.Style.Display == "table-row-group") rows.AddRange(child.children.Where(row => row.Style.Display == "table-row"));
        }
        int columns = rows.Select(row => row.children.Count(cell => cell.Style.Display == "table-cell")).DefaultIfEmpty().Max();
        if (columns > 128) throw new BrowserLimitException("CSS table column bound reached.");
        if (columns == 0) return 0;
        var widths = new double[columns];
        foreach (var row in rows)
        {
            int column = 0;
            foreach (var cell in row.children.Where(cell => cell.Style.Display == "table-cell"))
            {
                if (cell.Element?.HasAttribute("colspan") == true || cell.Element?.HasAttribute("rowspan") == true) Note("Table row/column spans are not implemented.");
                widths[column] = Math.Max(widths[column], IntrinsicWidth(cell, table.ContentBox.Width)); column++;
            }
        }
        bool collapse = table.Style.Get("border-collapse", "separate") == "collapse";
        var overlaps = new double[Math.Max(0,columns-1)];
        if(collapse)
            foreach(var row in rows)
            {
                var cells=row.children.Where(cell=>cell.Style.Display=="table-cell").ToArray();
                for(int i=0;i+1<cells.Length;i++)overlaps[i]=Math.Max(overlaps[i],Math.Min(cells[i].Style.Edges("border",table.ContentBox.Width,true).Right,cells[i+1].Style.Edges("border",table.ContentBox.Width,true).Left));
            }
        string[] spacing = CssValue.Split(table.Style.Get("border-spacing", "2px"), ' ').ToArray();
        double sx = collapse ? 0 : CssValue.Length(spacing.FirstOrDefault() ?? "2px", table.ContentBox.Width, table.Style.Font.Size, viewport, 16) ?? 2;
        double sy = collapse ? 0 : CssValue.Length(spacing.LastOrDefault() ?? "2px", table.ContentBox.Width, table.Style.Font.Size, viewport, 16) ?? 2;
        double total = widths.Sum(), available = Math.Max(0, table.ContentBox.Width - sx * (columns + 1) + overlaps.Sum());
        bool intrinsic = table.Style.Get("width", "auto") is "max-content" or "fit-content" or "auto";
        if (total < available && !intrinsic) for (int i = 0; i < widths.Length; i++) widths[i] += (available - total) / columns;
        else if (total > available && total > 0) for (int i = 0; i < widths.Length; i++) widths[i] *= available / total;
        double y = table.ContentBox.Y + sy;
        double previousBottomBorder=0;
        foreach (var row in rows)
        {
            var cells=row.children.Where(cell=>cell.Style.Display=="table-cell").ToArray();
            double topBorder=cells.Select(cell=>cell.Style.Edges("border",table.ContentBox.Width,true).Top).DefaultIfEmpty().Max();
            if(collapse)y-=Math.Min(previousBottomBorder,topBorder);
            double x = table.ContentBox.X + sx, height = 0; int column = 0;
            foreach (var cell in cells)
            {
                double extra = cell.Style.Edges("padding", table.ContentBox.Width).Horizontal + cell.Style.Edges("border", table.ContentBox.Width, true).Horizontal;
                LayoutBlock(cell, x, y, widths[column], 0, Math.Max(0, widths[column] - extra), null);
                height = Math.Max(height, cell.MarginBox.Height); x += widths[column] + sx - (column<overlaps.Length?overlaps[column]:0);column++;
            }
            foreach (var cell in row.children.Where(cell => cell.Style.Display == "table-cell"))
                UpdateRectangles(cell, cell.ContentBox with { Height = Math.Max(0, height - cell.Padding.Vertical - cell.Border.Vertical - cell.Margin.Vertical) });
            UpdateRectangles(row, new(table.ContentBox.X, y, widths.Sum() + sx * (columns + 1) - overlaps.Sum(), height)); y += height + sy;
            previousBottomBorder=cells.Select(cell=>cell.Border.Bottom).DefaultIfEmpty().Max();
        }
        foreach (var group in table.children.Where(group => group.Style.Display == "table-row-group"))
        {
            BrowserRect union = default; foreach (var row in group.children.Where(row => row.Style.Display == "table-row")) union = union.Union(row.BorderBox);
            UpdateRectangles(group, union);
        }
        if (intrinsic) table.ContentBox = table.ContentBox with { Width = widths.Sum() + sx * (columns + 1) - overlaps.Sum() };
        return y - table.ContentBox.Y;
    }
    private bool QueueAbsolute(BrowserLayoutBox child, BrowserLayoutBox parent)
    {
        if (child.Node.NodeType == NodeType.Text || child.Style.Position is not ("absolute" or "fixed")) return false;
        BrowserLayoutBox? anchor = parent; while (anchor is not null && anchor.Style.Position == "static" && anchor.Style.Get("transform", "none") == "none") anchor = anchor.Parent;
        pending.Add(new(child, parent, child.Style.Position == "fixed" ? null : anchor)); return true;
    }
    private void LayoutAbsolute(PendingAbsolute item)
    {
        var box = item.Box;
        BrowserRect containing = item.Anchor?.PaddingBox ?? new(box.Style.Position == "fixed" ? viewport.ScrollX : 0,
            box.Style.Position == "fixed" ? viewport.ScrollY : 0, viewport.Width, viewport.Height);
        double? left = box.Style.Length("left", containing.Width), right = box.Style.Length("right", containing.Width);
        double? top = box.Style.Length("top", containing.Height), bottom = box.Style.Length("bottom", containing.Height);
        var margin = box.Style.Edges("margin", containing.Width); var pad = box.Style.Edges("padding", containing.Width); var border = box.Style.Edges("border", containing.Width, true);
        box.ShrinkToFitWidth = box.Style.Length("width", containing.Width) is null && !(left is not null && right is not null);
        double? width = box.Style.Length("width", containing.Width) is null ? Math.Max(0, (left is not null && right is not null ? containing.Width - left.Value - right.Value : IntrinsicWidth(box, containing.Width, true)) - margin.Horizontal - pad.Horizontal - border.Horizontal) : null;
        double? height = box.Style.Length("height", containing.Height) is null && top is not null && bottom is not null ?
            Math.Max(0, containing.Height - top.Value - bottom.Value - margin.Vertical - pad.Vertical - border.Vertical) : null;
        LayoutBlock(box, containing.X + (left ?? 0), containing.Y + (top ?? 0), containing.Width, containing.Height, width, height);
        if (left is null && right is not null) Translate(box, containing.Right - right.Value - box.MarginBox.Right, 0);
        if (top is null && bottom is not null) Translate(box, 0, containing.Bottom - bottom.Value - box.MarginBox.Bottom);
    }
    private static void Translate(BrowserLayoutBox box, double x, double y)
    {
        box.ContentBox = box.ContentBox.Translate(x, y); box.PaddingBox = box.PaddingBox.Translate(x, y);
        box.BorderBox = box.BorderBox.Translate(x, y); box.MarginBox = box.MarginBox.Translate(x, y);
        for (int index = 0; index < box.fragments.Count; index++) box.fragments[index] = box.fragments[index].Translate(x, y);
        for (int index = 0; index < box.textCommands.Count; index++) box.textCommands[index] = box.textCommands[index] with { Bounds = box.textCommands[index].Bounds.Translate(x, y) };
        foreach (var child in box.children) Translate(child, x, y);
    }

    private void ApplyTranslations(BrowserLayoutBox box, BrowserAffineTransform? ancestor = null)
    {
        if (box.Style.Display == "none") return;
        var inherited = ancestor ?? BrowserAffineTransform.Identity;
        var local = BrowserAffineTransform.Identity;
        // Anonymous text boxes inherit the element's computed style, but do not
        // establish an additional transform of their own.
        if (box.Node is IElement)
        {
            string text = box.Style.Get("transform", "none");
            if (!BrowserAffineTransform.TryParse(text, box.BorderBox, box.Style, out var parsed))
                Note("Unsupported or invalid CSS 2D transform: " + text);
            else if (parsed.IsTranslation)
                Translate(box, parsed.E, parsed.F); // Retain the allocation-free existing translation path.
            else
            {
                var origin = BrowserAffineTransform.Origin(box.Style, box.BorderBox);
                local = BrowserAffineTransform.Translation(origin.X, origin.Y) * parsed * BrowserAffineTransform.Translation(-origin.X, -origin.Y);
            }
        }
        box.Transform = inherited * local;
        if (!box.Transform.TryInverse(out _)) { box.PaintSuppressed = true; return; }
        foreach (var child in box.children) ApplyTranslations(child, box.Transform);
    }

    private sealed record StackingLayer(BrowserLayoutBox Box, BrowserRect? Clip, double ParentOpacity,
        IReadOnlyList<BrowserRoundedClip>? RoundedClips, bool Context, int Z);
    private sealed class StackingGroup
    {
        public readonly List<StackingLayer> Layers = [];
        public readonly HashSet<BrowserLayoutBox> Deferred = [];
    }
    private static bool StackingContext(BrowserLayoutBox box) => box.Node is IElement &&
        (box.Style.Opacity < 1 || box.Style.Get("transform", "none") != "none" || box.Style.Position is "fixed" or "sticky" ||
         (box.Style.Position != "static" || box.Parent?.Style.Display is "flex" or "inline-flex" or "grid" or "inline-grid") && box.Style.Get("z-index", "auto") != "auto");
    private static BrowserRoundedClip? PositionedClipShape(BrowserLayoutBox box)
    {
        if (box.Style.Position is not ("absolute" or "fixed")) return null;
        string text = box.Style.Get("clip", "auto").Trim();
        if (!text.StartsWith("rect(", StringComparison.OrdinalIgnoreCase) || !text.EndsWith(')')) return null;
        var parts = text[5..^1].Replace(',', ' ').Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
        if (parts.Length != 4) return null;
        double[] defaults = [0, box.BorderBox.Width, box.BorderBox.Height, 0], edges = new double[4];
        for (int index = 0; index < 4; index++)
        {
            if (parts[index] == "auto") { edges[index] = defaults[index]; continue; }
            if (parts[index].Contains('%') || CssValue.Length(parts[index], 0, box.Style.Font.Size, box.Style.Viewport, 16) is not { } value) return null;
            edges[index] = value;
        }
        return new(new(box.BorderBox.X + edges[3], box.BorderBox.Y + edges[0], Math.Max(0, edges[1] - edges[3]), Math.Max(0, edges[2] - edges[0])), default, box.Transform);
    }
    private static BrowserRect? PositionedClip(BrowserLayoutBox box) => PositionedClipShape(box) is { } shape ? (shape.Transform ?? BrowserAffineTransform.Identity).Bounds(shape.Bounds) : null;
    private static (BrowserRect? Clip, IReadOnlyList<BrowserRoundedClip>? Rounded) ChildClipping(BrowserLayoutBox box,
        BrowserRect? inheritedClip, IReadOnlyList<BrowserRoundedClip>? roundedClips, out BrowserRoundedClip? added)
    {
        added = null;
        if (PositionedClip(box) is { } positioned)
        {
            inheritedClip = inheritedClip?.Intersect(positioned) ?? positioned;
            roundedClips = (roundedClips ?? []).Append(PositionedClipShape(box)!).ToArray();
        }
        if (box.Style.OverflowX == "visible" && box.Style.OverflowY == "visible") return (inheritedClip, roundedClips);
        var area = box.PaddingBox;
        if (box.Style.OverflowX == "visible") area = new(-1000000, area.Y, 2000000, area.Height);
        if (box.Style.OverflowY == "visible") area = new(area.X, -1000000, area.Width, 2000000);
        if (box.Style.OverflowX != "visible" && box.Style.OverflowY != "visible" && BrowserBoxDecoration.Create(box.Style, box.BorderBox, box.Border) is { } decoration &&
            decoration.Radius is { } radius && radius.Top + radius.Right + radius.Bottom + radius.Left > 0)
        {
            var inner = new BrowserEdges(Math.Max(0, radius.Top - Math.Max(box.Border.Top, box.Border.Left)), Math.Max(0, radius.Right - Math.Max(box.Border.Top, box.Border.Right)),
                Math.Max(0, radius.Bottom - Math.Max(box.Border.Bottom, box.Border.Right)), Math.Max(0, radius.Left - Math.Max(box.Border.Bottom, box.Border.Left)));
            added = new(box.PaddingBox, inner, box.Transform);
        }
        if (added is null && !box.Transform.IsIdentity) added = new(area, default, box.Transform);
        if (added is not null) roundedClips = (roundedClips ?? []).Append(added).ToArray();
        area = box.Transform.Bounds(area);
        return (inheritedClip?.Intersect(area) ?? area, roundedClips);
    }
    private static void CollectStacking(BrowserLayoutBox parent, BrowserRect? clip, double opacity,
        IReadOnlyList<BrowserRoundedClip>? rounded, StackingGroup group)
    {
        if (parent.Element?.LocalName == "svg") return;
        foreach (var child in parent.children)
        {
            if (child.Style.Display == "none" || child.PaintSuppressed) continue;
            bool context = StackingContext(child);
            if (context || child.Node is IElement && child.Style.Position != "static")
            {
                int z = child.Style.Position != "static" || child.Parent?.Style.Display is "flex" or "inline-flex" ? child.Style.ZIndex : 0;
                var layerClip = PositionedClip(child) is { } positioned ? clip?.Intersect(positioned) ?? positioned : clip;
                group.Layers.Add(new(child, layerClip, opacity, rounded, context, z)); group.Deferred.Add(child);
                if (context) continue;
            }
            var next = ChildClipping(child, clip, rounded, out _);
            CollectStacking(child, next.Clip, opacity * (child.Node is IElement ? child.Style.Opacity : 1), next.Rounded, group);
        }
    }
    private void PaintLayer(StackingLayer layer, StackingGroup group)
    {
        // A descendant may escape an ordinary ancestor for stacking, while
        // retaining that ancestor's rectangular and rounded overflow clips.
        int pushed = 0;
        if (layer.Clip is { } clip) { Command(new(BrowserDrawKind.PushClip, clip, layer.Box.NodeId, layer.Box.Node)); pushed++; }
        foreach (var rounded in layer.RoundedClips ?? [])
        { Command(new(BrowserDrawKind.PushClip, (rounded.Transform ?? BrowserAffineTransform.Identity).Bounds(rounded.Bounds), layer.Box.NodeId, layer.Box.Node, RoundedClip: rounded)); pushed++; }
        Paint(layer.Box, layer.Clip, layer.ParentOpacity, layer.RoundedClips, layer.Context ? null : group);
        for (int index = 0; index < pushed; index++) Command(new(BrowserDrawKind.PopClip, default, layer.Box.NodeId, layer.Box.Node));
    }
    private void Paint(BrowserLayoutBox box, BrowserRect? inheritedClip, double parentOpacity, IReadOnlyList<BrowserRoundedClip>? roundedClips = null,
        StackingGroup? group = null, bool contextRoot = false)
    {
        if (box.Style.Display == "none" || box.PaintSuppressed) return;
        if (group is null)
        {
            var ownClip = PositionedClip(box);
            if (ownClip is { } positioned) { inheritedClip = inheritedClip?.Intersect(positioned) ?? positioned; Command(new(BrowserDrawKind.PushClip, inheritedClip.Value, box.NodeId, box.Node, RoundedClip: PositionedClipShape(box))); }
            group = new(); var childrenClip = ChildClipping(box, inheritedClip, roundedClips, out _);
            CollectStacking(box, childrenClip.Clip, parentOpacity * (box.Node is IElement ? box.Style.Opacity : 1), childrenClip.Rounded, group);
            Paint(box, inheritedClip, parentOpacity, roundedClips, group, true);
            if (ownClip is not null) Command(box, new(BrowserDrawKind.PopClip, default, box.NodeId, box.Node));
            return;
        }
        double opacity = parentOpacity * (box.Node is IElement ? box.Style.Opacity : 1); box.EffectiveOpacity = opacity; box.Clip = inheritedClip; box.roundedClips=roundedClips; box.IsLaidOut = true; painted.Add(box);
        if (box.Style.Visibility != "hidden" && opacity > 0)
        {
            if (box.Node is IElement)
            {
                IEnumerable<BrowserRect> rectangles = box.Style.Display == "inline" && box.fragments.Count > 0 ? box.fragments : [box.BorderBox];
                bool decorated = false;
                foreach (var rect in rectangles)
                {
                    var decoration = BrowserBoxDecoration.Create(box.Style, rect, box.Border);
                    if (decoration is not null)
                    {
                        decorated = true;
                        if (ReferenceEquals(canvasBackgroundOwner, box)) decoration = decoration with { Background = new(0, 0) };
                        if (decoration.Shadows.Any(shadow => shadow.Inset)) Note("Inset box shadows are retained but not painted.");
                        Command(box, new(BrowserDrawKind.Fill, decoration.PaintBounds, box.NodeId, box.Node, Opacity: opacity, Decoration: decoration));
                    }
                    else if (box.Style.Background.Alpha > 0 && !ReferenceEquals(canvasBackgroundOwner, box))
                        Command(box, new(BrowserDrawKind.Fill, rect, box.NodeId, box.Node, box.Style.Background.Rgb, opacity * box.Style.Background.Alpha));
                }
                if (!decorated) PaintBorder(box, opacity);
                foreach (string source in BrowserCssImages.Sources(box.Style.Get("background-image", "none")))
                {
                    Command(new(BrowserDrawKind.PushClip, box.Transform.Bounds(box.BorderBox), box.NodeId, box.Node, RoundedClip: box.Transform.IsIdentity ? null : new(box.BorderBox, default, box.Transform)));
                    Command(box, new(BrowserDrawKind.Image, BrowserCssImages.Size(box.PaddingBox, imageLookup?.Invoke(source), box.Style), box.NodeId, box.Node, Opacity: opacity, ImageSource: source));
                    Command(box, new(BrowserDrawKind.PopClip, default, box.NodeId, box.Node));
                }
                if (box.Element?.LocalName == "img" && box.Element.GetAttribute("src") is { Length: > 0 } imageSource)
                    Command(box, new(BrowserDrawKind.Image, box.ContentBox, box.NodeId, box.Node, Opacity: opacity, ImageSource: imageSource));
                PaintListMarker(box, opacity);
                if (box.Node is IHtmlCanvasElement) Command(box, new(BrowserDrawKind.Canvas, box.ContentBox, box.NodeId, box.Node, Opacity: opacity));
                if (box.Element?.LocalName == "svg")
                {
                    Command(box, new(BrowserDrawKind.Image, box.ContentBox, box.NodeId, box.Node, Opacity: opacity, ImageSource: SvgSource(box)));
                    return;
                }
            }
            bool control = ControlValue(box.Node) is not null;
            if (control) Command(new(BrowserDrawKind.PushClip, box.Transform.Bounds(box.ContentBox), box.NodeId, box.Node, RoundedClip: box.Transform.IsIdentity ? null : new(box.ContentBox, default, box.Transform)));
            foreach (var text in box.textCommands) Command(box, text with { Opacity = text.Opacity * opacity });
            if (control) Command(box, new(BrowserDrawKind.PopClip, default, box.NodeId, box.Node));
        }
        var childClipping = ChildClipping(box, inheritedClip, roundedClips, out var rounded);
        BrowserRect? clip = childClipping.Clip; roundedClips = childClipping.Rounded;
        bool clips = box.Style.OverflowX != "visible" || box.Style.OverflowY != "visible";
        if (clips)
        {
            Command(new(BrowserDrawKind.PushClip, clip!.Value, box.NodeId, box.Node,RoundedClip:rounded));
        }
        if (contextRoot) foreach (var layer in group.Layers.Where(item => item.Z < 0).OrderBy(item => item.Z)) PaintLayer(layer, group);
        foreach (var child in box.children) if (!group.Deferred.Contains(child)) Paint(child, clip, opacity, roundedClips, group);
        if (contextRoot) foreach (var layer in group.Layers.Where(item => item.Z >= 0).OrderBy(item => item.Z)) PaintLayer(layer, group);
        if (clips) Command(box, new(BrowserDrawKind.PopClip, default, box.NodeId, box.Node));
    }
    private void PaintCanvasBackground(BrowserLayoutBox root)
    {
        // CSS 2 canvas background propagation: root background covers the canvas;
        // an HTML root with a transparent background uses its body's background.
        // The body can have zero in-flow height when every child is positioned.
        canvasBackgroundOwner = root.Style.Background.Alpha > 0 ? root : null;
        if (canvasBackgroundOwner is null && root.Element?.LocalName == "html")
            canvasBackgroundOwner = root.children.FirstOrDefault(child => child.Element?.LocalName == "body" && child.Style.Display != "none" && child.Style.Background.Alpha > 0);
        var rect = new BrowserRect(viewport.ScrollX, viewport.ScrollY, viewport.Width, viewport.Height);
        Command(new(BrowserDrawKind.Fill, rect, root.NodeId, root.Node, 0xFFFFFF, ViewportPinned: true));
        if (canvasBackgroundOwner is { } owner)
            Command(new(BrowserDrawKind.Fill, rect, owner.NodeId, owner.Node, owner.Style.Background.Rgb, owner.Style.Background.Alpha, ViewportPinned: true));
    }
    private string SvgSource(BrowserLayoutBox box)
    {
        var clone = (IElement)box.Element!.Clone(true);
        void Style(BrowserLayoutBox source, IElement target)
        {
            foreach (string property in new[] { "fill", "stroke", "stroke-width", "fill-rule", "fill-opacity", "stroke-opacity", "stroke-linecap", "stroke-linejoin", "color", "visibility" })
                if (source.Style.Properties.TryGetValue(property, out string? value)) target.SetAttribute(property, value);
            // The display command already includes the root's opacity.
            if (!ReferenceEquals(source, box)) target.SetAttribute("opacity", source.Style.Opacity.ToString(System.Globalization.CultureInfo.InvariantCulture));
            var children = source.children.Where(child => child.Node is IElement && child.GeneratedPseudo is null).ToArray();
            for (int index = 0; index < Math.Min(children.Length, target.Children.Length); index++) Style(children[index], target.Children[index]);
        }
        Style(box, clone);
        // A use element may reference a symbol in another inline SVG. Freeze
        // those local definitions into this self-contained image; no URL fetch
        // or live document reference crosses the paint publication boundary.
        var references = new Queue<string>(clone.QuerySelectorAll("use").Select(node => node.GetAttribute("href") ?? node.GetAttribute("xlink:href") ?? ""));
        var visited = new HashSet<string>(StringComparer.Ordinal); int referenceCount = 0;
        while (references.TryDequeue(out var reference))
        {
            if (!reference.StartsWith('#') || !visited.Add(reference)) continue;
            if (++referenceCount > 64) throw new BrowserLimitException("SVG local reference budget exceeded.");
            if (clone.QuerySelectorAll("[id]").Any(node => node.Id == reference[1..])) continue;
            var original = box.Element!.Owner?.GetElementById(reference[1..]);
            if (original is null || original.NamespaceUri != "http://www.w3.org/2000/svg") continue;
            var definition = (IElement)original.Clone(true);
            // The use instance inherits its own color/fill, not the original
            // definition's computed inheritance from an unrelated SVG parent.
            var definitions = clone.Owner!.CreateElement("http://www.w3.org/2000/svg", "defs");
            definitions.AppendChild(definition); clone.AppendChild(definitions);
            foreach (var nested in definition.QuerySelectorAll("use")) references.Enqueue(nested.GetAttribute("href") ?? nested.GetAttribute("xlink:href") ?? "");
        }
        clone.SetAttribute("width", CssValue.Number(box.ContentBox.Width.ToString(System.Globalization.CultureInfo.InvariantCulture), 1).ToString(System.Globalization.CultureInfo.InvariantCulture));
        clone.SetAttribute("height", box.ContentBox.Height.ToString(System.Globalization.CultureInfo.InvariantCulture));
        clone.RemoveAttribute("style"); clone.RemoveAttribute("opacity");
        return "data:image/svg+xml," + Uri.EscapeDataString(clone.OuterHtml);
    }
    private void PaintListMarker(BrowserLayoutBox box, double opacity)
    {
        if (box.Style.Display != "list-item") return;
        var style = box.MarkerStyle ?? box.Style;
        string type = box.Style.Get("list-style-type", "disc");
        if (type == "none") return;
        string marker;
        if (type is "decimal" or "lower-alpha" or "upper-alpha")
        {
            int number = 1;
            if (box.Element?.ParentElement is { } parent)
            {
                bool reversed = parent.HasAttribute("reversed");
                int.TryParse(parent.GetAttribute("start"), out int start);
                number = parent.HasAttribute("start") ? start : reversed ? parent.Children.Count(child => child.LocalName == "li") : 1;
                foreach (var sibling in parent.Children.Where(child => child.LocalName == "li"))
                {
                    if (int.TryParse(sibling.GetAttribute("value"), out int value)) number = value;
                    if (ReferenceEquals(sibling, box.Element)) break;
                    number += reversed ? -1 : 1;
                }
            }
            marker = type == "decimal" || number < 1 || number > 26 ? number.ToString(System.Globalization.CultureInfo.InvariantCulture) : ((char)((type == "upper-alpha" ? 'A' : 'a') + number - 1)).ToString();
            marker += ".";
        }
        else marker = type == "circle" ? "◦" : type == "square" ? "▪" : "•";
        double width = metrics.Measure(marker, style.Font), gap = style.Font.Size * .45;
        Command(box, new(BrowserDrawKind.Text, new(box.ContentBox.X - width - gap, box.ContentBox.Y, width, style.LineHeight), box.NodeId, box.Node, style.Color.Rgb, style.Color.Alpha * opacity, marker, style.Font));
    }
    private void PaintBorder(BrowserLayoutBox box, double opacity)
    {
        var b = box.BorderBox; var widths = box.Border;
        var sides = new[] { ("top", new BrowserRect(b.X, b.Y, b.Width, widths.Top)),
            ("right", new BrowserRect(b.Right - widths.Right, b.Y + widths.Top, widths.Right, Math.Max(0, b.Height - widths.Vertical))),
            ("bottom", new BrowserRect(b.X, b.Bottom - widths.Bottom, b.Width, widths.Bottom)),
            ("left", new BrowserRect(b.X, b.Y + widths.Top, widths.Left, Math.Max(0, b.Height - widths.Vertical))) };
        foreach (var (side, rect) in sides)
        {
            if (rect.Width <= 0 || rect.Height <= 0) continue;
            var color = CssValue.Color(box.Style.Get("border-" + side + "-color", "currentcolor"), box.Style.Color);
            if (box.Style.Get("border-" + side + "-style", "solid") != "solid") Note("Non-solid borders currently use a solid side rectangle.");
            Command(box, new(BrowserDrawKind.Border, rect, box.NodeId, box.Node, color.Rgb, color.Alpha * opacity));
        }
    }
    private void Command(BrowserLayoutBox box, BrowserDrawCommand command) => Command(command with { Transform = box.Transform.IsIdentity ? null : box.Transform });
    private void Command(BrowserDrawCommand command)
    {
        if (commands.Count >= limits.MaximumDisplayCommands) throw new BrowserLimitException("CSS display command limit reached.");
        commands.Add(command);
    }
    private void Note(string message) { if (diagnostics.Count < 256 && !diagnostics.Contains(message)) diagnostics.Add(message); }
}