diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 000000000..8890377a1 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,105 @@ +{ + "originHash" : "6c57d05a93b1a6b24af759f2ad90ce6166f484f12c6d5f744324fed58e7e684d", + "pins" : [ + { + "identity" : "cryptoswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", + "state" : { + "revision" : "729e01bc9b9dab466ac85f21fb9ee2bc1c61b258", + "version" : "1.8.4" + } + }, + { + "identity" : "differencekit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ra1028/DifferenceKit.git", + "state" : { + "revision" : "073b9671ce2b9b5b96398611427a1f929927e428", + "version" : "1.3.0" + } + }, + { + "identity" : "fuzi", + "kind" : "remoteSourceControl", + "location" : "https://github.com/readium/Fuzi.git", + "state" : { + "revision" : "347aab158ff8894966ff80469b384bb5337928cd", + "version" : "4.0.0" + } + }, + { + "identity" : "gcdwebserver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/readium/GCDWebServer.git", + "state" : { + "revision" : "584db89a4c3c3be27206cce6afde037b2b6e38d8", + "version" : "4.0.1" + } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/GRDB.swift.git", + "state" : { + "revision" : "2cf6c756e1e5ef6901ebae16576a7e4e4b834622", + "version" : "6.29.3" + } + }, + { + "identity" : "kingfisher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Kingfisher.git", + "state" : { + "revision" : "1a0c2df04b31ed7aa318354f3583faea24f006fc", + "version" : "5.15.8" + } + }, + { + "identity" : "mbprogresshud", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jdg/MBProgressHUD.git", + "state" : { + "revision" : "bca42b801100b2b3a4eda0ba8dd33d858c780b0d", + "version" : "1.2.0" + } + }, + { + "identity" : "sqlite.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stephencelis/SQLite.swift.git", + "state" : { + "revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8", + "version" : "0.15.3" + } + }, + { + "identity" : "swiftsoup", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scinfu/SwiftSoup.git", + "state" : { + "revision" : "bba848db50462894e7fc0891d018dfecad4ef11e", + "version" : "2.8.7" + } + }, + { + "identity" : "zip", + "kind" : "remoteSourceControl", + "location" : "https://github.com/marmelroy/Zip.git", + "state" : { + "revision" : "67fa55813b9e7b3b9acee9c0ae501def28746d76", + "version" : "2.1.2" + } + }, + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/readium/ZIPFoundation.git", + "state" : { + "revision" : "175c389832d90cb0e992b2cb9d5d7878eccfe725", + "version" : "3.0.0" + } + } + ], + "version" : 3 +} diff --git a/Sources/Navigator/DirectionalNavigationAdapter.swift b/Sources/Navigator/DirectionalNavigationAdapter.swift index ba057238e..bfa1a574c 100644 --- a/Sources/Navigator/DirectionalNavigationAdapter.swift +++ b/Sources/Navigator/DirectionalNavigationAdapter.swift @@ -13,15 +13,18 @@ import Foundation /// This takes into account the reading progression of the navigator to turn /// pages in the right direction. public final class DirectionalNavigationAdapter { - /// Indicates which viewport edges trigger page turns on tap. - public struct TapEdges: OptionSet { + @available(*, deprecated, renamed: "Edges") + public typealias TapEdges = Edges + + /// Indicates which viewport edges trigger page turns on pointer activation. + public struct Edges: OptionSet { /// The user can turn pages when tapping on the edges of both the /// horizontal and vertical axes. - public static let all: TapEdges = [.horizontal, .vertical] + public static let all: Edges = [.horizontal, .vertical] /// The user can turn pages when tapping on the left and right edges. - public static let horizontal = TapEdges(rawValue: 1 << 0) + public static let horizontal = Edges(rawValue: 1 << 0) /// The user can turn pages when tapping on the top and bottom edges. - public static let vertical = TapEdges(rawValue: 1 << 1) + public static let vertical = Edges(rawValue: 1 << 1) public var rawValue: Int @@ -30,12 +33,72 @@ public final class DirectionalNavigationAdapter { } } - private let tapEdges: TapEdges - private let handleTapsWhileScrolling: Bool - private let minimumHorizontalEdgeSize: Double - private let horizontalEdgeThresholdPercent: Double? - private let minimumVerticalEdgeSize: Double - private let verticalEdgeThresholdPercent: Double? + public struct PointerPolicy { + /// The types of pointer that will trigger page turns. + public var types: [PointerType] + + /// Indicates which viewport edges recognize pointer activation. + public var edges: Edges + + /// Indicates whether to ignore pointer events when the publication is + /// scrollable. + public var ignoreWhileScrolling: Bool + + /// The minimum horizontal edge dimension that triggers page turns, in + /// pixels. + public var minimumHorizontalEdgeSize: Double + + /// The percentage of the viewport dimension used to calculate the + /// horizontal edge size. If it is nil, a fixed edge of + /// `minimumHorizontalEdgeSize` will be used instead. + public var horizontalEdgeThresholdPercent: Double? + + /// The minimum vertical edge dimension that triggers page turns, in + /// pixels. + public var minimumVerticalEdgeSize: Double + + /// The percentage of the viewport dimension used to calculate the + /// vertical edge size. If it is nil, a fixed edge of + /// `minimumVerticalEdgeSize` will be used instead. + public var verticalEdgeThresholdPercent: Double? + + public init( + types: [PointerType] = [.touch, .mouse], + edges: Edges = .horizontal, + ignoreWhileScrolling: Bool = true, + minimumHorizontalEdgeSize: Double = 80.0, + horizontalEdgeThresholdPercent: Double? = 0.3, + minimumVerticalEdgeSize: Double = 80.0, + verticalEdgeThresholdPercent: Double? = 0.3 + ) { + self.types = types + self.edges = edges + self.ignoreWhileScrolling = ignoreWhileScrolling + self.minimumHorizontalEdgeSize = minimumHorizontalEdgeSize + self.horizontalEdgeThresholdPercent = horizontalEdgeThresholdPercent + self.minimumVerticalEdgeSize = minimumVerticalEdgeSize + self.verticalEdgeThresholdPercent = verticalEdgeThresholdPercent + } + } + + public struct KeyboardPolicy { + /// Indicates whether arrow keys should turn pages. + public var handleArrowKeys: Bool + + /// Indicates whether the space key should turn the page forward. + public var handleSpaceKey: Bool + + public init( + handleArrowKeys: Bool = true, + handleSpaceKey: Bool = true + ) { + self.handleArrowKeys = handleArrowKeys + self.handleSpaceKey = handleSpaceKey + } + } + + private let pointerPolicy: PointerPolicy + private let keyboardPolicy: KeyboardPolicy private let animatedTransition: Bool private weak var navigator: VisualNavigator? @@ -43,36 +106,17 @@ public final class DirectionalNavigationAdapter { /// Initializes a new `DirectionalNavigationAdapter`. /// /// - Parameters: - /// - tapEdges: Indicates which viewport edges handle taps. - /// - handleTapsWhileScrolling: Indicates whether the page turns should be - /// handled when the publication is scrollable. - /// - minimumHorizontalEdgeSize: The minimum horizontal edge dimension - /// triggering page turns, in pixels. - /// - horizontalEdgeThresholdPercent: The percentage of the viewport - /// dimension used to compute the horizontal edge size. When null, - /// `minimumHorizontalEdgeSize` will be used instead. - /// - minimumVerticalEdgeSize: The minimum vertical edge dimension - /// triggering page turns, in pixels. - /// - verticalEdgeThresholdPercent: The percentage of the viewport - /// dimension used to compute the vertical edge size. When null, - /// `minimumVerticalEdgeSize` will be used instead. + /// - pointerPolicy: Policy on page turns using pointers (touches, mouse). + /// - keyboardPolicy: Policy on page turns using the keyboard. /// - animatedTransition: Indicates whether the page turns should be /// animated. public init( - tapEdges: TapEdges = .horizontal, - handleTapsWhileScrolling: Bool = false, - minimumHorizontalEdgeSize: Double = 80.0, - horizontalEdgeThresholdPercent: Double? = 0.3, - minimumVerticalEdgeSize: Double = 80.0, - verticalEdgeThresholdPercent: Double? = 0.3, + pointerPolicy: PointerPolicy = PointerPolicy(), + keyboardPolicy: KeyboardPolicy = KeyboardPolicy(), animatedTransition: Bool = false ) { - self.tapEdges = tapEdges - self.handleTapsWhileScrolling = handleTapsWhileScrolling - self.minimumHorizontalEdgeSize = minimumHorizontalEdgeSize - self.horizontalEdgeThresholdPercent = horizontalEdgeThresholdPercent - self.minimumVerticalEdgeSize = minimumVerticalEdgeSize - self.verticalEdgeThresholdPercent = verticalEdgeThresholdPercent + self.pointerPolicy = pointerPolicy + self.keyboardPolicy = keyboardPolicy self.animatedTransition = animatedTransition } @@ -80,12 +124,28 @@ public final class DirectionalNavigationAdapter { /// /// It will automatically observe pointer and key events to turn pages. @MainActor public func bind(to navigator: VisualNavigator) { - navigator.addObserver(.tap { [self, weak navigator] event in - guard let navigator = navigator else { - return false + for pointerType in PointerType.allCases { + guard pointerPolicy.types.contains(pointerType) else { + continue } - return await onTap(at: event.location, in: navigator) - }) + + switch pointerType { + case .touch: + navigator.addObserver(.tap { [self, weak navigator] event in + guard let navigator = navigator else { + return false + } + return await onTap(at: event.location, in: navigator) + }) + case .mouse: + navigator.addObserver(.click { [self, weak navigator] event in + guard let navigator = navigator else { + return false + } + return await onTap(at: event.location, in: navigator) + }) + } + } navigator.addObserver(.key { [self, weak navigator] event in guard let navigator = navigator else { @@ -97,17 +157,17 @@ public final class DirectionalNavigationAdapter { @MainActor private func onTap(at point: CGPoint, in navigator: VisualNavigator) async -> Bool { - guard handleTapsWhileScrolling || !navigator.presentation.scroll else { + guard !pointerPolicy.ignoreWhileScrolling || !navigator.presentation.scroll else { return false } let bounds = navigator.view.bounds let options = NavigatorGoOptions(animated: animatedTransition) - if tapEdges.contains(.horizontal) { - let horizontalEdgeSize = horizontalEdgeThresholdPercent - .map { max(minimumHorizontalEdgeSize, $0 * bounds.width) } - ?? minimumHorizontalEdgeSize + if pointerPolicy.edges.contains(.horizontal) { + let horizontalEdgeSize = pointerPolicy.horizontalEdgeThresholdPercent + .map { max(pointerPolicy.minimumHorizontalEdgeSize, $0 * bounds.width) } + ?? pointerPolicy.minimumHorizontalEdgeSize let leftRange = 0.0 ... horizontalEdgeSize let rightRange = (bounds.width - horizontalEdgeSize) ... bounds.width @@ -118,10 +178,10 @@ public final class DirectionalNavigationAdapter { } } - if tapEdges.contains(.vertical) { - let verticalEdgeSize = verticalEdgeThresholdPercent - .map { max(minimumVerticalEdgeSize, $0 * bounds.height) } - ?? minimumVerticalEdgeSize + if pointerPolicy.edges.contains(.vertical) { + let verticalEdgeSize = pointerPolicy.verticalEdgeThresholdPercent + .map { max(pointerPolicy.minimumVerticalEdgeSize, $0 * bounds.height) } + ?? pointerPolicy.minimumVerticalEdgeSize let topRange = 0.0 ... verticalEdgeSize let bottomRange = (bounds.height - verticalEdgeSize) ... bounds.height @@ -143,23 +203,25 @@ public final class DirectionalNavigationAdapter { let options = NavigatorGoOptions(animated: animatedTransition) switch event.key { - case .arrowUp: + case .arrowUp where keyboardPolicy.handleArrowKeys: return await navigator.goBackward(options: options) - case .arrowDown, .space: + case .arrowDown where keyboardPolicy.handleArrowKeys: return await navigator.goForward(options: options) - case .arrowLeft: + case .arrowLeft where keyboardPolicy.handleArrowKeys: return await navigator.goLeft(options: options) - case .arrowRight: + case .arrowRight where keyboardPolicy.handleArrowKeys: return await navigator.goRight(options: options) + case .space where keyboardPolicy.handleSpaceKey: + return await navigator.goForward(options: options) default: return false } } - @available(*, deprecated, message: "Use the initializer without the navigator parameter and call `bind(to:)`. See the migration guide.") + @available(*, deprecated, message: "Use the new initializer without the navigator parameter and call `bind(to:)`. See the migration guide.") public init( navigator: VisualNavigator, - tapEdges: TapEdges = .horizontal, + tapEdges: Edges = .horizontal, handleTapsWhileScrolling: Bool = false, minimumHorizontalEdgeSize: Double = 80.0, horizontalEdgeThresholdPercent: Double? = 0.3, @@ -168,12 +230,15 @@ public final class DirectionalNavigationAdapter { animatedTransition: Bool = false ) { self.navigator = navigator - self.tapEdges = tapEdges - self.handleTapsWhileScrolling = handleTapsWhileScrolling - self.minimumHorizontalEdgeSize = minimumHorizontalEdgeSize - self.horizontalEdgeThresholdPercent = horizontalEdgeThresholdPercent - self.minimumVerticalEdgeSize = minimumVerticalEdgeSize - self.verticalEdgeThresholdPercent = verticalEdgeThresholdPercent + pointerPolicy = PointerPolicy( + types: [.touch, .mouse], + ignoreWhileScrolling: !handleTapsWhileScrolling, + minimumHorizontalEdgeSize: minimumHorizontalEdgeSize, + horizontalEdgeThresholdPercent: horizontalEdgeThresholdPercent, + minimumVerticalEdgeSize: minimumVerticalEdgeSize, + verticalEdgeThresholdPercent: verticalEdgeThresholdPercent + ) + keyboardPolicy = KeyboardPolicy() self.animatedTransition = animatedTransition } diff --git a/Sources/Navigator/Input/Pointer/PointerEvent.swift b/Sources/Navigator/Input/Pointer/PointerEvent.swift index 73d66f6f8..aeabdf0f4 100644 --- a/Sources/Navigator/Input/Pointer/PointerEvent.swift +++ b/Sources/Navigator/Input/Pointer/PointerEvent.swift @@ -79,7 +79,7 @@ public enum Pointer: Equatable, CustomStringConvertible { } /// Type of a pointer. -public enum PointerType: Equatable { +public enum PointerType: Equatable, CaseIterable { case touch case mouse } diff --git a/TestApp/Sources/Reader/Common/VisualReaderViewController.swift b/TestApp/Sources/Reader/Common/VisualReaderViewController.swift index 158c68ed9..ad81edc04 100644 --- a/TestApp/Sources/Reader/Common/VisualReaderViewController.swift +++ b/TestApp/Sources/Reader/Common/VisualReaderViewController.swift @@ -81,10 +81,12 @@ class VisualReaderViewController: ReaderViewCon /// /// Bind it to the navigator before adding your own observers to prevent /// triggering your actions when turning pages. - DirectionalNavigationAdapter().bind(to: navigator) + DirectionalNavigationAdapter( + pointerPolicy: .init(types: [.mouse, .touch]) + ).bind(to: navigator) // Clear the current search highlight on tap. - navigator.addObserver(.tap { [weak self] _ in + navigator.addObserver(.activate { [weak self] _ in guard let searchViewModel = self?.searchViewModel, searchViewModel.selectedLocator != nil @@ -97,7 +99,7 @@ class VisualReaderViewController: ReaderViewCon }) // Toggle the navigation bar on tap, if nothing else took precedence. - navigator.addObserver(.tap { [weak self] _ in + navigator.addObserver(.activate { [weak self] _ in self?.toggleNavigationBar() return true })