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
24 changes: 0 additions & 24 deletions Sources/OpenGestures/ConflictResolution/GesturePhaseQueue.swift

This file was deleted.

42 changes: 42 additions & 0 deletions Sources/OpenGestures/Core/GesturePhaseQueue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// GesturePhaseQueue.swift
// OpenGestures
//
// Audited for 9126.1.5
// Status: Complete

// MARK: - GesturePhaseQueue

/// A queue of gesture phase transitions.
package struct GesturePhaseQueue<Value: Sendable> {
package var timeSource: (any TimeSource)?
package var currentPhase: GesturePhase<Value>
package var pendingPhases: RingBuffer<GesturePhase<Value>>

package init(
timeSource: (any TimeSource)?,
currentPhase: GesturePhase<Value>,
pendingPhases: RingBuffer<GesturePhase<Value>>
) {
self.timeSource = timeSource
self.currentPhase = currentPhase
self.pendingPhases = pendingPhases
}
}

// MARK: - GesturePhaseQueue.InvalidTransition

extension GesturePhaseQueue {
package struct InvalidTransition: Error {
package var phase: GesturePhase<Value>
package var targetPhase: GesturePhase<Value>

package init(phase: GesturePhase<Value>, targetPhase: GesturePhase<Value>) {
self.phase = phase
self.targetPhase = targetPhase
}
}
}

extension GesturePhaseQueue.InvalidTransition: NestedCustomStringConvertible {}

35 changes: 17 additions & 18 deletions Sources/OpenGestures/GestureNode/GestureNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ public final class GestureNode<Value: Sendable>: AnyGestureNode, @unchecked Send
private var _didUpdatePhase: ((GesturePhase<Value>, GesturePhase<Value>) -> Void)?
private var _shouldActivate: (() -> Bool)?

public private(set) var phase: GesturePhase<Value> = .idle
public private(set) var latestPhase: GesturePhase<Value> = .idle
package var phaseQueue: GesturePhaseQueue<Value> = GesturePhaseQueue(
timeSource: nil,
currentPhase: .idle,
pendingPhases: RingBuffer(capacity: 5, emptyValue: .idle)
)

// MARK: - Init

Expand All @@ -39,14 +42,10 @@ public final class GestureNode<Value: Sendable>: AnyGestureNode, @unchecked Send
// MARK: - Update

public func update(value: Value, isFinalUpdate: Bool) throws {
let oldPhase = phase
if isFinalUpdate {
phase = .ended(value: value)
} else {
phase = .active(value: value)
}
latestPhase = phase
_didUpdatePhase?(phase, oldPhase)
let oldPhase = phaseQueue.currentPhase
let newPhase: GesturePhase<Value> = isFinalUpdate ? .ended(value: value) : .active(value: value)
phaseQueue.currentPhase = newPhase
_didUpdatePhase?(newPhase, oldPhase)
}

public override func update(someValue: Any, isFinalUpdate: Bool) throws {
Expand All @@ -59,17 +58,17 @@ public final class GestureNode<Value: Sendable>: AnyGestureNode, @unchecked Send
// MARK: - Abort / Fail

public override func abort() throws {
let oldPhase = phase
phase = .failed(reason:.aborted)
latestPhase = phase
_didUpdatePhase?(phase, oldPhase)
let oldPhase = phaseQueue.currentPhase
let newPhase: GesturePhase<Value> = .failed(reason: .aborted)
phaseQueue.currentPhase = newPhase
_didUpdatePhase?(newPhase, oldPhase)
}

public override func fail(with error: Error) throws {
let oldPhase = phase
let oldPhase = phaseQueue.currentPhase
// TODO: .error(Error) case once non-Sendable handling resolved
phase = .failed(reason:.aborted)
latestPhase = phase
_didUpdatePhase?(phase, oldPhase)
let newPhase: GesturePhase<Value> = .failed(reason: .aborted)
phaseQueue.currentPhase = newPhase
_didUpdatePhase?(newPhase, oldPhase)
}
}
116 changes: 116 additions & 0 deletions Sources/OpenGestures/Util/RingBuffer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//
// RingBuffer.swift
// OpenGestures
//
// Audited for 9126.1.5
// Status: Complete

// MARK: - RingBuffer

