- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
136 lines
6.0 KiB
Swift
136 lines
6.0 KiB
Swift
import UIKit
|
|
|
|
final class RDEPUBTextAnnotationOverlay: RDEPUBSelectionOverlayView {
|
|
|
|
private let normalSearchColor = UIColor(red: 0.21, green: 0.48, blue: 0.95, alpha: 0.16)
|
|
|
|
private let activeSearchColor = UIColor(red: 0.14, green: 0.42, blue: 0.95, alpha: 0.34)
|
|
|
|
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.35) ?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.35),
|
|
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 pageRange = absoluteOffsetRange(for: page)
|
|
let pageStart = pageRange.lowerBound
|
|
let pageEndExclusive = pageRange.upperBound
|
|
|
|
for match in searchState.matches {
|
|
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 ? activeSearchColor : normalSearchColor
|
|
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 {
|
|
for match in searchState.matches {
|
|
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 ? activeSearchColor : normalSearchColor
|
|
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.35)
|
|
?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.35)
|
|
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)
|
|
}
|
|
}
|