using AngleSharp.Dom; using AngleSharp.Html.Dom; namespace XFESystem.Browser; /// /// 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). /// 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 all = [], painted = []; private readonly List commands = []; private readonly List pending = []; private readonly List diagnostics = []; private readonly Dictionary identities = new(ReferenceEqualityComparer.Instance); private readonly HashSet currentIdentityNodes = new(ReferenceEqualityComparer.Instance); private readonly List detachedIdentityNodes = []; private IDocument? identityDocument; private int nextId, nodeCount; private BrowserViewport viewport; private IBrowserTextMetrics metrics = new BrowserFallbackTextMetrics(); private Func? 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()); /// Identity references retained for the most recent bounded layout; snapshots own their separate boxes. 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? linkedSheets = null, IBrowserTextMetrics? textMetrics = null, Func? 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)break; char quote=content[offset]; if(quote is '\'' or '"') { offset++; while(offsetbegin) {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(offset4096)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(); 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(); foreach (var leaf in leaves) foreach (var rect in leaf.Node.NodeType == NodeType.Text ? (IEnumerable)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 { 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(startroom)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 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(); 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 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 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(); 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? 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 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); } }