package struct RingBuffer<Element> {
package let capacity: Int
package var count: Int
package var storage: [Element]
package var emptyValue: Element
package var start: Int
package var end: Int

package init(capacity: Int, emptyValue: Element) {
self.capacity = capacity
self.count = 0
self.storage = Array(repeating: emptyValue, count: capacity)
self.emptyValue = emptyValue
self.start = 0
self.end = 0
}

package var isEmpty: Bool { count == 0 }

package var isFull: Bool { count == capacity }

package mutating func append(_ element: Element) {
storage[end] = element
end = (end + 1) % capacity
if isFull {
start = (start + 1) % capacity
} else {
count += 1
}
}

@discardableResult
package mutating func removeFirst() -> Element {
let value = storage[start]
storage[start] = emptyValue
start = (start + 1) % capacity
count -= 1
return value
}
}

// MARK: - RingBuffer + Sequence

extension RingBuffer: Sequence {
package func makeIterator() -> RingBufferIterator<Element> {
RingBufferIterator(
ringBuffer: self,
currentIndex: start,
elementsRemaining: count
)
}
}

// MARK: - RingBuffer + Collection

extension RingBuffer: Collection {
package var startIndex: Int { 0 }

package var endIndex: Int { count }

package func index(after i: Int) -> Int { i + 1 }

package subscript(position: Int) -> Element {
storage[(start + position) % capacity]
}
}

// MARK: - RingBuffer + BidirectionalCollection

extension RingBuffer: BidirectionalCollection {
package func index(before i: Int) -> Int { i - 1 }
}

// MARK: - RingBuffer + CustomStringConvertible

extension RingBuffer: CustomStringConvertible {
package var description: String {
"[" + map { "\($0)" }.joined(separator: ", ") + "]"
}
}

// MARK: - RingBufferIterator

package struct RingBufferIterator<Element>: IteratorProtocol {
package let ringBuffer: RingBuffer<Element>
package var currentIndex: Int
package var elementsRemaining: Int

package init(
ringBuffer: RingBuffer<Element>,
currentIndex: Int,
elementsRemaining: Int
) {
self.ringBuffer = ringBuffer
self.currentIndex = currentIndex
self.elementsRemaining = elementsRemaining
}

package mutating func next() -> Element? {
guard elementsRemaining > 0 else { return nil }
let value = ringBuffer.storage[currentIndex]
currentIndex = (currentIndex + 1) % ringBuffer.capacity
elementsRemaining -= 1
return value
}
}

85 changes: 85 additions & 0 deletions Tests/OpenGesturesTests/Core/GesturePhaseQueueTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//
// GesturePhaseQueueTests.swift
// OpenGesturesTests

@_spi(Private) import OpenGestures
import Testing

// MARK: - GesturePhaseQueueTests

@Suite
struct GesturePhaseQueueTests {

// MARK: - Init

@Test
func testInit() {
let queue = GesturePhaseQueue<Int>(
timeSource: nil,
currentPhase: .idle,
pendingPhases: RingBuffer(capacity: 5, emptyValue: .idle)
)
#expect(queue.currentPhase.isIdle == true)
#expect(queue.pendingPhases.isEmpty == true)
#expect(queue.timeSource == nil)
}

// MARK: - Properties

@Test
func testCurrentPhaseUpdate() {
var queue = GesturePhaseQueue<Int>(
timeSource: nil,
currentPhase: .idle,
pendingPhases: RingBuffer(capacity: 5, emptyValue: .idle)
)
queue.currentPhase = .active(value: 42)
#expect(queue.currentPhase.isActive == true)
}

@Test
func testPendingPhasesAppend() {
var queue = GesturePhaseQueue<Int>(
timeSource: nil,
currentPhase: .idle,
pendingPhases: RingBuffer(capacity: 5, emptyValue: .idle)
)
queue.pendingPhases.append(.active(value: 1))
#expect(queue.pendingPhases.count == 1)
}

// MARK: - InvalidTransition

@Test
func testInvalidTransitionInit() {
let transition = GesturePhaseQueue<Int>.InvalidTransition(
phase: .idle,
targetPhase: .active(value: 1)
)
#expect(transition.phase.isIdle == true)
#expect(transition.targetPhase.isActive == true)
}

@Test
func testInvalidTransitionIsError() {
let _: any Error = GesturePhaseQueue<Int>.InvalidTransition(
phase: .idle,
targetPhase: .active(value: 1)
)
}

@Test
func testInvalidTransitionDescription() {
let transition = GesturePhaseQueue<Int>.InvalidTransition(
phase: .idle,
targetPhase: .active(value: 1)
)
#expect(transition.description == #"""
InvalidTransition { \#("")
phase: idle
targetPhase: active
}
"""#)
}
}

Loading
Loading