Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions Sources/OpenGestures/Component/Components/EventSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//
// EventSource.swift
// OpenGestures
//
// Audited for 9126.1.5
// Status: Complete

// MARK: - EventSource

package struct EventSource<E: Event>: Sendable {
package enum Failure: Error, Hashable, Sendable {
case eventFailed
}

package struct State: GestureComponentState, NestedCustomStringConvertible, Sendable {
package var trackedId: EventID?

package init() {
trackedId = nil
}

package init(trackedId: EventID?) {
self.trackedId = trackedId
}
}

package var state: State

package init(state: State = State()) {
self.state = state
}
}

// MARK: - EventSource + GestureComponent

extension EventSource: GestureComponent {
package typealias Value = E

package mutating func update(
context: GestureComponentContext
) throws -> GestureOutput<E> {
guard context.updateSource == .event else {
return .empty(.timeUpdate, metadata: nil)
}
guard let store = matchingEventStore(context: context) else {
return makeEmptyOutput(traceAnnotation: "no event")
}
let event: E?
if let trackedId = state.trackedId {
event = trackedEvent(in: store, matching: trackedId)
} else {
event = store.bindNextUnboundEvent()
if let event {
state.trackedId = event.id
}
}
guard let event else {
if state.trackedId == nil {
return makeEmptyOutput(
traceAnnotation: "no unbound events"
)
} else {
return makeEmptyOutput(
traceAnnotation: "source is already bound"
)
}
}
return try makeOutputForEvent(event)
}

private func trackedEvent(
in store: EventStore<E>,
matching trackedId: EventID
) -> E? {
return store.events.first { $0.id == trackedId }
}

private func matchingEventStore(
context: GestureComponentContext
) -> EventStore<E>? {
guard context.eventStore.accepts(E.self) else {
return nil
}
return unsafeDowncast(context.eventStore, to: EventStore<E>.self)
}

private func makeOutputForEvent(_ event: E) throws -> GestureOutput<E> {
switch event.phase {
case .began, .active:
return .value(event, metadata: nil)
case .ended:
return .finalValue(event, metadata: nil)
case .failed:
throw Failure.eventFailed
}
}

private func makeEmptyOutput(traceAnnotation: String) -> GestureOutput<E> {
.empty(
.noData,
metadata: GestureOutputMetadata(
traceAnnotation: UpdateTraceAnnotation(value: traceAnnotation)
)
)
}

package func traits() -> GestureTraitCollection? {
nil
}

package func capacity<EventType: Event>(for eventType: EventType.Type) -> Int {
if state.trackedId == nil, eventType == E.self {
return 1
}
return 0
}
}

// MARK: - EventSource + StatefulGestureComponent

extension EventSource: StatefulGestureComponent {}
226 changes: 226 additions & 0 deletions Sources/OpenGestures/Component/Components/ExpirationComponent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
//
// ExpirationComponent.swift
// OpenGestures
//
// Audited for 9126.1.5
// Status: Complete

import Synchronization

// MARK: - Expirable

package protocol Expirable: Sendable {
associatedtype Value: Sendable

var payload: ExpirablePayload<Value> { get }

var expiration: Expiration? { get }
}

// MARK: - ExpirationComponent

package struct ExpirationComponent<Upstream>: Sendable where Upstream: GestureComponent, Upstream.Value: Expirable {

package var upstream: Upstream

package struct State: GestureComponentState, NestedCustomStringConvertible, Sendable {
package var request: UpdateRequest?

package init() {
request = nil
}

package init(request: UpdateRequest?) {
self.request = request
}
}

package var state: State

package init(
upstream: Upstream,
state: State = .init()
) {
self.upstream = upstream
self.state = state
}

package enum Failure: Error, Sendable {
case timeout(reason: ExpirationReason)
}
}

// MARK: - ExpirationComponent + GestureComponent

extension ExpirationComponent: GestureComponent {
package typealias Value = Upstream.Value.Value
}

extension ExpirationComponent: CompositeGestureComponent {}

extension ExpirationComponent: StatefulGestureComponent {}

