Skip to content

Refactor the DirectionalNavigationAdapter #592

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -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
}
189 changes: 127 additions & 62 deletions Sources/Navigator/DirectionalNavigationAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -30,62 +33,119 @@ 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?

/// 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
}

/// Binds the adapter to the given visual navigator.
///
/// 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 {
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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,
Expand All @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Navigator/Input/Pointer/PointerEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading