From bb2f880362a4f07a1a73c59ccaa6e5fe57ee330b Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 13 Apr 2026 03:21:15 +0800 Subject: [PATCH 1/6] Move GesturePhase/GestureOutput to Core/ and audit implementations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move from GestureCore/ to Core/ - Fix isRecognized: include ended case (matches binary truth table) - Rename .final → .finalValue for GestureOutput - Add GesturePhase/GestureFailureReason: CustomStringConvertible - Add GestureOutput: NestedCustomStringConvertible - Add missing types: UpdateTraceAnnotation, GestureOutputStatus, GestureOutputStatusCombiner, GestureOutputArrayCombiner, GestureOutputCombiner - Add traceAnnotation field to GestureOutputMetadata - Use conditional Sendable conformance instead of generic constraint - Add DocC documentation --- .../GestureComponentController.swift | 2 +- Sources/OpenGestures/Core/GestureOutput.swift | 164 ++++++++++++ Sources/OpenGestures/Core/GesturePhase.swift | 235 ++++++++++++++++++ .../GestureCore/GestureOutput.swift | 59 ----- .../GestureCore/GesturePhase.swift | 142 ----------- .../GestureNode/GestureNode.swift | 8 +- .../OpenGesturesTests/GesturePhaseTests.swift | 99 -------- 7 files changed, 404 insertions(+), 305 deletions(-) create mode 100644 Sources/OpenGestures/Core/GestureOutput.swift create mode 100644 Sources/OpenGestures/Core/GesturePhase.swift delete mode 100644 Sources/OpenGestures/GestureCore/GestureOutput.swift delete mode 100644 Sources/OpenGestures/GestureCore/GesturePhase.swift delete mode 100644 Tests/OpenGesturesTests/GesturePhaseTests.swift 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..2be290a --- /dev/null +++ b/Sources/OpenGestures/Core/GestureOutput.swift @@ -0,0 +1,164 @@ +// +// GestureOutput.swift +// OpenGestures +// +// Audited for 9126.1.5 +// Status: Complete + +// MARK: - GestureOutput + +public enum GestureOutput { + case empty(GestureOutputEmptyReason, metadata: GestureOutputMetadata?) + case value(Value, metadata: GestureOutputMetadata?) + case finalValue(Value, metadata: GestureOutputMetadata?) +} + +extension GestureOutput: Sendable where Value: Sendable {} + +extension GestureOutput { + public var value: Value? { + switch self { + case .value(let v, _): v + case .finalValue(let v, _): v + case .empty: nil + } + } + + public var isEmpty: Bool { + if case .empty = self { return true } + return false + } + + public var isFinal: Bool { + if case .finalValue = self { return true } + return false + } +} + +// MARK: - GestureOutput + NestedCustomStringConvertible + +extension GestureOutput: NestedCustomStringConvertible { + package func populateNestedDescription(_ nested: inout NestedDescription) { + nested.options.formUnion([.hideTypeName, .compact]) + nested.customPrefix = "" + nested.customSuffix = "" + switch self { + case .empty(let reason, _): + nested.append("empty(\(reason))") + case .value(_, _): + nested.append("value") + case .finalValue(_, _): + nested.append("finalValue") + } + } +} + +// MARK: - GestureOutputEmptyReason + +public enum GestureOutputEmptyReason: Hashable, Sendable { + case noData + case filtered + case timeUpdate +} + +// MARK: - GestureOutputMetadata + +public struct GestureOutputMetadata: Sendable { + public var updatesToSchedule: [UpdateRequest] + public var updatesToCancel: [UpdateRequest] + public var traceAnnotation: UpdateTraceAnnotation? + + public 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 + +public struct UpdateTraceAnnotation: Sendable { + public var value: String + + public init(value: String) { + self.value = value + } +} + +// 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: - GestureOutputStatus + +public enum GestureOutputStatus: Hashable, Sendable { + case empty + case value + case finalValue +} + +// MARK: - GestureOutputStatusCombiner + +public struct GestureOutputStatusCombiner: Sendable { + public var combine: @Sendable ([GestureOutputStatus]) throws -> GestureOutputStatus + + public init(combine: @escaping @Sendable ([GestureOutputStatus]) throws -> GestureOutputStatus) { + self.combine = combine + } +} + +// MARK: - GestureOutputArrayCombiner + +public struct GestureOutputArrayCombiner: Sendable { + public let statusCombiner: GestureOutputStatusCombiner + + public init(statusCombiner: GestureOutputStatusCombiner) { + self.statusCombiner = statusCombiner + } +} + +// MARK: - GestureOutputCombiner + +public struct GestureOutputCombiner: Sendable { + public let combineValues: (@Sendable (repeat each A) throws -> B)? + public let combineOptionals: (@Sendable (repeat (each A)?) throws -> B)? + public let statusCombiner: GestureOutputStatusCombiner + + public 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..88ca2eb --- /dev/null +++ b/Sources/OpenGestures/Core/GesturePhase.swift @@ -0,0 +1,235 @@ +// +// 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 { + + /// 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: Sendable where Value: Sendable {} + +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/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) - } -} From c20e754e474ca8b5ff43a477497efd2baf4380a7 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 13 Apr 2026 03:23:54 +0800 Subject: [PATCH 2/6] Add GesturePhase and GestureFailureReason compatibility tests - Add @_spi(Private) for all GesturePhase cases - Add Sendable constraint on mapValue - Add OpenAttributeGraph dependency for compat test target - Add Metadata+Enum helper for enum construction via injectEnumTag - Single parameterized test covers all phase boolean properties + description - mapValue test guarded with #if OPENGESTURES (generic symbol not linkable) --- Package.resolved | 11 +- Package.swift | 8 +- Sources/OpenGestures/Core/GesturePhase.swift | 8 +- .../Core/GesturePhaseCompatibilityTests.swift | 120 ++++++++++++++++++ .../Metadata+Enum.swift | 18 +++ 5 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 Tests/OpenGesturesCompatibilityTests/Core/GesturePhaseCompatibilityTests.swift create mode 100644 Tests/OpenGesturesCompatibilityTests/Metadata+Enum.swift diff --git a/Package.resolved b/Package.resolved index 9f2bfbe..367aada 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "1f5c963cefcebeb92c99c09e75ddc87ac641ecdfda959043b76386896c17affc", + "originHash" : "b0c693c0f8667ce8c6574c8df581b6938ebf72109273d8037d58f7baab49e995", "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..fdd8d98 100644 --- a/Package.swift +++ b/Package.swift @@ -134,10 +134,10 @@ let warningsAsErrorsCondition = envBoolValue("WERROR", default: isXcodeEnv && de let releaseVersion = envIntValue("TARGET_RELEASE", default: 2025) let libraryEvolutionCondition = envBoolValue("LIBRARY_EVOLUTION", default: buildForDarwinPlatform) -let compatibilityTestCondition = envBoolValue("COMPATIBILITY_TEST", default: false) +let compatibilityTestCondition = envBoolValue("COMPATIBILITY_TEST", default: true) let gesturesCondition = envBoolValue("OPENGESTURESSHIMS_GESTURES", default: false) -let useLocalDeps = envBoolValue("USE_LOCAL_DEPS") +let useLocalDeps = envBoolValue("USE_LOCAL_DEPS", default: true) 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/Core/GesturePhase.swift b/Sources/OpenGestures/Core/GesturePhase.swift index 88ca2eb..50a4b93 100644 --- a/Sources/OpenGestures/Core/GesturePhase.swift +++ b/Sources/OpenGestures/Core/GesturePhase.swift @@ -39,21 +39,27 @@ public enum GesturePhase { /// The gesture is not participating in recognition. + @_spi(Private) case idle /// The gesture is evaluating incoming events. + @_spi(Private) case possible /// The gesture is recognized but blocked by another gesture. + @_spi(Private) case blocked(value: Value, blockedBy: GestureNodeID) /// The gesture is actively recognized and producing values. + @_spi(Private) case active(value: Value) /// The gesture completed successfully. + @_spi(Private) case ended(value: Value) /// The gesture failed. + @_spi(Private) case failed(reason: GestureFailureReason) } @@ -154,7 +160,7 @@ extension GesturePhase { /// 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 { + 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)) diff --git a/Tests/OpenGesturesCompatibilityTests/Core/GesturePhaseCompatibilityTests.swift b/Tests/OpenGesturesCompatibilityTests/Core/GesturePhaseCompatibilityTests.swift new file mode 100644 index 0000000..88c461a --- /dev/null +++ b/Tests/OpenGesturesCompatibilityTests/Core/GesturePhaseCompatibilityTests.swift @@ -0,0 +1,120 @@ +// +// GesturePhaseCompatibilityTests.swift +// OpenGesturesCompatibilityTests + +import OpenAttributeGraphShims +import Testing + +// MARK: - GesturePhase Static Constructors + +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 + ) { + // FIXME: The "Will never be executed" is a false warning. And we have no way to disable it now. + #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 + + #if OPENGESTURES + @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) + } + #endif +} + +// 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)) + } +} From 941a8958068e3d51800769a665dfa41e199aad0d Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 14 Apr 2026 01:28:49 +0800 Subject: [PATCH 3/6] Fix the build warning --- Package.resolved | 2 +- Package.swift | 4 ++-- Sources/OpenGestures/Core/GesturePhase.swift | 10 +--------- Sources/OpenGestures/Util/LocationContaining.swift | 4 ++++ .../Core/GesturePhaseCompatibilityTests.swift | 7 +++---- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/Package.resolved b/Package.resolved index 367aada..4a8d2a7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "b0c693c0f8667ce8c6574c8df581b6938ebf72109273d8037d58f7baab49e995", + "originHash" : "45be19da1dba47dfaf2674a1d21cdd6aba74c6e9e4fadbcceb50dadb09b3f06c", "pins" : [ { "identity" : "openattributegraph", diff --git a/Package.swift b/Package.swift index fdd8d98..ebbeff4 100644 --- a/Package.swift +++ b/Package.swift @@ -134,10 +134,10 @@ let warningsAsErrorsCondition = envBoolValue("WERROR", default: isXcodeEnv && de let releaseVersion = envIntValue("TARGET_RELEASE", default: 2025) let libraryEvolutionCondition = envBoolValue("LIBRARY_EVOLUTION", default: buildForDarwinPlatform) -let compatibilityTestCondition = envBoolValue("COMPATIBILITY_TEST", default: true) +let compatibilityTestCondition = envBoolValue("COMPATIBILITY_TEST", default: false) let gesturesCondition = envBoolValue("OPENGESTURESSHIMS_GESTURES", default: false) -let useLocalDeps = envBoolValue("USE_LOCAL_DEPS", default: true) +let useLocalDeps = envBoolValue("USE_LOCAL_DEPS", default: false) let swiftCorelibsPath = envStringValue("LIB_SWIFT_PATH") ?? "\(Context.packageDirectory)/Sources/SwiftCorelibs/include" diff --git a/Sources/OpenGestures/Core/GesturePhase.swift b/Sources/OpenGestures/Core/GesturePhase.swift index 50a4b93..f2678e4 100644 --- a/Sources/OpenGestures/Core/GesturePhase.swift +++ b/Sources/OpenGestures/Core/GesturePhase.swift @@ -36,35 +36,27 @@ /// 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 { +public enum GesturePhase: Sendable { /// The gesture is not participating in recognition. - @_spi(Private) case idle /// The gesture is evaluating incoming events. - @_spi(Private) case possible /// The gesture is recognized but blocked by another gesture. - @_spi(Private) case blocked(value: Value, blockedBy: GestureNodeID) /// The gesture is actively recognized and producing values. - @_spi(Private) case active(value: Value) /// The gesture completed successfully. - @_spi(Private) case ended(value: Value) /// The gesture failed. - @_spi(Private) case failed(reason: GestureFailureReason) } -extension GesturePhase: Sendable where Value: Sendable {} - extension GesturePhase { /// Whether the phase is ``idle``. public var isIdle: Bool { 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/GesturePhaseCompatibilityTests.swift b/Tests/OpenGesturesCompatibilityTests/Core/GesturePhaseCompatibilityTests.swift index 88c461a..3bcd717 100644 --- a/Tests/OpenGesturesCompatibilityTests/Core/GesturePhaseCompatibilityTests.swift +++ b/Tests/OpenGesturesCompatibilityTests/Core/GesturePhaseCompatibilityTests.swift @@ -5,7 +5,9 @@ import OpenAttributeGraphShims import Testing -// MARK: - GesturePhase Static Constructors +// 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) @@ -75,7 +77,6 @@ struct GesturePhaseCompatibilityTests { _ isRecognized: Bool, _ expectedDescription: String ) { - // FIXME: The "Will never be executed" is a false warning. And we have no way to disable it now. #expect(phase.isIdle == isIdle) #expect(phase.isPossible == isPossible) #expect(phase.isActive == isActive) @@ -89,7 +90,6 @@ struct GesturePhaseCompatibilityTests { // MARK: - mapValue - #if OPENGESTURES @Test func mapValue() { let active: GesturePhase = .active(value: 5) @@ -100,7 +100,6 @@ struct GesturePhaseCompatibilityTests { let mappedIdle = idle.mapValue { String($0) } #expect(mappedIdle.isIdle == true) } - #endif } // MARK: - GestureFailureReasonCompatibilityTests From e493931cce5f23f4abcd2b74bc973fb25d1eefa8 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 14 Apr 2026 02:36:35 +0800 Subject: [PATCH 4/6] Add UpdateScheduler --- Sources/OpenGestures/Core/GestureOutput.swift | 69 +++++++++---------- .../OpenGestures/Time/UpdateScheduler.swift | 60 ++++++++++++++++ .../Time/UpdateSchedulerTests.swift | 35 ++++++++++ 3 files changed, 128 insertions(+), 36 deletions(-) create mode 100644 Sources/OpenGestures/Time/UpdateScheduler.swift create mode 100644 Tests/OpenGesturesTests/Time/UpdateSchedulerTests.swift diff --git a/Sources/OpenGestures/Core/GestureOutput.swift b/Sources/OpenGestures/Core/GestureOutput.swift index 2be290a..6d2d13e 100644 --- a/Sources/OpenGestures/Core/GestureOutput.swift +++ b/Sources/OpenGestures/Core/GestureOutput.swift @@ -7,31 +7,33 @@ // MARK: - GestureOutput -public enum GestureOutput { +public enum GestureOutput: Sendable { case empty(GestureOutputEmptyReason, metadata: GestureOutputMetadata?) case value(Value, metadata: GestureOutputMetadata?) case finalValue(Value, metadata: GestureOutputMetadata?) } -extension GestureOutput: Sendable where Value: Sendable {} - extension GestureOutput { public var value: Value? { switch self { - case .value(let v, _): v - case .finalValue(let v, _): v + case let .value(v, _): v + case let .finalValue(v, _): v case .empty: nil } } public var isEmpty: Bool { - if case .empty = self { return true } - return false + switch self { + case .empty: true + default: false + } } public var isFinal: Bool { - if case .finalValue = self { return true } - return false + switch self { + case .finalValue: true + default: false + } } } @@ -39,16 +41,20 @@ extension GestureOutput { extension GestureOutput: NestedCustomStringConvertible { package func populateNestedDescription(_ nested: inout NestedDescription) { - nested.options.formUnion([.hideTypeName, .compact]) - nested.customPrefix = "" - nested.customSuffix = "" + let metadata: GestureOutputMetadata? switch self { - case .empty(let reason, _): - nested.append("empty(\(reason))") - case .value(_, _): - nested.append("value") - case .finalValue(_, _): - nested.append("finalValue") + 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") } } } @@ -61,14 +67,14 @@ public enum GestureOutputEmptyReason: Hashable, Sendable { case timeUpdate } -// MARK: - GestureOutputMetadata +// MARK: - GestureOutputMetadata [TBA] public struct GestureOutputMetadata: Sendable { - public var updatesToSchedule: [UpdateRequest] - public var updatesToCancel: [UpdateRequest] + package var updatesToSchedule: [UpdateRequest] + package var updatesToCancel: [UpdateRequest] public var traceAnnotation: UpdateTraceAnnotation? - public init( + package init( updatesToSchedule: [UpdateRequest] = [], updatesToCancel: [UpdateRequest] = [], traceAnnotation: UpdateTraceAnnotation? = nil @@ -79,7 +85,7 @@ public struct GestureOutputMetadata: Sendable { } } -// MARK: - GestureOutputMetadata + NestedCustomStringConvertible +// MARK: - GestureOutputMetadata + NestedCustomStringConvertible [TBA] extension GestureOutputMetadata: NestedCustomStringConvertible { package func populateNestedDescription(_ nested: inout NestedDescription) { @@ -108,16 +114,7 @@ public struct UpdateTraceAnnotation: Sendable { } } -// 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: - GestureOutputStatus +// MARK: - GestureOutputStatus [TBA] public enum GestureOutputStatus: Hashable, Sendable { case empty @@ -125,7 +122,7 @@ public enum GestureOutputStatus: Hashable, Sendable { case finalValue } -// MARK: - GestureOutputStatusCombiner +// MARK: - GestureOutputStatusCombiner [TBA] public struct GestureOutputStatusCombiner: Sendable { public var combine: @Sendable ([GestureOutputStatus]) throws -> GestureOutputStatus @@ -135,7 +132,7 @@ public struct GestureOutputStatusCombiner: Sendable { } } -// MARK: - GestureOutputArrayCombiner +// MARK: - GestureOutputArrayCombiner [TBA] public struct GestureOutputArrayCombiner: Sendable { public let statusCombiner: GestureOutputStatusCombiner @@ -145,7 +142,7 @@ public struct GestureOutputArrayCombiner: Sendable { } } -// MARK: - GestureOutputCombiner +// MARK: - GestureOutputCombiner [TBA] public struct GestureOutputCombiner: Sendable { public let combineValues: (@Sendable (repeat each A) throws -> B)? 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/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) + } +} From aa6546492a83680d6ca192ee9da467dc19e2f11c Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 14 Apr 2026 02:42:30 +0800 Subject: [PATCH 5/6] Add GestureOutput compatibility tests --- Sources/OpenGestures/Core/GestureOutput.swift | 8 +- .../GestureOutputCompatibilityTests.swift | 79 +++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 Tests/OpenGesturesCompatibilityTests/Core/GestureOutputCompatibilityTests.swift diff --git a/Sources/OpenGestures/Core/GestureOutput.swift b/Sources/OpenGestures/Core/GestureOutput.swift index 6d2d13e..afa072e 100644 --- a/Sources/OpenGestures/Core/GestureOutput.swift +++ b/Sources/OpenGestures/Core/GestureOutput.swift @@ -67,12 +67,12 @@ public enum GestureOutputEmptyReason: Hashable, Sendable { case timeUpdate } -// MARK: - GestureOutputMetadata [TBA] +// MARK: - GestureOutputMetadata public struct GestureOutputMetadata: Sendable { package var updatesToSchedule: [UpdateRequest] package var updatesToCancel: [UpdateRequest] - public var traceAnnotation: UpdateTraceAnnotation? + package var traceAnnotation: UpdateTraceAnnotation? package init( updatesToSchedule: [UpdateRequest] = [], @@ -85,7 +85,7 @@ public struct GestureOutputMetadata: Sendable { } } -// MARK: - GestureOutputMetadata + NestedCustomStringConvertible [TBA] +// MARK: - GestureOutputMetadata + NestedCustomStringConvertible extension GestureOutputMetadata: NestedCustomStringConvertible { package func populateNestedDescription(_ nested: inout NestedDescription) { @@ -106,7 +106,7 @@ extension GestureOutputMetadata: NestedCustomStringConvertible { // MARK: - UpdateTraceAnnotation -public struct UpdateTraceAnnotation: Sendable { +package struct UpdateTraceAnnotation: Sendable { public var value: String public init(value: String) { 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)) + } +} From d351357a57228a1093eed08bebbcf73b8898ced5 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 14 Apr 2026 02:56:01 +0800 Subject: [PATCH 6/6] Update GestureOutputStatus --- Sources/OpenGestures/Core/GestureOutput.swift | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Sources/OpenGestures/Core/GestureOutput.swift b/Sources/OpenGestures/Core/GestureOutput.swift index afa072e..8b08d81 100644 --- a/Sources/OpenGestures/Core/GestureOutput.swift +++ b/Sources/OpenGestures/Core/GestureOutput.swift @@ -114,42 +114,42 @@ package struct UpdateTraceAnnotation: Sendable { } } -// MARK: - GestureOutputStatus [TBA] +// MARK: - GestureOutputStatusCombiner -public enum GestureOutputStatus: Hashable, Sendable { - case empty - case value - case finalValue -} +package struct GestureOutputStatusCombiner: Sendable { + package var combine: @Sendable ([GestureOutputStatus]) throws -> GestureOutputStatus -// MARK: - GestureOutputStatusCombiner [TBA] - -public struct GestureOutputStatusCombiner: Sendable { - public var combine: @Sendable ([GestureOutputStatus]) throws -> GestureOutputStatus - - public init(combine: @escaping @Sendable ([GestureOutputStatus]) throws -> GestureOutputStatus) { + package init(combine: @escaping @Sendable ([GestureOutputStatus]) throws -> GestureOutputStatus) { self.combine = combine } } -// MARK: - GestureOutputArrayCombiner [TBA] +// MARK: - GestureOutputStatus -public struct GestureOutputArrayCombiner: Sendable { - public let statusCombiner: GestureOutputStatusCombiner +package enum GestureOutputStatus: Hashable, Sendable { + case empty + case value + case finalValue +} - public init(statusCombiner: GestureOutputStatusCombiner) { +// MARK: - GestureOutputArrayCombiner + +package struct GestureOutputArrayCombiner: Sendable { + package let statusCombiner: GestureOutputStatusCombiner + + package init(statusCombiner: GestureOutputStatusCombiner) { self.statusCombiner = statusCombiner } } -// MARK: - GestureOutputCombiner [TBA] +// MARK: - GestureOutputCombiner -public struct GestureOutputCombiner: Sendable { - public let combineValues: (@Sendable (repeat each A) throws -> B)? - public let combineOptionals: (@Sendable (repeat (each A)?) throws -> B)? - public let statusCombiner: GestureOutputStatusCombiner +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 - public init( + package init( combineValues: (@Sendable (repeat each A) throws -> B)?, combineOptionals: (@Sendable (repeat (each A)?) throws -> B)?, statusCombiner: GestureOutputStatusCombiner