extension ExpirationComponent: ValueTransformingComponent {
package mutating func transform(
_ value: Upstream.Value,
isFinal: Bool,
context: GestureComponentContext
) throws -> GestureOutput<Value> {
let metadata = try metadata(
for: value.expiration,
context: context
)
switch value.payload {
case let .empty(reason):
return .empty(reason, metadata: metadata)
case let .value(payload):
return .value(
payload,
isFinal: isFinal,
metadata: metadata
)
}
}

private mutating func metadata(
for expiration: Expiration?,
context: GestureComponentContext
) throws -> GestureOutputMetadata {
let updatesToSchedule: [UpdateRequest]
let updatesToCancel: [UpdateRequest]
if let expiration {
guard context.currentTime < expiration.deadline else {
throw Failure.timeout(reason: expiration.reason)
}

if state.request?.targetTime == expiration.deadline {
updatesToSchedule = []
updatesToCancel = []
} else {
updatesToCancel = cancelStoredRequest()
updatesToSchedule = [scheduleRequest(for: expiration, context: context)]
}
} else {
updatesToSchedule = []
updatesToCancel = cancelStoredRequest()
}
return GestureOutputMetadata(
updatesToSchedule: updatesToSchedule,
updatesToCancel: updatesToCancel
)
}

private mutating func cancelStoredRequest() -> [UpdateRequest] {
guard let request = state.request else {
return []
}
state.request = nil
return [request]
}

private mutating func scheduleRequest(
for expiration: Expiration,
context: GestureComponentContext
) -> UpdateRequest {
let request = UpdateRequest(
id: ExpirationComponentRequestID.next(),
creationTime: context.currentTime,
targetTime: expiration.deadline,
tag: expiration.reason.rawValue
)
state.request = request
return request
}
}

// MARK: - ExpirationComponentRequestID

// FIXE: Should it in UpdateRequest namespace?
private enum ExpirationComponentRequestID {
private static let nextID = Atomic(UInt32.zero)

static func next() -> UInt32 {
let (_, id) = nextID.add(1, ordering: .relaxed)
return id
}
}

// MARK: - ExpirationRecord

package struct ExpirationRecord<Value: Sendable>: Expirable, NestedCustomStringConvertible, Sendable {
package var payload: ExpirablePayload<Value>
package var expiration: Expiration?

package init(
payload: ExpirablePayload<Value>,
expiration: Expiration?
) {
self.payload = payload
self.expiration = expiration
}
}

// MARK: - ExpirablePayload

package enum ExpirablePayload<Value: Sendable>: NestedCustomStringConvertible, Sendable {
case empty(GestureOutputEmptyReason)
case value(Value)

package func populateNestedDescription(_ nested: inout NestedDescription) {
switch self {
case let .empty(reason):
nested.append(reason, label: "reason")
case let .value(value):
nested.append(value, label: "value")
}
}
}

// MARK: - GestureOutput + ExpirationRecord

extension GestureOutput {
package static func value(
_ value: Value,
isFinal: Bool,
expiration: Expiration?
) -> GestureOutput<ExpirationRecord<Value>> {
.value(
ExpirationRecord(
payload: .value(value),
expiration: expiration
),
isFinal: isFinal
)
}

package func expired(
with expiration: Expiration?
) -> GestureOutput<ExpirationRecord<Value>> {
switch self {
case let .empty(reason, metadata):
return .value(
ExpirationRecord<Value>(
payload: .empty(reason),
expiration: expiration
),
metadata: metadata
)
case let .value(value, metadata):
return .value(
ExpirationRecord(
payload: .value(value),
expiration: expiration
),
metadata: metadata
)
case let .finalValue(value, metadata):
return .finalValue(
ExpirationRecord(
payload: .value(value),
expiration: expiration
),
metadata: metadata
)
}
}
}
36 changes: 36 additions & 0 deletions Sources/OpenGestures/Component/Components/MapComponent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// MapComponent.swift
// OpenGestures
//
// Audited for 9126.1.5
// Status: Complete

// MARK: - MapComponent

package struct MapComponent<Upstream, Output>: Sendable
where Upstream: GestureComponent, Output: Sendable {
package var upstream: Upstream
package let map: @Sendable (GestureOutput<Upstream.Value>) throws -> GestureOutput<Output>

package init(
upstream: Upstream,
map: @escaping @Sendable (GestureOutput<Upstream.Value>) throws -> GestureOutput<Output>
) {
self.upstream = upstream
self.map = map
}
}

// MARK: - MapComponent + GestureComponent

extension MapComponent: GestureComponent {
package typealias Value = Output

package mutating func update(context: GestureComponentContext) throws -> GestureOutput<Output> {
try map(upstream.tracingUpdate(context: context))
}
}

// MARK: - MapComponent + CompositeGestureComponent

extension MapComponent: CompositeGestureComponent {}
Loading
Loading