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)
    {
        public double X = x, Y = y, StartX = x, Width = width, LineHeight = lineHeight;
        public readonly bool MeasureOnly=measureOnly;
        public bool HasContent, PendingSpace;
        public void NewLine(double nextHeight)
        { Y += LineHeight; X = StartX; LineHeight = nextHeight; HasContent = false; PendingSpace = false; }
    }
    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;
    public long CssParseGeneration => cascade?.ParseGeneration ?? 0;
    public long CssReuseCount => cascade?.ReuseCount ?? 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;

    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)
    {
        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;
        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, floats, parent-child margin collapse, scale/rotate/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); }
        // The current native runtime does not unwind ordinary finally blocks.
        // Mirror cleanup on the exceptional path, as well as normal completion.
        catch (Exception) { PruneNodeIdentities(); throw; }
        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) { width = Math.Max(width, box.BorderBox.Right); height = Math.Max(height, box.BorderBox.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);
        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)
        {
            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);
        double? height = forcedHeight ?? (declaredHeight is { } h ? Math.Max(0, h - (style.BoxSizing == "border-box" ? verticalExtras : 0)) : null);
        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.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);
        double? previousBottomMargin = null;
        foreach (var child in parent.children)
        {
            if (child.Style.Display == "none") continue;
            if (QueueAbsolute(child, parent)) continue;
            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) flow.NewLine(parent.Style.LineHeight);
                LayoutBlock(child, flow.X, flow.Y, outer, fixedHeight ?? 0, null, null);
                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.X = flow.StartX; flow.LineHeight = parent.Style.LineHeight;
            previousBottomMargin=child.Margin.Bottom;
        }
        AlignInlineLines(parent);
        return Math.Max(0, flow.Y - content.Y + (flow.HasContent ? flow.LineHeight : 0));
    }

    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 MeasureAtomicInline(child,flow);
            }
            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 (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
            {
                double width = IntrinsicWidth(child, flow.Width);
                if (flow.HasContent && flow.X + width > flow.StartX + flow.Width) flow.NewLine(box.Style.LineHeight);
                LayoutBlock(child, flow.X, flow.Y, width, 0, null, null);
                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);
        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) 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 (parent.Style.Get("flex-wrap", "nowrap") != "nowrap") Note("Flex wrapping is not implemented; one flex line 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);
            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;
        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 double IntrinsicWidth(BrowserLayoutBox box, double available)
    {
        if (box.Node.NodeType == NodeType.Text) return Math.Min(1000000, metrics.Measure(box.Node.TextContent, box.Style.Font));
        double extras = box.Style.Edges("padding", available).Horizontal + box.Style.Edges("border", available, true).Horizontal;
        double margin = box.Style.Edges("margin", available).Horizontal;
        if (box.Style.Length("width", available) is { } width) return Math.Max(0, width + margin + (box.Style.BoxSizing == "border-box" ? 0 : extras));
        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;
        foreach (var child in box.children.Where(child => child.Style.Display != "none" && child.Style.Position is not ("absolute" or "fixed")))
            measured = child.Style.Display is "block" or "list-item" or "table" or "table-row" or "table-row-group" ? Math.Max(measured, IntrinsicWidth(child, available)) : measured + IntrinsicWidth(child, available);
        return Math.Min(1000000, measured + extras + margin);
    }
    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);
        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);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 MeasureAtomicInline(BrowserLayoutBox box,Flow flow)
    {
        double width=IntrinsicWidth(box,flow.Width),height=IntrinsicHeight(box,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);
        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)) - 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)
    {
        if (box.Node is not IElement || box.Style.Display == "none") return;
        string transform = box.Style.Get("transform", "none").Trim().ToLowerInvariant();
        double x = 0, y = 0;
        for (int offset = 0; offset < transform.Length && transform != "none";)
        {
            int open = transform.IndexOf('(', offset); if (open < 0) { Note("Unsupported CSS transform: " + transform); break; }
            string name = transform[offset..open].Trim(); int depth = 1, close = open + 1;
            while (close < transform.Length && depth > 0) { if (transform[close] == '(') depth++; else if (transform[close] == ')') depth--; close++; }
            if (depth != 0) { Note("Malformed CSS transform ignored"); break; }
            string arguments = transform[(open + 1)..(close - 1)]; var pieces = new List<string>(); int start = 0; depth = 0;
            for (int index = 0; index <= arguments.Length; index++)
            {
                if (index == arguments.Length || depth == 0 && (arguments[index] == ',' || char.IsWhiteSpace(arguments[index])))
                { if (index > start) pieces.Add(arguments[start..index].Trim()); start = index + 1; }
                else if (arguments[index] == '(') depth++; else if (arguments[index] == ')') depth--;
            }
            double? Length(int index, double basis) => index >= pieces.Count ? null : CssValue.Length(pieces[index], basis, box.Style.Font.Size, viewport, 16);
            double? dx = null, dy = null;
            if (name == "translate" && pieces.Count is 1 or 2) { dx = Length(0, box.BorderBox.Width); dy = pieces.Count == 1 ? 0 : Length(1, box.BorderBox.Height); }
            else if (name == "translatex" && pieces.Count == 1) { dx = Length(0, box.BorderBox.Width); dy = 0; }
            else if (name == "translatey" && pieces.Count == 1) { dx = 0; dy = Length(0, box.BorderBox.Height); }
            else if (name == "translate3d" && pieces.Count == 3 && Length(2, 0) == 0) { dx = Length(0, box.BorderBox.Width); dy = Length(1, box.BorderBox.Height); }
            else if (name == "matrix" && pieces.Count == 6 && CssValue.Number(pieces[0], 0) == 1 && CssValue.Number(pieces[1], 1) == 0 && CssValue.Number(pieces[2], 1) == 0 && CssValue.Number(pieces[3], 0) == 1)
            { dx = CssValue.Number(pieces[4], 0); dy = CssValue.Number(pieces[5], 0); }
            if (dx is null || dy is null) { Note("Unsupported CSS transform; only 2D translations are implemented: " + name); x = y = 0; break; }
            x = Math.Clamp(x + dx.Value, -1000000, 1000000); y = Math.Clamp(y + dy.Value, -1000000, 1000000); offset = close;
        }
        if (x != 0 || y != 0) Translate(box, x, y);
        foreach (var child in box.children) ApplyTranslations(child);
    }

    private void Paint(BrowserLayoutBox box, BrowserRect? inheritedClip, double parentOpacity, IReadOnlyList<BrowserRoundedClip>? roundedClips = null)
    {
        if (box.Style.Display == "none") 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(new(BrowserDrawKind.Fill, decoration.PaintBounds, box.NodeId, box.Node, Opacity: opacity, Decoration: decoration));
                    }
                    else if (box.Style.Background.Alpha > 0 && !ReferenceEquals(canvasBackgroundOwner, box))
                        Command(new(BrowserDrawKind.Fill, rect, box.NodeId, box.Node, box.Style.Background.Rgb, opacity * box.Style.Background.Alpha));
                }
                if (!decorated) PaintBorder(box, opacity);
                if (box.Style.Get("background-image", "none") != "none" && BrowserCssGradient.Parse(box.Style.Get("background-image", "none"),box.Style).Count == 0) Note("Unsupported CSS background image retained but not painted.");
                PaintListMarker(box, opacity);
                if (box.Node is IHtmlCanvasElement) Command(new(BrowserDrawKind.Canvas, box.ContentBox, box.NodeId, box.Node, Opacity: opacity));
            }
            bool control = ControlValue(box.Node) is not null;
            if (control) Command(new(BrowserDrawKind.PushClip, box.ContentBox, box.NodeId, box.Node));
            foreach (var text in box.textCommands) Command(text with { Opacity = text.Opacity * opacity });
            if (control) Command(new(BrowserDrawKind.PopClip, default, box.NodeId, box.Node));
        }
        BrowserRect? clip = inheritedClip;
        bool clips = box.Style.OverflowX != "visible" || box.Style.OverflowY != "visible";
        if (clips)
        {
            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);
            clip = inheritedClip?.Intersect(area) ?? area;
            BrowserRoundedClip? rounded=null;
            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)));
                rounded=new(box.PaddingBox,inner);
                roundedClips=(roundedClips??[]).Append(rounded).ToArray();
            }
            Command(new(BrowserDrawKind.PushClip, clip.Value, box.NodeId, box.Node,RoundedClip:rounded));
        }
        foreach (var child in box.children.OrderBy(child => child.Node.NodeType == NodeType.Text ? 0 : child.Style.ZIndex)) Paint(child, clip, opacity,roundedClips);
        if (clips) Command(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 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(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(new(BrowserDrawKind.Border, rect, box.NodeId, box.Node, color.Rgb, color.Alpha * opacity));
        }
    }
    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); }
}