From 4349adea9f67b9dd7788aa565e70871818ac6238 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 6 Apr 2026 23:53:27 +0800 Subject: [PATCH 01/15] Add GestureNodeMatcher and test case --- .../Core/GestureNodeMatcher.swift | 56 +++++++++++++++++++ .../GestureRelation/GestureNodeMatcher.swift | 14 ----- ...GestureNodeMatcherCompatibilityTests.swift | 52 +++++++++++++++++ 3 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 Sources/OpenGestures/Core/GestureNodeMatcher.swift delete mode 100644 Sources/OpenGestures/GestureRelation/GestureNodeMatcher.swift create mode 100644 Tests/OpenGesturesCompatibilityTests/Core/GestureNodeMatcherCompatibilityTests.swift diff --git a/Sources/OpenGestures/Core/GestureNodeMatcher.swift b/Sources/OpenGestures/Core/GestureNodeMatcher.swift new file mode 100644 index 0000000..f208828 --- /dev/null +++ b/Sources/OpenGestures/Core/GestureNodeMatcher.swift @@ -0,0 +1,56 @@ +// +// GestureNodeMatcher.swift +// OpenGestures +// +// Audited for 9126.1.5 +// Status: Complete + +// MARK: - GestureNodeMatcher + +public enum GestureNodeMatcher: Hashable, Sendable { + case id(GestureNodeID) + case tag(GestureTag) + case traits(GestureTraitCollection, position: RelativePosition) + case any(position: RelativePosition) + + public enum RelativePosition: Hashable, Sendable { + case any + case above + case below + } +} + +// MARK: - GestureNodeMatcher + Comparable + +extension GestureNodeMatcher: Comparable { + public static func < (lhs: GestureNodeMatcher, rhs: GestureNodeMatcher) -> Bool { + lhs.sortOrder < rhs.sortOrder + } + + private var sortOrder: Int { + switch self { + case .id: 0 + case .tag: 1 + case .traits: 2 + case .any: 3 + } + } +} + +// MARK: - GestureNodeMatcher + NestedCustomStringConvertible + +@_spi(Private) +extension GestureNodeMatcher: NestedCustomStringConvertible { + public var label: String { + switch self { + case let .id(id): "\(id)" + case let .tag(tag): "\(tag)" + case let .traits(collection, position): "\(collection), position: \(position)" + case let .any(position): "any, position: \(position)" + } + } + + public var description: String { label } + + public var debugDescription: String { label } +} diff --git a/Sources/OpenGestures/GestureRelation/GestureNodeMatcher.swift b/Sources/OpenGestures/GestureRelation/GestureNodeMatcher.swift deleted file mode 100644 index ddcfcf3..0000000 --- a/Sources/OpenGestures/GestureRelation/GestureNodeMatcher.swift +++ /dev/null @@ -1,14 +0,0 @@ -// MARK: - GestureNodeMatcher - -public enum GestureNodeMatcher: Hashable, Sendable { - case any - case id(GestureNodeID) - case tag(GestureTag) - case traits(GestureTraitCollection, RelativePosition) - - public enum RelativePosition: Hashable, Sendable { - case any - case above - case below - } -} diff --git a/Tests/OpenGesturesCompatibilityTests/Core/GestureNodeMatcherCompatibilityTests.swift b/Tests/OpenGesturesCompatibilityTests/Core/GestureNodeMatcherCompatibilityTests.swift new file mode 100644 index 0000000..74ba114 --- /dev/null +++ b/Tests/OpenGesturesCompatibilityTests/Core/GestureNodeMatcherCompatibilityTests.swift @@ -0,0 +1,52 @@ +// +// GestureNodeMatcherCompatibilityTests.swift +// OpenGesturesCompatibilityTests + +import Testing + +struct GestureNodeMatcherCompatibilityTests { + @Test + func equalitySameCase() { + #expect(GestureNodeMatcher.any(position: .any) == .any(position: .any)) + #expect(GestureNodeMatcher.any(position: .above) != .any(position: .below)) + #expect(GestureNodeMatcher.tag("a") == .tag("a")) + #expect(GestureNodeMatcher.tag("a") != .tag("b")) + } + + @Test + func equalityDifferentCases() { + #expect(GestureNodeMatcher.any(position: .any) != .tag("x")) + } + + @Test + func hashable() { + var set: Set = [] + set.insert(.any(position: .any)) + set.insert(.any(position: .any)) + set.insert(.tag("a")) + #expect(set.count == 2) + } + + @Test + func comparable() { + #expect(GestureNodeMatcher.id(GestureNodeID(rawValue: 0)) < .tag("x")) + #expect(GestureNodeMatcher.tag("x") < .traits(.init(), position: .any)) + #expect(GestureNodeMatcher.traits(.init(), position: .any) < .any(position: .any)) + } + + @Test + func relativePositionEquality() { + #expect(GestureNodeMatcher.RelativePosition.any == .any) + #expect(GestureNodeMatcher.RelativePosition.above != .below) + } + + @Test(arguments: [ + (GestureNodeMatcher.id(.init(rawValue: 2)), "2"), + (GestureNodeMatcher.tag("A"), "\"A\""), + (GestureNodeMatcher.traits(.withTrait(.pan()), position: .below), "[pan], position: below"), + (GestureNodeMatcher.any(position: .above), "any, position: above"), + ]) + func description(_ matcher: GestureNodeMatcher, expectedDescription: String) { + #expect(String(describing: matcher) == expectedDescription) + } +} From 6399bc421127af5e84d928e934f9527c3bf4acb0 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 7 Apr 2026 00:53:20 +0800 Subject: [PATCH 02/15] Update GestureRelation --- Package.resolved | 11 +- Package.swift | 2 + .../OpenGestures/Core/GestureRelation.swift | 173 ++++++++++++++++++ .../GestureNode/AnyGestureNode.swift | 8 +- .../GestureRelation/GestureRelation.swift | 20 -- .../GestureRelation/GestureRelationType.swift | 43 ----- .../Core/GestureRelationTests.swift | 10 + 7 files changed, 199 insertions(+), 68 deletions(-) create mode 100644 Sources/OpenGestures/Core/GestureRelation.swift delete mode 100644 Sources/OpenGestures/GestureRelation/GestureRelation.swift delete mode 100644 Sources/OpenGestures/GestureRelation/GestureRelationType.swift create mode 100644 Tests/OpenGesturesTests/Core/GestureRelationTests.swift diff --git a/Package.resolved b/Package.resolved index 827c297..9f2bfbe 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "3285b10886befaf57021412f0039b61d5a78dd9a550de53e7443f8dc57681b6c", + "originHash" : "1f5c963cefcebeb92c99c09e75ddc87ac641ecdfda959043b76386896c17affc", "pins" : [ { "identity" : "opencoregraphics", @@ -9,6 +9,15 @@ "branch" : "main", "revision" : "050239bd42b19a4c8b1ef3936bfb9f3589ecfc46" } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "6675bc0ff86e61436e615df6fc5174e043e57924", + "version" : "1.4.1" + } } ], "version" : 3 diff --git a/Package.swift b/Package.swift index 454b21b..a597ece 100644 --- a/Package.swift +++ b/Package.swift @@ -205,6 +205,7 @@ let openGesturesTarget = Target.target( dependencies: [ .target(name: cOpenGesturesTarget.name), .product(name: "OpenCoreGraphicsShims", package: "OpenCoreGraphics"), + .product(name: "OrderedCollections", package: "swift-collections"), ], swiftSettings: sharedSwiftSettings ) @@ -236,6 +237,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/OpenSwiftUIProject/OpenCoreGraphics.git", branch: "main"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), ], targets: [ cOpenGesturesTarget, diff --git a/Sources/OpenGestures/Core/GestureRelation.swift b/Sources/OpenGestures/Core/GestureRelation.swift new file mode 100644 index 0000000..9ad11d5 --- /dev/null +++ b/Sources/OpenGestures/Core/GestureRelation.swift @@ -0,0 +1,173 @@ +// +// GestureRelation.swift +// OpenGestures +// +// Audited for 9126.1.5 +// Status: Complete + +import OrderedCollections + +// MARK: - GestureRelationType + +public enum GestureRelationType: Hashable, Sendable { + case exclusion + case activeExclusion + case failureRequirement +} + +// MARK: - GestureRelationRole + +public enum GestureRelationRole: Hashable, Sendable { + case regular + case blocking +} + +// MARK: - GestureRelationDirection + +public enum GestureRelationDirection: Hashable, Sendable { + case outgoing + case incoming +} + +// MARK: - GestureRelation + +public struct GestureRelation: Equatable, Sendable { + public var type: GestureRelationType + public var direction: GestureRelationDirection + public var role: GestureRelationRole? + public var target: GestureNodeMatcher + + public init( + type: GestureRelationType, + direction: GestureRelationDirection, + role: GestureRelationRole?, + target: GestureNodeMatcher + ) { + self.type = type + self.direction = direction + self.role = role + self.target = target + } +} + +// MARK: - [GestureRelation] Default + +extension [GestureRelation] { + public static var `default`: [GestureRelation] { + [ + GestureRelation(type: .exclusion, direction: .outgoing, role: .regular, target: .any(position: .any)), + GestureRelation(type: .activeExclusion, direction: .outgoing, role: .regular, target: .any(position: .any)), + GestureRelation(type: .activeExclusion, direction: .incoming, role: .blocking, target: .any(position: .any)), + ] + } +} + +// MARK: - RelationMap + +package struct RelationMap: Sendable { + private var relations: OrderedDictionary> + + package init() { + self.relations = [:] + } + + init(relations: OrderedDictionary>) { + self.relations = relations + } + + package mutating func add(_ definition: RelationDefinition, for matcher: GestureNodeMatcher) { + relations[matcher, default: []].insert(definition) + } + + package mutating func remove(_ definition: RelationDefinition, for matcher: GestureNodeMatcher) { + relations[matcher]?.remove(definition) + if relations[matcher]?.isEmpty == true { + relations.removeValue(forKey: matcher) + } + } + + package mutating func addRelation(_ relation: GestureRelation) { + let definition = RelationDefinition( + type: relation.type, + direction: relation.direction, + role: relation.role + ) + add(definition, for: relation.target) + } + + mutating func removeRelation(_ relation: GestureRelation) { + let definition = RelationDefinition( + type: relation.type, + direction: relation.direction, + role: relation.role + ) + remove(definition, for: relation.target) + } + + package func toRelations() -> [GestureRelation] { + var result: [GestureRelation] = [] + for (matcher, definitions) in relations { + for definition in definitions { + result.append(GestureRelation( + type: definition.type, + direction: definition.direction, + role: definition.role, + target: matcher + )) + } + } + return result + } +} + +// MARK: - RelationMap + Sequence [TBA] + +extension RelationMap: Sequence { + package func makeIterator() -> some IteratorProtocol { + relations.makeIterator() + } +} + +// MARK: - RelationMap + NestedCustomStringConvertible [TBA] + +extension RelationMap: NestedCustomStringConvertible { + package var label: String { "RelationMap" } + + package var description: String { + if relations.isEmpty { + return "\(label) {}" + } + let entries = relations.map { "\($0.key): \($0.value)" }.joined(separator: ", ") + return "\(label) {\(entries)}" + } + + package var debugDescription: String { + description + } +} + +// MARK: - RelationDefinition + +package struct RelationDefinition: Hashable, Sendable, CustomStringConvertible { + package var type: GestureRelationType + package var direction: GestureRelationDirection + package var role: GestureRelationRole? + + package init( + type: GestureRelationType, + direction: GestureRelationDirection, + role: GestureRelationRole? = nil + ) { + self.type = type + self.direction = direction + self.role = role + } + + package var description: String { + if let role { + "\(type) \(direction) (\(role))" + } else { + "\(type) \(direction)" + } + } +} diff --git a/Sources/OpenGestures/GestureNode/AnyGestureNode.swift b/Sources/OpenGestures/GestureNode/AnyGestureNode.swift index d6c1a75..f0a2306 100644 --- a/Sources/OpenGestures/GestureNode/AnyGestureNode.swift +++ b/Sources/OpenGestures/GestureNode/AnyGestureNode.swift @@ -37,18 +37,18 @@ open class AnyGestureNode: Hashable, Identifiable, @unchecked Sendable { // MARK: - Relations - private var _relations: [GestureRelation] = [] + package var relationMap = RelationMap() public var relations: [GestureRelation] { - _relations + relationMap.toRelations() } open func addRelation(_ relation: GestureRelation) { - _relations.append(relation) + relationMap.addRelation(relation) } open func removeRelation(_ relation: GestureRelation) { - // TODO: Equatable-based removal + relationMap.removeRelation(relation) } open func addRelations(_ relations: [GestureRelation]) { diff --git a/Sources/OpenGestures/GestureRelation/GestureRelation.swift b/Sources/OpenGestures/GestureRelation/GestureRelation.swift deleted file mode 100644 index 0776360..0000000 --- a/Sources/OpenGestures/GestureRelation/GestureRelation.swift +++ /dev/null @@ -1,20 +0,0 @@ -// MARK: - GestureRelation - -public struct GestureRelation: Sendable { - public var type: GestureRelationType - public var direction: GestureRelationDirection - public var role: GestureRelationRole? - public var target: GestureNodeMatcher - - public init( - type: GestureRelationType, - direction: GestureRelationDirection, - role: GestureRelationRole? = nil, - target: GestureNodeMatcher - ) { - self.type = type - self.direction = direction - self.role = role - self.target = target - } -} diff --git a/Sources/OpenGestures/GestureRelation/GestureRelationType.swift b/Sources/OpenGestures/GestureRelation/GestureRelationType.swift deleted file mode 100644 index 0281b2c..0000000 --- a/Sources/OpenGestures/GestureRelation/GestureRelationType.swift +++ /dev/null @@ -1,43 +0,0 @@ -// MARK: - GestureRelationType - -/// The type of relationship between gesture nodes. -public enum GestureRelationType: Hashable, Sendable { - case exclusion - case activeExclusion - case failureRequirement -} - -extension GestureRelationType: CustomStringConvertible { - public var description: String { - switch self { - case .exclusion: "exclusion" - case .activeExclusion: "activeExclusion" - case .failureRequirement: "failureRequirement" - } - } -} - -// MARK: - GestureRelationRole - -/// The role a gesture node plays in a relation. -public enum GestureRelationRole: Hashable, Sendable { - case regular - case blocking -} - -extension GestureRelationRole: CustomStringConvertible { - public var description: String { - switch self { - case .regular: "regular" - case .blocking: "blocking" - } - } -} - -// MARK: - GestureRelationDirection - -/// The direction of a gesture relation. -public enum GestureRelationDirection: Hashable, Sendable { - case outgoing - case incoming -} diff --git a/Tests/OpenGesturesTests/Core/GestureRelationTests.swift b/Tests/OpenGesturesTests/Core/GestureRelationTests.swift new file mode 100644 index 0000000..ab5cb76 --- /dev/null +++ b/Tests/OpenGesturesTests/Core/GestureRelationTests.swift @@ -0,0 +1,10 @@ +// +// GestureRelationTests.swift +// OpenGesturesTests + +import OpenGestures +import Testing + +@Suite +struct GestureRelationTests { +} From a5b08e92914f64710b0a17dbf97a18f1dfe0a263 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 7 Apr 2026 01:01:38 +0800 Subject: [PATCH 03/15] Add test case for Relation --- .../OpenGestures/Core/GestureRelation.swift | 2 +- .../GestureRelationCompatibilityTests.swift | 62 +++++++++++ .../Core/GestureRelationTests.swift | 104 ++++++++++++++++++ 3 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 Tests/OpenGesturesCompatibilityTests/Core/GestureRelationCompatibilityTests.swift diff --git a/Sources/OpenGestures/Core/GestureRelation.swift b/Sources/OpenGestures/Core/GestureRelation.swift index 9ad11d5..19e4491 100644 --- a/Sources/OpenGestures/Core/GestureRelation.swift +++ b/Sources/OpenGestures/Core/GestureRelation.swift @@ -95,7 +95,7 @@ package struct RelationMap: Sendable { add(definition, for: relation.target) } - mutating func removeRelation(_ relation: GestureRelation) { + package mutating func removeRelation(_ relation: GestureRelation) { let definition = RelationDefinition( type: relation.type, direction: relation.direction, diff --git a/Tests/OpenGesturesCompatibilityTests/Core/GestureRelationCompatibilityTests.swift b/Tests/OpenGesturesCompatibilityTests/Core/GestureRelationCompatibilityTests.swift new file mode 100644 index 0000000..e0ab5c5 --- /dev/null +++ b/Tests/OpenGesturesCompatibilityTests/Core/GestureRelationCompatibilityTests.swift @@ -0,0 +1,62 @@ +// +// GestureRelationCompatibilityTests.swift +// OpenGesturesCompatibilityTests + +import Testing + +struct GestureRelationCompatibilityTests { + @Test + func initAndProperties() { + let r = GestureRelation( + type: .exclusion, + direction: .outgoing, + role: .regular, + target: .any(position: .any) + ) + #expect(r.type == .exclusion) + #expect(r.direction == .outgoing) + #expect(r.role == .regular) + #expect(r.target == .any(position: .any)) + } + + @Test + func nilRole() { + let r = GestureRelation( + type: .failureRequirement, + direction: .incoming, + role: nil, + target: .tag("x") + ) + #expect(r.role == nil) + } + + @Test + func equality() { + let a = GestureRelation(type: .exclusion, direction: .outgoing, role: .regular, target: .any(position: .any)) + let b = GestureRelation(type: .exclusion, direction: .outgoing, role: .regular, target: .any(position: .any)) + let c = GestureRelation(type: .exclusion, direction: .incoming, role: .regular, target: .any(position: .any)) + #expect(a == b) + #expect(a != c) + } + + @Test + func equalityDifferentTarget() { + let a = GestureRelation(type: .exclusion, direction: .outgoing, role: .regular, target: .any(position: .any)) + let b = GestureRelation(type: .exclusion, direction: .outgoing, role: .regular, target: .tag("x")) + #expect(a != b) + } + + @Test + func defaultRelations() { + let defaults: [GestureRelation] = .default + #expect(defaults.count == 3) + #expect(defaults[0].type == .exclusion) + #expect(defaults[0].direction == .outgoing) + #expect(defaults[0].role == .regular) + #expect(defaults[1].type == .activeExclusion) + #expect(defaults[1].direction == .outgoing) + #expect(defaults[2].type == .activeExclusion) + #expect(defaults[2].direction == .incoming) + #expect(defaults[2].role == .blocking) + } +} diff --git a/Tests/OpenGesturesTests/Core/GestureRelationTests.swift b/Tests/OpenGesturesTests/Core/GestureRelationTests.swift index ab5cb76..8c2bd3b 100644 --- a/Tests/OpenGesturesTests/Core/GestureRelationTests.swift +++ b/Tests/OpenGesturesTests/Core/GestureRelationTests.swift @@ -7,4 +7,108 @@ import Testing @Suite struct GestureRelationTests { + // MARK: - RelationDefinition + + @Test + func relationDefinitionInit() { + let def = RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular) + #expect(def.type == .exclusion) + #expect(def.direction == .outgoing) + #expect(def.role == .regular) + } + + @Test + func relationDefinitionNilRole() { + let def = RelationDefinition(type: .failureRequirement, direction: .incoming) + #expect(def.role == nil) + } + + @Test + func relationDefinitionEquality() { + let a = RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular) + let b = RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular) + let c = RelationDefinition(type: .exclusion, direction: .incoming, role: .regular) + #expect(a == b) + #expect(a != c) + } + + @Test + func relationDefinitionHashable() { + var set: Set = [] + set.insert(RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular)) + set.insert(RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular)) + set.insert(RelationDefinition(type: .exclusion, direction: .incoming, role: .blocking)) + #expect(set.count == 2) + } + + // MARK: - RelationMap + + @Test + func relationMapEmpty() { + let map = RelationMap() + #expect(map.toRelations().isEmpty) + } + + @Test + func relationMapAddRelation() { + var map = RelationMap() + let relation = GestureRelation(type: .exclusion, direction: .outgoing, role: .regular, target: .any(position: .any)) + map.addRelation(relation) + let relations = map.toRelations() + #expect(relations.count == 1) + #expect(relations[0] == relation) + } + + @Test + func relationMapRemoveRelation() { + var map = RelationMap() + let relation = GestureRelation(type: .exclusion, direction: .outgoing, role: .regular, target: .any(position: .any)) + map.addRelation(relation) + map.removeRelation(relation) + #expect(map.toRelations().isEmpty) + } + + @Test + func relationMapAddDefinition() { + var map = RelationMap() + let def = RelationDefinition(type: .activeExclusion, direction: .incoming, role: .blocking) + map.add(def, for: .any(position: .any)) + let relations = map.toRelations() + #expect(relations.count == 1) + #expect(relations[0].type == .activeExclusion) + #expect(relations[0].direction == .incoming) + #expect(relations[0].role == .blocking) + #expect(relations[0].target == .any(position: .any)) + } + + @Test + func relationMapRemoveDefinition() { + var map = RelationMap() + let def = RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular) + map.add(def, for: .tag("a")) + map.remove(def, for: .tag("a")) + #expect(map.toRelations().isEmpty) + } + + @Test + func relationMapGroupsByMatcher() { + var map = RelationMap() + map.add(RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular), for: .any(position: .any)) + map.add(RelationDefinition(type: .activeExclusion, direction: .incoming, role: .blocking), for: .any(position: .any)) + let relations = map.toRelations() + #expect(relations.count == 2) + #expect(relations.allSatisfy { $0.target == .any(position: .any) }) + } + + @Test + func relationMapSequence() { + var map = RelationMap() + map.add(RelationDefinition(type: .exclusion, direction: .outgoing), for: .any(position: .any)) + map.add(RelationDefinition(type: .failureRequirement, direction: .outgoing), for: .tag("x")) + var count = 0 + for _ in map { + count += 1 + } + #expect(count == 2) + } } From ed2bbb42175326751f023c85b92dda1f0766da52 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 7 Apr 2026 01:14:35 +0800 Subject: [PATCH 04/15] Update GestureRelationTests --- .../Core/GestureRelationTests.swift | 77 ++++++++++--------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/Tests/OpenGesturesTests/Core/GestureRelationTests.swift b/Tests/OpenGesturesTests/Core/GestureRelationTests.swift index 8c2bd3b..a280cd5 100644 --- a/Tests/OpenGesturesTests/Core/GestureRelationTests.swift +++ b/Tests/OpenGesturesTests/Core/GestureRelationTests.swift @@ -5,44 +5,10 @@ import OpenGestures import Testing -@Suite -struct GestureRelationTests { - // MARK: - RelationDefinition - - @Test - func relationDefinitionInit() { - let def = RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular) - #expect(def.type == .exclusion) - #expect(def.direction == .outgoing) - #expect(def.role == .regular) - } - - @Test - func relationDefinitionNilRole() { - let def = RelationDefinition(type: .failureRequirement, direction: .incoming) - #expect(def.role == nil) - } - - @Test - func relationDefinitionEquality() { - let a = RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular) - let b = RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular) - let c = RelationDefinition(type: .exclusion, direction: .incoming, role: .regular) - #expect(a == b) - #expect(a != c) - } - - @Test - func relationDefinitionHashable() { - var set: Set = [] - set.insert(RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular)) - set.insert(RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular)) - set.insert(RelationDefinition(type: .exclusion, direction: .incoming, role: .blocking)) - #expect(set.count == 2) - } - - // MARK: - RelationMap +// MARK: - RelationMapTests +@Suite +struct RelationMapTests { @Test func relationMapEmpty() { let map = RelationMap() @@ -112,3 +78,40 @@ struct GestureRelationTests { #expect(count == 2) } } + +// MARK: - RelationDefinitionTests + +@Suite +struct RelationDefinitionTests { + @Test + func relationDefinitionInit() { + let def = RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular) + #expect(def.type == .exclusion) + #expect(def.direction == .outgoing) + #expect(def.role == .regular) + } + + @Test + func relationDefinitionNilRole() { + let def = RelationDefinition(type: .failureRequirement, direction: .incoming) + #expect(def.role == nil) + } + + @Test + func relationDefinitionEquality() { + let a = RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular) + let b = RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular) + let c = RelationDefinition(type: .exclusion, direction: .incoming, role: .regular) + #expect(a == b) + #expect(a != c) + } + + @Test + func relationDefinitionHashable() { + var set: Set = [] + set.insert(RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular)) + set.insert(RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular)) + set.insert(RelationDefinition(type: .exclusion, direction: .incoming, role: .blocking)) + #expect(set.count == 2) + } +} From 640cf98592531593341ee6ca3602b843ed570841 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 7 Apr 2026 01:16:54 +0800 Subject: [PATCH 05/15] Update RelationDefinition.description --- Sources/OpenGestures/Core/GestureRelation.swift | 11 ++++++----- .../Core/GestureRelationTests.swift | 12 ++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Sources/OpenGestures/Core/GestureRelation.swift b/Sources/OpenGestures/Core/GestureRelation.swift index 19e4491..1905a67 100644 --- a/Sources/OpenGestures/Core/GestureRelation.swift +++ b/Sources/OpenGestures/Core/GestureRelation.swift @@ -120,7 +120,7 @@ package struct RelationMap: Sendable { } } -// MARK: - RelationMap + Sequence [TBA] +// MARK: - RelationMap + Sequence extension RelationMap: Sequence { package func makeIterator() -> some IteratorProtocol { @@ -164,10 +164,11 @@ package struct RelationDefinition: Hashable, Sendable, CustomStringConvertible { } package var description: String { - if let role { - "\(type) \(direction) (\(role))" - } else { - "\(type) \(direction)" + let dir = switch direction { + case .outgoing: "out" + case .incoming: "in" } + let roleStr = if let role { "\(role)" } else { "dynamic" } + return "\(type)[\(dir)]=\(roleStr)" } } diff --git a/Tests/OpenGesturesTests/Core/GestureRelationTests.swift b/Tests/OpenGesturesTests/Core/GestureRelationTests.swift index a280cd5..01b1731 100644 --- a/Tests/OpenGesturesTests/Core/GestureRelationTests.swift +++ b/Tests/OpenGesturesTests/Core/GestureRelationTests.swift @@ -114,4 +114,16 @@ struct RelationDefinitionTests { set.insert(RelationDefinition(type: .exclusion, direction: .incoming, role: .blocking)) #expect(set.count == 2) } + + @Test(arguments: [ + (RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular), "exclusion[out]=regular"), + (RelationDefinition(type: .exclusion, direction: .incoming, role: .regular), "exclusion[in]=regular"), + (RelationDefinition(type: .activeExclusion, direction: .outgoing, role: .blocking), "activeExclusion[out]=blocking"), + (RelationDefinition(type: .activeExclusion, direction: .incoming, role: .blocking), "activeExclusion[in]=blocking"), + (RelationDefinition(type: .failureRequirement, direction: .outgoing), "failureRequirement[out]=dynamic"), + (RelationDefinition(type: .failureRequirement, direction: .incoming, role: nil), "failureRequirement[in]=dynamic"), + ]) + func description(_ definition: RelationDefinition, _ expected: String) { + #expect(definition.description == expected) + } } From 377e9196f5d0e2492b1d286702db6591156ddde5 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 12 Apr 2026 20:57:17 +0800 Subject: [PATCH 06/15] Add AGENT soft link --- AGENT.md | 1 + 1 file changed, 1 insertion(+) create mode 120000 AGENT.md diff --git a/AGENT.md b/AGENT.md new file mode 120000 index 0000000..ef495c0 --- /dev/null +++ b/AGENT.md @@ -0,0 +1 @@ +./CLAUDE.md \ No newline at end of file From d36a9f5fba3c3555675f5d6be074c878c3d947f0 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 12 Apr 2026 20:57:58 +0800 Subject: [PATCH 07/15] Add NestedDescription --- .../Util/NestedCustomStringConvertible.swift | 56 +++++++- .../OpenGestures/Util/NestedDescription.swift | 120 ++++++++++++++++++ 2 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 Sources/OpenGestures/Util/NestedDescription.swift diff --git a/Sources/OpenGestures/Util/NestedCustomStringConvertible.swift b/Sources/OpenGestures/Util/NestedCustomStringConvertible.swift index 9e2d405..e1b26c1 100644 --- a/Sources/OpenGestures/Util/NestedCustomStringConvertible.swift +++ b/Sources/OpenGestures/Util/NestedCustomStringConvertible.swift @@ -3,11 +3,59 @@ // OpenGestures // // Audited for 9126.1.5 -// Status: WIP +// Status: Complete // MARK: - NestedCustomStringConvertible -@_spi(Private) -public protocol NestedCustomStringConvertible: CustomDebugStringConvertible, CustomStringConvertible { - var label: String { get } +package protocol NestedCustomStringConvertible: CustomDebugStringConvertible, CustomStringConvertible { + func populateNestedDescription(_ nested: inout NestedDescription) +} + +extension NestedCustomStringConvertible { + @_spi(Private) + public var description: String { + var nested = NestedDescription(depth: 0, target: self) + populateNestedDescription(&nested) + var result = nested.buildOpening() + result += nested.buildBody() + result += nested.buildClosing() + return result + } + + @_spi(Private) + public var debugDescription: String { + description + } +} + +// MARK: - Standard Library Conformances + +extension Array: NestedCustomStringConvertible where Element: NestedCustomStringConvertible { + package func populateNestedDescription(_ nested: inout NestedDescription) { + for element in self { + var child = NestedDescription(depth: nested.depth + 1, target: element) + element.populateNestedDescription(&child) + nested.append(child.description) + } + } +} + +extension Set: NestedCustomStringConvertible where Element: NestedCustomStringConvertible { + package func populateNestedDescription(_ nested: inout NestedDescription) { + for element in self { + var child = NestedDescription(depth: nested.depth + 1, target: element) + element.populateNestedDescription(&child) + nested.append(child.description) + } + } +} + +extension Dictionary: NestedCustomStringConvertible where Value: NestedCustomStringConvertible { + package func populateNestedDescription(_ nested: inout NestedDescription) { + for (key, value) in self { + var child = NestedDescription(depth: nested.depth + 1, target: value) + value.populateNestedDescription(&child) + nested.append(child.description, label: String(describing: key)) + } + } } diff --git a/Sources/OpenGestures/Util/NestedDescription.swift b/Sources/OpenGestures/Util/NestedDescription.swift new file mode 100644 index 0000000..81390e9 --- /dev/null +++ b/Sources/OpenGestures/Util/NestedDescription.swift @@ -0,0 +1,120 @@ +// +// NestedDescription.swift +// OpenGestures +// +// Audited for 9126.1.5 +// Status: Complete + +package struct NestedDescription { + package struct Options: OptionSet { + package let rawValue: Int + + package init(rawValue: Int) { + self.rawValue = rawValue + } + + package static let hideTypeName = Self(rawValue: 1 << 0) + package static let hideIdentity = Self(rawValue: 1 << 1) + package static let hideClassAddress = Self(rawValue: 1 << 2) + package static let compact = Self(rawValue: 1 << 3) + } + package var options: Options = [] + package var customPrefix: String? + package var customSuffix: String? + package let depth: Int + package let target: Any + package var buffer: [String] = [] + + mutating package func append( + _ content: String?, + label: String? = nil + ) { + guard let content else { + return + } + var result: String = "" + if let label { + result += "\(label): " + } + result += "\(content)" + if !result.isEmpty { + buffer.append(result) + } + } + + // MARK: - Build + + package func buildOpening() -> String { + if let customPrefix { + return customPrefix + } + var result: String + if !options.contains(.hideTypeName) { + result = "\(type(of: target))" + } else { + result = "" + } + var hasClassIdentity = false + if !options.contains(.hideClassAddress) { + let dynamicType = type(of: target) + if dynamicType is AnyObject.Type { + let obj = target as AnyObject + let address = UInt(bitPattern: ObjectIdentifier(obj)) + let addressString = "0x\(String(address, radix: 16, uppercase: false))" + result += " <\(addressString)" + hasClassIdentity = true + } + } + if !options.contains(.hideIdentity), + let identifiable = target as? any Identifiable { + if hasClassIdentity { + result += " \(identifiable.id)>" + } else { + result += " <\(identifiable.id)>" + } + } else if hasClassIdentity { + result += ">" + } + result += result.isEmpty ? "" : " " + result += "{" + result += buffer.isEmpty ? "" : " " + return result + } + + package func buildBody() -> String { + guard !options.contains(.compact) else { + return buffer.joined(separator: ", ") + } + let separator: String + if buffer.isEmpty { + separator = "" + } else { + let depth = depth + 1 + let indent = String(repeating: " ", count: 2) + let indentWithDepth = String(repeating: indent, count: depth) + separator = "\n" + indentWithDepth + } + return separator + buffer.joined(separator: separator) + } + + package func buildClosing() -> String { + var result = customSuffix ?? "}" + guard !buffer.isEmpty else { + return result + } + let leading: String + if options.contains(.compact) { + leading = customSuffix == nil ? " " : "" + } else { + let indent = String(repeating: " ", count: 2) + let indentWithDepth = String(repeating: indent, count: depth) + leading = "\n\(indentWithDepth)" + } + result = leading + result + return result + } + + package var description: String { + buildOpening() + buildBody() + buildClosing() + } +} From 6a17c7c7996c4108a26d35a34cd30d9739dc005d Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 12 Apr 2026 22:02:35 +0800 Subject: [PATCH 08/15] Update GestureTrait conformance for NestedCustomStringConvertible --- Sources/OpenGestures/Core/GestureTrait.swift | 39 ++++++++++++++----- .../Core/GestureTraitCompatibilityTests.swift | 22 +++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/Sources/OpenGestures/Core/GestureTrait.swift b/Sources/OpenGestures/Core/GestureTrait.swift index 7f2a987..85f9b49 100644 --- a/Sources/OpenGestures/Core/GestureTrait.swift +++ b/Sources/OpenGestures/Core/GestureTrait.swift @@ -95,22 +95,25 @@ public struct GestureTrait: Hashable, Identifiable, Sendable { } } -@_spi(Private) extension GestureTrait: NestedCustomStringConvertible { public var label: String { TraitLabelStore.shared.label(for: id.rawValue) } - public var description: String { + package func populateNestedDescription(_ nested: inout NestedDescription) { + nested.options.formUnion([.hideTypeName, .compact]) + nested.customPrefix = "" + nested.customSuffix = "" + nested.options.formUnion(.hideIdentity) if attributes.isEmpty { - return label + nested.append(label) + } else { + nested.customPrefix = label + " {" + nested.customSuffix = "}" + for (key, value) in attributes { + nested.append("\(key.label): \(value)") + } } - let attrs = attributes.map { "\($0.key.label): \($0.value)" }.joined(separator: ", ") - return "\(label) {\(attrs)}" - } - - public var debugDescription: String { - description } } @@ -175,6 +178,24 @@ extension GestureTraitCollection: Sequence { @_spi(Private) extension GestureTraitCollection: NestedCustomStringConvertible { + + package func populateNestedDescription(_ nested: inout NestedDescription) { + for trait in _traits.values { + var traitNested = NestedDescription( + options: [], + customPrefix: nil, + customSuffix: nil, + depth: nested.depth + 1, + target: trait, + buffer: [] + ) + trait.populateNestedDescription(&traitNested) + for item in traitNested.buffer { + nested.append(item) + } + } + } + public var label: String { "GestureTraitCollection" } public var description: String { diff --git a/Tests/OpenGesturesCompatibilityTests/Core/GestureTraitCompatibilityTests.swift b/Tests/OpenGesturesCompatibilityTests/Core/GestureTraitCompatibilityTests.swift index e5ff655..33e319c 100644 --- a/Tests/OpenGesturesCompatibilityTests/Core/GestureTraitCompatibilityTests.swift +++ b/Tests/OpenGesturesCompatibilityTests/Core/GestureTraitCompatibilityTests.swift @@ -20,6 +20,28 @@ struct GestureTraitCompatibilityTests { _ = box.id } + // MARK: - Description + + @Test(arguments: [ + (GestureTrait.pan(), "pan"), + (GestureTrait.tap(), "tap"), + (GestureTrait.longPress(), "longPress"), + (GestureTrait.tap(tapCount: 1), "tap {tapCount: 1}"), + (GestureTrait.tap(pointCount: 2), "tap {pointCount: 2}"), + (GestureTrait.longPress(maximumMovement: 10.0), "longPress {maximumMovement: 10.0}"), + ]) + func description(_ trait: GestureTrait, _ expectedDescription: String) { + #expect("\(trait)" == expectedDescription) + } + + @Test + func descriptionMultipleAttributes() { + let trait = GestureTrait.tap(tapCount: 1, pointCount: 3) + let description = "\(trait)" + // Dictionary ordering of attributes is non-deterministic + #expect(description == "tap {tapCount: 1, pointCount: 3}" || description == "tap {pointCount: 3, tapCount: 1}") + } + // MARK: - GestureTrait Factory Methods @Test From 5ee35792847b2dbfe5e65eb337e36cc92deace3e Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 12 Apr 2026 22:06:35 +0800 Subject: [PATCH 09/15] Add NestedCustomStringConvertibleTests --- .../OpenGestures/Util/NestedDescription.swift | 20 +++- .../NestedCustomStringConvertibleTests.swift | 94 +++++++++++++++++++ 2 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 Tests/OpenGesturesTests/Util/NestedCustomStringConvertibleTests.swift diff --git a/Sources/OpenGestures/Util/NestedDescription.swift b/Sources/OpenGestures/Util/NestedDescription.swift index 81390e9..5f86d1d 100644 --- a/Sources/OpenGestures/Util/NestedDescription.swift +++ b/Sources/OpenGestures/Util/NestedDescription.swift @@ -18,12 +18,28 @@ package struct NestedDescription { package static let hideClassAddress = Self(rawValue: 1 << 2) package static let compact = Self(rawValue: 1 << 3) } - package var options: Options = [] + package var options: Options package var customPrefix: String? package var customSuffix: String? package let depth: Int package let target: Any - package var buffer: [String] = [] + package var buffer: [String] + + package init( + options: Options = [], + customPrefix: String? = nil, + customSuffix: String? = nil, + depth: Int, + target: Any, + buffer: [String] = [] + ) { + self.options = options + self.customPrefix = customPrefix + self.customSuffix = customSuffix + self.depth = depth + self.target = target + self.buffer = buffer + } mutating package func append( _ content: String?, diff --git a/Tests/OpenGesturesTests/Util/NestedCustomStringConvertibleTests.swift b/Tests/OpenGesturesTests/Util/NestedCustomStringConvertibleTests.swift new file mode 100644 index 0000000..dc20743 --- /dev/null +++ b/Tests/OpenGesturesTests/Util/NestedCustomStringConvertibleTests.swift @@ -0,0 +1,94 @@ +// +// NestedCustomStringConvertibleTests.swift +// OpenGesturesTests + +@_spi(Private) import OpenGestures +import Testing + +// MARK: - Test Helpers + +private struct TestNode: NestedCustomStringConvertible { + var name: String + var children: [TestNode] = [] + + func populateNestedDescription(_ nested: inout NestedDescription) { + nested.options.formUnion([.hideTypeName, .hideIdentity]) + if children.isEmpty { + nested.customPrefix = name + nested.customSuffix = "" + } else { + nested.customPrefix = name + " {" + nested.customSuffix = "}" + for child in children { + var childNested = NestedDescription(depth: nested.depth + 1, target: child) + child.populateNestedDescription(&childNested) + nested.append(childNested.description) + } + } + } +} + +// MARK: - Tests + +@Suite +struct NestedCustomStringConvertibleTests { + @Test + func leafDescription() { + let leaf = TestNode(name: "tap") + #expect(leaf.description == "tap") + } + + @Test + func leafDebugDescription() { + let leaf = TestNode(name: "pan") + #expect(leaf.debugDescription == "pan") + } + + @Test + func containerWithOneChild() { + let c = TestNode(name: "Root", children: [TestNode(name: "child1")]) + #expect(c.description == #""" + Root { + child1 + } + """#) + } + + @Test + func containerWithMultipleChildren() { + let c = TestNode(name: "Root", children: [ + TestNode(name: "a"), + TestNode(name: "b"), + TestNode(name: "c"), + ]) + #expect(c.description == #""" + Root { + a + b + c + } + """#) + } + + @Test + func emptyContainer() { + let c = TestNode(name: "Empty") + #expect(c.description == "Empty") + } + + @Test + func nestedContainers() { + let c = TestNode(name: "L0", children: [ + TestNode(name: "L1", children: [ + TestNode(name: "leaf"), + ]), + ]) + #expect(c.description == #""" + L0 { + L1 { + leaf + } + } + """#) + } +} From 35aaa85cd1ecffac05ee6c51fb9a5dcb5bf5b1a9 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 12 Apr 2026 22:20:58 +0800 Subject: [PATCH 10/15] Fix GestureRelation description and test case --- .../OpenGestures/Core/GestureRelation.swift | 21 ++++-------- .../Core/GestureRelationTests.swift | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/Sources/OpenGestures/Core/GestureRelation.swift b/Sources/OpenGestures/Core/GestureRelation.swift index 1905a67..3ff7e42 100644 --- a/Sources/OpenGestures/Core/GestureRelation.swift +++ b/Sources/OpenGestures/Core/GestureRelation.swift @@ -5,7 +5,7 @@ // Audited for 9126.1.5 // Status: Complete -import OrderedCollections +package import OrderedCollections // MARK: - GestureRelationType @@ -71,7 +71,7 @@ package struct RelationMap: Sendable { self.relations = [:] } - init(relations: OrderedDictionary>) { + package init(relations: OrderedDictionary>) { self.relations = relations } @@ -128,21 +128,14 @@ extension RelationMap: Sequence { } } -// MARK: - RelationMap + NestedCustomStringConvertible [TBA] +// MARK: - RelationMap + NestedCustomStringConvertible extension RelationMap: NestedCustomStringConvertible { - package var label: String { "RelationMap" } - - package var description: String { - if relations.isEmpty { - return "\(label) {}" + package func populateNestedDescription(_ nested: inout NestedDescription) { + nested.options.formUnion(.hideTypeName) + for (matcher, definition) in relations { + nested.append("\(matcher)", label: "\(definition)") } - let entries = relations.map { "\($0.key): \($0.value)" }.joined(separator: ", ") - return "\(label) {\(entries)}" - } - - package var debugDescription: String { - description } } diff --git a/Tests/OpenGesturesTests/Core/GestureRelationTests.swift b/Tests/OpenGesturesTests/Core/GestureRelationTests.swift index 01b1731..cfa212e 100644 --- a/Tests/OpenGesturesTests/Core/GestureRelationTests.swift +++ b/Tests/OpenGesturesTests/Core/GestureRelationTests.swift @@ -77,6 +77,38 @@ struct RelationMapTests { } #expect(count == 2) } + + // MARK: - Description + + @Test(arguments: [ + (RelationMap(), "{}"), + ( + RelationMap(relations: [ + GestureNodeMatcher.any(position: .any): [RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular)], + ]), + #""" + { \#("") + [exclusion[out]=regular]: { any, any } + } + """# + ), + ( + RelationMap(relations: [ + GestureNodeMatcher.any(position: .any): [ + RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular), + RelationDefinition(type: .failureRequirement, direction: .incoming), + ], + ]), + #""" + { \#("") + [exclusion[out]=regular, failureRequirement[in]=dynamic]: { any, any } + } + """# + ), + ]) + func description(_ map: RelationMap, _ expected: String) { + #expect("\(map)" == expected) + } } // MARK: - RelationDefinitionTests From 9a12aa11d997bbddf06dfaa96035cc9d0854a49f Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 12 Apr 2026 22:57:06 +0800 Subject: [PATCH 11/15] Fix RelationMapTests --- .../Core/GestureRelationTests.swift | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Tests/OpenGesturesTests/Core/GestureRelationTests.swift b/Tests/OpenGesturesTests/Core/GestureRelationTests.swift index cfa212e..58eb2ae 100644 --- a/Tests/OpenGesturesTests/Core/GestureRelationTests.swift +++ b/Tests/OpenGesturesTests/Core/GestureRelationTests.swift @@ -88,20 +88,27 @@ struct RelationMapTests { ]), #""" { \#("") - [exclusion[out]=regular]: { any, any } + [exclusion[out]=regular]: any, position: any } """# ), ( - RelationMap(relations: [ - GestureNodeMatcher.any(position: .any): [ + { + var map = RelationMap() + let matcher = GestureNodeMatcher.any(position: .any) + map.add( RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular), + for: matcher + ) + map.add( RelationDefinition(type: .failureRequirement, direction: .incoming), - ], - ]), + for: matcher + ) + return map + }(), #""" { \#("") - [exclusion[out]=regular, failureRequirement[in]=dynamic]: { any, any } + [exclusion[out]=regular, failureRequirement[in]=dynamic]: any, position: any } """# ), From df6289c48c3d32d969c46776107892f9eb3e3ad5 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 12 Apr 2026 22:58:14 +0800 Subject: [PATCH 12/15] Update GestureNodeMatcher implementation --- .../Core/GestureNodeMatcher.swift | 24 +++++++++++-------- .../OpenGestures/Util/NestedDescription.swift | 6 ++--- ...GestureNodeMatcherCompatibilityTests.swift | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Sources/OpenGestures/Core/GestureNodeMatcher.swift b/Sources/OpenGestures/Core/GestureNodeMatcher.swift index f208828..ef8bec0 100644 --- a/Sources/OpenGestures/Core/GestureNodeMatcher.swift +++ b/Sources/OpenGestures/Core/GestureNodeMatcher.swift @@ -39,18 +39,22 @@ extension GestureNodeMatcher: Comparable { // MARK: - GestureNodeMatcher + NestedCustomStringConvertible -@_spi(Private) extension GestureNodeMatcher: NestedCustomStringConvertible { - public var label: String { + package func populateNestedDescription(_ nested: inout NestedDescription) { + nested.options.formUnion([.hideTypeName, .compact]) + nested.customPrefix = "" + nested.customSuffix = "" switch self { - case let .id(id): "\(id)" - case let .tag(tag): "\(tag)" - case let .traits(collection, position): "\(collection), position: \(position)" - case let .any(position): "any, position: \(position)" + case let .id(id): + nested.append(id) + case let .tag(tag): + nested.append(tag) + case let .traits(collection, position): + nested.append(collection) + nested.append(position, label: "position") + case let .any(position): + nested.append("any") + nested.append(position, label: "position") } } - - public var description: String { label } - - public var debugDescription: String { label } } diff --git a/Sources/OpenGestures/Util/NestedDescription.swift b/Sources/OpenGestures/Util/NestedDescription.swift index 5f86d1d..20be3a1 100644 --- a/Sources/OpenGestures/Util/NestedDescription.swift +++ b/Sources/OpenGestures/Util/NestedDescription.swift @@ -41,11 +41,11 @@ package struct NestedDescription { self.buffer = buffer } - mutating package func append( - _ content: String?, + mutating package func append( + _ content: @autoclosure () -> T?, label: String? = nil ) { - guard let content else { + guard let content = content() else { return } var result: String = "" diff --git a/Tests/OpenGesturesCompatibilityTests/Core/GestureNodeMatcherCompatibilityTests.swift b/Tests/OpenGesturesCompatibilityTests/Core/GestureNodeMatcherCompatibilityTests.swift index 74ba114..6075f23 100644 --- a/Tests/OpenGesturesCompatibilityTests/Core/GestureNodeMatcherCompatibilityTests.swift +++ b/Tests/OpenGesturesCompatibilityTests/Core/GestureNodeMatcherCompatibilityTests.swift @@ -42,7 +42,7 @@ struct GestureNodeMatcherCompatibilityTests { @Test(arguments: [ (GestureNodeMatcher.id(.init(rawValue: 2)), "2"), - (GestureNodeMatcher.tag("A"), "\"A\""), + (GestureNodeMatcher.tag("A"), #""A""#), (GestureNodeMatcher.traits(.withTrait(.pan()), position: .below), "[pan], position: below"), (GestureNodeMatcher.any(position: .above), "any, position: above"), ]) From 4bdfa3fb020b5ecf1d1beda908509253ff54d91f Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 12 Apr 2026 23:03:40 +0800 Subject: [PATCH 13/15] Update Mergeable --- Sources/OpenGestures/Core/GestureTrait.swift | 3 +-- Sources/OpenGestures/Util/Mergeable.swift | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/OpenGestures/Core/GestureTrait.swift b/Sources/OpenGestures/Core/GestureTrait.swift index 85f9b49..6cc391e 100644 --- a/Sources/OpenGestures/Core/GestureTrait.swift +++ b/Sources/OpenGestures/Core/GestureTrait.swift @@ -209,9 +209,8 @@ extension GestureTraitCollection: NestedCustomStringConvertible { // MARK: - GestureTraitCollection + Mergeable -@_spi(Private) extension GestureTraitCollection: Mergeable { - public mutating func merge(_ other: GestureTraitCollection) { + package mutating func merge(_ other: GestureTraitCollection) { _traits.merge(other._traits) { $1 } } } diff --git a/Sources/OpenGestures/Util/Mergeable.swift b/Sources/OpenGestures/Util/Mergeable.swift index d1b6a09..298174b 100644 --- a/Sources/OpenGestures/Util/Mergeable.swift +++ b/Sources/OpenGestures/Util/Mergeable.swift @@ -7,7 +7,6 @@ // MARK: - Mergeable -@_spi(Private) -public protocol Mergeable { +package protocol Mergeable { mutating func merge(_ other: Self) } From 4291d117d9856e822a55f5097e026333f2ea7516 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 12 Apr 2026 23:57:37 +0800 Subject: [PATCH 14/15] Fix GestureTraitCollection description --- Sources/OpenGestures/Core/GestureTrait.swift | 30 +++----------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/Sources/OpenGestures/Core/GestureTrait.swift b/Sources/OpenGestures/Core/GestureTrait.swift index 6cc391e..15de1bd 100644 --- a/Sources/OpenGestures/Core/GestureTrait.swift +++ b/Sources/OpenGestures/Core/GestureTrait.swift @@ -176,34 +176,12 @@ extension GestureTraitCollection: Sequence { // MARK: - GestureTraitCollection + CustomStringConvertible -@_spi(Private) extension GestureTraitCollection: NestedCustomStringConvertible { - package func populateNestedDescription(_ nested: inout NestedDescription) { - for trait in _traits.values { - var traitNested = NestedDescription( - options: [], - customPrefix: nil, - customSuffix: nil, - depth: nested.depth + 1, - target: trait, - buffer: [] - ) - trait.populateNestedDescription(&traitNested) - for item in traitNested.buffer { - nested.append(item) - } - } - } - - public var label: String { "GestureTraitCollection" } - - public var description: String { - "[\(_traits.values.map(\.description).joined(separator: ", "))]" - } - - public var debugDescription: String { - description + nested.options.formUnion([.hideTypeName, .compact]) + nested.customPrefix = "" + nested.customSuffix = "" + nested.append(_traits.values) } } From 547b8d18f0f45bc2d2a440120b9bd21f117520cf Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 13 Apr 2026 00:11:54 +0800 Subject: [PATCH 15/15] Fix RelationMap description test for non-deterministic Set ordering --- .../Core/GestureRelationTests.swift | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/Tests/OpenGesturesTests/Core/GestureRelationTests.swift b/Tests/OpenGesturesTests/Core/GestureRelationTests.swift index 58eb2ae..0cf6283 100644 --- a/Tests/OpenGesturesTests/Core/GestureRelationTests.swift +++ b/Tests/OpenGesturesTests/Core/GestureRelationTests.swift @@ -92,29 +92,38 @@ struct RelationMapTests { } """# ), - ( - { - var map = RelationMap() - let matcher = GestureNodeMatcher.any(position: .any) - map.add( - RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular), - for: matcher - ) - map.add( - RelationDefinition(type: .failureRequirement, direction: .incoming), - for: matcher - ) - return map - }(), - #""" + ]) + func description(_ map: RelationMap, _ expected: String) { + #expect("\(map)" == expected) + } + + @Test + func descriptionMultipleDefinitions() { + var map = RelationMap() + let matcher = GestureNodeMatcher.any(position: .any) + map.add( + RelationDefinition(type: .exclusion, direction: .outgoing, role: .regular), + for: matcher + ) + map.add( + RelationDefinition(type: .failureRequirement, direction: .incoming), + for: matcher + ) + let description = "\(map)" + // Set ordering is non-deterministic + #expect( + description == #""" { \#("") [exclusion[out]=regular, failureRequirement[in]=dynamic]: any, position: any } """# - ), - ]) - func description(_ map: RelationMap, _ expected: String) { - #expect("\(map)" == expected) + || + description == #""" + { \#("") + [failureRequirement[in]=dynamic, exclusion[out]=regular]: any, position: any + } + """# + ) } }