137 lines
6.3 KiB
Swift
137 lines
6.3 KiB
Swift
import UIKit
|
||
|
||
/// 前景覆盖层,负责绘制高亮、搜索命中和当前选区。
|
||
final class RDEPUBTextAnnotationOverlay: RDEPUBSelectionOverlayView {
|
||
func applyHighlights(
|
||
_ highlights: [RDEPUBHighlight],
|
||
to content: NSMutableAttributedString,
|
||
page: RDEPUBTextPage,
|
||
contentBaseOffset: Int
|
||
) {
|
||
let pageRange = absoluteOffsetRange(for: page)
|
||
let pageStart = pageRange.lowerBound
|
||
let pageEndExclusive = pageRange.upperBound
|
||
|
||
for highlight in highlights where highlight.location.href == page.href {
|
||
guard let range = RDEPUBTextOffsetRangeInfo.decode(from: highlight.rangeInfo)?.nsRange else { continue }
|
||
let overlapStart = max(range.location, pageStart)
|
||
let overlapEnd = min(range.location + range.length, pageEndExclusive)
|
||
guard overlapStart < overlapEnd else { continue }
|
||
|
||
let relativeRange = NSRange(
|
||
location: overlapStart - contentBaseOffset,
|
||
length: overlapEnd - overlapStart
|
||
)
|
||
switch highlight.style {
|
||
case .highlight:
|
||
content.addAttribute(
|
||
.backgroundColor,
|
||
value: UIColor(rdHexString: highlight.color, alpha: 0.45) ?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.45),
|
||
range: relativeRange
|
||
)
|
||
case .underline:
|
||
content.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: relativeRange)
|
||
if let color = UIColor(rdHexString: highlight.color, alpha: 1) {
|
||
content.addAttribute(.underlineColor, value: color, range: relativeRange)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func applySearchHighlights(
|
||
to content: NSMutableAttributedString,
|
||
page: RDEPUBTextPage,
|
||
searchState: RDEPUBSearchState?,
|
||
contentBaseOffset: Int
|
||
) {
|
||
guard let searchState else { return }
|
||
|
||
let normalColor = UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.55)
|
||
let activeColor = UIColor(red: 255 / 255, green: 159 / 255, blue: 67 / 255, alpha: 0.75)
|
||
let pageRange = absoluteOffsetRange(for: page)
|
||
let pageStart = pageRange.lowerBound
|
||
let pageEndExclusive = pageRange.upperBound
|
||
|
||
for match in searchState.matches where match.href == page.href {
|
||
guard let matchStart = match.rangeLocation else { continue }
|
||
let matchEnd = matchStart + match.rangeLength
|
||
let overlapStart = max(matchStart, pageStart)
|
||
let overlapEnd = min(matchEnd, pageEndExclusive)
|
||
guard overlapStart < overlapEnd else { continue }
|
||
|
||
let relativeRange = NSRange(location: Int(overlapStart - contentBaseOffset), length: Int(overlapEnd - overlapStart))
|
||
let color = match == searchState.currentMatch ? activeColor : normalColor
|
||
content.addAttribute(.backgroundColor, value: color, range: relativeRange)
|
||
}
|
||
}
|
||
|
||
func buildDecorations(
|
||
page: RDEPUBTextPage,
|
||
highlights: [RDEPUBHighlight],
|
||
searchState: RDEPUBSearchState?,
|
||
interactionController: RDEPUBPageInteractionController
|
||
) -> (background: [RDEPUBTextOverlayDecoration], foreground: [RDEPUBTextOverlayDecoration]) {
|
||
var background: [RDEPUBTextOverlayDecoration] = []
|
||
var foreground: [RDEPUBTextOverlayDecoration] = []
|
||
let pageRange = absoluteOffsetRange(for: page)
|
||
let pageStart = pageRange.lowerBound
|
||
let pageEndExclusive = pageRange.upperBound
|
||
|
||
if let searchState {
|
||
let normalColor = UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.55)
|
||
let activeColor = UIColor(red: 255 / 255, green: 159 / 255, blue: 67 / 255, alpha: 0.75)
|
||
|
||
for match in searchState.matches where match.href == page.href {
|
||
guard let matchStart = match.rangeLocation else { continue }
|
||
let matchEnd = matchStart + match.rangeLength
|
||
let overlapStart = max(matchStart, pageStart)
|
||
let overlapEnd = min(matchEnd, pageEndExclusive)
|
||
guard overlapStart < overlapEnd else { continue }
|
||
|
||
let absoluteRange = NSRange(location: overlapStart, length: overlapEnd - overlapStart)
|
||
let rects = interactionController.selectionRects(for: absoluteRange)
|
||
guard !rects.isEmpty else { continue }
|
||
|
||
let isActive = match == searchState.currentMatch
|
||
let kind: RDEPUBTextOverlayDecoration.Kind = isActive ? .activeSearch : .search
|
||
let color = isActive ? activeColor : normalColor
|
||
background.append(RDEPUBTextOverlayDecoration(kind: kind, absoluteRange: absoluteRange, rects: rects, color: color))
|
||
}
|
||
}
|
||
|
||
for highlight in highlights where highlight.location.href == page.href {
|
||
guard let range = RDEPUBTextOffsetRangeInfo.decode(from: highlight.rangeInfo)?.nsRange else { continue }
|
||
let overlapStart = max(range.location, pageStart)
|
||
let overlapEnd = min(range.location + range.length, pageEndExclusive)
|
||
guard overlapStart < overlapEnd else { continue }
|
||
|
||
let absoluteRange = NSRange(location: overlapStart, length: overlapEnd - overlapStart)
|
||
let rects = interactionController.selectionRects(for: absoluteRange)
|
||
guard !rects.isEmpty else { continue }
|
||
|
||
let color = UIColor(rdHexString: highlight.color, alpha: 0.45)
|
||
?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.45)
|
||
let decoration = RDEPUBTextOverlayDecoration(
|
||
kind: highlight.style == .underline ? .underline : .highlight,
|
||
absoluteRange: absoluteRange,
|
||
rects: rects,
|
||
color: color
|
||
)
|
||
|
||
if decoration.kind == .underline {
|
||
foreground.append(decoration)
|
||
} else {
|
||
background.append(decoration)
|
||
}
|
||
}
|
||
|
||
return (background, foreground)
|
||
}
|
||
|
||
private func absoluteOffsetRange(for page: RDEPUBTextPage) -> Range<Int> {
|
||
let lowerBound = page.pageStartOffset
|
||
let upperBound = page.pageEndOffset + 1
|
||
return lowerBound..<max(upperBound, lowerBound)
|
||
}
|
||
}
|