diff --git a/Package.resolved b/Package.resolved index 9f2bfbe..4a8d2a7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "1f5c963cefcebeb92c99c09e75ddc87ac641ecdfda959043b76386896c17affc", + "originHash" : "45be19da1dba47dfaf2674a1d21cdd6aba74c6e9e4fadbcceb50dadb09b3f06c", "pins" : [ + { + "identity" : "openattributegraph", + "kind" : "remoteSourceControl", + "location" : "https://github.com/OpenSwiftUIProject/OpenAttributeGraph.git", + "state" : { + "branch" : "main", + "revision" : "03c96a02a2427b6520bb7937e86221c171076562" + } + }, { "identity" : "opencoregraphics", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index a597ece..ebbeff4 100644 --- a/Package.swift +++ b/Package.swift @@ -137,7 +137,7 @@ let libraryEvolutionCondition = envBoolValue("LIBRARY_EVOLUTION", default: build let compatibilityTestCondition = envBoolValue("COMPATIBILITY_TEST", default: false) let gesturesCondition = envBoolValue("OPENGESTURESSHIMS_GESTURES", default: false) -let useLocalDeps = envBoolValue("USE_LOCAL_DEPS") +let useLocalDeps = envBoolValue("USE_LOCAL_DEPS", default: false) let swiftCorelibsPath = envStringValue("LIB_SWIFT_PATH") ?? "\(Context.packageDirectory)/Sources/SwiftCorelibs/include" @@ -225,6 +225,9 @@ let openGesturesShimsTarget = Target.target( let openGesturesCompatibilityTestsTarget = Target.testTarget( name: "OpenGesturesCompatibilityTests", + dependencies: [ + .product(name: "OpenAttributeGraphShims", package: "OpenAttributeGraph"), + ], swiftSettings: sharedSwiftSettings ) @@ -237,6 +240,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/OpenSwiftUIProject/OpenCoreGraphics.git", branch: "main"), + .package(url: "https://github.com/OpenSwiftUIProject/OpenAttributeGraph.git", branch: "main"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), ], targets: [ diff --git a/Sources/OpenGestures/Component/GestureComponentController.swift b/Sources/OpenGestures/Component/GestureComponentController.swift index 3cf25c9..d8ce036 100644 --- a/Sources/OpenGestures/Component/GestureComponentController.swift +++ b/Sources/OpenGestures/Component/GestureComponentController.swift @@ -59,7 +59,7 @@ public final class GestureComponentController: AnyGestureCo break case .value(let value, _): try node.update(someValue: value, isFinalUpdate: false) - case .final(let value, _): + case .finalValue(let value, _): try node.update(someValue: value, isFinalUpdate: true) } } catch { diff --git a/Sources/OpenGestures/Core/GestureOutput.swift b/Sources/OpenGestures/Core/GestureOutput.swift new file mode 100644 index 0000000..8b08d81 --- /dev/null +++ b/Sources/OpenGestures/Core/GestureOutput.swift @@ -0,0 +1,161 @@ +// +// GestureOutput.swift +// OpenGestures +// +// Audited for 9126.1.5 +// Status: Complete + +// MARK: - GestureOutput + +public enum GestureOutput: Sendable { + case empty(GestureOutputEmptyReason, metadata: GestureOutputMetadata?) + case value(Value, metadata: GestureOutputMetadata?) + case finalValue(Value, metadata: GestureOutputMetadata?) +} + +extension GestureOutput { + public var value: Value? { + switch self { + case let .value(v, _): v + case let .finalValue(v, _): v + case .empty: nil + } + } + + public var isEmpty: Bool { + switch self { + case .empty: true + default: false + } + } + + public var isFinal: Bool { + switch self { + case .finalValue: true + default: false + } + } +} + +// MARK: - GestureOutput + NestedCustomStringConvertible + +extension GestureOutput: NestedCustomStringConvertible { + package func populateNestedDescription(_ nested: inout NestedDescription) { + let metadata: GestureOutputMetadata? + switch self { + case let .empty(reason, m): + nested.append(reason, label: "emptyReason") + metadata = m + case let .value(v, m): + nested.append(v, label: "value") + metadata = m + case let .finalValue(v, m): + nested.append(v, label: "finalValue") + metadata = m + } + if let metadata { + nested.append(metadata, label: "metadata") + } + } +} + +// MARK: - GestureOutputEmptyReason + +public enum GestureOutputEmptyReason: Hashable, Sendable { + case noData + case filtered + case timeUpdate +} + +// MARK: - GestureOutputMetadata + +public struct GestureOutputMetadata: Sendable { + package var updatesToSchedule: [UpdateRequest] + package var updatesToCancel: [UpdateRequest] + package var traceAnnotation: UpdateTraceAnnotation? + + package init( + updatesToSchedule: [UpdateRequest] = [], + updatesToCancel: [UpdateRequest] = [], + traceAnnotation: UpdateTraceAnnotation? = nil + ) { + self.updatesToSchedule = updatesToSchedule + self.updatesToCancel = updatesToCancel + self.traceAnnotation = traceAnnotation + } +} + +// MARK: - GestureOutputMetadata + NestedCustomStringConvertible + +extension GestureOutputMetadata: NestedCustomStringConvertible { + package func populateNestedDescription(_ nested: inout NestedDescription) { + nested.options.formUnion([.hideTypeName, .compact]) + nested.customPrefix = "" + nested.customSuffix = "" + if !updatesToSchedule.isEmpty { + nested.append("\(updatesToSchedule)", label: "updatesToSchedule") + } + if !updatesToCancel.isEmpty { + nested.append("\(updatesToCancel)", label: "updatesToCancel") + } + if let traceAnnotation { + nested.append(traceAnnotation.value, label: "traceAnnotation") + } + } +} + +// MARK: - UpdateTraceAnnotation + +package struct UpdateTraceAnnotation: Sendable { + public var value: String + + public init(value: String) { + self.value = value + } +} + +// MARK: - GestureOutputStatusCombiner + +package struct GestureOutputStatusCombiner: Sendable { + package var combine: @Sendable ([GestureOutputStatus]) throws -> GestureOutputStatus + + package init(combine: @escaping @Sendable ([GestureOutputStatus]) throws -> GestureOutputStatus) { + self.combine = combine + } +} + +// MARK: - GestureOutputStatus + +package enum GestureOutputStatus: Hashable, Sendable { + case empty + case value + case finalValue +} + +// MARK: - GestureOutputArrayCombiner + +package struct GestureOutputArrayCombiner: Sendable { + package let statusCombiner: GestureOutputStatusCombiner + + package init(statusCombiner: GestureOutputStatusCombiner) { + self.statusCombiner = statusCombiner + } +} + +// MARK: - GestureOutputCombiner + +package struct GestureOutputCombiner: Sendable { + package let combineValues: (@Sendable (repeat each A) throws -> B)? + package let combineOptionals: (@Sendable (repeat (each A)?) throws -> B)? + package let statusCombiner: GestureOutputStatusCombiner + + package init( + combineValues: (@Sendable (repeat each A) throws -> B)?, + combineOptionals: (@Sendable (repeat (each A)?) throws -> B)?, + statusCombiner: GestureOutputStatusCombiner + ) { + self.combineValues = combineValues + self.combineOptionals = combineOptionals + self.statusCombiner = statusCombiner + } +} diff --git a/Sources/OpenGestures/Core/GesturePhase.swift b/Sources/OpenGestures/Core/GesturePhase.swift new file mode 100644 index 0000000..f2678e4 --- /dev/null +++ b/Sources/OpenGestures/Core/GesturePhase.swift @@ -0,0 +1,233 @@ +// +// GesturePhase.swift +// OpenGestures +// +// Audited for 9126.1.5 +// Status: Complete + +// MARK: - GesturePhase + +/// The phase of a gesture recognition state machine. +/// +/// A gesture progresses through phases as it processes events: +/// +/// ``` +/// ┌──────────┐ +/// │ idle │ +/// └────┬─────┘ +/// ▼ +/// ┌──────────┐ +/// │ possible │ +/// └──┬────┬──┘ +/// ▼ └────►┌─────────┐ +/// ┌────────┐◄──►│ blocked │ +/// │ active │ └────┬────┘ +/// └───┬────┘ ▼ +/// ▼ ┌────────┐ +/// ┌───────┐ │ failed │ +/// │ ended │ └────────┘ +/// └───────┘ +/// ``` +/// +/// - `idle`: The gesture is not participating in recognition. +/// - `possible`: The gesture is evaluating incoming events. +/// - `active`: The gesture is actively recognized and producing values. +/// - `blocked`: The gesture is recognized but blocked by another gesture. +/// Can transition to `active` when the blocking gesture resolves. +/// - `ended`: The gesture completed successfully with a final value. +/// - `failed`: The gesture failed for a specific reason. +public enum GesturePhase: Sendable { + + /// The gesture is not participating in recognition. + case idle + + /// The gesture is evaluating incoming events. + case possible + + /// The gesture is recognized but blocked by another gesture. + case blocked(value: Value, blockedBy: GestureNodeID) + + /// The gesture is actively recognized and producing values. + case active(value: Value) + + /// The gesture completed successfully. + case ended(value: Value) + + /// The gesture failed. + case failed(reason: GestureFailureReason) +} + +extension GesturePhase { + /// Whether the phase is ``idle``. + public var isIdle: Bool { + switch self { + case .idle: true + default: false + } + } + + /// Whether the phase is ``possible``. + public var isPossible: Bool { + switch self { + case .possible: true + default: false + } + } + + /// Whether the phase is ``active(value:)``. + public var isActive: Bool { + switch self { + case .active: true + default: false + } + } + + /// Whether the phase is ``blocked(value:blockedBy:)``. + public var isBlocked: Bool { + switch self { + case .blocked: true + default: false + } + } + + /// Whether the phase is ``ended(value:)``. + public var isEnded: Bool { + switch self { + case .ended: true + default: false + } + } + + /// Whether the phase is ``failed(reason:)``. + public var isFailed: Bool { + switch self { + case .failed: true + default: false + } + } + + /// Whether the phase is terminal (``ended(value:)`` or ``failed(reason:)``). + /// + /// A terminal phase indicates the gesture has finished processing + /// and will not produce further updates. + public var isTerminal: Bool { + switch self { + case .ended, .failed: true + default: false + } + } + + /// Whether the gesture has been recognized. + /// + /// Returns `true` for ``blocked(value:blockedBy:)``, ``active(value:)``, + /// and ``ended(value:)`` — all phases where the gesture has produced a value. + public var isRecognized: Bool { + switch self { + case .blocked, .active, .ended: true + default: false + } + } + + /// The associated value, if the phase carries one. + /// + /// Returns a value for ``blocked(value:blockedBy:)``, ``active(value:)``, + /// and ``ended(value:)``. Returns `nil` for all other phases. + public var value: Value? { + switch self { + case .blocked(let v, _): v + case .active(let v): v + case .ended(let v): v + default: nil + } + } + + /// The failure reason, if the phase is ``failed(reason:)``. + public var failureReason: GestureFailureReason? { + if case .failed(let reason) = self { return reason } + return nil + } + + /// Returns a new phase with the value transformed by the given closure. + /// + /// For phases that carry a value (``blocked(value:blockedBy:)``, + /// ``active(value:)``, ``ended(value:)``), the closure is applied to + /// produce the new value. Other phases are passed through unchanged. + public func mapValue(_ transform: (Value) -> T) -> GesturePhase { + switch self { + case .blocked(let v, let id): .blocked(value: transform(v), blockedBy: id) + case .active(let v): .active(value: transform(v)) + case .ended(let v): .ended(value: transform(v)) + case .failed(let r): .failed(reason: r) + case .idle: .idle + case .possible: .possible + } + } +} + +// MARK: - GesturePhase + CustomStringConvertible + +extension GesturePhase: CustomStringConvertible { + public var description: String { + switch self { + case .blocked(_, let id): "blocked(by: \(id))" + case .active: "active" + case .ended: "ended" + case .failed(let reason): "failed(\(reason))" + case .idle: "idle" + case .possible: "possible" + } + } +} + +// MARK: - GestureFailureReason + +/// The reason a gesture recognition failed. +/// +/// Failure reasons fall into two categories: +/// - **External**: caused by another gesture (``excluded(by:)``, ``failureDependency(on:)``). +/// - **Internal**: caused by the gesture system or component logic +/// (``disabled``, ``removedFromContainer``, ``activationDenied``, +/// ``aborted``, ``coordinatorChanged``, ``custom(_:)``). +public enum GestureFailureReason: Sendable { + + /// The gesture was excluded by another gesture's exclusion relation. + case excluded(by: GestureNodeID) + + /// The gesture failed because a required gesture dependency failed. + case failureDependency(on: GestureNodeID) + + /// The gesture failed with a custom error from the component. + case custom(any Error) + + /// The gesture node is disabled. + case disabled + + /// The gesture node was removed from its container. + case removedFromContainer + + /// The gesture's activation was denied by the coordinator. + case activationDenied + + /// The gesture was aborted. + case aborted + + /// The gesture's coordinator changed. + case coordinatorChanged +} + +// MARK: - GestureFailureReason + CustomStringConvertible + +extension GestureFailureReason: CustomStringConvertible { + public var description: String { + switch self { + case .excluded(let id): "excludedBy: \(id)" + case .failureDependency(let id): "failureDependency(on: \(id))" + case .custom(let error): "\(error)" + case .disabled: "disabled" + case .removedFromContainer: "removedFromContainer" + case .activationDenied: "activationDenied" + case .aborted: "aborted" + case .coordinatorChanged: "coordinatorChanged" + } + } +} diff --git a/Sources/OpenGestures/GestureCore/GestureOutput.swift b/Sources/OpenGestures/GestureCore/GestureOutput.swift deleted file mode 100644 index 1f89a12..0000000 --- a/Sources/OpenGestures/GestureCore/GestureOutput.swift +++ /dev/null @@ -1,59 +0,0 @@ -// MARK: - GestureOutput - -/// The output of a gesture component update. -public enum GestureOutput: Sendable { - case empty(GestureOutputEmptyReason, metadata: GestureOutputMetadata?) - case value(Value, metadata: GestureOutputMetadata?) - case `final`(Value, metadata: GestureOutputMetadata?) -} - -extension GestureOutput { - public var value: Value? { - switch self { - case .value(let v, _): v - case .final(let v, _): v - case .empty: nil - } - } - - public var isEmpty: Bool { - if case .empty = self { return true } - return false - } - - /// True only for the `final` case. - public var isFinal: Bool { - if case .final = self { return true } - return false - } -} - -// MARK: - GestureOutputMetadata - -public struct GestureOutputMetadata: Sendable { - public var updatesToSchedule: [UpdateRequest] - public var updatesToCancel: [UpdateRequest] - // TODO: traceAnnotation - - public init(updatesToSchedule: [UpdateRequest] = [], updatesToCancel: [UpdateRequest] = []) { - self.updatesToSchedule = updatesToSchedule - self.updatesToCancel = updatesToCancel - } -} - -// MARK: - UpdateRequest - -public struct UpdateRequest: Hashable, Sendable, Identifiable { - public let id: UInt32 - public let creationTime: Timestamp - public let targetTime: Timestamp - public let tag: String? -} - -// MARK: - GestureOutputEmptyReason - -public enum GestureOutputEmptyReason: Hashable, Sendable { - case noData - case filtered - case timeUpdate -} diff --git a/Sources/OpenGestures/GestureCore/GesturePhase.swift b/Sources/OpenGestures/GestureCore/GesturePhase.swift deleted file mode 100644 index 441e85f..0000000 --- a/Sources/OpenGestures/GestureCore/GesturePhase.swift +++ /dev/null @@ -1,142 +0,0 @@ -// MARK: - GesturePhase - -/// The phase of a gesture recognition. -public enum GesturePhase: Sendable { - case blocked(value: Value, blockedBy: GestureNodeID) - case active(Value) - case ended(Value) - case failed(GestureFailureReason) - case idle - case possible -} - -extension GesturePhase { - /// True for `ended` and `failed`. - public var isTerminal: Bool { - switch self { - case .ended, .failed: true - default: false - } - } - - /// True only for `blocked`. - public var isBlocked: Bool { - if case .blocked = self { return true } - return false - } - - /// True only for `active`. - public var isActive: Bool { - if case .active = self { return true } - return false - } - - /// True for `blocked` and `active`. - public var isRecognized: Bool { - switch self { - case .blocked, .active: true - default: false - } - } - - /// True only for `possible`. - public var isPossible: Bool { - if case .possible = self { return true } - return false - } - - /// True only for `idle`. - public var isIdle: Bool { - if case .idle = self { return true } - return false - } - - /// True only for `failed`. - public var isFailed: Bool { - if case .failed = self { return true } - return false - } - - /// True only for `ended`. - public var isEnded: Bool { - if case .ended = self { return true } - return false - } - - /// Extracts value from `blocked`, `active`, or `ended`. - public var value: Value? { - switch self { - case .blocked(let v, _): v - case .active(let v): v - case .ended(let v): v - default: nil - } - } - - /// Extracts failure reason from `failed`. - public var failureReason: GestureFailureReason? { - if case .failed(let reason) = self { return reason } - return nil - } - - public func mapValue(_ transform: (Value) -> T) -> GesturePhase { - switch self { - case .blocked(let v, let id): .blocked(value: transform(v), blockedBy: id) - case .active(let v): .active(transform(v)) - case .ended(let v): .ended(transform(v)) - case .failed(let r): .failed(r) - case .idle: .idle - case .possible: .possible - } - } -} - -// MARK: - GestureFailureReason - -/// The reason a gesture recognition failed. -public enum GestureFailureReason: Sendable { - case excluded(by: GestureNodeID) - case failureDependency(on: GestureNodeID) - case custom(any Error) - case disabled - case removedFromContainer - case activationDenied - case aborted - case coordinatorChanged -} - -extension GestureFailureReason: Equatable { - public static func == (lhs: GestureFailureReason, rhs: GestureFailureReason) -> Bool { - switch (lhs, rhs) { - case (.excluded(let a), .excluded(let b)): a == b - case (.failureDependency(let a), .failureDependency(let b)): a == b - case (.custom, .custom): false - case (.disabled, .disabled): true - case (.removedFromContainer, .removedFromContainer): true - case (.activationDenied, .activationDenied): true - case (.aborted, .aborted): true - case (.coordinatorChanged, .coordinatorChanged): true - default: false - } - } -} - -extension GestureFailureReason: Hashable { - public func hash(into hasher: inout Hasher) { - switch self { - case .excluded(let id): - hasher.combine(0) - hasher.combine(id) - case .failureDependency(let id): - hasher.combine(1) - hasher.combine(id) - case .custom: - hasher.combine(2) - case .disabled: hasher.combine(3) - case .removedFromContainer: hasher.combine(4) - case .activationDenied: hasher.combine(5) - case .aborted: hasher.combine(6) - case .coordinatorChanged: hasher.combine(7) - } - } -} diff --git a/Sources/OpenGestures/GestureNode/GestureNode.swift b/Sources/OpenGestures/GestureNode/GestureNode.swift index 8d993e5..2a8f6a7 100644 --- a/Sources/OpenGestures/GestureNode/GestureNode.swift +++ b/Sources/OpenGestures/GestureNode/GestureNode.swift @@ -41,9 +41,9 @@ public final class GestureNode: AnyGestureNode, @unchecked Send public func update(value: Value, isFinalUpdate: Bool) throws { let oldPhase = phase if isFinalUpdate { - phase = .ended(value) + phase = .ended(value: value) } else { - phase = .active(value) + phase = .active(value: value) } latestPhase = phase _didUpdatePhase?(phase, oldPhase) @@ -60,7 +60,7 @@ public final class GestureNode: AnyGestureNode, @unchecked Send public override func abort() throws { let oldPhase = phase - phase = .failed(.aborted) + phase = .failed(reason:.aborted) latestPhase = phase _didUpdatePhase?(phase, oldPhase) } @@ -68,7 +68,7 @@ public final class GestureNode: AnyGestureNode, @unchecked Send public override func fail(with error: Error) throws { let oldPhase = phase // TODO: .error(Error) case once non-Sendable handling resolved - phase = .failed(.aborted) + phase = .failed(reason:.aborted) latestPhase = phase _didUpdatePhase?(phase, oldPhase) } diff --git a/Sources/OpenGestures/Time/UpdateScheduler.swift b/Sources/OpenGestures/Time/UpdateScheduler.swift new file mode 100644 index 0000000..ff0df4f --- /dev/null +++ b/Sources/OpenGestures/Time/UpdateScheduler.swift @@ -0,0 +1,60 @@ +// +// UpdateScheduler.swift +// OpenGestures +// +// Audited for 9126.1.5 +// Status: Complete + +// MARK: - UpdateScheduler + +public final class UpdateScheduler { + package let timeScheduler: any TimeScheduler + + package var scheduledRequests: [UpdateRequest: TimeSchedulerToken] + + package init( + timeScheduler: any TimeScheduler, + scheduledRequests: [UpdateRequest : TimeSchedulerToken] + ) { + self.timeScheduler = timeScheduler + self.scheduledRequests = scheduledRequests + } +} + +@_spi(Private) +extension UpdateScheduler: TimeSource { + public var timestamp: Timestamp { + timeScheduler.timestamp + } +} + +// MARK: - UpdateRequest + +package struct UpdateRequest: Hashable, Identifiable, CustomStringConvertible { + package let id: UInt32 + package let creationTime: Timestamp + package let targetTime: Timestamp + package let tag: String? + + package init( + id: UInt32, + creationTime: Timestamp, + targetTime: Timestamp, + tag: String? + ) { + self.id = id + self.creationTime = creationTime + self.targetTime = targetTime + self.tag = tag + } + + package var description: String { + let duration = targetTime - creationTime + var result = "{ \(id)" + if let tag { + result += " \"\(tag)\"" + } + result += ", \(duration) }" + return result + } +} diff --git a/Sources/OpenGestures/Util/LocationContaining.swift b/Sources/OpenGestures/Util/LocationContaining.swift index 11f830b..9e7e71a 100644 --- a/Sources/OpenGestures/Util/LocationContaining.swift +++ b/Sources/OpenGestures/Util/LocationContaining.swift @@ -5,7 +5,11 @@ // Audited for 9126.1.5 // Status: Complete +#if canImport(Darwin) +import OpenCoreGraphicsShims +#else package import OpenCoreGraphicsShims +#endif package protocol LocationContaining { var location: CGPoint { get } diff --git a/Tests/OpenGesturesCompatibilityTests/Core/GestureOutputCompatibilityTests.swift b/Tests/OpenGesturesCompatibilityTests/Core/GestureOutputCompatibilityTests.swift new file mode 100644 index 0000000..78a573e --- /dev/null +++ b/Tests/OpenGesturesCompatibilityTests/Core/GestureOutputCompatibilityTests.swift @@ -0,0 +1,79 @@ +// +// GestureOutputCompatibilityTests.swift +// OpenGesturesCompatibilityTests + +import OpenAttributeGraphShims +import Testing + +// MARK: - GestureOutput Static Constructors + +extension GestureOutput { + @inline(__always) + private static func make(tag: Int, _ body: (UnsafeMutableRawPointer) -> Void) -> GestureOutput { + let layout = MemoryLayout.self + let ptr = UnsafeMutableRawPointer.allocate( + byteCount: layout.size, + alignment: layout.alignment + ) + defer { ptr.deallocate() } + body(ptr) + Metadata(GestureOutput.self).injectEnumTag(tag: UInt32(tag), ptr) + return ptr.load(as: GestureOutput.self) + } + + // case 0: .empty(reason, metadata:) + static func empty(_ reason: GestureOutputEmptyReason, metadata: GestureOutputMetadata?) -> GestureOutput { + make(tag: 0) { ptr in + ptr.initializeMemory(as: UInt8.self, repeating: 0, count: MemoryLayout.size) + ptr.storeBytes(of: reason, as: GestureOutputEmptyReason.self) + let metadataOffset = MemoryLayout.stride + (ptr + metadataOffset).initializeMemory(as: GestureOutputMetadata?.self, repeating: metadata, count: 1) + } + } + + // case 1: .value(v, metadata:) + static func value(_ v: Value, metadata: GestureOutputMetadata?) -> GestureOutput { + make(tag: 1) { ptr in + ptr.initializeMemory(as: UInt8.self, repeating: 0, count: MemoryLayout.size) + ptr.initializeMemory(as: Value.self, repeating: v, count: 1) + let metadataOffset = MemoryLayout.stride + (ptr + metadataOffset).initializeMemory(as: GestureOutputMetadata?.self, repeating: metadata, count: 1) + } + } + + // case 2: .finalValue(v, metadata:) + static func finalValue(_ v: Value, metadata: GestureOutputMetadata?) -> GestureOutput { + make(tag: 2) { ptr in + ptr.initializeMemory(as: UInt8.self, repeating: 0, count: MemoryLayout.size) + ptr.initializeMemory(as: Value.self, repeating: v, count: 1) + let metadataOffset = MemoryLayout.stride + (ptr + metadataOffset).initializeMemory(as: GestureOutputMetadata?.self, repeating: metadata, count: 1) + } + } +} + +// MARK: - GestureOutputCompatibilityTests + +// Arguments: (output, isEmpty, isFinal, value, descriptionContains) +@Suite +struct GestureOutputCompatibilityTests { + @Test( + arguments: [ + (GestureOutput.empty(.noData, metadata: nil), true, false, nil as Int?, "emptyReason"), + (GestureOutput.value(42, metadata: nil), false, false, 42, "value"), + (GestureOutput.finalValue(99, metadata: nil), false, true, 99, "finalValue"), + ] + ) + func outputAPI( + _ output: GestureOutput, + _ isEmpty: Bool, + _ isFinal: Bool, + _ expectedValue: Int?, + _ descriptionContains: String + ) { + #expect(output.isEmpty == isEmpty) + #expect(output.isFinal == isFinal) + #expect(output.value == expectedValue) + #expect("\(output)".contains(descriptionContains)) + } +} diff --git a/Tests/OpenGesturesCompatibilityTests/Core/GesturePhaseCompatibilityTests.swift b/Tests/OpenGesturesCompatibilityTests/Core/GesturePhaseCompatibilityTests.swift new file mode 100644 index 0000000..3bcd717 --- /dev/null +++ b/Tests/OpenGesturesCompatibilityTests/Core/GesturePhaseCompatibilityTests.swift @@ -0,0 +1,119 @@ +// +// GesturePhaseCompatibilityTests.swift +// OpenGesturesCompatibilityTests + +import OpenAttributeGraphShims +import Testing + +// MARK: - GesturePhase Static Constructors to fix the link issue +// Note: we can't use package/@_spi(Private) to hide the case in swiftinterface. +// Otherwize we'll got a "Will never be executed" warning, and `ptr.load(as: GesturePhase.self)` will result a crash. + +extension GesturePhase { + @inline(__always) + private static func make(tag: Int, _ body: (UnsafeMutableRawPointer) -> Void) -> GesturePhase { + let layout = MemoryLayout.self + let ptr = UnsafeMutableRawPointer.allocate( + byteCount: layout.size, + alignment: layout.alignment + ) + defer { ptr.deallocate() } + body(ptr) + Metadata(GesturePhase.self).injectEnumTag(tag: UInt32(tag), ptr) + return ptr.load(as: GesturePhase.self) + } + + static func idle() -> GesturePhase { + make(tag: 4) { $0.initializeMemory(as: UInt8.self, repeating: 0, count: MemoryLayout.size) } + } + + static func possible() -> GesturePhase { + make(tag: 5) { $0.initializeMemory(as: UInt8.self, repeating: 0, count: MemoryLayout.size) } + } + + static func active(value: Value) -> GesturePhase { + make(tag: 1) { $0.initializeMemory(as: Value.self, repeating: value, count: 1) } + } + + static func blocked(value: Value, blockedBy: GestureNodeID) -> GesturePhase { + make(tag: 0) { ptr in + ptr.initializeMemory(as: Value.self, repeating: value, count: 1) + (ptr + MemoryLayout.stride).initializeMemory(as: GestureNodeID.self, repeating: blockedBy, count: 1) + } + } + + static func ended(value: Value) -> GesturePhase { + make(tag: 2) { $0.initializeMemory(as: Value.self, repeating: value, count: 1) } + } + + static func failed(reason: GestureFailureReason) -> GesturePhase { + make(tag: 3) { $0.initializeMemory(as: GestureFailureReason.self, repeating: reason, count: 1) } + } +} + +// MARK: - GesturePhaseCompatibilityTests + +@Suite +struct GesturePhaseCompatibilityTests { + @Test( + arguments: [ + (GesturePhase.idle(), true, false, false, false, false, false, false, false, "idle"), + (GesturePhase.possible(), false, true, false, false, false, false, false, false, "possible"), + (GesturePhase.active(value: 42), false, false, true, false, false, false, false, true, "active"), + (GesturePhase.blocked(value: 42, blockedBy: GestureNodeID(rawValue: 1)), false, false, false, true, false, false, false, true, "blocked(by: 1)"), + (GesturePhase.ended(value: 42), false, false, false, false, true, false, true, true, "ended"), + (GesturePhase.failed(reason: .disabled), false, false, false, false, false, true, true, false, "failed(disabled)"), + ] + ) + func phaseAPI( + _ phase: GesturePhase, + _ isIdle: Bool, + _ isPossible: Bool, + _ isActive: Bool, + _ isBlocked: Bool, + _ isEnded: Bool, + _ isFailed: Bool, + _ isTerminal: Bool, + _ isRecognized: Bool, + _ expectedDescription: String + ) { + #expect(phase.isIdle == isIdle) + #expect(phase.isPossible == isPossible) + #expect(phase.isActive == isActive) + #expect(phase.isBlocked == isBlocked) + #expect(phase.isEnded == isEnded) + #expect(phase.isFailed == isFailed) + #expect(phase.isTerminal == isTerminal) + #expect(phase.isRecognized == isRecognized) + #expect(phase.description == expectedDescription) + } + + // MARK: - mapValue + + @Test + func mapValue() { + let active: GesturePhase = .active(value: 5) + let mapped = active.mapValue { String($0) } + #expect(mapped.isActive == true) + + let idle: GesturePhase = .idle() + let mappedIdle = idle.mapValue { String($0) } + #expect(mappedIdle.isIdle == true) + } +} + +// MARK: - GestureFailureReasonCompatibilityTests + +@Suite +struct GestureFailureReasonCompatibilityTests { + @Test(arguments: [ + (GestureFailureReason.disabled, "disabled"), + (.removedFromContainer, "removedFromContainer"), + (.activationDenied, "activationDenied"), + (.aborted, "aborted"), + (.coordinatorChanged, "coordinatorChanged"), + ]) + func description(_ reason: GestureFailureReason, _ expected: String) { + #expect(reason.description == expected) + } +} diff --git a/Tests/OpenGesturesCompatibilityTests/Metadata+Enum.swift b/Tests/OpenGesturesCompatibilityTests/Metadata+Enum.swift new file mode 100644 index 0000000..5d9f6ca --- /dev/null +++ b/Tests/OpenGesturesCompatibilityTests/Metadata+Enum.swift @@ -0,0 +1,18 @@ +// +// Metadata+Enum.swift +// OpenGesturesCompatibilityTests + +import OpenAttributeGraphShims + +extension Metadata { + @inline(__always) + package func projectEnum( + at ptr: UnsafeRawPointer, + tag: Int, + _ body: (UnsafeRawPointer) -> Void + ) { + projectEnumData(UnsafeMutableRawPointer(mutating: ptr)) + body(ptr) + injectEnumTag(tag: UInt32(tag), UnsafeMutableRawPointer(mutating: ptr)) + } +} diff --git a/Tests/OpenGesturesTests/GesturePhaseTests.swift b/Tests/OpenGesturesTests/GesturePhaseTests.swift deleted file mode 100644 index a61de04..0000000 --- a/Tests/OpenGesturesTests/GesturePhaseTests.swift +++ /dev/null @@ -1,99 +0,0 @@ -import Testing -@testable import OpenGestures - -struct GesturePhaseTests { - @Test func testIdlePhase() { - let phase: GesturePhase = .idle - #expect(phase.isIdle) - #expect(!phase.isActive) - #expect(!phase.isTerminal) - #expect(!phase.isBlocked) - #expect(!phase.isRecognized) - #expect(phase.value == nil) - } - - @Test func testActivePhase() { - let phase: GesturePhase = .active(42) - #expect(phase.isActive) - #expect(phase.isRecognized) - #expect(!phase.isTerminal) - #expect(!phase.isIdle) - #expect(phase.value == 42) - } - - @Test func testBlockedPhase() { - let id = GestureNodeID(rawValue: 7 as UInt32) - let phase: GesturePhase = .blocked(value: 99, blockedBy: id) - #expect(phase.isBlocked) - #expect(phase.isRecognized) - #expect(!phase.isTerminal) - #expect(!phase.isActive) - #expect(phase.value == 99) - } - - @Test func testEndedPhase() { - let phase: GesturePhase = .ended(100) - #expect(phase.isEnded) - #expect(phase.isTerminal) - #expect(!phase.isRecognized) - #expect(!phase.isActive) - #expect(phase.value == 100) - } - - @Test func testFailedPhase() { - let phase: GesturePhase = .failed(.disabled) - #expect(phase.isFailed) - #expect(phase.isTerminal) - #expect(!phase.isRecognized) - #expect(phase.failureReason == .disabled) - #expect(phase.value == nil) - } - - @Test func testPossiblePhase() { - let phase: GesturePhase = .possible - #expect(phase.isPossible) - #expect(!phase.isIdle) - #expect(!phase.isActive) - #expect(!phase.isTerminal) - } - - @Test func testMapValue() { - let phase: GesturePhase = .active(10) - let mapped = phase.mapValue { String($0) } - #expect(mapped.value == "10") - } - - @Test func testMapValueBlocked() { - let id = GestureNodeID(rawValue: 3 as UInt32) - let phase: GesturePhase = .blocked(value: 5, blockedBy: id) - let mapped = phase.mapValue { $0 * 2 } - #expect(mapped.value == 10) - if case .blocked(_, let blockedBy) = mapped { - #expect(blockedBy == id) - } - } - - @Test func testGestureFailureReasonEquality() { - let id = GestureNodeID(rawValue: 1 as UInt32) - #expect(GestureFailureReason.excluded(by: id) == .excluded(by: id)) - #expect(GestureFailureReason.disabled == .disabled) - #expect(GestureFailureReason.disabled != .aborted) - } - - @Test func testGestureOutputCases() { - let empty: GestureOutput = .empty(.noData, metadata: nil) - #expect(empty.isEmpty) - #expect(!empty.isFinal) - #expect(empty.value == nil) - - let value: GestureOutput = .value(42, metadata: GestureOutputMetadata()) - #expect(!value.isEmpty) - #expect(!value.isFinal) - #expect(value.value == 42) - - let final: GestureOutput = .final(99, metadata: GestureOutputMetadata()) - #expect(!final.isEmpty) - #expect(final.isFinal) - #expect(final.value == 99) - } -} diff --git a/Tests/OpenGesturesTests/Time/UpdateSchedulerTests.swift b/Tests/OpenGesturesTests/Time/UpdateSchedulerTests.swift new file mode 100644 index 0000000..fa181f3 --- /dev/null +++ b/Tests/OpenGesturesTests/Time/UpdateSchedulerTests.swift @@ -0,0 +1,35 @@ +// +// UpdateSchedulerTests.swift +// OpenGesturesTests + +import OpenGestures +import Testing + +@Suite +struct UpdateRequestTests { + @Test( + arguments: [ + ( + UpdateRequest( + id: 42, + creationTime: Timestamp(value: .seconds(10)), + targetTime: Timestamp(value: .seconds(15)), + tag: nil + ), + "{ 42, 5.0 seconds }" + ), + ( + UpdateRequest( + id: 7, + creationTime: Timestamp(value: .seconds(0)), + targetTime: Timestamp(value: .milliseconds(500)), + tag: "timer" + ), + #"{ 7 "timer", 0.5 seconds }"# + ) + ] + ) + func description(_ req: UpdateRequest, _ expectedDescription: String) { + #expect(req.description == expectedDescription) + } +}