249 lines
9.1 KiB
Swift
249 lines
9.1 KiB
Swift
import UIKit
|
|
|
|
final class RDEPUBTextContentInteractionCoordinator: NSObject {
|
|
|
|
enum SelectionInteractionState: Equatable {
|
|
case idle
|
|
case selectionPending
|
|
case selecting
|
|
case selectionActive
|
|
case adjustingHandle
|
|
}
|
|
|
|
struct Dependencies {
|
|
let hasRenderableContent: () -> Bool
|
|
let currentSelectionProvider: () -> RDEPUBSelection?
|
|
let isSelectionControllerSelecting: () -> Bool
|
|
let hasActiveSelection: () -> Bool
|
|
let selectionHandleAtPoint: (CGPoint) -> RDEPUBTextSelectionController.BoundaryHandle?
|
|
let selectionContainsPoint: (CGPoint) -> Bool
|
|
let renderPointForGesture: (UIGestureRecognizer) -> CGPoint
|
|
let renderPointForTouch: (UITouch) -> CGPoint
|
|
let performLongPressSelection: (UILongPressGestureRecognizer) -> Void
|
|
let performPanSelection: (UIPanGestureRecognizer) -> Void
|
|
let adjustSelection: (RDEPUBTextSelectionController.BoundaryHandle, CGPoint) -> Void
|
|
let presentLoupeAtPoint: (CGPoint) -> Void
|
|
let dismissLoupe: () -> Void
|
|
let showSelectionMenu: () -> Void
|
|
let hideSelectionMenu: () -> Void
|
|
let selectionTapSuppressionDidChange: (Bool) -> Void
|
|
let selectionPagingSuppressionDidChange: (Bool) -> Void
|
|
}
|
|
|
|
private let dependencies: Dependencies
|
|
|
|
private var activeSelectionHandle: RDEPUBTextSelectionController.BoundaryHandle?
|
|
|
|
private(set) var interactionState: SelectionInteractionState = .idle
|
|
|
|
var isInteractionInProgress: Bool {
|
|
interactionState != .idle
|
|
}
|
|
|
|
init(dependencies: Dependencies) {
|
|
self.dependencies = dependencies
|
|
super.init()
|
|
}
|
|
|
|
func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
|
|
dependencies.performLongPressSelection(gesture)
|
|
|
|
switch gesture.state {
|
|
case .began:
|
|
activeSelectionHandle = nil
|
|
updateSelectionInteractionState(.selecting)
|
|
dependencies.hideSelectionMenu()
|
|
dependencies.presentLoupeAtPoint(dependencies.renderPointForGesture(gesture))
|
|
case .changed:
|
|
updateSelectionInteractionState(.selecting)
|
|
dependencies.presentLoupeAtPoint(dependencies.renderPointForGesture(gesture))
|
|
case .ended:
|
|
activeSelectionHandle = nil
|
|
dependencies.dismissLoupe()
|
|
dependencies.showSelectionMenu()
|
|
case .cancelled, .failed:
|
|
activeSelectionHandle = nil
|
|
dependencies.dismissLoupe()
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func handlePan(_ gesture: UIPanGestureRecognizer) {
|
|
let point = dependencies.renderPointForGesture(gesture)
|
|
|
|
if gesture.state == .began, activeSelectionHandle == nil,
|
|
let handle = dependencies.selectionHandleAtPoint(point) {
|
|
activeSelectionHandle = handle
|
|
updateSelectionInteractionState(.adjustingHandle)
|
|
dependencies.hideSelectionMenu()
|
|
dependencies.presentLoupeAtPoint(point)
|
|
}
|
|
|
|
if let activeSelectionHandle {
|
|
dependencies.adjustSelection(activeSelectionHandle, point)
|
|
dependencies.presentLoupeAtPoint(point)
|
|
} else {
|
|
dependencies.performPanSelection(gesture)
|
|
if dependencies.isSelectionControllerSelecting() {
|
|
dependencies.presentLoupeAtPoint(point)
|
|
}
|
|
}
|
|
|
|
switch gesture.state {
|
|
case .ended:
|
|
activeSelectionHandle = nil
|
|
updateSelectionInteractionState(
|
|
dependencies.currentSelectionProvider() == nil ? .idle : .selectionActive
|
|
)
|
|
dependencies.dismissLoupe()
|
|
dependencies.showSelectionMenu()
|
|
case .cancelled, .failed:
|
|
activeSelectionHandle = nil
|
|
updateSelectionInteractionState(
|
|
dependencies.currentSelectionProvider() == nil ? .idle : .selectionActive
|
|
)
|
|
dependencies.dismissLoupe()
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func selectionControllerStateDidChange(_ state: RDEPUBTextSelectionController.InteractionState) {
|
|
switch state {
|
|
case .idle:
|
|
if dependencies.currentSelectionProvider() == nil, activeSelectionHandle == nil {
|
|
updateSelectionInteractionState(.idle)
|
|
}
|
|
case .selecting:
|
|
updateSelectionInteractionState(.selecting)
|
|
case .selectionActive:
|
|
updateSelectionInteractionState(
|
|
dependencies.currentSelectionProvider() == nil ? .idle : .selectionActive
|
|
)
|
|
case .adjustingHandle:
|
|
updateSelectionInteractionState(.adjustingHandle)
|
|
}
|
|
}
|
|
|
|
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
guard interactionState == .idle,
|
|
activeSelectionHandle == nil,
|
|
dependencies.hasRenderableContent(),
|
|
let touch = touches.first else {
|
|
return
|
|
}
|
|
|
|
let point = dependencies.renderPointForTouch(touch)
|
|
guard dependencies.selectionHandleAtPoint(point) == nil else { return }
|
|
updateSelectionInteractionState(.selectionPending)
|
|
}
|
|
|
|
func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
resetSelectionPendingIfNeeded()
|
|
}
|
|
|
|
func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
resetSelectionPendingIfNeeded()
|
|
}
|
|
|
|
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
if gestureRecognizer is UIPanGestureRecognizer {
|
|
if dependencies.isSelectionControllerSelecting() {
|
|
return true
|
|
}
|
|
let point = dependencies.renderPointForGesture(gestureRecognizer)
|
|
return dependencies.selectionHandleAtPoint(point) != nil
|
|
}
|
|
|
|
if gestureRecognizer is UILongPressGestureRecognizer {
|
|
guard dependencies.hasRenderableContent() else {
|
|
return true
|
|
}
|
|
let point = dependencies.renderPointForGesture(gestureRecognizer)
|
|
if dependencies.selectionHandleAtPoint(point) != nil {
|
|
return false
|
|
}
|
|
if dependencies.hasActiveSelection(), dependencies.selectionContainsPoint(point) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
guard dependencies.hasRenderableContent() else {
|
|
return true
|
|
}
|
|
|
|
let point = dependencies.renderPointForTouch(touch)
|
|
if let handle = dependencies.selectionHandleAtPoint(point) {
|
|
if gestureRecognizer is UIPanGestureRecognizer {
|
|
activeSelectionHandle = handle
|
|
updateSelectionInteractionState(.adjustingHandle)
|
|
dependencies.hideSelectionMenu()
|
|
return true
|
|
}
|
|
if gestureRecognizer is UILongPressGestureRecognizer || gestureRecognizer is UITapGestureRecognizer {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func gestureRecognizer(
|
|
_ gestureRecognizer: UIGestureRecognizer,
|
|
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
|
|
) -> Bool {
|
|
gestureRecognizer is UILongPressGestureRecognizer || gestureRecognizer is UIPanGestureRecognizer
|
|
}
|
|
|
|
func reset() {
|
|
activeSelectionHandle = nil
|
|
updateSelectionInteractionState(.idle)
|
|
dependencies.dismissLoupe()
|
|
}
|
|
|
|
private func resetSelectionPendingIfNeeded() {
|
|
guard interactionState == .selectionPending else { return }
|
|
if dependencies.currentSelectionProvider() != nil {
|
|
updateSelectionInteractionState(.selectionActive)
|
|
} else {
|
|
updateSelectionInteractionState(.idle)
|
|
}
|
|
}
|
|
|
|
private func updateSelectionInteractionState(_ state: SelectionInteractionState) {
|
|
let previousTapSuppressed = interactionState != .idle
|
|
let previousPagingSuppressed = shouldSuppressPagingInteraction(for: interactionState)
|
|
let previousState = interactionState
|
|
interactionState = state
|
|
let currentTapSuppressed = interactionState != .idle
|
|
let currentPagingSuppressed = shouldSuppressPagingInteraction(for: interactionState)
|
|
if previousState != state {
|
|
RDReaderTapDebug.log(
|
|
"TextContentInteraction.state",
|
|
"transition \(previousState) -> \(state) tapSuppressed=\(currentTapSuppressed) pagingSuppressed=\(currentPagingSuppressed)"
|
|
)
|
|
}
|
|
if previousTapSuppressed != currentTapSuppressed {
|
|
dependencies.selectionTapSuppressionDidChange(currentTapSuppressed)
|
|
}
|
|
if previousPagingSuppressed != currentPagingSuppressed {
|
|
dependencies.selectionPagingSuppressionDidChange(currentPagingSuppressed)
|
|
}
|
|
}
|
|
|
|
private func shouldSuppressPagingInteraction(for state: SelectionInteractionState) -> Bool {
|
|
switch state {
|
|
case .idle, .selectionPending, .selectionActive:
|
|
return false
|
|
case .selecting, .adjustingHandle:
|
|
return true
|
|
}
|
|
}
|
|
}
|