using AngleSharp.Dom; namespace XFESystem.Browser; /// DOM UTF-16 boundary offsets mapped to actual laid-out text runs and renderer metrics. internal static class BrowserRangeGeometry { public static IReadOnlyList 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(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(); bool collapsed = ReferenceEquals(start, end) && startOffset == endOffset; BrowserRect Client(BrowserRect rectangle) => 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)) { 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; } }