- 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
178 lines
5.2 KiB
Swift
178 lines
5.2 KiB
Swift
import UIKit
|
|
|
|
struct RDEPUBTextOverlayDecoration {
|
|
|
|
enum Kind: String {
|
|
|
|
case selection
|
|
|
|
case highlight
|
|
|
|
case underline
|
|
|
|
case search
|
|
|
|
case activeSearch
|
|
|
|
case locate
|
|
}
|
|
|
|
var kind: Kind
|
|
|
|
var absoluteRange: NSRange
|
|
|
|
var rects: [CGRect]
|
|
|
|
var color: UIColor
|
|
}
|
|
|
|
class RDEPUBSelectionOverlayView: UIView {
|
|
|
|
private(set) var page: RDEPUBTextPage?
|
|
|
|
private var snapshot: RDEPUBPageLayoutSnapshot?
|
|
|
|
private(set) var selectionRange: NSRange?
|
|
|
|
private var selectionRects: [CGRect] = []
|
|
|
|
private var decorations: [RDEPUBTextOverlayDecoration] = []
|
|
|
|
private let selectionVerticalAdjustment: CGFloat = -1
|
|
|
|
var selectionColor: UIColor = UIColor(red: 70 / 255, green: 140 / 255, blue: 1, alpha: 0.24) {
|
|
didSet {
|
|
setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = .clear
|
|
isOpaque = false
|
|
isUserInteractionEnabled = false
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func configure(page: RDEPUBTextPage, selectionColor: UIColor, snapshot: RDEPUBPageLayoutSnapshot? = nil) {
|
|
self.page = page
|
|
self.snapshot = snapshot
|
|
self.selectionColor = selectionColor
|
|
selectionRange = nil
|
|
decorations = []
|
|
setNeedsDisplay()
|
|
}
|
|
|
|
func updateSnapshot(_ snapshot: RDEPUBPageLayoutSnapshot?) {
|
|
self.snapshot = snapshot
|
|
}
|
|
|
|
func updateSelection(absoluteRange: NSRange?) {
|
|
selectionRange = absoluteRange
|
|
selectionRects = []
|
|
setNeedsDisplay()
|
|
}
|
|
|
|
func updateSelection(absoluteRange: NSRange?, rects: [CGRect]) {
|
|
selectionRange = absoluteRange
|
|
selectionRects = rects
|
|
setNeedsDisplay()
|
|
}
|
|
|
|
func applyDecorations(_ decorations: [RDEPUBTextOverlayDecoration]) {
|
|
self.decorations = decorations
|
|
setNeedsDisplay()
|
|
}
|
|
|
|
func clearSelection() {
|
|
updateSelection(absoluteRange: nil)
|
|
}
|
|
|
|
func absoluteRange(at point: CGPoint) -> NSRange? {
|
|
if let selectionRange,
|
|
selectionRects.contains(where: { $0.insetBy(dx: -4, dy: -4).contains(point) }) {
|
|
return selectionRange
|
|
}
|
|
|
|
if let snapshot,
|
|
let absoluteRange = snapshot.absoluteRange(at: point, in: resolvedDecorations) {
|
|
return absoluteRange
|
|
}
|
|
|
|
for decoration in resolvedDecorations {
|
|
for rect in decoration.rects {
|
|
if rect.insetBy(dx: -4, dy: -4).contains(point) {
|
|
return decoration.absoluteRange
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func decorationSummary() -> String {
|
|
let counts = Dictionary(grouping: resolvedDecorations, by: \.kind).mapValues(\.count)
|
|
let selectionLabel = selectionRange.map(NSStringFromRange) ?? "none"
|
|
let highlightCount = counts[.highlight, default: 0]
|
|
let underlineCount = counts[.underline, default: 0]
|
|
let searchCount = counts[.search, default: 0]
|
|
let activeSearchCount = counts[.activeSearch, default: 0]
|
|
let locateLabel = resolvedDecorations.first(where: { $0.kind == .locate }).map { NSStringFromRange($0.absoluteRange) } ?? "none"
|
|
return [
|
|
"selection \(selectionLabel)",
|
|
"highlight \(highlightCount)",
|
|
"underline \(underlineCount)",
|
|
"search \(searchCount)",
|
|
"activeSearch \(activeSearchCount)",
|
|
"locate \(locateLabel)"
|
|
].joined(separator: " · ")
|
|
}
|
|
|
|
override func draw(_ rect: CGRect) {
|
|
guard let context = UIGraphicsGetCurrentContext() else { return }
|
|
|
|
for decoration in resolvedDecorations {
|
|
switch decoration.kind {
|
|
case .underline:
|
|
context.setStrokeColor(decoration.color.cgColor)
|
|
context.setLineWidth(2)
|
|
for underlineRect in decoration.rects {
|
|
let y = underlineRect.maxY - 1
|
|
context.move(to: CGPoint(x: underlineRect.minX, y: y))
|
|
context.addLine(to: CGPoint(x: underlineRect.maxX, y: y))
|
|
context.strokePath()
|
|
}
|
|
default:
|
|
context.setFillColor(decoration.color.cgColor)
|
|
for selectionRect in decoration.rects {
|
|
let adjustedRect = selectionRect.offsetBy(dx: 0, dy: selectionVerticalAdjustment)
|
|
let path = UIBezierPath(roundedRect: adjustedRect.insetBy(dx: -1, dy: -1), cornerRadius: 4)
|
|
context.addPath(path.cgPath)
|
|
context.fillPath()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var resolvedDecorations: [RDEPUBTextOverlayDecoration] {
|
|
guard page != nil else { return [] }
|
|
|
|
var result = decorations.filter { !$0.rects.isEmpty }
|
|
|
|
if let selectionRange, !selectionRects.isEmpty {
|
|
result.append(
|
|
RDEPUBTextOverlayDecoration(
|
|
kind: .selection,
|
|
absoluteRange: selectionRange,
|
|
rects: selectionRects,
|
|
color: selectionColor
|
|
)
|
|
)
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|