From 80cc702ce21c92b3d2b7ba6512335f7f4ffc2188 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 1 Jun 2026 01:25:50 +0800 Subject: [PATCH 01/21] Update Package.swift --- Package.resolved | 2 +- Package.swift | 26 +++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Package.resolved b/Package.resolved index 34fb06653..9743943bc 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "5103f85dbbced6ddddbe8d671804a1c9bdede9e4d43fb5507e296d9d44beadaf", + "originHash" : "57f62f1d8483f61ad08ac4c46692ec2e13644d24831e5e9f8248974cee905784", "pins" : [ { "identity" : "darwinprivateframeworks", diff --git a/Package.swift b/Package.swift index e4d182d6e..555423825 100644 --- a/Package.swift +++ b/Package.swift @@ -161,6 +161,7 @@ let linkCoreUI = envBoolValue("LINK_COREUI", default: buildForDarwinPlatform && let linkCoreSVG = envBoolValue("LINK_CORESVG", default: buildForDarwinPlatform && !isSPIBuild) let linkSFSymbols = envBoolValue("LINK_SFSYMBOLS", default: buildForDarwinPlatform && !isSPIBuild) let linkBacklightServices = envBoolValue("LINK_BACKLIGHTSERVICES", default: buildForDarwinPlatform && !isSPIBuild) +let linkGestures = envBoolValue("LINK_GESTURES", default: buildForDarwinPlatform && !isSPIBuild && releaseVersion >= 2025) // This should be disabled for UI test target due to link issue of Testing. // Only enable for non-UI test targets. let linkTesting = envBoolValue("LINK_TESTING") @@ -289,6 +290,9 @@ if linkBacklightServices { ) ) } +if linkGestures { + sharedSwiftSettings.append(.define("OPENSWIFTUI_LINK_GESTURES")) +} if swiftUIRenderCondition { sharedCSettings.append(.define("OPENSWIFTUI_SWIFTUI_RENDERER", .when(platforms: .darwinPlatforms))) @@ -419,6 +423,10 @@ extension Target { ) } + func addGesturesSettings() { + dependencies.append(.product(name: "Gestures", package: "DarwinPrivateFrameworks")) + } + func addOpenCombineSettings() { dependencies.append(.product(name: "OpenCombine", package: "OpenCombine")) dependencies.append(.product(name: "OpenCombineFoundation", package: "OpenCombine")) @@ -837,12 +845,24 @@ if renderBoxCondition { openSwiftUIBridgeTestTarget.addRBSettings() } -if attributeGraphCondition || renderBoxCondition { +if linkGestures { + openSwiftUICoreTarget.addGesturesSettings() + openSwiftUITarget.addGesturesSettings() + + openSwiftUISPITestTarget.addGesturesSettings() + openSwiftUICoreTestTarget.addGesturesSettings() + openSwiftUITestTarget.addGesturesSettings() + openSwiftUICompatibilityTestTarget.addGesturesSettings() + openSwiftUIBridgeTestTarget.addGesturesSettings() +} + +if attributeGraphCondition || renderBoxCondition || linkGestures { let release = EnvManager.shared.withDomain("DarwinPrivateFrameworks") { envIntValue("TARGET_RELEASE", default: 2024) } package.platforms = switch release { case 2024: [.iOS(.v18), .macOS(.v15), .macCatalyst(.v18), .tvOS(.v18), .watchOS(.v10), .visionOS(.v2)] + case 2025: [.iOS("26.0"), .macOS("26.0"), .macCatalyst("26.0"), .tvOS("26.0"), .watchOS("26.0"), .visionOS("26.0")] default: nil } } else { @@ -876,7 +896,7 @@ if useLocalDeps { .package(path: "../OpenRenderBox"), .package(path: "../OpenObservation"), ] - if attributeGraphCondition || renderBoxCondition || linkCoreUI || linkCoreSVG || linkSFSymbols || linkBacklightServices { + if attributeGraphCondition || renderBoxCondition || linkCoreUI || linkCoreSVG || linkSFSymbols || linkBacklightServices || linkGestures { dependencies.append(.package(path: "../DarwinPrivateFrameworks")) } package.dependencies += dependencies @@ -888,7 +908,7 @@ if useLocalDeps { .package(url: "https://github.com/OpenSwiftUIProject/OpenRenderBox", branch: "main"), .package(url: "https://github.com/OpenSwiftUIProject/OpenObservation", branch: "main"), ] - if attributeGraphCondition || renderBoxCondition || linkCoreUI || linkCoreSVG || linkSFSymbols { + if attributeGraphCondition || renderBoxCondition || linkCoreUI || linkCoreSVG || linkSFSymbols || linkGestures { dependencies.append(.package(url: "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", branch: "main")) } package.dependencies += dependencies From c158408ee92a5ce2de87c34cde3a46186f5a1d59 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 1 Jun 2026 01:29:06 +0800 Subject: [PATCH 02/21] Optimize OPENSWIFTUI_LINK_COREUI --- .../Shape/ShapeStyle/ResolvedMulticolorStyle.swift | 2 +- Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift | 2 ++ Sources/OpenSwiftUICore/View/Image/Image.swift | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ResolvedMulticolorStyle.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ResolvedMulticolorStyle.swift index 9f8ae4103..64fe753e6 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ResolvedMulticolorStyle.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ResolvedMulticolorStyle.swift @@ -53,7 +53,7 @@ package struct ResolvedMulticolorStyle: Equatable, @unchecked Sendable { case "black": return .black default: - #if canImport(Darwin) + #if canImport(Darwin) && OPENSWIFTUI_LINK_COREUI return Color.Resolved.named(name, bundle: bundle, environment: environment) #else return nil diff --git a/Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift b/Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift index cc461b8f8..38b02cc4b 100644 --- a/Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift +++ b/Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift @@ -196,7 +196,9 @@ package struct ResolvedVectorGlyph: Equatable { allowsContentTransitions = false variableValue = value.map { CGFloat($0) } ?? .infinity } + #if OPENSWIFTUI_LINK_COREUI animator.glyph = glyph + #endif animator.variableValue = variableValue animator.flipsRightToLeft = flipsRightToLeft animator.renderingMode = context.effectiveSymbolRenderingMode?.rbRenderingMode ?? 255 diff --git a/Sources/OpenSwiftUICore/View/Image/Image.swift b/Sources/OpenSwiftUICore/View/Image/Image.swift index 8124cfe1b..c82877e39 100644 --- a/Sources/OpenSwiftUICore/View/Image/Image.swift +++ b/Sources/OpenSwiftUICore/View/Image/Image.swift @@ -155,7 +155,7 @@ package struct ImageResolutionContext { to glyph: CUINamedVectorGlyph, variableValue: CGFloat ) -> Bool { - #if canImport(Darwin) + #if canImport(Darwin) && OPENSWIFTUI_LINK_COREUI guard let symbolAnimator, !options.contains(.animationsDisabled), let transactionAttribute = transaction.attribute @@ -273,7 +273,7 @@ package struct ImageResolutionContext { } #else _openSwiftUIUnimplementedWarning() - return false + return true #endif } From 720d0ea03c97abda49d48b53a085bb6c5e48c526 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 1 Jun 2026 01:48:41 +0800 Subject: [PATCH 03/21] Update MapGesture --- .../Event/Gesture/MapGesture.swift | 73 ++++++++++++++++--- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/Sources/OpenSwiftUICore/Event/Gesture/MapGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/MapGesture.swift index 9ec56733d..58c27cfba 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/MapGesture.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/MapGesture.swift @@ -2,9 +2,14 @@ // MapGesture.swift // OpenSwiftUICore // -// Status: Unimplmented +// Audited for 6.5.4 +// Status: Complete // ID: EA8BFBF553A9179E7F3A85C72F795A9F (SwiftUICore) +import OpenAttributeGraphShims + +// MARK: - MapGesture + package struct MapGesture: GestureModifier { package var body: (GesturePhase) -> GesturePhase @@ -21,14 +26,14 @@ package struct MapGesture: GestureModifier { inputs: _GestureInputs, body: (_GestureInputs) -> _GestureOutputs ) -> _GestureOutputs { - - - -// ModifierGesture>.makeDebuggableGesture( -// gesture: modifier[offset: { .of(&$0.body) }], -// inputs: inputs -// ) - _openSwiftUIUnimplementedFailure() + let outputs = body(inputs) + let phase = Attribute(MapPhase( + modifier: modifier.value, + phase: outputs.phase, + resetSeed: inputs.resetSeed, + lastResetSeed: .zero + )) + return outputs.withPhase(phase) } package typealias BodyValue = From @@ -36,6 +41,7 @@ package struct MapGesture: GestureModifier { package typealias Value = To } +@available(OpenSwiftUI_v1_0, *) extension Gesture { package func mapPhase( _ body: @escaping (GesturePhase) -> GesturePhase @@ -43,22 +49,44 @@ extension Gesture { modifier(MapGesture(body)) } + /// Returns a gesture that uses the given closure to map over this + /// gesture's value. public func map(_ body: @escaping (Value) -> T) -> _MapGesture { - _openSwiftUIUnimplementedFailure() + _MapGesture(_body: modifier(MapGesture(body))) } package func discrete(_ enabled: Bool = true) -> ModifierGesture, Self> { - _openSwiftUIUnimplementedFailure() + mapPhase { phase in + guard enabled, + case let .active(value) = phase else { + return phase + } + return .possible(value) + } } } @available(OpenSwiftUI_v1_0, *) public struct _MapGesture: PrimitiveGesture where Content: Gesture { + package var _body: ModifierGesture, Content> + + package init(_body: ModifierGesture, Content>) { + self._body = _body + } + public static func _makeGesture( gesture: _GraphValue<_MapGesture>, inputs: _GestureInputs ) -> _GestureOutputs { - _openSwiftUIUnimplementedFailure() + MapGesture.makeDebuggableGesture( + modifier: gesture[offset: { .of(&$0._body.modifier) }], + inputs: inputs + ) { inputs in + Content.makeDebuggableGesture( + gesture: gesture[offset: { .of(&$0._body.content) }], + inputs: inputs + ) + } } public typealias Body = Never @@ -66,3 +94,24 @@ public struct _MapGesture: PrimitiveGesture where Content: Gestu @available(*, unavailable) extension _MapGesture: Sendable {} + +private struct MapPhase: ResettableGestureRule, CustomStringConvertible { + @Attribute var modifier: MapGesture + @Attribute var phase: GesturePhase + @Attribute var resetSeed: UInt32 + var lastResetSeed: UInt32 + + typealias PhaseValue = To + typealias Value = GesturePhase + + mutating func updateValue() { + guard resetIfNeeded() else { + return + } + value = modifier.body(phase) + } + + var description: String { + "Map → \(To.self)" + } +} From 19dde52f7850b4642d6d5cad0bedfd49cc6d69be Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 1 Jun 2026 02:08:16 +0800 Subject: [PATCH 04/21] Update RepeatGesture --- .../Event/Gesture/RepeatGesture.swift | 80 ++++++++++++++++++- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/Sources/OpenSwiftUICore/Event/Gesture/RepeatGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/RepeatGesture.swift index 32c209f24..100b99478 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/RepeatGesture.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/RepeatGesture.swift @@ -2,12 +2,13 @@ // RepeatGesture.swift // OpenSwiftUICore // -// Status: Unimplmented +// Audited for 6.5.4 +// Status: Complete // ID: BECD07FC80B4CA0BF429B041392E806A (SwiftUICore) import OpenAttributeGraphShims -// MARK: - RepeatGesture [6.5.4] +// MARK: - RepeatGesture extension Gesture { package func repeatCount( @@ -32,7 +33,26 @@ package struct RepeatGesture: GestureModifier { inputs: _GestureInputs, body: (_GestureInputs) -> _GestureOutputs ) -> _GestureOutputs { - _openSwiftUIUnimplementedFailure() + let resetDelta = Attribute(value: UInt32.zero) + let resetSeed = Attribute(RepeatResetSeed( + resetSeed: inputs.resetSeed, + delta: resetDelta + )) + var childInputs = inputs + childInputs.resetSeed = resetSeed + let outputs = body(childInputs) + let phase = Attribute(RepeatPhase( + modifier: modifier.value, + phase: outputs.phase, + time: inputs.viewInputs.time, + resetSeed: inputs.resetSeed, + resetDelta: resetDelta, + useGestureGraph: inputs.options.contains(.gestureGraph), + deadline: nil, + index: .zero, + lastResetSeed: .zero + )) + return outputs.withPhase(phase) } } @@ -50,10 +70,62 @@ private struct RepeatPhase: ResettableGestureRule { typealias PhaseValue = V typealias Value = GesturePhase + mutating func resetPhase() { + deadline = nil + index = .zero + } + mutating func updateValue() { guard resetIfNeeded() else { return } - _openSwiftUIUnimplementedFailure() + if let deadline, deadline < time { + value = .failed + return + } + switch phase { + case .possible: + value = phase + case let .active(value): + deadline = nil + let repeatLimit = modifier.count - 1 + if repeatLimit > Int(index) { + self.value = .possible(value) + } else { + self.value = phase + } + case let .ended(wrapped): + index &+= 1 + if modifier.count > Int(index) { + deadline = time + modifier.maximumDelay + value = .possible(wrapped) + GraphHost.currentHost.continueTransaction { [_resetDelta, index] in + _resetDelta.value = index + } + } else { + deadline = nil + value = phase + } + case .failed: + value = phase + } + guard let deadline else { + return + } + if useGestureGraph { + let gestureGraph = GestureGraph.current + gestureGraph.nextUpdateTime = min(gestureGraph.nextUpdateTime, deadline) + } else { + ViewGraph.current.nextUpdate.gestures.at(deadline) + } + } +} + +private struct RepeatResetSeed: Rule { + @Attribute var resetSeed: UInt32 + @Attribute var delta: UInt32 + + var value: UInt32 { + resetSeed &+ delta } } From 259c4dc4b395035d55cea444fbcbcd46afac9dd7 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 1 Jun 2026 02:53:11 +0800 Subject: [PATCH 05/21] Add StateContainerGesture --- .../Event/Gesture/StateContainerGesture.swift | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Event/Gesture/StateContainerGesture.swift diff --git a/Sources/OpenSwiftUICore/Event/Gesture/StateContainerGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/StateContainerGesture.swift new file mode 100644 index 000000000..333f6c6ae --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/StateContainerGesture.swift @@ -0,0 +1,78 @@ +// +// StateContainerGesture.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: EA62389F5A6356B5DBAFB6A6AFC6ECC7 (SwiftUICore) + +import OpenAttributeGraphShims + +// MARK: - GestureStateProtocol + +package protocol GestureStateProtocol { + init() +} + +extension GestureStateProtocol { + package static func gesture( + content: T, + _ body: @escaping (inout Self, GesturePhase) -> GesturePhase + ) -> ModifierGesture, T> where T: Gesture { + content.modifier(StateContainerGesture(body)) + } +} + +// MARK: - StateContainerGesture + +package struct StateContainerGesture: GestureModifier where StateType: GestureStateProtocol { + package var body: (inout StateType, GesturePhase) -> GesturePhase + + package init(_ body: @escaping (inout StateType, GesturePhase) -> GesturePhase) { + self.body = body + } + + package static func _makeGesture( + modifier: _GraphValue, + inputs: _GestureInputs, + body: (_GestureInputs) -> _GestureOutputs + ) -> _GestureOutputs { + let outputs = body(inputs) + let phase = Attribute(StateContainerPhase( + modifier: modifier.value, + childPhase: outputs.phase, + resetSeed: inputs.resetSeed, + state: StateType(), + lastResetSeed: .zero + )) + return outputs.withPhase(phase) + } +} + +// MARK: - StateContainerPhase + +private struct StateContainerPhase: ResettableGestureRule, CustomStringConvertible where StateType: GestureStateProtocol { + @Attribute var modifier: StateContainerGesture + @Attribute var childPhase: GesturePhase + @Attribute var resetSeed: UInt32 + var state: StateType + var lastResetSeed: UInt32 + + typealias PhaseValue = ResultValue + typealias Value = GesturePhase + + mutating func resetPhase() { + state = StateType() + } + + mutating func updateValue() { + guard resetIfNeeded() else { + return + } + value = modifier.body(&state, childPhase) + } + + var description: String { + "State → \(StateType.self)" + } +} From a1f0c8e31ce10d2e0049178d4e13632676771f35 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 1 Jun 2026 02:35:40 +0800 Subject: [PATCH 06/21] Add CallbacksGesture --- Sources/OpenSwiftUICore/Data/Update.swift | 12 +- .../Event/Gesture/CallbacksGesture.swift | 394 ++++++++++++++++++ .../Event/Gesture/GestureInputs.swift | 7 +- 3 files changed, 406 insertions(+), 7 deletions(-) create mode 100644 Sources/OpenSwiftUICore/Event/Gesture/CallbacksGesture.swift diff --git a/Sources/OpenSwiftUICore/Data/Update.swift b/Sources/OpenSwiftUICore/Data/Update.swift index 7ef3db858..69c694c96 100644 --- a/Sources/OpenSwiftUICore/Data/Update.swift +++ b/Sources/OpenSwiftUICore/Data/Update.swift @@ -231,12 +231,12 @@ package enum Update { // FIXME package enum CustomEventTrace { package enum ActionEventType { - package enum Reason { - case onAppear - case onChange - case onDisappear - case gesture - case didReleaseButton + package enum Reason: UInt32 { + case onAppear = 65 + case onChange = 67 + case onDisappear = 68 + case gesture = 71 + case didReleaseButton = 82 } } } diff --git a/Sources/OpenSwiftUICore/Event/Gesture/CallbacksGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/CallbacksGesture.swift new file mode 100644 index 000000000..a8519d07e --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/CallbacksGesture.swift @@ -0,0 +1,394 @@ +// +// CallbacksGesture.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: E484392718A4E902E7DCD559BC215BF0 (SwiftUICore) + +import OpenAttributeGraphShims + +// MARK: - GestureCallbacks + +package protocol GestureCallbacks { + associatedtype StateType = Void + + static var initialState: StateType { get } + + associatedtype Value + + func dispatch(phase: GesturePhase, state: inout StateType) -> (() -> Void)? + + func cancel(state: StateType) -> (() -> Void)? +} + +extension GestureCallbacks where StateType: GestureStateProtocol { + package static var initialState: StateType { + StateType() + } +} + +extension GestureCallbacks where StateType == Void { + package static var initialState: Void { + () + } +} + +extension GestureCallbacks { + package func cancel(state: StateType) -> (() -> Void)? { + nil + } +} + +// MARK: - CallbacksGesture + +package struct CallbacksGesture: GestureModifier where Callbacks: GestureCallbacks { + package var callbacks: Callbacks + + package init(callbacks: Callbacks) { + self.callbacks = callbacks + } + + package static func _makeGesture( + modifier: _GraphValue, + inputs: _GestureInputs, + body: (_GestureInputs) -> _GestureOutputs + ) -> _GestureOutputs { + let outputs = body(inputs) + let phase = Attribute(CallbacksPhase( + modifier: modifier.value, + phase: outputs.phase, + resetSeed: inputs.resetSeed, + useGestureGraph: inputs.options.contains(.gestureGraph), + state: Callbacks.initialState, + cancel: nil, + lastResetSeed: .zero + )) + defer { phase.setFlags([.transactional, .removable], mask: .all) } + return outputs.withPhase(phase) + } + + package typealias BodyValue = Callbacks.Value + + package typealias Value = Callbacks.Value +} + +// MARK: - FullGestureCallbacks + +package struct FullGestureCallbacks: GestureCallbacks where T: Equatable { + package typealias Value = T + + package struct StateType: GestureStateProtocol { + var active: Bool + var oldPhase: GesturePhase? + + package init() { + active = false + oldPhase = nil + } + } + + package var possible: ((Value?) -> Void)? + package var changed: ((Value) -> Void)? + package var ended: ((Value) -> Void)? + package var failed: (() -> Void)? + + package init( + possible: ((Value?) -> Void)? = nil, + changed: ((Value) -> Void)? = nil, + ended: ((Value) -> Void)? = nil, + failed: (() -> Void)? = nil + ) { + self.possible = possible + self.changed = changed + self.ended = ended + self.failed = failed + } + + package func dispatch( + phase: GesturePhase, + state: inout StateType + ) -> (() -> Void)? { + guard state.oldPhase.map({ $0 != phase }) ?? true else { + return nil + } + state.oldPhase = phase + + switch phase { + case let .possible(value): + state.active = false + return { + possible?(value) + } + case let .active(value): + state.active = true + guard let changed else { + return nil + } + return { + withAnimation(nil) { + changed(value) + } + } + case let .ended(value): + return bind(ended, value) + case .failed: + return failed + } + } + + package func cancel(state: StateType) -> (() -> Void)? { + failed + } +} + +package typealias FullCallbacksGesture = ModifierGesture>, T> where T: Gesture, T.Value: Equatable + +// MARK: - Gesture + Callback + +@available(OpenSwiftUI_v1_0, *) +extension Gesture { + package func callbacks( + _ callbacks: Callbacks + ) -> ModifierGesture, Self> where Callbacks: GestureCallbacks, Value == Callbacks.Value { + modifier(CallbacksGesture(callbacks: callbacks)) + } + + /// Adds an action to perform when the gesture ends. + /// + /// - Important: The action is only performed if the gesture ends successfully. + /// Use a `@GestureState` property to track state that is reset + /// regardless of how the gesture ends. + /// + /// - Parameter action: The action to perform when this gesture ends. The + /// `action` closure's parameter contains the final value of the gesture. + /// + /// - Returns: A gesture that triggers `action` when the gesture ends. + nonisolated public func onEnded(@_inheritActorContext _ action: @escaping (Value) -> Void) -> _EndedGesture { + _EndedGesture(_body: callbacks(EndedCallbacks(ended: action))) + } + + package func onFailed(_ action: @escaping () -> Void) -> ModifierGesture>, Self> { + callbacks(FailedCallbacks(failed: action)) + } +} + +@available(OpenSwiftUI_v1_0, *) +extension Gesture where Value: Equatable { + + /// Adds an action to perform when the gesture's value changes. + /// + /// - Parameter action: The action to perform when this gesture's value + /// changes. The `action` closure's parameter contains the gesture's new + /// value. + /// + /// - Returns: A gesture that triggers `action` when this gesture's value + /// changes. + public func onChanged(@_inheritActorContext _ action: @escaping (Value) -> Void) -> _ChangedGesture { + _ChangedGesture(_body: callbacks(ChangedCallbacks(changed: action))) + } + + package func callbacks( + possible: ((Value?) -> Void)? = nil, + changed: ((Value) -> Void)? = nil, + ended: ((Value) -> Void)? = nil, + failed: (() -> Void)? = nil + ) -> FullCallbacksGesture { + callbacks(FullGestureCallbacks( + possible: possible, + changed: changed, + ended: ended, + failed: failed + )) + } +} + +// MARK: - _EndedGesture + +@available(OpenSwiftUI_v1_0, *) +public struct _EndedGesture: PrimitiveGesture where Content: Gesture { + fileprivate var _body: _Body + + fileprivate init(_body: _Body) { + self._body = _body + } + + fileprivate typealias _Body = ModifierGesture>, Content> + + nonisolated public static func _makeGesture( + gesture: _GraphValue, + inputs: _GestureInputs + ) -> _GestureOutputs { + _Body.makeDebuggableGesture( + gesture: gesture[offset: { .of(&$0._body) }], + inputs: inputs + ) + } +} + +@available(*, unavailable) +extension _EndedGesture: Sendable {} + +// MARK: - _ChangedGesture + +public struct _ChangedGesture: PrimitiveGesture where Content: Gesture, Content.Value: Equatable { + fileprivate var _body: _Body + + fileprivate init(_body: _Body) { + self._body = _body + } + + fileprivate typealias _Body = ModifierGesture>, Content> + + nonisolated public static func _makeGesture( + gesture: _GraphValue, + inputs: _GestureInputs + ) -> _GestureOutputs { + var inputs = inputs + inputs.options.formUnion(.hasChangedCallbacks) + return _Body.makeDebuggableGesture( + gesture: gesture[offset: { .of(&$0._body) }], + inputs: inputs + ) + } +} + +@available(*, unavailable) +extension _ChangedGesture: Sendable {} + +// MARK: - FailedCallbacks + +package struct FailedCallbacks: GestureCallbacks { + package let failed: () -> Void + + package func dispatch( + phase: GesturePhase, + state: inout Void + ) -> (() -> Void)? { + guard case .failed = phase else { + return nil + } + return failed + } + + package func cancel(state: Void) -> (() -> Void)? { + failed + } + + package typealias StateType = Void +} + +// MARK: - ChangedCallbacks + +private struct ChangedCallbacks: GestureCallbacks where Value: Equatable { + package let changed: (Value) -> Void + + package struct StateType: GestureStateProtocol { + var oldValue: Value? + + package init() { + oldValue = nil + } + } + + package func dispatch( + phase: GesturePhase, + state: inout StateType + ) -> (() -> Void)? { + guard case let .active(value) = phase else { + return nil + } + let hasChanged = state.oldValue.map { $0 != value } ?? true + guard hasChanged else { + return nil + } + state.oldValue = value + return { + withAnimation(nil) { + changed(value) + } + } + } +} + +// MARK: - EndedCallbacks + +private struct EndedCallbacks: GestureCallbacks { + package let ended: (Value) -> Void + + package func dispatch( + phase: GesturePhase, + state: inout Void + ) -> (() -> Void)? { + guard case let .ended(value) = phase else { + return nil + } + return { + ended(value) + } + } + + package typealias StateType = Void +} + +// MARK: - CallbacksPhase + +private struct CallbacksPhase: ResettableGestureRule, RemovableAttribute where Callbacks: GestureCallbacks { + @Attribute var modifier: CallbacksGesture + @Attribute var phase: GesturePhase + @Attribute var resetSeed: UInt32 + var useGestureGraph: Bool + var state: Callbacks.StateType + var cancel: ((Callbacks.StateType) -> (() -> Void)?)? + var lastResetSeed: UInt32 + + typealias PhaseValue = Callbacks.Value + typealias Value = GesturePhase + + mutating func resetPhase() { + if let action = cancel?(state) { + Update.enqueueAction(reason: nil, action) + } + state = Callbacks.initialState + cancel = nil + } + + mutating func updateValue() { + guard resetIfNeeded() else { + return + } + let (newPhase, phaseChanged) = $phase.changedValue() + guard phaseChanged else { + if !hasValue { + value = newPhase + } + return + } + let callbacks = modifier.callbacks + if let action = callbacks.dispatch(phase: newPhase, state: &state) { + if useGestureGraph { + GestureGraph.current.enqueueAction(action) + } else { + Update.enqueueAction(reason: nil, action) + } + } + value = newPhase + if newPhase.isTerminal { + cancel = nil + } else { + cancel = { state in + callbacks.cancel(state: state) + } + } + } + + static func willRemove(attribute: AnyAttribute) { + let phasePointer = UnsafeMutableRawPointer(mutating: attribute.info.body) + .assumingMemoryBound(to: Self.self) + phasePointer.pointee.resetPhase() + } + + static func didReinsert(attribute: AnyAttribute) { + _openSwiftUIEmptyStub() + } +} diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureInputs.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureInputs.swift index bc49349e1..1cdfba8f6 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureInputs.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureInputs.swift @@ -6,7 +6,7 @@ package import OpenAttributeGraphShims -// MARK: - GestureInputs [6.5.4] +// MARK: - GestureInputs /// Input (aka inherited) attributes for gesture objects. @available(OpenSwiftUI_v1_0, *) @@ -149,6 +149,11 @@ extension _GestureInputs { package static var gestureGraph: _GestureInputs.Options { .init(rawValue: 1 << 4) } + + @inlinable + package static var hasChangedCallbacks: _GestureInputs.Options { + .init(rawValue: 1 << 5) + } } } From 6e0606c9b16933570a8ae2f0dd53e96000f9194e Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 8 Jun 2026 23:29:47 +0800 Subject: [PATCH 07/21] Update Gesture version mark --- .../Event/Gesture/AnyGesture.swift | 3 +- .../Event/Gesture/EmptyGesture.swift | 3 +- .../Event/Gesture/Gesture.swift | 11 ++--- .../Event/Gesture/GestureCategory.swift | 3 +- .../Event/Gesture/GestureDebug.swift | 5 ++- .../Event/Gesture/GestureDependency.swift | 3 +- .../Event/Gesture/GestureDescriptor.swift | 5 ++- .../Event/Gesture/GestureGraph.swift | 5 ++- .../Event/Gesture/GestureMask.swift | 3 +- .../Event/Gesture/GestureOutputs.swift | 3 +- .../Event/Gesture/GesturePhase.swift | 7 ++-- .../Event/Gesture/GestureViewModifier.swift | 41 ++++++++++--------- .../Event/Gesture/LayoutGesture.swift | 7 ++-- .../Event/Gesture/PlatformGestureInputs.swift | 3 +- .../Event/Gesture/ResettableGestureRule.swift | 3 +- 15 files changed, 60 insertions(+), 45 deletions(-) diff --git a/Sources/OpenSwiftUICore/Event/Gesture/AnyGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/AnyGesture.swift index 18c333b4d..ff40bc58f 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/AnyGesture.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/AnyGesture.swift @@ -2,12 +2,13 @@ // AnyGesture.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: Complete // ID: 9726BF9F3BA5F571B5F201AD7C8C86F0 (SwiftUICore) import OpenAttributeGraphShims -// MARK: - AnyGesture [6.5.4] +// MARK: - AnyGesture /// A type-erased gesture. @available(OpenSwiftUI_v1_0, *) diff --git a/Sources/OpenSwiftUICore/Event/Gesture/EmptyGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/EmptyGesture.swift index 2f4f8d1ae..034ce4d61 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/EmptyGesture.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/EmptyGesture.swift @@ -2,9 +2,10 @@ // EmptyGesture.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: Complete -// MARK: - EmptyGesture [6.5.4] +// MARK: - EmptyGesture package struct EmptyGesture: PrimitiveGesture { package init() {} diff --git a/Sources/OpenSwiftUICore/Event/Gesture/Gesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/Gesture.swift index f23bed73c..4549df904 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/Gesture.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/Gesture.swift @@ -2,10 +2,11 @@ // Gesture.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: Complete // ID: 5DF390A778F4D193C5F92C06542566B0 (SwiftUICore) -// MARK: - Gesture [6.5.4] +// MARK: - Gesture /// An instance that matches a sequence of events to a gesture, and returns a /// stream of values for each of its states. @@ -31,11 +32,11 @@ public protocol Gesture { var body: Body { get } } -// MARK: - PrimitiveGesture [6.5.4] +// MARK: - PrimitiveGesture package protocol PrimitiveGesture: Gesture where Body == Never {} -// MARK: - PubliclyPrimitiveGesture [6.5.4] +// MARK: - PubliclyPrimitiveGesture package protocol PubliclyPrimitiveGesture: PrimitiveGesture { associatedtype InternalBody: Gesture where Value == InternalBody.Value @@ -63,7 +64,7 @@ extension PubliclyPrimitiveGesture { } } -// MARK: - Never + Gesture [6.5.4] +// MARK: - Never + Gesture @available(OpenSwiftUI_v1_0, *) extension Never: Gesture { @@ -77,7 +78,7 @@ extension PrimitiveGesture { } } -// MARK: - GestureBodyAccessor [6.5.4] +// MARK: - GestureBodyAccessor private struct GestureBodyAccessor: BodyAccessor where Container: Gesture { typealias Body = Container.Body diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureCategory.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureCategory.swift index dace27ba9..1922b6ef6 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureCategory.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureCategory.swift @@ -2,9 +2,10 @@ // GestureCategory.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: Complete -// MARK: - GestureCategory [6.5.4] +// MARK: - GestureCategory @_spi(ForOpenSwiftUIOnly) @available(OpenSwiftUI_v6_0, *) diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureDebug.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureDebug.swift index 961dc2195..466ec0c6e 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureDebug.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureDebug.swift @@ -2,6 +2,7 @@ // GestureDebug.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: WIP // ID: 40D5679141F478561068F8E300838A67 (SwiftUICore) @@ -71,11 +72,11 @@ extension GestureDebug.Data: Defaultable { package static let defaultValue: GestureDebug.Data = .init() } -// MARK: - PrimitiveDebuggableGesture [6.5.4] +// MARK: - PrimitiveDebuggableGesture package protocol PrimitiveDebuggableGesture {} -// MARK: - DebuggableGesturePhase [6.5.4] +// MARK: - DebuggableGesturePhase package protocol DebuggableGesturePhase { associatedtype PhaseValue diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureDependency.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureDependency.swift index fc17d3e53..7994e3130 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureDependency.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureDependency.swift @@ -2,12 +2,13 @@ // GestureDependency.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: Complete // ID: 8687835E41FEE17B108D67665C1D2D0B (SwiftUICore) import OpenAttributeGraphShims -// MARK: - GestureDependency [6.5.4] +// MARK: - GestureDependency package enum GestureDependency { case none diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureDescriptor.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureDescriptor.swift index 17dcab2bf..77c0922f9 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureDescriptor.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureDescriptor.swift @@ -2,11 +2,12 @@ // GestureDescriptor.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: Complete import OpenSwiftUI_SPI -// MARK: - GestureDescriptor [6.5.4] +// MARK: - GestureDescriptor package struct GestureDescriptor: TupleDescriptor { package static var typeCache: [ObjectIdentifier: TupleTypeDescription] = [:] @@ -16,7 +17,7 @@ package struct GestureDescriptor: TupleDescriptor { } } -// MARK: - GestureModifierDescriptor [6.5.4] +// MARK: - GestureModifierDescriptor package struct GestureModifierDescriptor: TupleDescriptor { package static var typeCache: [ObjectIdentifier: TupleTypeDescription] = [:] diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureGraph.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureGraph.swift index a3903b28a..1c3c1d01e 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureGraph.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureGraph.swift @@ -2,17 +2,18 @@ // GestureGraph.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: WIP import OpenAttributeGraphShims -// MARK: - GestureGraphDelegate [6.5.4] +// MARK: - GestureGraphDelegate package protocol GestureGraphDelegate: AnyObject { func enqueueAction(_ action: @escaping () -> Void) } -// MARK: - GestureGraph [6.5.4] [WIP] +// MARK: - GestureGraph [WIP] final package class GestureGraph: GraphHost, EventGraphHost, CustomStringConvertible { weak var rootResponder: AnyGestureResponder? diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureMask.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureMask.swift index 087f3bfbb..e1a7c94ab 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureMask.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureMask.swift @@ -2,9 +2,10 @@ // GestureMask.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: Complete -// MARK: - GestureMask [6.5.4] +// MARK: - GestureMask /// Options that control how adding a gesture to a view affects other gestures /// recognized by the view and its subviews. diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureOutputs.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureOutputs.swift index 1ba879c95..cdec7b375 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureOutputs.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureOutputs.swift @@ -2,11 +2,12 @@ // GestureOutputs.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: Complete package import OpenAttributeGraphShims -// MARK: - GestureOutputs [6.5.4] +// MARK: - GestureOutputs /// Output (aka synthesized) attributes for gesture objects. @available(OpenSwiftUI_v1_0, *) diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GesturePhase.swift b/Sources/OpenSwiftUICore/Event/Gesture/GesturePhase.swift index 858ee29fd..decd19810 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GesturePhase.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GesturePhase.swift @@ -2,9 +2,10 @@ // GesturePhase.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: Complete -// MARK: - GesturePhase [6.5.4] +// MARK: - GesturePhase @_spi(ForOpenSwiftUIOnly) @available(OpenSwiftUI_v6_0, *) @@ -82,14 +83,14 @@ extension GesturePhase { } } -// MARK: - GesturePhase + Defaultable [6.5.4] +// MARK: - GesturePhase + Defaultable @_spi(ForOpenSwiftUIOnly) extension GesturePhase: Defaultable { package static var defaultValue: GesturePhase { .failed } } -// MARK: - GestureCategory + Defaultable [6.5.4] +// MARK: - GestureCategory + Defaultable @_spi(ForOpenSwiftUIOnly) extension GestureCategory: Defaultable { diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureViewModifier.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureViewModifier.swift index 1d76c7721..39f71e3dc 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureViewModifier.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureViewModifier.swift @@ -2,12 +2,13 @@ // GestureViewModifier.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: WIP // ID: 9DF46B4E935FF03A55FF3DDFB0B1FF2B (SwiftUICore) package import OpenAttributeGraphShims -// MARK: - GestureViewModifier [6.5.4] +// MARK: - GestureViewModifier package protocol GestureViewModifier: MultiViewModifier, PrimitiveViewModifier { associatedtype ContentGesture: Gesture @@ -21,7 +22,7 @@ package protocol GestureViewModifier: MultiViewModifier, PrimitiveViewModifier { var gestureMask: GestureMask { get } } -// MARK: - GestureResponderExclusionPolicy [6.5.4] +// MARK: - GestureResponderExclusionPolicy @_spi(ForOpenSwiftUIOnly) @available(OpenSwiftUI_v6_0, *) @@ -37,7 +38,7 @@ public enum GestureResponderExclusionPolicy { @available(*, unavailable) extension GestureResponderExclusionPolicy: Sendable {} -// MARK: - GestureCombiner [6.5.4] +// MARK: - GestureCombiner package protocol GestureCombiner { associatedtype Result: Gesture where Result.Value == () @@ -50,7 +51,7 @@ package protocol GestureCombiner { static var exclusionPolicy: GestureResponderExclusionPolicy { get } } -// MARK: - GestureViewModifier + Default Implementation [6.5.4] +// MARK: - GestureViewModifier + Default Implementation extension GestureViewModifier { package var name: String? { nil } @@ -90,7 +91,7 @@ extension GestureViewModifier { } } -// MARK: - AddGestureModifier [6.5.4] +// MARK: - AddGestureModifier package struct AddGestureModifier: GestureViewModifier where T: Gesture { package var gesture: T @@ -112,7 +113,7 @@ package struct AddGestureModifier: GestureViewModifier where T: Gesture { package typealias ContentGesture = T } -// MARK: - DefaultGestureCombiner [6.5.4] +// MARK: - DefaultGestureCombiner package struct DefaultGestureCombiner: GestureCombiner { package typealias Base = ExclusiveGesture, AnyGesture> @@ -129,7 +130,7 @@ package struct DefaultGestureCombiner: GestureCombiner { } } -// MARK: - AnyGestureContainingResponder [6.5.4] +// MARK: - AnyGestureContainingResponder package protocol AnyGestureContainingResponder: ViewResponder { var viewSubgraph: Subgraph { get } @@ -143,7 +144,7 @@ package protocol AnyGestureContainingResponder: ViewResponder { func detachContainer() } -// MARK: - AnyGestureResponder [6.5.4] +// MARK: - AnyGestureResponder package protocol AnyGestureResponder: AnyGestureContainingResponder { var inputs: _ViewInputs { get } @@ -271,7 +272,7 @@ extension AnyGestureResponder { } } -// MARK: - GestureResponder [6.5.4] [WIP] +// MARK: - GestureResponder [WIP] private class GestureResponder: DefaultLayoutViewResponder, AnyGestureResponder where Modifier: GestureViewModifier { let modifier: Attribute @@ -407,7 +408,7 @@ private class GestureResponder: DefaultLayoutViewResponder, AnyGesture } } -// MARK: - GestureFilter [6.5.4] +// MARK: - GestureFilter private struct GestureFilter: StatefulRule where Modifier: GestureViewModifier { typealias Value = [ViewResponder] @@ -441,7 +442,7 @@ private struct GestureFilter: StatefulRule where Modifier: GestureView } } -// MARK: - GestureAccessibilityProvider [6.5.4] +// MARK: - GestureAccessibilityProvider package protocol GestureAccessibilityProvider { nonisolated static func makeGesture( @@ -478,7 +479,7 @@ extension _ViewInputs { } } -// MARK: - GestureViewChild [6.5.4] +// MARK: - GestureViewChild private struct GestureViewChild: Rule where Modifier: GestureViewModifier { @Attribute var modifier: Modifier @@ -496,7 +497,7 @@ private struct GestureViewChild: Rule where Modifier: GestureViewModif } } -// MARK: - CombiningGestureViewChild [6.5.4] +// MARK: - CombiningGestureViewChild private struct CombiningGestureViewChild: Rule where Modifier: GestureViewModifier { @Attribute var modifier: Modifier @@ -542,12 +543,12 @@ private struct CombiningGestureViewChild: Rule where Modifier: Gesture } } -// MARK: - GestureViewDebug [6.5.4] [WIP] +// MARK: - GestureViewDebug [WIP] private struct GestureViewDebug where Modifier: GestureViewModifier { } -// MARK: - SubviewsGesture [6.5.4] +// MARK: - SubviewsGesture private struct SubviewsGesture: PrimitiveGesture, PrimitiveDebuggableGesture { typealias Value = () @@ -576,15 +577,15 @@ private struct SubviewsGesture: PrimitiveGesture, PrimitiveDebuggableGesture { } } -// MARK: - SimultaneousGestureCombiner [6.5.4] [WIP] +// MARK: - SimultaneousGestureCombiner [WIP] struct SimultaneousGestureCombiner {} -// MARK: - HighPriorityGestureCombiner [6.5.4] [WIP] +// MARK: - HighPriorityGestureCombiner [WIP] struct HighPriorityGestureCombiner {} -// MARK: - SubviewsPhase [6.5.4] [WIP] +// MARK: - SubviewsPhase [WIP] private struct SubviewsPhase: StatefulRule, ObservedAttribute { struct Value { @@ -612,7 +613,7 @@ private struct SubviewsPhase: StatefulRule, ObservedAttribute { } } -// MARK: - ContentGesture [6.5.4] +// MARK: - ContentGesture private struct ContentGesture: GestureModifier { typealias Value = Void @@ -634,7 +635,7 @@ private struct ContentGesture: GestureModifier { } } -// MARK: - ContentPhase [6.5.4] +// MARK: - ContentPhase private struct ContentPhase: ResettableGestureRule { @Attribute var phase: GesturePhase diff --git a/Sources/OpenSwiftUICore/Event/Gesture/LayoutGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/LayoutGesture.swift index 2b3e6d59e..8159ae504 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/LayoutGesture.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/LayoutGesture.swift @@ -2,9 +2,10 @@ // LayoutGesture.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: WIP -// MARK: - LayoutGesture [6.5.4] [WIP] +// MARK: - LayoutGesture [WIP] package protocol LayoutGesture: PrimitiveDebuggableGesture, PrimitiveGesture where Value == () { var responder: MultiViewResponder { get } @@ -31,7 +32,7 @@ extension LayoutGesture { } } -// MARK: - DefaultLayoutGesture [6.5.4] [WIP] +// MARK: - DefaultLayoutGesture [WIP] package struct DefaultLayoutGesture: LayoutGesture { package var responder: MultiViewResponder @@ -40,7 +41,7 @@ package struct DefaultLayoutGesture: LayoutGesture { package typealias Value = () } -// MARK: - LayoutGestureChildProxy [6.5.4] [WIP] +// MARK: - LayoutGestureChildProxy [WIP] package struct LayoutGestureChildProxy: RandomAccessCollection { package struct Child { diff --git a/Sources/OpenSwiftUICore/Event/Gesture/PlatformGestureInputs.swift b/Sources/OpenSwiftUICore/Event/Gesture/PlatformGestureInputs.swift index 762420663..efe7b991e 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/PlatformGestureInputs.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/PlatformGestureInputs.swift @@ -2,8 +2,9 @@ // PlatformGestureInputs.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: Complete -// MARK: - PlatformGestureInputs [6.5.4] +// MARK: - PlatformGestureInputs package struct PlatformGestureInputs {} diff --git a/Sources/OpenSwiftUICore/Event/Gesture/ResettableGestureRule.swift b/Sources/OpenSwiftUICore/Event/Gesture/ResettableGestureRule.swift index 6b1d077ee..6e78f31d3 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/ResettableGestureRule.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/ResettableGestureRule.swift @@ -2,11 +2,12 @@ // ResettableGestureRule.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: Complete package import OpenAttributeGraphShims -// MARK: - ResettableGestureRule [6.5.4] +// MARK: - ResettableGestureRule package protocol ResettableGestureRule: StatefulRule { associatedtype PhaseValue = Void From f58acaf5172180a550e6274a483a9041bd8cde72 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 8 Jun 2026 23:48:38 +0800 Subject: [PATCH 08/21] Add gesture category modifiers --- .../Event/Gesture/CategoryGesture.swift | 109 ++++++++++++++++++ .../Event/Gesture/GestureCategory.swift | 23 +++- 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 Sources/OpenSwiftUICore/Event/Gesture/CategoryGesture.swift diff --git a/Sources/OpenSwiftUICore/Event/Gesture/CategoryGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/CategoryGesture.swift new file mode 100644 index 000000000..bf562844a --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/CategoryGesture.swift @@ -0,0 +1,109 @@ +// +// CategoryGesture.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: BD70527AFCE562B27D7DD6D56847C2B8 (SwiftUICore) + +import OpenAttributeGraphShims + +// MARK: - Gesture + category + +extension Gesture { + package func category( + _ category: GestureCategory, + includeChildren: Bool = true + ) -> ModifierGesture, Self> { + modifier(CategoryGesture(category: category, includeChildren: includeChildren)) + } + + package func categoryReader( + _ callback: @escaping (GestureCategory) -> Void + ) -> ModifierGesture, Self> { + modifier(GestureCategoryReader(callback: callback)) + } +} + +// MARK: - CategoryGesture + +package struct CategoryGesture: GestureModifier { + private struct Combiner: Rule { + @Attribute var modifier: CategoryGesture + @OptionalAttribute var existingCategory: GestureCategory? + + typealias Value = GestureCategory + + var value: GestureCategory { + var category = modifier.category + if modifier.includeChildren { + category.formUnion(existingCategory ?? []) + } + return category + } + } + + package var category: GestureCategory + + package var includeChildren: Bool + + package static func _makeGesture( + modifier: _GraphValue>, + inputs: _GestureInputs, + body: (_GestureInputs) -> _GestureOutputs + ) -> _GestureOutputs { + var outputs = body(inputs) + guard inputs.preferences.containsGestureCategory else { + return outputs + } + outputs.preferences.gestureCategory = Attribute(Combiner( + modifier: modifier.value, + existingCategory: OptionalAttribute(outputs.preferences.gestureCategory) + )) + return outputs + } + + package typealias BodyValue = Value +} + +// MARK: - GestureCategoryReader + +package struct GestureCategoryReader: GestureModifier { + private struct Reader: Rule { + @Attribute var modifier: GestureCategoryReader + @OptionalAttribute var gestureCategory: GestureCategory? + + typealias Value = GestureCategory + + var value: GestureCategory { + Update.enqueueAction(reason: nil) { + modifier.callback(gestureCategory ?? []) + } + return gestureCategory ?? [] + } + } + + package var callback: (GestureCategory) -> Void + + package init(callback: @escaping (GestureCategory) -> Void) { + self.callback = callback + } + + package static func _makeGesture( + modifier: _GraphValue>, + inputs: _GestureInputs, + body: (_GestureInputs) -> _GestureOutputs + ) -> _GestureOutputs { + var outputs = body(inputs) + guard inputs.preferences.containsGestureCategory else { + return outputs + } + outputs.preferences.gestureCategory = Attribute(Reader( + modifier: modifier.value, + gestureCategory: OptionalAttribute(outputs.preferences.gestureCategory) + )) + return outputs + } + + package typealias BodyValue = Value +} diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureCategory.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureCategory.swift index 1922b6ef6..9eca3cd2e 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureCategory.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureCategory.swift @@ -5,6 +5,8 @@ // Audited for 6.5.4 // Status: Complete +package import OpenAttributeGraphShims + // MARK: - GestureCategory @_spi(ForOpenSwiftUIOnly) @@ -35,10 +37,29 @@ public struct GestureCategory: OptionSet { value: inout GestureCategory.Key.Value, nextValue: () -> GestureCategory.Key.Value ) { - value = GestureCategory(rawValue: value.rawValue | nextValue().rawValue) + value.formUnion(nextValue()) } } } @available(*, unavailable) extension GestureCategory: Sendable {} + +// MARK: - PreferencesInputs + GestureCategory + +extension PreferencesInputs { + @inline(__always) + var containsGestureCategory: Bool { + contains(GestureCategory.Key.self) + } +} + +// MARK: - PreferencesOutputs + GestureCategory + +extension PreferencesOutputs { + @inline(__always) + var gestureCategory: Attribute? { + get { self[GestureCategory.Key.self] } + set { self[GestureCategory.Key.self] = newValue } + } +} From 75c5ad50ca8db082e968288eb762877a26f51735 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 9 Jun 2026 00:36:48 +0800 Subject: [PATCH 09/21] Add CoordinateSpaceGesture --- .../Gesture/CoordinateSpaceGesture.swift | 76 +++++++++++++++++++ .../Event/Gesture/GestureInputs.swift | 10 +++ 2 files changed, 86 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Event/Gesture/CoordinateSpaceGesture.swift diff --git a/Sources/OpenSwiftUICore/Event/Gesture/CoordinateSpaceGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/CoordinateSpaceGesture.swift new file mode 100644 index 000000000..d76bf12cc --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/CoordinateSpaceGesture.swift @@ -0,0 +1,76 @@ +// +// CoordinateSpaceGesture.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 8ECA7037C26636F2BB3D86159C23C2C5 (SwiftUICore) + +import Foundation +import OpenAttributeGraphShims + +// MARK: - CoordinateSpaceGesture + +package struct CoordinateSpaceGesture: GestureModifier { + private struct CoordinateSpaceEvents: Rule { + @Attribute var modifier: CoordinateSpaceGesture + @Attribute var events: [EventID: any EventType] + @Attribute var position: CGPoint + @Attribute var transform: ViewTransform + + typealias Value = [EventID: any EventType] + + var value: [EventID: any EventType] { + var events = events + let coordinateSpace = modifier.coordinateSpace + if coordinateSpace.isGlobal { + defaultConvertEventLocations(&events) { _ in } + } else { + let resolvedTransform = Graph.withoutUpdate { transform } + let resolvedPosition = Graph.withoutUpdate { position } + let convertedTransform = resolvedTransform.withPosition(resolvedPosition) + defaultConvertEventLocations(&events) { points in + convertedTransform.convert( + ViewTransform.Conversion.globalToSpace(coordinateSpace), + points: &points + ) + } + } + return events + } + } + + package var coordinateSpace: CoordinateSpace + + package init(coordinateSpace: CoordinateSpace) { + self.coordinateSpace = coordinateSpace + } + + package static func _makeGesture( + modifier: _GraphValue>, + inputs: _GestureInputs, + body: (_GestureInputs) -> _GestureOutputs + ) -> _GestureOutputs { + var newInputs = inputs + newInputs.events = Attribute(CoordinateSpaceEvents( + modifier: modifier.value, + events: inputs.events, + position: inputs.animatedPosition(), + transform: inputs.transform + )) + newInputs.options.formUnion(.preconvertedEventLocations) + return body(newInputs) + } + + package typealias BodyValue = Value +} + +// MARK: - Gesture + coordinateSpace + +extension Gesture { + package func coordinateSpace( + _ coordinateSpace: CoordinateSpace + ) -> ModifierGesture, Self> { + modifier(CoordinateSpaceGesture(coordinateSpace: coordinateSpace)) + } +} diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureInputs.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureInputs.swift index 1cdfba8f6..014fa5097 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureInputs.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureInputs.swift @@ -2,6 +2,7 @@ // GestureInputs.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: Complete package import OpenAttributeGraphShims @@ -82,6 +83,15 @@ public struct _GestureInputs { } } + package var transform: Attribute { + let defaultTransform = intern(ViewTransform(), id: .defaultValue) + return viewSubgraph.apply { + var transform = IndirectAttribute(source: defaultTransform) + transform.source = viewInputs.transform + return transform.projectedValue + } + } + package func intern( _ value: T, id: GraphHost.ConstantID From d6cc713a0fd19e846b7037d3d58de48f04b9ad1d Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 9 Jun 2026 01:08:59 +0800 Subject: [PATCH 10/21] Add DurationGesture --- .../Event/Gesture/DurationGesture.swift | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Event/Gesture/DurationGesture.swift diff --git a/Sources/OpenSwiftUICore/Event/Gesture/DurationGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/DurationGesture.swift new file mode 100644 index 000000000..3e1640cb3 --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/DurationGesture.swift @@ -0,0 +1,139 @@ +// +// DurationGesture.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: C4CC4B4F23572B057F5F0CA55A7B1301 (SwiftUICore) + +import OpenAttributeGraphShims + +// MARK: - Gesture + duration + +extension Gesture { + package func duration( + minimum: Double = 0, + maximum: Double = .infinity + ) -> ModifierGesture, Self> { + modifier(DurationGesture(minimumDuration: minimum, maximumDuration: maximum)) + } +} + +// MARK: - DurationGesture + +package struct DurationGesture: GestureModifier { + package var minimumDuration: Double + + package var maximumDuration: Double + + package var trackFromEventStart: Bool + + package init( + minimumDuration: Double = 0, + maximumDuration: Double = .infinity, + trackFromEventStart: Bool = false + ) { + self.minimumDuration = minimumDuration + self.maximumDuration = maximumDuration + self.trackFromEventStart = trackFromEventStart + } + + package static func _makeGesture( + modifier: _GraphValue>, + inputs: _GestureInputs, + body: (_GestureInputs) -> _GestureOutputs + ) -> _GestureOutputs { + let outputs = body(inputs) + let phase = Attribute(DurationPhase( + modifier: modifier.value, + childPhase: outputs.phase, + time: inputs.viewInputs.time, + resetSeed: inputs.resetSeed, + useGestureGraph: inputs.options.contains(.gestureGraph), + start: nil, + lastResetSeed: .zero + )) + return outputs.withPhase(phase) + } + + package typealias Value = Double +} + +// MARK: - DurationPhase + +private struct DurationPhase: ResettableGestureRule { + @Attribute var modifier: DurationGesture + @Attribute var childPhase: GesturePhase + @Attribute var time: Time + @Attribute var resetSeed: UInt32 + var useGestureGraph: Bool + var start: Time? + var lastResetSeed: UInt32 + + typealias PhaseValue = Double + typealias Value = GesturePhase + + mutating func resetPhase() { + start = nil + } + + mutating func updateValue() { + guard resetIfNeeded() else { + return + } + let elapsed: Double? + if let start { + elapsed = time - start + } else { + let childPhase = childPhase + if childPhase.isActive || modifier.trackFromEventStart { + start = time + elapsed = .zero + } else { + elapsed = nil + } + } + let nextPhase: GesturePhase + defer { value = nextPhase } + switch childPhase { + case .possible: + nextPhase = .possible(elapsed) + case .active: + let elapsed = elapsed! + if modifier.minimumDuration > elapsed { + nextPhase = .possible(elapsed) + } else if modifier.maximumDuration > elapsed { + nextPhase = .active(elapsed) + } else { + nextPhase = .failed + return + } + case .ended: + let elapsed = elapsed! + if modifier.minimumDuration > elapsed || modifier.maximumDuration <= elapsed { + nextPhase = .failed + return + } else { + nextPhase = .ended(elapsed) + } + return + case .failed: + nextPhase = .failed + return + } + if let start { + let deadline: Time + if let elapsed, modifier.minimumDuration > elapsed { + deadline = start + modifier.minimumDuration + } else { + deadline = start + modifier.maximumDuration + } + if useGestureGraph { + let gestureGraph = GestureGraph.current + gestureGraph.nextUpdateTime = min(gestureGraph.nextUpdateTime, deadline) + } else { + ViewGraph.current.nextUpdate.gestures.at(deadline) + } + } + } +} From f257a0adade46e53aa8a1e8258b602edb606cca1 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 9 Jun 2026 01:47:13 +0800 Subject: [PATCH 11/21] Add DistanceGesture --- .../Event/Gesture/DistanceGesture.swift | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Event/Gesture/DistanceGesture.swift diff --git a/Sources/OpenSwiftUICore/Event/Gesture/DistanceGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/DistanceGesture.swift new file mode 100644 index 000000000..4b0c8208e --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/DistanceGesture.swift @@ -0,0 +1,81 @@ +// +// DistanceGesture.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: AE77061B4A25E6848CE6B7A87EEE80F8 (SwiftUICore) + +package import Foundation + +// MARK: - DistanceGesture + +package struct DistanceGesture: Gesture { + package struct StateType: GestureStateProtocol { + var start: CGPoint? + var maxDistance: CGFloat + + package init() { + start = nil + maxDistance = .zero + } + + @inline(__always) + mutating func updateDistance(to location: CGPoint) -> CGFloat { + let movement: CGFloat + if let start { + movement = distance(start, location) + maxDistance = max(maxDistance, movement) + } else { + start = location + movement = .zero + } + return movement + } + } + + package var minimumDistance: CGFloat + + package var maximumDistance: CGFloat + + package init( + minimumDistance: CGFloat = 0, + maximumDistance: CGFloat = .infinity + ) { + self.minimumDistance = minimumDistance + self.maximumDistance = maximumDistance + } + + package var body: some Gesture { + let minimumDistance = minimumDistance + let maximumDistance = maximumDistance + return StateType.gesture(content: EventListener()) { state, phase in + switch phase { + case let .possible(event): + guard let event else { + return .possible(nil) + } + return .possible(state.updateDistance(to: event.location)) + case let .active(event): + let distance = state.updateDistance(to: event.location) + guard maximumDistance > distance else { + return .failed + } + guard state.maxDistance >= minimumDistance else { + return .possible(distance) + } + return .active(distance) + case let .ended(event): + let distance = state.updateDistance(to: event.location) + guard state.maxDistance >= minimumDistance, maximumDistance > distance else { + return .failed + } + return .ended(distance) + case .failed: + return .failed + } + } + } + + package typealias Value = CGFloat +} From 14b982cd90bd95d4f3dddd783edd99724826247b Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 9 Jun 2026 02:06:46 +0800 Subject: [PATCH 12/21] Add EventFilter --- .../Event/Gesture/EventFilter.swift | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Event/Gesture/EventFilter.swift diff --git a/Sources/OpenSwiftUICore/Event/Gesture/EventFilter.swift b/Sources/OpenSwiftUICore/Event/Gesture/EventFilter.swift new file mode 100644 index 000000000..023f72236 --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/EventFilter.swift @@ -0,0 +1,151 @@ +// +// EventFilter.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: DE98B8F5384114B687077BAB0EFA27D9 (SwiftUICore) + +import OpenAttributeGraphShims + +// MARK: - EventFilter + +package struct EventFilter: GestureModifier { + package var predicate: (any EventType) -> Bool + + package init(predicate: @escaping (any EventType) -> Bool) { + self.predicate = predicate + } + + package static func _makeGesture( + modifier: _GraphValue>, + inputs: _GestureInputs, + body: (_GestureInputs) -> _GestureOutputs + ) -> _GestureOutputs { + let filteredEvents = Attribute(EventFilterEvents( + modifier: modifier.value, + events: inputs.events + )) + var newInputs = inputs + newInputs.events = filteredEvents[offset: { .of(&$0.events) }] + let outputs = body(newInputs) + let phase = Attribute(EventFilterPhase( + phase: outputs.phase, + filteredEvents: filteredEvents + )) + return outputs.withPhase(phase) + } + + package typealias BodyValue = Value +} + +// MARK: - EventFilterPhase + +private struct EventFilterPhase: Rule { + @Attribute var phase: GesturePhase + @Attribute var filteredEvents: FilteredEvents + + typealias Value = GesturePhase + + var value: GesturePhase { + guard !filteredEvents.failed else { + return .failed + } + return phase + } +} + +// MARK: - FilteredEvents + +private struct FilteredEvents { + var events: [EventID: any EventType] + var failed: Bool +} + +// MARK: - EventFilterEvents + +private struct EventFilterEvents: Rule { + @Attribute var modifier: EventFilter + @Attribute var events: [EventID: any EventType] + + typealias Value = FilteredEvents + + var value: FilteredEvents { + let filtered = events.optimisticFilter { event in + modifier.predicate(event.value) + } + return FilteredEvents( + events: filtered, + failed: filtered.count != events.count + ) + } +} + +// MARK: - Gesture + eventFilter + +extension Gesture { + package func eventFilter( + _ predicate: @escaping (any EventType) -> Bool + ) -> ModifierGesture, Self> { + modifier(EventFilter(predicate: predicate)) + } + + package func eventFilter( + allowedTypes: [any EventType.Type] + ) -> ModifierGesture, Self> { + eventFilter { event in + allowedTypes.contains { allowedType in + allowedType.init(event) != nil + } + } + } + + package func eventFilter( + allowedTypes: any EventType.Type... + ) -> ModifierGesture, Self> { + eventFilter(allowedTypes: allowedTypes) + } + + package func eventFilter( + allowedType: any EventType.Type + ) -> ModifierGesture, Self> { + eventFilter { event in + allowedType.init(event) != nil + } + } + + package func eventFilter( + excludedType: any EventType.Type + ) -> ModifierGesture, Self> { + eventFilter { event in + excludedType.init(event) == nil + } + } + + private func eventFilter( + _ filteredType: FilteredEventType.Type, + allowOtherTypes: Bool, + _ predicate: @escaping (FilteredEventType) -> Bool + ) -> ModifierGesture, Self> where FilteredEventType: EventType { + eventFilter { event in + guard let event = FilteredEventType(event) else { + return allowOtherTypes + } + return predicate(event) + } + } + + package func eventFilter( + forType filteredType: FilteredEventType.Type, + _ predicate: @escaping (FilteredEventType) -> Bool + ) -> ModifierGesture, Self> where FilteredEventType: EventType { + eventFilter(filteredType, allowOtherTypes: true, predicate) + } + + package func eventFilter( + allowedType filteredType: FilteredEventType.Type, + _ predicate: @escaping (FilteredEventType) -> Bool + ) -> ModifierGesture, Self> where FilteredEventType: EventType { + eventFilter(filteredType, allowOtherTypes: false, predicate) + } +} From 71b2d06a90775b2aafe838b0473bbcb2649854ae Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 9 Jun 2026 02:42:23 +0800 Subject: [PATCH 13/21] Add EventListener --- .../Event/Gesture/EventListener.swift | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Event/Gesture/EventListener.swift diff --git a/Sources/OpenSwiftUICore/Event/Gesture/EventListener.swift b/Sources/OpenSwiftUICore/Event/Gesture/EventListener.swift new file mode 100644 index 000000000..e6f478652 --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/EventListener.swift @@ -0,0 +1,218 @@ +// +// EventListener.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: D4E5D14C6252B45A30FB249B3DBDFD35 (SwiftUICore) + +import Foundation +import OpenAttributeGraphShims + +// MARK: - EventListener + +package struct EventListener: PrimitiveGesture where Event: EventType { + package var ignoresOtherEvents: Bool + + package init(ignoresOtherEvents: Bool = false) { + self.ignoresOtherEvents = ignoresOtherEvents + } + + package static func _makeGesture( + gesture: _GraphValue>, + inputs: _GestureInputs + ) -> _GestureOutputs { + let phase = Attribute(EventListenerPhase( + listener: gesture.value, + events: inputs.events, + position: inputs.animatedPosition(), + transform: inputs.transform, + resetSeed: inputs.resetSeed, + preconvertedEventLocations: inputs.options.contains(.preconvertedEventLocations), + allowsIncompleteEventSequences: inputs.options.contains(.allowsIncompleteEventSequences), + trackingID: nil, + lastResetSeed: .zero + )) + return _GestureOutputs(phase: phase.phase()) + } + + package typealias Body = Never + + package typealias Value = Event +} + +extension EventListener: PrimitiveDebuggableGesture {} + +// MARK: - EventListenerPhase + +private struct EventListenerPhase: ResettableGestureRule, CustomStringConvertible where Event: EventType { + enum FailureReason: Hashable { + case rebound + case eventArrivedMidstream + case multipleMatchingEvents + case unexpectedEvent + case eventFailed + } + + struct Value: DebuggableGesturePhase { + var phase: GesturePhase + var trackingID: EventID? + var failureReason: FailureReason? + + var properties: ArrayWith2Inline<(String, String)> { + var properties = ArrayWith2Inline<(String, String)>() + if let failureReason { + properties.append(("failure", String(describing: failureReason))) + } + if let trackingID { + properties.append(("trackingID", trackingID.description)) + } + return properties + } + } + + @Attribute var listener: EventListener + @Attribute var events: [EventID: any EventType] + @Attribute var position: ViewOrigin + @Attribute var transform: ViewTransform + @Attribute var resetSeed: UInt32 + let preconvertedEventLocations: Bool + let allowsIncompleteEventSequences: Bool + var trackingID: EventID? + var lastResetSeed: UInt32 + + typealias PhaseValue = Event + + var description: String { + var description = "Listener[\(Event.self)]" + if let trackingID { + description += " \(trackingID)" + } + return description + } + + mutating func resetPhase() { + trackingID = nil + value = Value(phase: .possible(nil), trackingID: nil, failureReason: nil) + } + + // TBA + mutating func updateValue() { + guard resetIfNeeded() else { + return + } + + var matchedEventID: EventID? + var matchedEvent: Event? + var failureReason: FailureReason? + + for (eventID, rawEvent) in events { + guard rawEvent.binding != nil else { + if trackingID == eventID { + failureReason = .rebound + break + } + continue + } + + if !allowsIncompleteEventSequences { + if trackingID != eventID && rawEvent.phase != .began { + if trackingID != nil, listener.ignoresOtherEvents { + continue + } + failureReason = .eventArrivedMidstream + break + } + } + + guard let event = Event(rawEvent) else { + if trackingID != nil, listener.ignoresOtherEvents { + continue + } + failureReason = .unexpectedEvent + break + } + + if let trackingID { + guard trackingID == eventID else { + if listener.ignoresOtherEvents { + continue + } + failureReason = .multipleMatchingEvents + break + } + } else { + trackingID = eventID + } + + guard matchedEvent == nil else { + failureReason = .multipleMatchingEvents + break + } + matchedEventID = eventID + matchedEvent = event + } + + guard failureReason == nil else { + value = Value( + phase: .failed, + trackingID: trackingID, + failureReason: failureReason + ) + return + } + + guard var matchedEvent, let matchedEventID else { + guard !hasValue else { + return + } + value = Value( + phase: .possible(nil), + trackingID: trackingID, + failureReason: nil + ) + return + } + + if !preconvertedEventLocations { + var events = [matchedEventID: matchedEvent] + let resolvedTransform = Graph.withoutUpdate { transform } + let resolvedPosition = Graph.withoutUpdate { position } + let convertedTransform = resolvedTransform.withPosition(resolvedPosition) + defaultConvertEventLocations(&events) { points in + convertedTransform.convert( + ViewTransform.Conversion.globalToSpace(.local), + points: &points + ) + } + matchedEvent = events[matchedEventID] ?? matchedEvent + } + + switch matchedEvent.phase { + case .began: + value = Value( + phase: .possible(matchedEvent), + trackingID: trackingID, + failureReason: nil + ) + case .active: + value = Value( + phase: .active(matchedEvent), + trackingID: trackingID, + failureReason: nil + ) + case .ended: + value = Value( + phase: .ended(matchedEvent), + trackingID: trackingID, + failureReason: nil + ) + case .failed: + value = Value( + phase: .failed, + trackingID: trackingID, + failureReason: .eventFailed + ) + } + } +} From 936b9aa1c7cc43fc31cf985218daa6b8b2b33557 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 9 Jun 2026 18:30:38 +0800 Subject: [PATCH 14/21] Add required tap count writer --- .../Event/Gesture/RequiredTapCount.swift | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Event/Gesture/RequiredTapCount.swift diff --git a/Sources/OpenSwiftUICore/Event/Gesture/RequiredTapCount.swift b/Sources/OpenSwiftUICore/Event/Gesture/RequiredTapCount.swift new file mode 100644 index 000000000..593935b5b --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/RequiredTapCount.swift @@ -0,0 +1,72 @@ +// +// RequiredTapCount.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 7C0ADFDC1D38FCDDCFDE5CE8530A0B2E (SwiftUICore) + +import OpenAttributeGraphShims + +// MARK: - Gesture + requiredTapCount + +extension Gesture { + package func requiredTapCount(_ count: Int?) -> some Gesture { + modifier(RequiredTapCountWriter(count: count)) + } +} + +// MARK: - RequiredTapCountWriter + +private struct RequiredTapCountWriter: GestureModifier { + private struct Child: Rule { + @Attribute var modifier: RequiredTapCountWriter + + typealias Value = (inout Int?) -> Void + + var value: (inout Int?) -> Void { + let count = modifier.count + return { value in + if let currentValue = value { + value = max(currentValue, count ?? currentValue) + } else { + value = count + } + } + } + } + + var count: Int? + + static func _makeGesture( + modifier: _GraphValue>, + inputs: _GestureInputs, + body: (_GestureInputs) -> _GestureOutputs + ) -> _GestureOutputs { + var outputs = body(inputs) + outputs.preferences.makePreferenceTransformer( + inputs: inputs.preferences, + key: RequiredTapCountKey.self, + transform: Attribute(Child(modifier: modifier.value)) + ) + return outputs + } + + typealias Value = GestureValue + + typealias BodyValue = GestureValue +} + +// MARK: - RequiredTapCountKey + +struct RequiredTapCountKey: PreferenceKey { + typealias Value = Int? + + static func reduce(value: inout Int?, nextValue: () -> Int?) { + if let currentValue = value { + value = max(currentValue, nextValue() ?? currentValue) + } else { + value = nextValue() + } + } +} From 973bc3037a6bcd6bcc25dcf9025a5f4042f5623f Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 9 Jun 2026 20:56:23 +0800 Subject: [PATCH 15/21] Add true preference writers --- .../PreferenceWritingModifier.swift | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/Sources/OpenSwiftUICore/Data/Preference/PreferenceWritingModifier.swift b/Sources/OpenSwiftUICore/Data/Preference/PreferenceWritingModifier.swift index f10b22a91..c0bf0206a 100644 --- a/Sources/OpenSwiftUICore/Data/Preference/PreferenceWritingModifier.swift +++ b/Sources/OpenSwiftUICore/Data/Preference/PreferenceWritingModifier.swift @@ -112,10 +112,6 @@ extension PreferencesOutputs { } } -// TODO: - View + truePreference - -// TODO: - Gesture + truePreference - // MARK: - HostPreferencesWriter private struct HostPreferencesWriter: StatefulRule, AsyncAttribute, CustomStringConvertible where K: PreferenceKey { @@ -167,3 +163,62 @@ private struct HostPreferencesWriter: StatefulRule, AsyncAttribute, CustomStr "Preference: \(K.readableName)" } } + +// MARK: - View + truePreference + +@available(OpenSwiftUI_v1_0, *) +extension View { + @MainActor + @preconcurrency + package func truePreference(_ key: K.Type = K.self) -> some View where K: PreferenceKey, K.Value == Bool { + modifier(TruePreferenceWritingModifier()) + } +} + +// MARK: - TruePreferenceWritingModifier + +private struct TruePreferenceWritingModifier: ViewModifier, MultiViewModifier, PrimitiveViewModifier where K: PreferenceKey, K.Value == Bool { + nonisolated static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + var outputs = body(_Graph(), inputs) + outputs.preferences.makePreferenceWriter( + inputs: inputs.preferences, + key: K.self, + value: inputs.intern(true, id: .trueValue) + ) + return outputs + } +} + +// MARK: - Gesture + truePreference + +extension Gesture { + package func truePreference(_ key: K.Type = K.self) -> some Gesture where K: PreferenceKey, K.Value == Bool { + modifier(TruePreferenceWritingGestureModifier()) + } +} + +// MARK: - TruePreferenceWritingGestureModifier + +private struct TruePreferenceWritingGestureModifier: GestureModifier where K: PreferenceKey, K.Value == Bool { + static func _makeGesture( + modifier: _GraphValue>, + inputs: _GestureInputs, + body: (_GestureInputs) -> _GestureOutputs + ) -> _GestureOutputs { + var outputs = body(inputs) + outputs.preferences.makePreferenceWriter( + inputs: inputs.preferences, + key: K.self, + value: inputs.intern(true, id: .trueValue) + ) + return outputs + } + + typealias Value = GestureValue + + typealias BodyValue = GestureValue +} From f17d9282ce4bd33a119f5adcf4ec54ddee03289e Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 9 Jun 2026 21:33:45 +0800 Subject: [PATCH 16/21] Update Audit information --- Sources/OpenSwiftUICore/Event/Gesture/ExclusiveGesture.swift | 1 + Sources/OpenSwiftUICore/Event/Gesture/SimultaneousGesture.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Sources/OpenSwiftUICore/Event/Gesture/ExclusiveGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/ExclusiveGesture.swift index 8af0a6f6e..5c6ba93f5 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/ExclusiveGesture.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/ExclusiveGesture.swift @@ -2,6 +2,7 @@ // ExclusiveGesture.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: Complete // ID: C6A5F4DE707A20D3CFD8B7768E28573B (SwiftUICore) diff --git a/Sources/OpenSwiftUICore/Event/Gesture/SimultaneousGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/SimultaneousGesture.swift index b4726c1d6..92656432a 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/SimultaneousGesture.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/SimultaneousGesture.swift @@ -2,6 +2,7 @@ // SimultaneousGesture.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: Complete // ID: FD72499B2A88A75B09DC7635754CA91F (SwiftUICore) From 0b73dd24782b9bd83f0d9794cb169fb8a63707e3 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 9 Jun 2026 21:29:27 +0800 Subject: [PATCH 17/21] Add Map2Gesture combining APIs --- .../Event/Gesture/Map2Gesture.swift | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Event/Gesture/Map2Gesture.swift diff --git a/Sources/OpenSwiftUICore/Event/Gesture/Map2Gesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/Map2Gesture.swift new file mode 100644 index 000000000..09ebbee47 --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/Map2Gesture.swift @@ -0,0 +1,192 @@ +// +// Map2Gesture.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: BE6C3883808EC258A2B6649DC967D317 (SwiftUICore) + +import OpenAttributeGraphShims + +// MARK: - Gesture + combining + +extension Gesture { + package func combined( + with other: G, + body: @escaping (GesturePhase, GesturePhase) -> GesturePhase + ) -> some Gesture where G: Gesture { + modifier(Map2Gesture(content: other, body: body)) + } + + package func zip( + with other: G + ) -> some Gesture<(Value, G.Value)> where G: Gesture { + combined(with: other) { phase, otherPhase in + phase.and(otherPhase) + } + } + + package func gated( + by other: some Gesture + ) -> some Gesture { + combined(with: other) { phase, otherPhase in + guard !otherPhase.isFailed else { + return .failed + } + return phase + } + } + + package func enabled( + by other: some Gesture + ) -> some Gesture { + combined(with: other) { phase, otherPhase in + switch otherPhase { + case .possible: + return .possible(phase.unwrapped) + case .active, .ended: + return phase + case .failed: + return .failed + } + } + } + + package func ended( + by other: some Gesture, + advanceImmediately: Bool = false + ) -> some Gesture { + combined(with: other) { phase, otherPhase in + switch otherPhase { + case .possible: + if advanceImmediately || !(CoreTesting.isRunning || GestureContainerFeature.isEnabled) { + switch phase { + case let .ended(value): + return .active(value) + case .possible, .active, .failed: + return phase + } + } else { + return phase.paused() + } + case .active, .ended: + return phase + case .failed: + return .failed + } + } + } +} + +// MARK: - GesturePhase + combining + +@_spi(ForSwiftUIOnly) +extension GesturePhase { + @_spi(ForSwiftUIOnly) + package func and( + _ phase: GesturePhase, + value transform: (Wrapped, Other) -> Result + ) -> GesturePhase { + switch (self, phase) { + case (.failed, _), (_, .failed): + return .failed + case (.possible, _), (_, .possible): + return .possible(nil) + case let (.ended(value), .ended(otherValue)): + return .ended(transform(value, otherValue)) + case let (.active(value), .active(otherValue)), + let (.active(value), .ended(otherValue)), + let (.ended(value), .active(otherValue)): + return .active(transform(value, otherValue)) + } + } + + @_spi(ForSwiftUIOnly) + package func and( + _ phase: GesturePhase + ) -> GesturePhase<(Wrapped, Other)> { + and(phase) { ($0, $1) } + } + + @_spi(ForSwiftUIOnly) + package func and( + _ phase: GesturePhase + ) -> GesturePhase { + and(phase) { _, _ in } + } +} + +// MARK: - Map2Gesture + +struct Map2Gesture: GestureModifier where Content: Gesture { + var content: Content + + var body: (GesturePhase, GesturePhase) -> GesturePhase + + nonisolated static func _makeGesture( + modifier: _GraphValue>, + inputs: _GestureInputs, + body: (_GestureInputs) -> _GestureOutputs + ) -> _GestureOutputs { + let outputs1 = body(inputs) + let outputs2 = Content.makeDebuggableGesture( + gesture: modifier[offset: { .of(&$0.content) }], + inputs: inputs + ) + let phase = Attribute(Map2Phase( + body: modifier[offset: { .of(&$0.body) }].value, + phase1: outputs1.phase, + phase2: outputs2.phase, + resetSeed: inputs.resetSeed, + lastResetSeed: .zero + )) + var outputs = _GestureOutputs(phase: phase) + outputs.wrapDebugOutputs( + Self.self, + kind: .modifier, + inputs: inputs, + combiningOutputs: (outputs1, outputs2) + ) + + var firstOutputs = _ViewOutputs() + firstOutputs.preferences = outputs1.preferences + var secondOutputs = _ViewOutputs() + secondOutputs.preferences = outputs2.preferences + var visitor = PairwisePreferenceCombinerVisitor(outputs: (firstOutputs, secondOutputs)) + for key in inputs.preferences.keys { + key.visitKey(&visitor) + } + outputs.preferences = visitor.result.preferences + return outputs + } + + typealias BodyValue = InputValue + + typealias Value = OutputValue +} + +extension Map2Gesture: PrimitiveDebuggableGesture {} + +// MARK: - Map2Phase + +private struct Map2Phase: ResettableGestureRule, CustomStringConvertible { + @Attribute var body: (GesturePhase, GesturePhase) -> GesturePhase + @Attribute var phase1: GesturePhase + @Attribute var phase2: GesturePhase + @Attribute var resetSeed: UInt32 + var lastResetSeed: UInt32 + + typealias PhaseValue = OutputValue + typealias Value = GesturePhase + + mutating func updateValue() { + guard resetIfNeeded() else { + return + } + value = body(phase1, phase2) + } + + var description: String { + "Map2 → \(OutputValue.self)" + } +} From 275763da63f4876f00816b6a32c6f93d61346ee6 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 9 Jun 2026 21:36:37 +0800 Subject: [PATCH 18/21] Add cancellable gesture preference --- .../Event/Gesture/CancellableGesture.swift | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Event/Gesture/CancellableGesture.swift diff --git a/Sources/OpenSwiftUICore/Event/Gesture/CancellableGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/CancellableGesture.swift new file mode 100644 index 000000000..4bd36794b --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/CancellableGesture.swift @@ -0,0 +1,26 @@ +// +// CancellableGesture.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +// MARK: - Gesture + cancellable + +extension Gesture { + package func cancellable() -> some Gesture { + truePreference(IsCancellableGestureKey.self) + } +} + +// MARK: - IsCancellableGestureKey + +package struct IsCancellableGestureKey: PreferenceKey { + package typealias Value = Bool + + package static let defaultValue = false + + package static func reduce(value: inout Bool, nextValue: () -> Bool) { + value = value || nextValue() + } +} From b20f7a6ee76416a7bbc8a7eb1da2f82030145833 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 9 Jun 2026 21:44:07 +0800 Subject: [PATCH 19/21] Add delayed gesture modifier --- .../Event/Gesture/DelayedGesture.swift | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Event/Gesture/DelayedGesture.swift diff --git a/Sources/OpenSwiftUICore/Event/Gesture/DelayedGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/DelayedGesture.swift new file mode 100644 index 000000000..48bed1d8c --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/DelayedGesture.swift @@ -0,0 +1,109 @@ +// +// DelayedGesture.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 6BD2EA000179DFF5C40EA49FDDB323CC (SwiftUICore) + +import OpenAttributeGraphShims + +// MARK: - Gesture + delayed + +extension Gesture { + package func delayed( + by duration: Double, + filter: @escaping (Value) -> Bool = { _ in true } + ) -> ModifierGesture, Self> { + modifier(DelayedGesture(duration: duration, filter: filter)) + } +} + +// MARK: - DelayedGesture + +package struct DelayedGesture: GestureModifier { + package var duration: Double + + package var filter: (BodyValue) -> Bool + + package static func _makeGesture( + modifier: _GraphValue>, + inputs: _GestureInputs, + body: (_GestureInputs) -> _GestureOutputs + ) -> _GestureOutputs { + let outputs = body(inputs) + let phase = Attribute(DelayedPhase( + modifier: modifier.value, + childPhase: outputs.phase, + time: inputs.viewInputs.time, + resetSeed: inputs.resetSeed, + useGestureGraph: inputs.options.contains(.gestureGraph), + start: nil, + lastResetSeed: .zero + )) + return outputs.withPhase(phase) + } + + package typealias Value = BodyValue +} + +// MARK: - DelayedPhase + +private struct DelayedPhase: ResettableGestureRule { + @Attribute var modifier: DelayedGesture + @Attribute var childPhase: GesturePhase + @Attribute var time: Time + @Attribute var resetSeed: UInt32 + var useGestureGraph: Bool + var start: Time? + var lastResetSeed: UInt32 + + typealias PhaseValue = BodyValue + typealias Value = GesturePhase + + mutating func resetPhase() { + start = nil + } + + mutating func updateValue() { + guard resetIfNeeded() else { + return + } + let delayedModifier = modifier + guard delayedModifier.duration > .zero, !CoreTesting.isRunning else { + value = childPhase + return + } + let currentPhase = childPhase + let delayedValue: BodyValue + switch currentPhase { + case let .possible(value?): + delayedValue = value + case let .active(value): + delayedValue = value + case .possible(nil), .ended, .failed: + value = currentPhase + return + } + guard delayedModifier.filter(delayedValue) else { + value = currentPhase + return + } + let currentTime = time + let startTime = start ?? currentTime + self.start = startTime + guard delayedModifier.duration > currentTime - startTime else { + value = currentPhase + return + } + + let deadline = startTime + delayedModifier.duration + if useGestureGraph { + let gestureGraph = GestureGraph.current + gestureGraph.nextUpdateTime = min(gestureGraph.nextUpdateTime, deadline) + } else { + ViewGraph.current.nextUpdate.gestures.at(deadline) + } + value = .possible(delayedValue) + } +} From 02c3a5c6a325bf999a5d8f7b1fe1219368fa0c16 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 9 Jun 2026 22:37:26 +0800 Subject: [PATCH 20/21] Update GestureModifier --- .../OpenSwiftUICore/Event/Gesture/GestureModifier.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureModifier.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureModifier.swift index f17155bc6..816868667 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureModifier.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureModifier.swift @@ -2,8 +2,11 @@ // GestureModifier.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: Complete +// MARK: - GestureModifier + package protocol GestureModifier { associatedtype Value @@ -16,12 +19,16 @@ package protocol GestureModifier { ) -> _GestureOutputs } +// MARK: - Gesture + modifier + extension Gesture { package func modifier(_ modifier: T) -> ModifierGesture where T: GestureModifier, Value == T.BodyValue { ModifierGesture(content: self, modifier: modifier) } } +// MARK: - ModifierGesture + package struct ModifierGesture: PrimitiveGesture where ContentModifier: GestureModifier, Content: Gesture, From ab2467a68475a2404199040b8c4f7b436cf472c9 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 9 Jun 2026 22:41:14 +0800 Subject: [PATCH 21/21] Add gesture debug label modifier --- .../Event/Gesture/GestureLabelModifier.swift | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Event/Gesture/GestureLabelModifier.swift diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureLabelModifier.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureLabelModifier.swift new file mode 100644 index 000000000..dc69fef63 --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureLabelModifier.swift @@ -0,0 +1,68 @@ +// +// GestureLabelModifier.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +import OpenAttributeGraphShims + +// MARK: - Gesture + debugLabel + +@_spi(Private) +@available(OpenSwiftUI_v6_0, *) +extension Gesture { + public func debugLabel(_ l: String?) -> some Gesture { + modifier(GestureLabelModifier(label: l)) + } +} + +// MARK: - GestureLabelModifier + +struct GestureLabelModifier: GestureModifier { + var label: String? + + static func _makeGesture( + modifier: _GraphValue>, + inputs: _GestureInputs, + body: (_GestureInputs) -> _GestureOutputs + ) -> _GestureOutputs { + var outputs = body(inputs) + guard inputs.preferences.containsGestureLabel else { + return outputs + } + outputs.preferences.gestureLabel = modifier[offset: { .of(&$0.label) }].value + return outputs + } + + typealias BodyValue = Value +} + +// MARK: - GestureLabelKey + +struct GestureLabelKey: PreferenceKey { + typealias Value = String? + + static func reduce(value: inout String?, nextValue: () -> String?) { + value = value ?? nextValue() + } +} + +// MARK: - PreferencesInputs + GestureLabel + +extension PreferencesInputs { + @inline(__always) + var containsGestureLabel: Bool { + contains(GestureLabelKey.self) + } +} + +// MARK: - PreferencesOutputs + GestureLabel + +extension PreferencesOutputs { + @inline(__always) + var gestureLabel: Attribute? { + get { self[GestureLabelKey.self] } + set { self[GestureLabelKey.self] = newValue } + } +}