Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions Sources/MarkdownEngine/Renderer/MarkdownTextLayoutFragment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ final class MarkdownTextLayoutFragment: NSTextLayoutFragment {
height: snappedMaxY - snappedY
)

let selectionRects = selectionRectsInDrawCoordinates(drawPoint: point, snappedY: snappedY, snappedMaxY: snappedMaxY)
let selectionRects = selectionRectsInDrawCoordinates(drawPoint: point, scale: scale)
color.setFill()
if selectionRects.isEmpty {
NSBezierPath(rect: bgRect).fill()
Expand All @@ -253,11 +253,10 @@ final class MarkdownTextLayoutFragment: NSTextLayoutFragment {

/// Returns active text-selection rectangles intersecting this fragment, in
/// the same draw-relative coordinate system used by `drawCodeBlockBackground`.
private func selectionRectsInDrawCoordinates(drawPoint: CGPoint, snappedY: CGFloat, snappedMaxY: CGFloat) -> [CGRect] {
private func selectionRectsInDrawCoordinates(drawPoint: CGPoint, scale: CGFloat) -> [CGRect] {
guard let tlm = textLayoutManager else { return [] }
var rects: [CGRect] = []

let dx = drawPoint.x - layoutFragmentFrame.origin.x
let myRange = self.rangeInElement

for selection in tlm.textSelections {
Expand All @@ -270,22 +269,37 @@ final class MarkdownTextLayoutFragment: NSTextLayoutFragment {
let intersection = NSTextRange(location: interStart, end: interEnd) else { continue }

tlm.enumerateTextSegments(in: intersection, type: .selection, options: []) { _, segFrame, _, _ in
// Expand vertically to match the bgRect's snapped span so the
// even-odd cut-out is geometrically congruent with the fill.
let drawRect = CGRect(
x: segFrame.origin.x + dx,
y: snappedY,
width: segFrame.width,
height: snappedMaxY - snappedY
)
rects.append(drawRect)
rects.append(Self.selectionDrawRect(
segmentFrame: segFrame,
drawPoint: drawPoint,
fragmentFrame: layoutFragmentFrame,
scale: scale
))
return true
}
}
}
return rects
}

static func selectionDrawRect(
segmentFrame: CGRect,
drawPoint: CGPoint,
fragmentFrame: CGRect,
scale: CGFloat
) -> CGRect {
let dx = drawPoint.x - fragmentFrame.minX
let dy = drawPoint.y - fragmentFrame.minY
let minY = floor((segmentFrame.minY + dy) * scale) / scale
let maxY = ceil((segmentFrame.maxY + dy) * scale) / scale
return CGRect(
x: segmentFrame.minX + dx,
y: minY,
width: segmentFrame.width,
height: maxY - minY
)
}

private func isCodeBlockBackgroundColor(_ color: NSColor) -> Bool {
let highlighter = (textLayoutManager?.textContainer?.textView as? NativeTextView)?
.configuration.services.syntaxHighlighter
Expand Down
31 changes: 31 additions & 0 deletions Tests/MarkdownEngineTests/SelectionGeometryTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import AppKit
import Testing
@testable import MarkdownEngine

@Suite("Selection geometry")
struct SelectionGeometryTests {
@Test("wrapped-line selection rectangles stay line-local")
func wrappedLineSelectionStaysLineLocal() {
let fragmentFrame = CGRect(x: 20, y: 100, width: 300, height: 60)
let drawPoint = CGPoint(x: 5, y: 10)
let firstSegment = CGRect(x: 40, y: 105, width: 120, height: 18)
let secondSegment = CGRect(x: 60, y: 125, width: 90, height: 18)

let first = MarkdownTextLayoutFragment.selectionDrawRect(
segmentFrame: firstSegment,
drawPoint: drawPoint,
fragmentFrame: fragmentFrame,
scale: 2
)
let second = MarkdownTextLayoutFragment.selectionDrawRect(
segmentFrame: secondSegment,
drawPoint: drawPoint,
fragmentFrame: fragmentFrame,
scale: 2
)

#expect(first.maxY <= second.minY)
#expect(first.height == 18)
#expect(second.height == 18)
}
}