using AngleSharp.Dom;
namespace XFESystem.Browser;
public readonly record struct BrowserRect(double X, double Y, double Width, double Height)
{
public double Left => X;
public double Top => Y;
public double Right => X + Width;
public double Bottom => Y + Height;
public bool Contains(double x, double y) => Width > 0 && Height > 0 && x >= X && y >= Y && x < Right && y < Bottom;
public BrowserRect Translate(double x, double y) => new(X + x, Y + y, Width, Height);
public BrowserRect Intersect(BrowserRect other)
{
double x = Math.Max(X, other.X), y = Math.Max(Y, other.Y);
return new(x, y, Math.Max(0, Math.Min(Right, other.Right) - x), Math.Max(0, Math.Min(Bottom, other.Bottom) - y));
}
public BrowserRect Union(BrowserRect other)
{
if (Width <= 0 || Height <= 0) return other;
if (other.Width <= 0 || other.Height <= 0) return this;
double x = Math.Min(X, other.X), y = Math.Min(Y, other.Y);
return new(x, y, Math.Max(Right, other.Right) - x, Math.Max(Bottom, other.Bottom) - y);
}
}
public readonly record struct BrowserViewport(double Width, double Height, double ScrollX = 0, double ScrollY = 0);
/// <summary>Native input state, kept outside DOM attributes. Hover/active include their ancestor chain.</summary>
public sealed record BrowserInteractionState(INode? Hovered = null, INode? Active = null, INode? Focus = null);
public readonly record struct BrowserEdges(double Top, double Right, double Bottom, double Left)
{
public double Horizontal => Left + Right;
public double Vertical => Top + Bottom;
}
public readonly record struct BrowserFont(double Size = 16, string Family = "monospace", int Weight = 400, bool Italic = false);
public interface IBrowserTextMetrics { double Measure(string text, BrowserFont font); }
/// <summary>Host fallback only; native callers provide the actual renderer's font metrics.</summary>
public sealed class BrowserFallbackTextMetrics : IBrowserTextMetrics
{
public double Measure(string text, BrowserFont font)
{
double units = 0;
foreach (char character in text) if (!char.IsLowSurrogate(character)) units += character > 255 ? 1 : .6;
return units * font.Size;
}
}
public sealed record BrowserStyleSheet(string CssText, IElement? Owner = null, string? Source = null);
public enum BrowserDrawKind { Fill, Text, Border, PushClip, PopClip, Canvas, Image }
public sealed record BrowserRoundedClip(BrowserRect Bounds, BrowserEdges Radius, BrowserAffineTransform? Transform = null)
{
internal double Coverage(double x, double y)
{
if (!(Transform ?? BrowserAffineTransform.Identity).TryInverse(out var inverse)) return 0;
var local = inverse.Point(x, y);
return BrowserBoxRasterPainter.Coverage(Bounds, Radius, local.X, local.Y);
}
}
/// <summary>Bounds are CSS pixels in document coordinates. Border commands fill their exact side rectangle.</summary>
public sealed record BrowserDrawCommand(BrowserDrawKind Kind, BrowserRect Bounds, int NodeId, INode? Node,
uint Color = 0, double Opacity = 1, string Text = "", BrowserFont Font = default, BrowserBoxDecoration? Decoration = null, BrowserRoundedClip? RoundedClip = null,
bool ViewportPinned = false, string ImageSource = "", BrowserAffineTransform? Transform = null);
public sealed class BrowserLayoutBox
{
public required INode Node { get; init; }
public required int NodeId { get; init; }
public required CssComputedStyle Style { get; init; }
public BrowserRect ContentBox { get; internal set; }
public BrowserRect PaddingBox { get; internal set; }
public BrowserRect BorderBox { get; internal set; }
public BrowserRect MarginBox { get; internal set; }
public BrowserRect? Clip { get; internal set; }
public bool Visible => !PaintSuppressed && Style.Visibility != "hidden" && Style.Display != "none" && EffectiveOpacity > 0;
public IElement? Element => Node as IElement ?? Node.ParentElement;
public IReadOnlyList<BrowserRect> Fragments => fragments;
internal readonly List<BrowserRect> fragments = [];
internal readonly List<BrowserLayoutBox> children = [];
internal readonly List<BrowserDrawCommand> textCommands = [];
internal readonly List<BrowserTextSourceRun> sourceRuns = [];
internal bool IsLaidOut;
internal BrowserEdges Padding, Border, Margin;
internal CssComputedStyle? MarkerStyle;
public string? GeneratedPseudo {get;internal set;}
internal string? GeneratedText;
internal IReadOnlyList<BrowserRoundedClip>? roundedClips;
internal BrowserLayoutBox? Parent;
internal double EffectiveOpacity = 1;
internal BrowserAffineTransform Transform = BrowserAffineTransform.Identity;
internal bool PaintSuppressed;
internal bool ShrinkToFitWidth;
}
internal sealed record BrowserTextSourceRun(int CommandIndex, int[] Offsets);
public sealed class BrowserDisplayList
{
internal BrowserDisplayList(BrowserViewport viewport, IReadOnlyList<BrowserDrawCommand> commands,
IReadOnlyList<BrowserLayoutBox> boxes, IReadOnlyList<string> diagnostics, double documentWidth, double documentHeight,
IReadOnlyList<BrowserLayoutBox>? allBoxes = null, IBrowserTextMetrics? textMetrics = null)
{
Viewport = viewport; Commands = commands; Boxes = boxes; Diagnostics = diagnostics;
DocumentWidth = documentWidth; DocumentHeight = documentHeight;
metrics = textMetrics ?? new BrowserFallbackTextMetrics();
foreach (var box in allBoxes ?? boxes) if(box.GeneratedPseudo is null)byNode[box.Node] = box;
}
private readonly Dictionary<INode, BrowserLayoutBox> byNode = new(ReferenceEqualityComparer.Instance);
private readonly IBrowserTextMetrics metrics;
internal bool CanReuseForScroll(BrowserViewport target) => !Boxes.Any(box => box.Style.Position == "fixed" ||
box.Style.Position == "sticky" &&
(target.ScrollY != Viewport.ScrollY && (box.Style.Get("top", "auto") != "auto" || box.Style.Get("bottom", "auto") != "auto") ||
target.ScrollX != Viewport.ScrollX && (box.Style.Get("left", "auto") != "auto" || box.Style.Get("right", "auto") != "auto")));
internal BrowserDisplayList WithScroll(BrowserViewport viewport) => new(this, viewport);
private BrowserDisplayList(BrowserDisplayList source, BrowserViewport viewport)
{
if (source.Viewport.Width != viewport.Width || source.Viewport.Height != viewport.Height ||
!double.IsFinite(viewport.ScrollX) || !double.IsFinite(viewport.ScrollY) || !source.CanReuseForScroll(viewport))
throw new ArgumentException("Only viewport scrolling of ordinary flow can reuse this layout.");
Viewport = viewport; Boxes = source.Boxes; Diagnostics = source.Diagnostics;
DocumentWidth = source.DocumentWidth; DocumentHeight = source.DocumentHeight;
metrics = source.metrics; byNode = source.byNode; RangePositions = source.RangePositions;
Commands = source.Commands.Select(command => command.ViewportPinned
? command with { Bounds = new(viewport.ScrollX, viewport.ScrollY, viewport.Width, viewport.Height) }
: command).ToArray();
}
internal Dictionary<INode, (long Start, long End)>? RangePositions;
public BrowserViewport Viewport { get; }
public IReadOnlyList<BrowserDrawCommand> Commands { get; }
public IReadOnlyList<BrowserLayoutBox> Boxes { get; }
public IReadOnlyList<string> Diagnostics { get; }
public double DocumentWidth { get; }
public double DocumentHeight { get; }
public BrowserRect GetBoundingClientRect(INode node) => byNode.TryGetValue(node, out var box)
? box.Transform.Bounds(box.BorderBox).Translate(-Viewport.ScrollX, -Viewport.ScrollY) : default;
public IReadOnlyList<BrowserRect> GetClientRects(INode node) => byNode.TryGetValue(node, out var box)
? box.fragments.Select(rect => box.Transform.Bounds(rect).Translate(-Viewport.ScrollX, -Viewport.ScrollY)).ToArray() : [];
public CssComputedStyle? GetComputedStyle(INode node) => byNode.TryGetValue(node, out var box) ? box.Style : null;
internal int GetNodeIdentity(INode node) => byNode.TryGetValue(node, out var box) ? box.NodeId : 0;
public IReadOnlyList<BrowserRect> GetRangeRects(INode start, int startOffset, INode end, int endOffset) =>
BrowserRangeGeometry.Measure(this, start, startOffset, end, endOffset, metrics);
public IReadOnlyList<BrowserRect> GetRangeRects(INode start, int startOffset, INode end, int endOffset, IBrowserTextMetrics textMetrics) =>
BrowserRangeGeometry.Measure(this, start, startOffset, end, endOffset, textMetrics);
public IReadOnlyList<BrowserRect> GetRangeClientRects(INode start, int startOffset, INode end, int endOffset) => GetRangeRects(start, startOffset, end, endOffset);
private static bool Contains(BrowserLayoutBox box, (double X, double Y) point) => box.BorderBox.Contains(point.X, point.Y);
public BrowserLayoutBox? HitTest(double viewportX, double viewportY)
{
double x = viewportX + Viewport.ScrollX, y = viewportY + Viewport.ScrollY;
for (int index = Boxes.Count - 1; index >= 0; index--)
{
var box = Boxes[index];
if (box.Visible && box.Style.PointerEvents != "none" && box.Transform.TryInverse(out var inverse) && Contains(box, inverse.Point(x, y)) &&
(box.Clip is not { } clip || clip.Contains(x, y)) &&
(box.roundedClips is null || box.roundedClips.All(shape=>shape.Coverage(x,y)>.5))) return box;
}
return null;
}
}
public sealed record BrowserCssLimits
{
public int MaximumNodes { get; init; } = 20000;
public int MaximumDepth { get; init; } = 256;
public int MaximumCssCharacters { get; init; } = 2097152;
// A complete stylesheet set may include thousands of generated-content
// selectors in addition to its normal rules. Keep a finite default large
// enough to parse both; selector work still has its independent budget.
public int MaximumRules { get; init; } = 8192;
public int MaximumDeclarations { get; init; } = 65536;
public int MaximumSelectorEvaluations { get; init; } = 2000000;
public int MaximumDisplayCommands { get; init; } = 100000;
}
using AngleSharp.Dom;
namespace XFESystem.Browser;
public readonly record struct BrowserRect(double X, double Y, double Width, double Height)
{
public double Left => X;
public double Top => Y;
public double Right => X + Width;
public double Bottom => Y + Height;
public bool Contains(double x, double y) => Width > 0 && Height > 0 && x >= X && y >= Y && x < Right && y < Bottom;
public BrowserRect Translate(double x, double y) => new(X + x, Y + y, Width, Height);
public BrowserRect Intersect(BrowserRect other)
{
double x = Math.Max(X, other.X), y = Math.Max(Y, other.Y);
return new(x, y, Math.Max(0, Math.Min(Right, other.Right) - x), Math.Max(0, Math.Min(Bottom, other.Bottom) - y));
}
public BrowserRect Union(BrowserRect other)
{
if (Width <= 0 || Height <= 0) return other;
if (other.Width <= 0 || other.Height <= 0) return this;
double x = Math.Min(X, other.X), y = Math.Min(Y, other.Y);
return new(x, y, Math.Max(Right, other.Right) - x, Math.Max(Bottom, other.Bottom) - y);
}
}
public readonly record struct BrowserViewport(double Width, double Height, double ScrollX = 0, double ScrollY = 0);
/// <summary>Native input state, kept outside DOM attributes. Hover/active include their ancestor chain.</summary>
public sealed record BrowserInteractionState(INode? Hovered = null, INode? Active = null, INode? Focus = null);
public readonly record struct BrowserEdges(double Top, double Right, double Bottom, double Left)
{
public double Horizontal => Left + Right;
public double Vertical => Top + Bottom;
}
public readonly record struct BrowserFont(double Size = 16, string Family = "monospace", int Weight = 400, bool Italic = false);
public interface IBrowserTextMetrics { double Measure(string text, BrowserFont font); }
/// <summary>Host fallback only; native callers provide the actual renderer's font metrics.</summary>
public sealed class BrowserFallbackTextMetrics : IBrowserTextMetrics
{
public double Measure(string text, BrowserFont font)
{
double units = 0;
foreach (char character in text) if (!char.IsLowSurrogate(character)) units += character > 255 ? 1 : .6;
return units * font.Size;
}
}
public sealed record BrowserStyleSheet(string CssText, IElement? Owner = null, string? Source = null);
public enum BrowserDrawKind { Fill, Text, Border, PushClip, PopClip, Canvas, Image }
public sealed record BrowserRoundedClip(BrowserRect Bounds, BrowserEdges Radius, BrowserAffineTransform? Transform = null)
{
internal double Coverage(double x, double y)
{
if (!(Transform ?? BrowserAffineTransform.Identity).TryInverse(out var inverse)) return 0;
var local = inverse.Point(x, y);
return BrowserBoxRasterPainter.Coverage(Bounds, Radius, local.X, local.Y);
}
}
/// <summary>Bounds are CSS pixels in document coordinates. Border commands fill their exact side rectangle.</summary>
public sealed record BrowserDrawCommand(BrowserDrawKind Kind, BrowserRect Bounds, int NodeId, INode? Node,
uint Color = 0, double Opacity = 1, string Text = "", BrowserFont Font = default, BrowserBoxDecoration? Decoration = null, BrowserRoundedClip? RoundedClip = null,
bool ViewportPinned = false, string ImageSource = "", BrowserAffineTransform? Transform = null);
public sealed class BrowserLayoutBox
{
public required INode Node { get; init; }
public required int NodeId { get; init; }
public required CssComputedStyle Style { get; init; }
public BrowserRect ContentBox { get; internal set; }
public BrowserRect PaddingBox { get; internal set; }
public BrowserRect BorderBox { get; internal set; }
public BrowserRect MarginBox { get; internal set; }
public BrowserRect? Clip { get; internal set; }
public bool Visible => !PaintSuppressed && Style.Visibility != "hidden" && Style.Display != "none" && EffectiveOpacity > 0;
public IElement? Element => Node as IElement ?? Node.ParentElement;
public IReadOnlyList<BrowserRect> Fragments => fragments;
internal readonly List<BrowserRect> fragments = [];
internal readonly List<BrowserLayoutBox> children = [];
internal readonly List<BrowserDrawCommand> textCommands = [];
internal readonly List<BrowserTextSourceRun> sourceRuns = [];
internal bool IsLaidOut;
internal BrowserEdges Padding, Border, Margin;
internal CssComputedStyle? MarkerStyle;
public string? GeneratedPseudo {get;internal set;}
internal string? GeneratedText;
internal IReadOnlyList<BrowserRoundedClip>? roundedClips;
internal BrowserLayoutBox? Parent;
internal double EffectiveOpacity = 1;
internal BrowserAffineTransform Transform = BrowserAffineTransform.Identity;
internal bool PaintSuppressed;
internal bool ShrinkToFitWidth;
}
internal sealed record BrowserTextSourceRun(int CommandIndex, int[] Offsets);
public sealed class BrowserDisplayList
{
internal BrowserDisplayList(BrowserViewport viewport, IReadOnlyList<BrowserDrawCommand> commands,
IReadOnlyList<BrowserLayoutBox> boxes, IReadOnlyList<string> diagnostics, double documentWidth, double documentHeight,
IReadOnlyList<BrowserLayoutBox>? allBoxes = null, IBrowserTextMetrics? textMetrics = null)
{
Viewport = viewport; Commands = commands; Boxes = boxes; Diagnostics = diagnostics;
DocumentWidth = documentWidth; DocumentHeight = documentHeight;
metrics = textMetrics ?? new BrowserFallbackTextMetrics();
foreach (var box in allBoxes ?? boxes) if(box.GeneratedPseudo is null)byNode[box.Node] = box;
}
private readonly Dictionary<INode, BrowserLayoutBox> byNode = new(ReferenceEqualityComparer.Instance);
private readonly IBrowserTextMetrics metrics;
internal bool CanReuseForScroll(BrowserViewport target) => !Boxes.Any(box => box.Style.Position == "fixed" ||
box.Style.Position == "sticky" &&
(target.ScrollY != Viewport.ScrollY && (box.Style.Get("top", "auto") != "auto" || box.Style.Get("bottom", "auto") != "auto") ||
target.ScrollX != Viewport.ScrollX && (box.Style.Get("left", "auto") != "auto" || box.Style.Get("right", "auto") != "auto")));
internal BrowserDisplayList WithScroll(BrowserViewport viewport) => new(this, viewport);
private BrowserDisplayList(BrowserDisplayList source, BrowserViewport viewport)
{
if (source.Viewport.Width != viewport.Width || source.Viewport.Height != viewport.Height ||
!double.IsFinite(viewport.ScrollX) || !double.IsFinite(viewport.ScrollY) || !source.CanReuseForScroll(viewport))
throw new ArgumentException("Only viewport scrolling of ordinary flow can reuse this layout.");
Viewport = viewport; Boxes = source.Boxes; Diagnostics = source.Diagnostics;
DocumentWidth = source.DocumentWidth; DocumentHeight = source.DocumentHeight;
metrics = source.metrics; byNode = source.byNode; RangePositions = source.RangePositions;
Commands = source.Commands.Select(command => command.ViewportPinned
? command with { Bounds = new(viewport.ScrollX, viewport.ScrollY, viewport.Width, viewport.Height) }
: command).ToArray();
}
internal Dictionary<INode, (long Start, long End)>? RangePositions;
public BrowserViewport Viewport { get; }
public IReadOnlyList<BrowserDrawCommand> Commands { get; }
public IReadOnlyList<BrowserLayoutBox> Boxes { get; }
public IReadOnlyList<string> Diagnostics { get; }
public double DocumentWidth { get; }
public double DocumentHeight { get; }
public BrowserRect GetBoundingClientRect(INode node) => byNode.TryGetValue(node, out var box)
? box.Transform.Bounds(box.BorderBox).Translate(-Viewport.ScrollX, -Viewport.ScrollY) : default;
public IReadOnlyList<BrowserRect> GetClientRects(INode node) => byNode.TryGetValue(node, out var box)
? box.fragments.Select(rect => box.Transform.Bounds(rect).Translate(-Viewport.ScrollX, -Viewport.ScrollY)).ToArray() : [];
public CssComputedStyle? GetComputedStyle(INode node) => byNode.TryGetValue(node, out var box) ? box.Style : null;
internal int GetNodeIdentity(INode node) => byNode.TryGetValue(node, out var box) ? box.NodeId : 0;
public IReadOnlyList<BrowserRect> GetRangeRects(INode start, int startOffset, INode end, int endOffset) =>
BrowserRangeGeometry.Measure(this, start, startOffset, end, endOffset, metrics);
public IReadOnlyList<BrowserRect> GetRangeRects(INode start, int startOffset, INode end, int endOffset, IBrowserTextMetrics textMetrics) =>
BrowserRangeGeometry.Measure(this, start, startOffset, end, endOffset, textMetrics);
public IReadOnlyList<BrowserRect> GetRangeClientRects(INode start, int startOffset, INode end, int endOffset) => GetRangeRects(start, startOffset, end, endOffset);
private static bool Contains(BrowserLayoutBox box, (double X, double Y) point) => box.BorderBox.Contains(point.X, point.Y);
public BrowserLayoutBox? HitTest(double viewportX, double viewportY)
{
double x = viewportX + Viewport.ScrollX, y = viewportY + Viewport.ScrollY;
for (int index = Boxes.Count - 1; index >= 0; index--)
{
var box = Boxes[index];
if (box.Visible && box.Style.PointerEvents != "none" && box.Transform.TryInverse(out var inverse) && Contains(box, inverse.Point(x, y)) &&
(box.Clip is not { } clip || clip.Contains(x, y)) &&
(box.roundedClips is null || box.roundedClips.All(shape=>shape.Coverage(x,y)>.5))) return box;
}
return null;
}
}
public sealed record BrowserCssLimits
{
public int MaximumNodes { get; init; } = 20000;
public int MaximumDepth { get; init; } = 256;
public int MaximumCssCharacters { get; init; } = 2097152;
// A complete stylesheet set may include thousands of generated-content
// selectors in addition to its normal rules. Keep a finite default large
// enough to parse both; selector work still has its independent budget.
public int MaximumRules { get; init; } = 8192;
public int MaximumDeclarations { get; init; } = 65536;
public int MaximumSelectorEvaluations { get; init; } = 2000000;
public int MaximumDisplayCommands { get; init; } = 100000;
}