using AngleSharp.Dom;
namespace XFESystem.Browser;
/// <summary>DOM UTF-16 boundary offsets mapped to actual laid-out text runs and renderer metrics.</summary>
internal static class BrowserRangeGeometry
{
public static IReadOnlyList<BrowserRect> Measure(BrowserDisplayList list, INode start, int startOffset, INode end, int endOffset, IBrowserTextMetrics metrics)
{
ArgumentNullException.ThrowIfNull(start); ArgumentNullException.ThrowIfNull(end); ArgumentNullException.ThrowIfNull(metrics);
INode root = start; while (root.Parent is not null) root = root.Parent;
INode endRoot = end; while (endRoot.Parent is not null) endRoot = endRoot.Parent;
if (!ReferenceEquals(root, endRoot)) return [];
if (list.Boxes.Count == 0) return [];
INode layoutRoot = list.Boxes[0].Node; while (layoutRoot.Parent is not null) layoutRoot = layoutRoot.Parent;
if (!ReferenceEquals(root, layoutRoot)) return [];
var positions = list.RangePositions ?? new Dictionary<INode, (long Start, long End)>(ReferenceEqualityComparer.Instance); long cursor = 0;
void Visit(INode node)
{
long begin = cursor;
if (node.NodeType == NodeType.Text) cursor += node.TextContent.Length;
else foreach (var child in node.ChildNodes) Visit(child);
positions[node] = (begin, cursor);
}
if (list.RangePositions is null) { Visit(root); list.RangePositions = positions; }
if (!positions.ContainsKey(start) || !positions.ContainsKey(end)) return [];
long Boundary(INode node, int offset)
{
if (offset < 0 || node.NodeType == NodeType.Text && offset > node.TextContent.Length || node.NodeType != NodeType.Text && offset > node.ChildNodes.Length)
throw new ArgumentOutOfRangeException(nameof(offset), "DOM range boundary is outside its container.");
var position = positions[node];
if (node.NodeType == NodeType.Text) return position.Start + offset;
return offset == 0 ? position.Start : positions[node.ChildNodes[offset - 1]].End;
}
long rangeStart = Boundary(start, startOffset), rangeEnd = Boundary(end, endOffset);
if (rangeStart > rangeEnd) return [];
var result = new List<BrowserRect>(); bool collapsed = ReferenceEquals(start, end) && startOffset == endOffset;
BrowserAffineTransform transform = BrowserAffineTransform.Identity;
BrowserRect Client(BrowserRect rectangle) => transform.Bounds(rectangle).Translate(-list.Viewport.ScrollX, -list.Viewport.ScrollY);
foreach (var box in list.Boxes.Where(box => box.Node.NodeType == NodeType.Text && box.IsLaidOut).OrderBy(box => positions[box.Node].Start))
{
transform = box.Transform;
var position = positions[box.Node];
if (position.End < rangeStart || position.Start > rangeEnd) continue;
int localStart = (int)Math.Clamp(rangeStart - position.Start, 0, box.Node.TextContent.Length);
int localEnd = (int)Math.Clamp(rangeEnd - position.Start, 0, box.Node.TextContent.Length);
if (collapsed && !ReferenceEquals(box.Node, start) && start.NodeType == NodeType.Text) continue;
BrowserRect? lastCaret = null;
foreach (var source in box.sourceRuns)
{
var command = box.textCommands[source.CommandIndex]; var offsets = source.Offsets;
if (offsets.Length != command.Text.Length + 1 || offsets[0] == offsets[^1]) continue;
double Width(int count)
{
double value = metrics.Measure(command.Text[..count], command.Font);
if (!double.IsFinite(value) || value < 0 || value > 1000000) throw new BrowserLimitException("Invalid text range metrics.");
return value;
}
int Index(int offset) { int index = 0; while (index < offsets.Length - 1 && offsets[index] < offset) index++; return index; }
if (collapsed)
{
if (localStart < offsets[0]) { result.Add(Client(command.Bounds with { Width = 0 })); return result; }
if (localStart <= offsets[^1])
{
var caret = new BrowserRect(command.Bounds.X + Width(Index(localStart)), command.Bounds.Y, 0, command.Bounds.Height);
if (localStart < offsets[^1]) { result.Add(Client(caret)); return result; }
lastCaret = caret;
}
continue;
}
if (localEnd <= offsets[0] || localStart >= offsets[^1]) continue;
int first = Index(Math.Max(localStart, offsets[0])), last = Index(Math.Min(localEnd, offsets[^1]));
double left = Width(first), right = Width(last);
if (last > first) result.Add(Client(new(command.Bounds.X + left, command.Bounds.Y, Math.Max(0, right - left), command.Bounds.Height)));
}
if (collapsed)
{
if (lastCaret is { } caret) result.Add(Client(caret));
else if (ReferenceEquals(box.Node, start) && box.sourceRuns.Count == 0) result.Add(Client(box.BorderBox with { Width = 0, Height = box.Style.LineHeight }));
if (result.Count > 0) return result;
}
}
return result;
}
}
using AngleSharp.Dom;
namespace XFESystem.Browser;
/// <summary>DOM UTF-16 boundary offsets mapped to actual laid-out text runs and renderer metrics.</summary>
internal static class BrowserRangeGeometry
{
public static IReadOnlyList<BrowserRect> Measure(BrowserDisplayList list, INode start, int startOffset, INode end, int endOffset, IBrowserTextMetrics metrics)
{
ArgumentNullException.ThrowIfNull(start); ArgumentNullException.ThrowIfNull(end); ArgumentNullException.ThrowIfNull(metrics);
INode root = start; while (root.Parent is not null) root = root.Parent;
INode endRoot = end; while (endRoot.Parent is not null) endRoot = endRoot.Parent;
if (!ReferenceEquals(root, endRoot)) return [];
if (list.Boxes.Count == 0) return [];
INode layoutRoot = list.Boxes[0].Node; while (layoutRoot.Parent is not null) layoutRoot = layoutRoot.Parent;
if (!ReferenceEquals(root, layoutRoot)) return [];
var positions = list.RangePositions ?? new Dictionary<INode, (long Start, long End)>(ReferenceEqualityComparer.Instance); long cursor = 0;
void Visit(INode node)
{
long begin = cursor;
if (node.NodeType == NodeType.Text) cursor += node.TextContent.Length;
else foreach (var child in node.ChildNodes) Visit(child);
positions[node] = (begin, cursor);
}
if (list.RangePositions is null) { Visit(root); list.RangePositions = positions; }
if (!positions.ContainsKey(start) || !positions.ContainsKey(end)) return [];
long Boundary(INode node, int offset)
{
if (offset < 0 || node.NodeType == NodeType.Text && offset > node.TextContent.Length || node.NodeType != NodeType.Text && offset > node.ChildNodes.Length)
throw new ArgumentOutOfRangeException(nameof(offset), "DOM range boundary is outside its container.");
var position = positions[node];
if (node.NodeType == NodeType.Text) return position.Start + offset;
return offset == 0 ? position.Start : positions[node.ChildNodes[offset - 1]].End;
}
long rangeStart = Boundary(start, startOffset), rangeEnd = Boundary(end, endOffset);
if (rangeStart > rangeEnd) return [];
var result = new List<BrowserRect>(); bool collapsed = ReferenceEquals(start, end) && startOffset == endOffset;
BrowserAffineTransform transform = BrowserAffineTransform.Identity;
BrowserRect Client(BrowserRect rectangle) => transform.Bounds(rectangle).Translate(-list.Viewport.ScrollX, -list.Viewport.ScrollY);
foreach (var box in list.Boxes.Where(box => box.Node.NodeType == NodeType.Text && box.IsLaidOut).OrderBy(box => positions[box.Node].Start))
{
transform = box.Transform;
var position = positions[box.Node];
if (position.End < rangeStart || position.Start > rangeEnd) continue;
int localStart = (int)Math.Clamp(rangeStart - position.Start, 0, box.Node.TextContent.Length);
int localEnd = (int)Math.Clamp(rangeEnd - position.Start, 0, box.Node.TextContent.Length);
if (collapsed && !ReferenceEquals(box.Node, start) && start.NodeType == NodeType.Text) continue;
BrowserRect? lastCaret = null;
foreach (var source in box.sourceRuns)
{
var command = box.textCommands[source.CommandIndex]; var offsets = source.Offsets;
if (offsets.Length != command.Text.Length + 1 || offsets[0] == offsets[^1]) continue;
double Width(int count)
{
double value = metrics.Measure(command.Text[..count], command.Font);
if (!double.IsFinite(value) || value < 0 || value > 1000000) throw new BrowserLimitException("Invalid text range metrics.");
return value;
}
int Index(int offset) { int index = 0; while (index < offsets.Length - 1 && offsets[index] < offset) index++; return index; }
if (collapsed)
{
if (localStart < offsets[0]) { result.Add(Client(command.Bounds with { Width = 0 })); return result; }
if (localStart <= offsets[^1])
{
var caret = new BrowserRect(command.Bounds.X + Width(Index(localStart)), command.Bounds.Y, 0, command.Bounds.Height);
if (localStart < offsets[^1]) { result.Add(Client(caret)); return result; }
lastCaret = caret;
}
continue;
}
if (localEnd <= offsets[0] || localStart >= offsets[^1]) continue;
int first = Index(Math.Max(localStart, offsets[0])), last = Index(Math.Min(localEnd, offsets[^1]));
double left = Width(first), right = Width(last);
if (last > first) result.Add(Client(new(command.Bounds.X + left, command.Bounds.Y, Math.Max(0, right - left), command.Bounds.Height)));
}
if (collapsed)
{
if (lastCaret is { } caret) result.Add(Client(caret));
else if (ReferenceEquals(box.Node, start) && box.sourceRuns.Count == 0) result.Add(Client(box.BorderBox with { Width = 0, Height = box.Style.LineHeight }));
if (result.Count > 0) return result;
}
}
return result;
}
}