diff --git a/Sources/OpenSwiftUI/EventHandling/Focus/FocusStore.swift b/Sources/OpenSwiftUI/EventHandling/Focus/FocusStore.swift index c12434923..b5769e4aa 100644 --- a/Sources/OpenSwiftUI/EventHandling/Focus/FocusStore.swift +++ b/Sources/OpenSwiftUI/EventHandling/Focus/FocusStore.swift @@ -1,6 +1,3 @@ -// FIXME -protocol ResponderNode {} - struct FocusStore { var seed: VersionSeed var focusedResponders: ContiguousArray diff --git a/Sources/OpenSwiftUICore/Event/Event.swift b/Sources/OpenSwiftUICore/Event/Event.swift deleted file mode 100644 index c625848ec..000000000 --- a/Sources/OpenSwiftUICore/Event/Event.swift +++ /dev/null @@ -1,4 +0,0 @@ -// FIXME - -package struct EventID: Hashable {} -package protocol EventType {} diff --git a/Sources/OpenSwiftUICore/Event/Event/Event.swift b/Sources/OpenSwiftUICore/Event/Event/Event.swift new file mode 100644 index 000000000..feee8738b --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Event/Event.swift @@ -0,0 +1,217 @@ +// +// Event.swift +// OpenSwiftUICore +// +// Status: Complete + +package import Foundation + +// MARK: - MouseEvent [6.5.4] + +package struct MouseEvent: SpatialEventType, TappableEventType, ModifiersEventType, Equatable { + package struct Button: RawRepresentable, Equatable { + package let rawValue: Int + + package init(rawValue: Int) { + self.rawValue = rawValue + } + + package static let primary: MouseEvent.Button = .init(rawValue: 1 << 0) + + package static let secondary: MouseEvent.Button = .init(rawValue: 1 << 1) + + package static func other(_ index: Int) -> MouseEvent.Button { + .init(rawValue: index) + } + } + + package var timestamp: Time + package var binding: EventBinding? + package var button: MouseEvent.Button + package var phase: EventPhase + package var location: CGPoint + package var globalLocation: CGPoint + package var modifiers: EventModifiers + + package init( + timestamp: Time, + binding: EventBinding? = nil, + button: MouseEvent.Button, + phase: EventPhase, + location: CGPoint, + globalLocation: CGPoint, + modifiers: EventModifiers + ) { + self.timestamp = timestamp + self.binding = binding + self.button = button + self.phase = phase + self.location = location + self.globalLocation = globalLocation + self.modifiers = modifiers + } + + package var radius: CGFloat { .zero } + + package var kind: SpatialEvent.Kind? { .mouse } +} + +extension MouseEvent: HitTestableEventType {} + +// MARK: - EventModifiers [6.5.4] + +/// A set of key modifiers that you can add to a gesture. +@available(OpenSwiftUI_v1_0, *) +@frozen +public struct EventModifiers: OptionSet { + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + /// The Caps Lock key. + public static let capsLock: EventModifiers = .init(rawValue: 1 << 0) + + /// The Shift key. + public static let shift: EventModifiers = .init(rawValue: 1 << 1) + + /// The Control key. + public static let control: EventModifiers = .init(rawValue: 1 << 2) + + /// The Option key. + public static let option: EventModifiers = .init(rawValue: 1 << 3) + + /// The Command key. + public static let command: EventModifiers = .init(rawValue: 1 << 4) + + /// Any key on the numeric keypad. + public static let numericPad: EventModifiers = .init(rawValue: 1 << 5) + + /// The Function key. + @available(*, deprecated, message: "Function modifier is reserved for system applications") + public static let function: EventModifiers = .init(rawValue: 1 << 6) + + package static let _function: EventModifiers = .init(rawValue: 1 << 6) + + /// All possible modifier keys. + public static let all: EventModifiers = [.capsLock, .shift, .control, .option, .command, .numericPad] + + package static let _all: EventModifiers = [.capsLock, .shift, .control, .option, .command, .numericPad, ._function] +} + +// MARK: - ModifiersEventType [6.5.4] + +package protocol ModifiersEventType: EventType { + var modifiers: EventModifiers { get set } +} + +// MARK: - EventPhase [6.5.4] + +@_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) +public enum EventPhase: Hashable { + case began + case active + case ended + case failed +} + +@available(*, unavailable) +extension EventPhase: Sendable {} + +extension EventPhase { + package var isTerminal: Bool { + switch self { + case .began, .active: false + case .ended, .failed: true + } + } +} + +// MARK: - EventType [6.5.4] + +@_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) +public protocol EventType { + var phase: EventPhase { get } + + var timestamp: Time { get } + + var binding: EventBinding? { get set } + + init?(_ event: any EventType) +} + +extension EventType { + package init?(_ event: any EventType) { + guard let event = event as? Self else { + return nil + } + self = event + } + + package var isFocusEvent: Bool { + HitTestableEvent(self) == nil + } +} + +// MARK: - Event [6.5.4] + +package struct Event: EventType { + package var phase: EventPhase + package var timestamp: Time + package var binding: EventBinding? + + package init(_ event: T) where T: EventType { + phase = event.phase + timestamp = event.timestamp + binding = event.binding + } + + package init?(_ event: any EventType) { + phase = event.phase + timestamp = event.timestamp + binding = event.binding + } +} + +// MARK: - EventID [6.5.4] + +@_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) +public struct EventID: Hashable { + package var type: any Any.Type + + package var serial: Int + + package init(type: any Any.Type, serial: Int) { + self.type = type + self.serial = serial + } + + public static func == (lhs: EventID, rhs: EventID) -> Bool { + lhs.type == rhs.type && lhs.serial == rhs.serial + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(type)) + hasher.combine(serial) + } +} + +@available(*, unavailable) +extension EventID: Sendable {} + +extension EventID { + package init(_ obj: T, subtype: S.Type) where T: NSObject { + type = (T, S).self + serial = unsafeBitCast(obj, to: Int.self) + } +} + +extension EventID: CustomStringConvertible { + public var description: String { + "\(type)#\(serial)" + } +} diff --git a/Sources/OpenSwiftUICore/Event/Event/EventBinding.swift b/Sources/OpenSwiftUICore/Event/Event/EventBinding.swift new file mode 100644 index 000000000..6010eddbd --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Event/EventBinding.swift @@ -0,0 +1,25 @@ +// +// EventBinding.swift +// OpenSwiftUICore +// +// Status: Complete + +// MARK: - EventBinding [6.5.4] + +@_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) +public struct EventBinding: Equatable { + package var responder: ResponderNode + + package init(responder: ResponderNode) { + self.responder = responder + } + + public static func == (lhs: EventBinding, rhs: EventBinding) -> Bool { + lhs.responder === rhs.responder + } +} + +@_spi(ForOpenSwiftUIOnly) +@available(*, unavailable) +extension EventBinding: Sendable {} diff --git a/Sources/OpenSwiftUICore/Event/Event/EventBindingBridge.swift b/Sources/OpenSwiftUICore/Event/Event/EventBindingBridge.swift new file mode 100644 index 000000000..167b51f9b --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Event/EventBindingBridge.swift @@ -0,0 +1,100 @@ +// +// EventBindingBridge.swift +// OpenSwiftUICore +// +// Status: WIP +// ID: E11AC34B5BFF53E1001A61D61F5B9E0F (SwiftUICore) + +// MARK: - EventBindingBridge [6.5.4] [WIP] + +@_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) +open class EventBindingBridge { + package private(set) weak var eventBindingManager: EventBindingManager? + + package var responderWasBoundHandler: ((ResponderNode) -> Void)? + + private struct TrackedEventState { + var sourceID: ObjectIdentifier + var reset: Bool + } + + private var trackedEvents: [EventID: TrackedEventState] = [:] + + public init(eventBindingManager: EventBindingManager) { + self.eventBindingManager = eventBindingManager + } + + public init() {} + + open var eventSources: [any EventBindingSource] { [] } + + @discardableResult + open func send( + _ events: [EventID: any EventType], + source: any EventBindingSource + ) -> Set { + preconditionFailure("TODO") + } + + open func reset( + eventSource: any EventBindingSource, + resetForwardedEventDispatchers: Bool = false + ) { + preconditionFailure("TODO") + } + + private func resetEvent() { + preconditionFailure("TODO") + } + + open func setInheritedPhase(_ phase: _GestureInputs.InheritedPhase) { + eventBindingManager?.setInheritedPhase(phase) + } + + open func source(for sourceType: EventSourceType) -> (any EventBindingSource)? { + nil + } +} + +@_spi(ForOpenSwiftUIOnly) +@available(*, unavailable) +extension EventBindingBridge: Sendable {} + +// MARK: - EventBindingBridge + EventBindingManagerDelegate [6.5.4] + +@_spi(ForOpenSwiftUIOnly) +extension EventBindingBridge: EventBindingManagerDelegate { + package func didBind( + to newBinding: EventBinding, + id: EventID + ) { + if let responderWasBoundHandler { + // TODO: Update.enqueueAction(reason:_:) + Update.enqueueAction { + responderWasBoundHandler(newBinding.responder) + } + } + for eventSource in eventSources { + eventSource.didBind(to: newBinding, id: id, in: self) + } + } + + package func didUpdate( + phase: GesturePhase, + in eventBindingManager: EventBindingManager + ) { + for eventSource in eventSources { + eventSource.didUpdate(phase: phase, in: self) + } + } + + package func didUpdate( + gestureCategory: GestureCategory, + in eventBindingManager: EventBindingManager + ) { + for eventSource in eventSources { + eventSource.didUpdate(gestureCategory: gestureCategory, in: self) + } + } +} diff --git a/Sources/OpenSwiftUICore/Event/Event/EventBindingManager.swift b/Sources/OpenSwiftUICore/Event/Event/EventBindingManager.swift new file mode 100644 index 000000000..c50305fc4 --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Event/EventBindingManager.swift @@ -0,0 +1,153 @@ +// +// EventBindingManager.swift +// OpenSwiftUICore +// +// Status: WIP + +import Foundation + +// MARK: - EventBindingManager [6.5.4] [WIP] + +@_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) +final public class EventBindingManager { + package weak var host: (any EventGraphHost)? + + package weak var delegate: (any EventBindingManagerDelegate)? + + private var forwardedEventDispatchers: [ObjectIdentifier: any ForwardedEventDispatcher] = [:] + + private var eventBindings: [EventID: EventBinding] = [:] + + private(set) package var isActive: Bool = false + + package static var current: EventBindingManager? { + guard let delegate = ViewGraph.current.delegate, + let host = delegate as? ViewRendererHost, + let eventGraphHost = host.as(EventGraphHost.self) + else { + return nil + } + return eventGraphHost.eventBindingManager + } + + private var eventTimer: Timer? + + package init() {} + + deinit { + eventTimer?.invalidate() + } + + package func addForwardedEventDispatcher(_ dispatcher: any ForwardedEventDispatcher) { + forwardedEventDispatchers[ObjectIdentifier(type(of: dispatcher).eventType)] = dispatcher + } + + package func rebindEvent( + _ identifier: EventID, + to: ResponderNode? + ) -> (from: EventBinding?, to: EventBinding?)? { + preconditionFailure("TODO") + } + + package func willRemoveResponder(_ from: ResponderNode) { + preconditionFailure("TODO") + } + + package func setInheritedPhase(_ phase: _GestureInputs.InheritedPhase) { + preconditionFailure("TODO") + } + + @discardableResult + package func send(_ events: [EventID: any EventType]) -> Set { + preconditionFailure("TODO") + } + + package func send(_ event: E, id: Int) where E: EventType { + preconditionFailure("TODO") + } + + package var rootResponder: ResponderNode? { preconditionFailure("TODO") } + + package var focusedResponder: ResponderNode? { preconditionFailure("TODO") } + + package func reset(resetForwardedEventDispatchers: Bool = false) { + preconditionFailure("TODO") + } + + package func isActive(for eventType: E.Type) -> Bool where E: EventType { + preconditionFailure("TODO") + } + + package func binds(_ event: E) -> Bool where E: EventType { + preconditionFailure("TODO") + } +} + +@_spi(ForOpenSwiftUIOnly) +@available(*, unavailable) +extension EventBindingManager: Sendable {} + +// MARK: - ForwardedEventDispatcher [6.5.4] + +package protocol ForwardedEventDispatcher { + static var eventType: any EventType.Type { get } + + var isActive: Bool { get } + + func wantsEvent( + _ event: any EventType, + manager: EventBindingManager + ) -> Bool + + mutating func receiveEvents( + _ events: [EventID: any EventType], + manager: EventBindingManager + ) -> Set + + mutating func reset() +} + +extension ForwardedEventDispatcher { + package var isActive: Bool { false } + + package func wantsEvent( + _ event: any EventType, + manager: EventBindingManager + ) -> Bool { + true + } + + package mutating func reset() {} +} + +// MARK: - EventBindingManagerDelegate [6.5.4] + +package protocol EventBindingManagerDelegate: AnyObject { + func didBind( + to newBinding: EventBinding, + id: EventID + ) + + func didUpdate( + phase: GesturePhase, + in eventBindingManager: EventBindingManager + ) + + func didUpdate( + gestureCategory: GestureCategory, + in eventBindingManager: EventBindingManager + ) +} + +extension EventBindingManagerDelegate { + package func didBind( + to newBinding: EventBinding, + id: EventID + ) {} + + package func didUpdate( + gestureCategory: GestureCategory, + in eventBindingManager: EventBindingManager + ) {} +} diff --git a/Sources/OpenSwiftUICore/Event/Event/EventBindingSource.swift b/Sources/OpenSwiftUICore/Event/Event/EventBindingSource.swift new file mode 100644 index 000000000..68f633186 --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Event/EventBindingSource.swift @@ -0,0 +1,80 @@ +// +// EventBindingSource.swift +// OpenSwiftUICore +// +// Status: Complete + +// MARK: - EventBindingSource [6.5.4] + +@_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) +public protocol EventBindingSource: AnyObject { + func attach(to eventBridge: EventBindingBridge) + + func `as`(_ type: T.Type) -> T? + + func didUpdate( + phase: GesturePhase, + in eventBridge: EventBindingBridge + ) + + func didUpdate( + gestureCategory: GestureCategory, + in eventBridge: EventBindingBridge + ) + + func didBind( + to newBinding: EventBinding, + id: EventID, + in eventBridge: EventBindingBridge + ) +} + +@_spi(ForOpenSwiftUIOnly) +extension EventBindingSource { + public func `as`(_ type: T.Type) -> T? { nil } + + public func didUpdate( + phase: GesturePhase, + in eventBridge: EventBindingBridge + ) {} + + public func didUpdate( + gestureCategory: GestureCategory, + in eventBridge: EventBindingBridge + ) {} + + public func didBind( + to newBinding: EventBinding, + id: EventID, + in eventBridge: EventBindingBridge + ) {} +} + +// MARK: - EventBindingBridgeFactory [6.5.4] + +package protocol EventBindingBridgeFactory { + static func makeEventBindingBridge( + bindingManager: EventBindingManager, + responder: any AnyGestureResponder + ) -> any EventBindingBridge & GestureGraphDelegate +} + +package struct EventBindingBridgeFactoryInput: ViewInput { + package static let defaultValue: (any EventBindingBridgeFactory.Type)? = nil +} + +extension _ViewInputs { + package func makeEventBindingBridge( + bindingManager: EventBindingManager, + responder: any AnyGestureResponder + ) -> any EventBindingBridge & GestureGraphDelegate { + guard let factory = self[EventBindingBridgeFactoryInput.self] else { + preconditionFailure("Event binding factory must be configured") + } + return factory.makeEventBindingBridge( + bindingManager: bindingManager, + responder: responder + ) + } +} diff --git a/Sources/OpenSwiftUICore/Event/Event/EventGraphHost.swift b/Sources/OpenSwiftUICore/Event/Event/EventGraphHost.swift new file mode 100644 index 000000000..60605f92b --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Event/EventGraphHost.swift @@ -0,0 +1,29 @@ +// +// EventGraphHost.swift +// OpenSwiftUICore +// +// Status: Complete + +// MARK: - EventGraphHost [6.5.4] + +package protocol EventGraphHost: AnyObject { + var eventBindingManager: EventBindingManager { get } + + var responderNode: ResponderNode? { get } + + var focusedResponder: ResponderNode? { get } + + var nextGestureUpdateTime: Time { get } + + func setInheritedPhase(_ phase: _GestureInputs.InheritedPhase) + + func sendEvents( + _ events: [EventID: any EventType], + rootNode: ResponderNode, + at time: Time + ) -> GesturePhase + + func resetEvents() + + func gestureCategory() -> GestureCategory? +} diff --git a/Sources/OpenSwiftUICore/Event/Event/EventSourceType.swift b/Sources/OpenSwiftUICore/Event/Event/EventSourceType.swift new file mode 100644 index 000000000..2fd8506ef --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Event/EventSourceType.swift @@ -0,0 +1,49 @@ +// +// EventSourceType.swift +// OpenSwiftUICore +// +// Status: Complete + +// MARK: - EventSourceType [6.5.4] + +@_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) +public enum EventSourceType: CaseIterable { + case platformGestureRecognizer + + @available(iOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + @available(visionOS, unavailable) + case platformHostingView + + @available(macOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + case hoverGestureRecognizer + + @available(iOS, unavailable) + @available(macOS, unavailable) + @available(watchOS, unavailable) + @available(visionOS, unavailable) + case selectGestureRecognizer + + public static var allCases: [EventSourceType] { + var cases: [EventSourceType] = [] + cases.append(.platformGestureRecognizer) + #if !os(iOS) && !os(tvOS) && !os(watchOS) && !os(visionOS) + cases.append(.platformHostingView) + #endif + #if !os(macOS) && !os(tvOS) && !os(watchOS) + cases.append(.hoverGestureRecognizer) + #endif + #if !os(iOS) && !os(macOS) && !os(watchOS) && !os(visionOS) + cases.append(.selectGestureRecognizer) + #endif + return cases + } +} + +@_spi(ForOpenSwiftUIOnly) +@available(*, unavailable) +extension EventSourceType: Sendable {} diff --git a/Sources/OpenSwiftUICore/Event/Event/HitTestableEvent.swift b/Sources/OpenSwiftUICore/Event/Event/HitTestableEvent.swift new file mode 100644 index 000000000..55684b9e4 --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Event/HitTestableEvent.swift @@ -0,0 +1,53 @@ +// +// HitTestableEvent.swift +// OpenSwiftUICore +// +// Status: Complete + +package import Foundation + +// MARK: - HitTestableEventType [6.5.4] + +package protocol HitTestableEventType: EventType { + var hitTestLocation: CGPoint { get } + var hitTestRadius: CGFloat { get } +} + +// MARK: - HitTestableEvent [6.5.4] + +package struct HitTestableEvent: HitTestableEventType, Equatable { + package var phase: EventPhase + package var timestamp: Time + package var binding: EventBinding? + package var hitTestLocation: CGPoint + package var hitTestRadius: CGFloat + + package init(_ event: T) where T: HitTestableEventType { + phase = event.phase + timestamp = event.timestamp + binding = event.binding + hitTestLocation = event.hitTestLocation + hitTestRadius = event.hitTestRadius + } + + package init(_ event: any HitTestableEventType) { + phase = event.phase + timestamp = event.timestamp + binding = event.binding + hitTestLocation = event.hitTestLocation + hitTestRadius = event.hitTestRadius + } + + package init?(_ event: any EventType) { + guard let event = event as? any HitTestableEventType else { + return nil + } + self.init(event) + } +} + +extension HitTestableEventType where Self: SpatialEventType { + package var hitTestLocation: CGPoint { globalLocation } + + package var hitTestRadius: CGFloat { radius } +} diff --git a/Sources/OpenSwiftUICore/Event/Event/SpatialEvent.swift b/Sources/OpenSwiftUICore/Event/Event/SpatialEvent.swift new file mode 100644 index 000000000..a23a3f11c --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Event/SpatialEvent.swift @@ -0,0 +1,91 @@ +// +// SpatialEvent.swift +// OpenSwiftUICore +// +// Status: Complete + +package import Foundation + +// MARK: - SpatialEventType [6.5.4] + +package protocol SpatialEventType: EventType { + var globalLocation: CGPoint { get set } + var location: CGPoint { get set } + var radius: CGFloat { get } + var kind: SpatialEvent.Kind? { get } +} + +extension SpatialEventType { + package var kind: SpatialEvent.Kind? { nil } +} + +// MARK: - SpatialEvent [6.5.4] + +package struct SpatialEvent: SpatialEventType, Equatable { + package enum Kind: Equatable { + case touch + case mouse + case pan + } + + package var phase: EventPhase + package var timestamp: Time + package var binding: EventBinding? + package var kind: SpatialEvent.Kind? + package var globalLocation: CGPoint + package var location: CGPoint + package var radius: CGFloat + + package init(_ event: T) where T: SpatialEventType { + phase = event.phase + timestamp = event.timestamp + binding = event.binding + kind = event.kind + globalLocation = event.globalLocation + location = event.location + radius = event.radius + } + + package init(_ event: any SpatialEventType) { + phase = event.phase + timestamp = event.timestamp + binding = event.binding + kind = event.kind + globalLocation = event.globalLocation + location = event.location + radius = event.radius + } + + package init?(_ event: any EventType) { + guard let event = event as? any SpatialEventType else { + return nil + } + self.init(event) + } +} + +package func defaultConvertEventLocations( + _ events: inout [EventID: E], + converter: (inout [CGPoint]) -> Void +) { + var eventIDs: [EventID] = [] + var points: [CGPoint] = [] + for (eventID, event) in events { + guard var spatialEvent = event as? SpatialEventType else { + continue + } + eventIDs.append(eventID) + points.append(spatialEvent.globalLocation) + } + guard !points.isEmpty else { + return + } + converter(&points) + for (index, eventID) in eventIDs.enumerated() { + guard var spatialEvent = events[eventID] as? SpatialEventType else { + continue + } + spatialEvent.location = points[index] + events[eventID] = spatialEvent as? E + } +} diff --git a/Sources/OpenSwiftUICore/Event/Event/TappableEvent.swift b/Sources/OpenSwiftUICore/Event/Event/TappableEvent.swift new file mode 100644 index 000000000..399af22a9 --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Event/TappableEvent.swift @@ -0,0 +1,82 @@ +// +// TappableEvent.swift +// OpenSwiftUICore +// +// Status: Complete + +package import Foundation + +// MARK: - TappableEventType [6.5.4] + +package protocol TappableEventType: EventType {} + +package typealias PlatformTappableSpatialEvent = TappableSpatialEvent + +// MARK: - TappableEvent [6.5.4] + +package struct TappableEvent: TappableEventType, Equatable { + package var phase: EventPhase + package var timestamp: Time + package var binding: EventBinding? + + package init(_ event: T) where T: TappableEventType { + self.phase = event.phase + self.timestamp = event.timestamp + self.binding = event.binding + } + + package init(_ event: any TappableEventType) { + self.phase = event.phase + self.timestamp = event.timestamp + self.binding = event.binding + } + + package init?(_ event: any EventType) { + guard let event = event as? any TappableEventType else { + return nil + } + self.init(event) + } +} + +// MARK: - TappableSpatialEvent [6.5.4] + +package struct TappableSpatialEvent: TappableEventType, SpatialEventType, Equatable { + package var phase: EventPhase + package var timestamp: Time + package var binding: EventBinding? + package var globalLocation: CGPoint + package var location: CGPoint + package var radius: CGFloat + + package init(_ event: T) where T: SpatialEventType, T: TappableEventType { + self.phase = event.phase + self.timestamp = event.timestamp + self.binding = event.binding + self.globalLocation = event.globalLocation + self.location = event.location + self.radius = event.radius + } + + package init(_ event: any SpatialEventType & TappableEventType) { + self.phase = event.phase + self.timestamp = event.timestamp + self.binding = event.binding + self.globalLocation = event.globalLocation + self.location = event.location + self.radius = event.radius + } + + package init?(_ event: any EventType) { + guard let event = event as? any SpatialEventType & TappableEventType else { + return nil + } + self.init(event) + } +} + +// MARK: - TouchTypeProviding [6.5.4] + +package protocol TouchTypeProviding { + var touchType: TouchType { get } +} diff --git a/Sources/OpenSwiftUICore/Event/Event/TouchType.swift b/Sources/OpenSwiftUICore/Event/Event/TouchType.swift new file mode 100644 index 000000000..9387dcac5 --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Event/TouchType.swift @@ -0,0 +1,39 @@ +// +// TouchType.swift +// OpenSwiftUICore +// +// Status: Complete + +// MARK: - TouchType [6.5.4] + +@_spi(_) +@available(OpenSwiftUI_v5_0, *) +public enum TouchType: Hashable { + case direct + + case indirect + + @available(macOS, obsoleted: 14.0) + case pencil + + @available(macOS, obsoleted: 14.0) + case indirectPointer +} + +@_spi(_) +@available(*, unavailable) +extension TouchType: Sendable {} + +@_spi(_) +extension TouchType { + #if os(macOS) + package static let allTypes: Set = [ + .direct, .indirect, + ] + #else + package static let allTypes: Set = [ + .direct, .indirect, + .pencil, .indirectPointer, + ] + #endif +} diff --git a/Sources/OpenSwiftUICore/Event/Event_TODO.swift b/Sources/OpenSwiftUICore/Event/Event_TODO.swift new file mode 100644 index 000000000..41518eabb --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Event_TODO.swift @@ -0,0 +1,13 @@ +// FIXME + +package class ResponderNode {} + +package struct ExclusiveGesture: PrimitiveGesture {} + +package struct _MapGesture: PrimitiveGesture where Content: Gesture { + nonisolated package static func _makeGesture(gesture: _GraphValue<_MapGesture>, inputs: _GestureInputs) -> _GestureOutputs { + preconditionFailure("TODO") + } +} + +package struct AnyGesture: PrimitiveGesture {} diff --git a/Sources/OpenSwiftUICore/Event/Gesture/EmptyGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/EmptyGesture.swift new file mode 100644 index 000000000..2f4f8d1ae --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/EmptyGesture.swift @@ -0,0 +1,19 @@ +// +// EmptyGesture.swift +// OpenSwiftUICore +// +// Status: Complete + +// MARK: - EmptyGesture [6.5.4] + +package struct EmptyGesture: PrimitiveGesture { + package init() {} + + nonisolated package static func _makeGesture( + gesture: _GraphValue, + inputs: _GestureInputs + ) -> _GestureOutputs { + let phase = inputs.intern(GesturePhase.failed, id: .failedValue) + return _GestureOutputs(phase: phase) + } +} diff --git a/Sources/OpenSwiftUICore/Event/Gesture/Gesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/Gesture.swift new file mode 100644 index 000000000..049f68e50 --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/Gesture.swift @@ -0,0 +1,351 @@ +// +// Gesture.swift +// OpenSwiftUICore +// +// Status: WIP +// ID: 5DF390A778F4D193C5F92C06542566B0 (SwiftUICore) + +package import OpenGraphShims +import OpenSwiftUI_SPI + +// MARK: - Gesture [6.5.4] + +/// An instance that matches a sequence of events to a gesture, and returns a +/// stream of values for each of its states. +/// +/// Create custom gestures by declaring types that conform to the `Gesture` +/// protocol. +@available(OpenSwiftUI_v1_0, *) +@MainActor +@preconcurrency +public protocol Gesture { + /// The type representing the gesture's value. + associatedtype Value + + nonisolated static func _makeGesture( + gesture: _GraphValue, + inputs: _GestureInputs + ) -> _GestureOutputs + + /// The type of gesture representing the body of `Self`. + associatedtype Body: Gesture + + /// The content and behavior of the gesture. + var body: Body { get } +} + +// MARK: - PrimitiveGesture [6.5.4] + +package protocol PrimitiveGesture: Gesture where Body == Never {} + +// MARK: - PubliclyPrimitiveGesture [6.5.4] + +package protocol PubliclyPrimitiveGesture: PrimitiveGesture { + associatedtype InternalBody: Gesture where Value == InternalBody.Value + + var internalBody: InternalBody { get } +} + +@available(OpenSwiftUI_v1_0, *) +extension PubliclyPrimitiveGesture { + nonisolated public static func _makeGesture( + gesture: _GraphValue, + inputs: _GestureInputs + ) -> _GestureOutputs { + makeGesture(gesture: gesture, inputs: inputs) + } + + nonisolated package static func makeGesture( + gesture: _GraphValue, + inputs: _GestureInputs + ) -> _GestureOutputs { + InternalBody.makeDebuggableGesture( + gesture: gesture[\.internalBody], + inputs: inputs + ) + } +} + +// MARK: - Never + Gesture [6.5.4] + +@available(OpenSwiftUI_v1_0, *) +extension Never: Gesture { + public typealias Value = Never +} + +@available(OpenSwiftUI_v1_0, *) +extension PrimitiveGesture { + public var body: Never { + preconditionFailure("body() should not be called on \(Self.self).") + } +} + +// MARK: - GestureBodyAccessor [6.5.4] + +private struct GestureBodyAccessor: BodyAccessor where Container: Gesture { + typealias Body = Container.Body + + func updateBody(of container: Container, changed: Bool) { + guard changed else { return } + setBody { container.body } + } +} + +@available(OpenSwiftUI_v1_0, *) +extension Gesture where Value == Body.Value { + public static func _makeGesture( + gesture: _GraphValue, + inputs: _GestureInputs + ) -> _GestureOutputs { + let fields = DynamicPropertyCache.fields(of: Self.self) + var inputs = inputs + let (body, _) = GestureBodyAccessor().makeBody(container: gesture, inputs: &inputs.viewInputs.base, fields: fields) + return Body.makeDebuggableGesture(gesture: body, inputs: inputs) + } +} + +// MARK: - GestureInputs [6.5.4] [WIP] + +/// Input (aka inherited) attributes for gesture objects. +@available(OpenSwiftUI_v1_0, *) +public struct _GestureInputs { + package var viewInputs: _ViewInputs + + package var viewSubgraph: Subgraph + + package var preferences: PreferencesInputs + + package var events: Attribute<[EventID : any EventType]> + + package var resetSeed: Attribute + + @_spi(ForOpenSwiftUIOnly) + public struct InheritedPhase: OptionSet, Defaultable { + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + package static let failed: _GestureInputs.InheritedPhase = .init(rawValue: 1 << 0) + + package static let active: _GestureInputs.InheritedPhase = .init(rawValue: 1 << 1) + + package static let defaultValue: _GestureInputs.InheritedPhase = .failed + } + + package var inheritedPhase: Attribute<_GestureInputs.InheritedPhase> + + package var failedPhase: Attribute> { + get { preconditionFailure("TODO") } + } + + package var options: _GestureInputs.Options + + package var platformInputs: PlatformGestureInputs + + package init( + _ inputs: _ViewInputs, + viewSubgraph: Subgraph, + events: Attribute<[EventID : any EventType]>, + time: Attribute