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 { let lowerBound = page.pageStartOffset let upperBound = page.pageEndOffset + 1 return lowerBound..