Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add gestures initial functionality #538

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
19a9d8c
Add gestures
Jul 31, 2023
927fded
fix style
Jul 31, 2023
be15819
Add missing type
Jul 31, 2023
fddad3c
fix header comments
Aug 1, 2023
d31f7f3
Address PR feedback + add view data changes reaction modifiers
Aug 9, 2023
1b52c51
quick fix
Aug 9, 2023
6ace747
Add missing onChangedAction for long press
Aug 9, 2023
779c6d3
remove prints
Aug 9, 2023
4ec2d79
gestures standard, simultaneous and highPriority handling
Aug 13, 2023
72269c1
Fix minor tap gesture issue
Aug 15, 2023
5781500
Global listeners to track continuous pointer movement/up events
Aug 15, 2023
b73ae74
Squashed commit of the following:
Aug 19, 2023
eba439f
Address PR feedback
Aug 19, 2023
f6c7227
Merge branch 'shial4/feat/530-add-gestures' into shial4/feat/530-coor…
Aug 19, 2023
8a0a468
Update base branch
Aug 19, 2023
cbf9ef3
some cosmetics
Aug 19, 2023
4bab2f2
Add gesture phase context
Aug 20, 2023
46f2a30
move to separate file
Aug 20, 2023
4682ca5
Remove target.getBoundingClientRect to improve performance
Aug 20, 2023
086a5a1
Fix minor issues
Aug 21, 2023
28757d5
Add global publisher, fix drag issue state
Aug 23, 2023
98915cc
remove print
Aug 23, 2023
a1f4419
Merge pull request #1 from shial4/shial4/feat/530-coordinate-space
shial4 Aug 23, 2023
b60386a
Add Fibre support for gestures
Aug 25, 2023
73d4d71
swap to root
Aug 25, 2023
d89bc06
Add tests & clean up
Aug 26, 2023
2a7485a
Squashed commit of the following:
Aug 26, 2023
37db9e4
fix test
Sep 1, 2023
6a97c98
Remove redundant functionality
Sep 3, 2023
39bb535
run checks
Sep 4, 2023
64af12a
Update change modifiers
Sep 7, 2023
cb0818a
due to the use of Task in the code, we bump min version
Sep 7, 2023
cf86cbf
Remove _onUpdate from on change modifier
Sep 10, 2023
29998d7
Fix change & receive
Sep 15, 2023
1ef34f8
Add gesture tests
Sep 17, 2023
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
39 changes: 39 additions & 0 deletions Sources/TokamakCore/Gestures/Composing/ExclusiveGesture.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Szymon on 16/7/2023.
//

@frozen
/// The ExclusiveGesture gives precedence to its first gesture.
public struct ExclusiveGesture<First, Second> where First : Gesture, Second : Gesture {
/// The value of an exclusive gesture that indicates which of two gestures succeeded.
public typealias Value = ExclusiveGesture.ExclusiveValue

public struct ExclusiveValue {
public var first: First.Value
public var second: First.Value
}

/// The first of two gestures.
public var first: First
/// The second of two gestures.
public var second: Second

/// Creates a gesture from two gestures where only one of them succeeds.
init(first: First, second: Second) {
self.first = first
self.second = second
}
}
38 changes: 38 additions & 0 deletions Sources/TokamakCore/Gestures/Composing/SequenceGesture.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Szymon on 16/7/2023.
//

@frozen
public struct SequenceGesture<First, Second> where First : Gesture, Second : Gesture {
/// The value of a sequence gesture that helps to detect whether the first gesture succeeded, so the second gesture can start.
public typealias Value = SequenceGesture.SequenceValue

public struct SequenceValue {
public var first: First.Value
public var second: First.Value
}

/// The first gesture in a sequence of two gestures.
public var first: First
/// The second gesture in a sequence of two gestures.
public var second: Second

/// Creates a sequence gesture with two gestures.
init(first: First, second: Second) {
self.first = first
self.second = second
}
}
37 changes: 37 additions & 0 deletions Sources/TokamakCore/Gestures/Composing/SimultaneousGesture.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Szymon on 16/7/2023.
//

@frozen
public struct SimultaneousGesture<First, Second> where First : Gesture, Second : Gesture {
public typealias Value = SimultaneousGesture.SimultaneousValue

public struct SimultaneousValue {
public let first: First.Value?
public let second: First.Value?
}

/// The first of two gestures that can happen simultaneously.
public let first: First
/// The second of two gestures that can happen simultaneously.
public let second: Second

/// Creates a gesture with two gestures that can receive updates or succeed independently of each other.
init(first: First, second: Second) {
self.first = first
self.second = second
}
}
124 changes: 124 additions & 0 deletions Sources/TokamakCore/Gestures/Gesture.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Szymon on 16/7/2023.
//

import Foundation

public protocol Gesture {
// MARK: Required

/// The type representing the gesture’s value.
associatedtype Value

/// The type of gesture representing the body of Self.
associatedtype Body: Gesture

/// The content and behavior of the gesture.
var body: Self.Body { get }

mutating func _onPhaseChange(_ phase: _GesturePhase)
func _onEnded(perform action: @escaping (Value) -> Void) -> Self
func _onChanged(perform action: @escaping (Value) -> Void) -> Self
}

// MARK: Performing the gesture

extension Gesture {
/// Adds an action to perform when the gesture ends.
public func onEnded(_ action: @escaping (Self.Value) -> Void) -> _EndedGesture<Self> {
_EndedGesture(self, onEnded: action)
}

/// Updates the provided gesture value property as the gesture’s value changes.
public func updating<State>(
_ state: GestureState<State>,
body: @escaping (Self.Value, inout State, inout Transaction) -> Void
) -> GestureStateGesture<Self, State> {
GestureStateGesture(base: self, state: state, updatingBody: body)
}
}

// MARK: Performing the gesture

extension Gesture where Value: Equatable {
/// Adds an action to perform when the gesture’s value changes.
/// Available when Value conforms to Equatable.
public func onChanged(_ action: @escaping (Self.Value) -> Void) -> _ChangedGesture<Self> {
_ChangedGesture(self, onChanged: action)
}
}

// MARK: Composing gestures

extension Gesture {
/// Combines a gesture with another gesture to create a new gesture that recognizes both gestures at the same time.
public func simultaneously<Other>(with gesture: Other) -> SimultaneousGesture<Self, Other> {
SimultaneousGesture(first: self, second: gesture)
}

/// Sequences a gesture with another one to create a new gesture, which results in the second gesture only receiving events after the first gesture succeeds.
public func sequenced<Other>(before gesture: Other) -> SequenceGesture<Self, Other> {
SequenceGesture(first: self, second: gesture)
}

/// Combines two gestures exclusively to create a new gesture where only one gesture succeeds, giving precedence to the first gesture.
public func exclusively<Other>(before gesture: Other) -> ExclusiveGesture<Self, Other> {
ExclusiveGesture(first: self, second: gesture)
}
}

// MARK: Transforming a gesture

extension Gesture {}

// MARK: Private Helpers

extension Gesture {
func calculateDistance(xOffset: Double, yOffset: Double) -> Double {
let xSquared = pow(xOffset, 2)
let ySquared = pow(yOffset, 2)
let sumOfSquares = xSquared + ySquared
let distance = sqrt(sumOfSquares)
return distance
}

func calculateTranslation(from pointA: CGPoint, to pointB: CGPoint) -> CGSize {
let dx = pointB.x - pointA.x
let dy = pointB.y - pointA.y
return CGSize(width: dx, height: dy)
}

func calculateVelocity(from translation: CGSize, timeElapsed: Double) -> CGSize {
let velocityX = translation.width / timeElapsed
let velocityY = translation.height / timeElapsed

return CGSize(width: velocityX, height: velocityY)
}

func calculatePredictedEndLocation(from location: CGPoint, velocity: CGSize) -> CGPoint {
let predictedX = location.x + velocity.width
let predictedY = location.y + velocity.height

return CGPoint(x: predictedX, y: predictedY)
}

func calculatePredictedEndTranslation(from translation: CGSize, velocity: CGSize) -> CGSize {
let predictedWidth = translation.width + velocity.width
let predictedHeight = translation.height + velocity.height

return CGSize(width: predictedWidth, height: predictedHeight)
}
}
94 changes: 94 additions & 0 deletions Sources/TokamakCore/Gestures/GestureMask.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Szymon on 30/7/2023.
//

import Foundation

/// Options that control how adding a gesture to a view affects other gestures recognized by the view and its subviews.
@frozen public struct GestureMask: Equatable, ExpressibleByArrayLiteral, OptionSet, Sendable {
public typealias RawValue = Int8
public var rawValue: Int8

// MARK: - OptionSet

public init(rawValue: Int8) {
self.rawValue = rawValue
}

// MARK: - Equatable

public static func == (lhs: GestureMask, rhs: GestureMask) -> Bool {
return lhs.rawValue == rhs.rawValue
}

// MARK: - ExpressibleByArrayLiteral

/// Creates a gesture mask from an array of gesture options.
///
/// - Parameter elements: An array of `GestureMask` elements.
public init(arrayLiteral elements: GestureMask...) {
self.rawValue = elements.reduce(0) { $0 | $1.rawValue }
}

// MARK: - SetAlgebra

static var allZeros: GestureMask {
return GestureMask(rawValue: 0)
}

static func | (lhs: GestureMask, rhs: GestureMask) -> GestureMask {
return GestureMask(rawValue: lhs.rawValue | rhs.rawValue)
}

static func & (lhs: GestureMask, rhs: GestureMask) -> GestureMask {
return GestureMask(rawValue: lhs.rawValue & rhs.rawValue)
}

static prefix func ~ (x: GestureMask) -> GestureMask {
return GestureMask(rawValue: ~x.rawValue)
}
carson-katri marked this conversation as resolved.
Show resolved Hide resolved

// MARK: - Gesture Options

/// Enable both the added gesture as well as all other gestures on the view and its subviews.
public static let all: Self = .gesture | .subviews

/// Enable the added gesture but disable all gestures in the subview hierarchy.
public static let gesture: Self = GestureMask(rawValue: 1 << 0)

/// Enable all gestures in the subview hierarchy but disable the added gesture.
public static let subviews: Self = GestureMask(rawValue: 1 << 1)

/// Disable all gestures in the subview hierarchy, including the added gesture.
public static let none: Self = []

// MARK: - Helper Methods

/// Enables a specific gesture option in the mask.
///
/// - Parameter option: The `GestureMask` representing the gesture option to enable.
mutating func enableGesture(_ option: GestureMask) {
self.insert(option)
}

/// Disables a specific gesture option in the mask.
///
/// - Parameter option: The `GestureMask` representing the gesture option to disable.
mutating func disableGesture(_ option: GestureMask) {
self.remove(option)
}
shial4 marked this conversation as resolved.
Show resolved Hide resolved
}

47 changes: 47 additions & 0 deletions Sources/TokamakCore/Gestures/GestureState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Szymon on 16/7/2023.
//


@propertyWrapper
public struct GestureState<Value>: DynamicProperty {
private let initialValue: Value

var anyInitialValue: Any { initialValue }

var getter: (() -> Any)?
var setter: ((Any, Transaction) -> ())?

public init(wrappedValue value: Value) {
initialValue = value
}

public var wrappedValue: Value {
get { getter?() as? Value ?? initialValue }
nonmutating set { setter?(newValue, Transaction._active ?? .init(animation: nil)) }
}

public var projectedValue: GestureState<Value> {
self
}
}

extension GestureState: WritableValueStorage {}

public extension GestureState where Value: ExpressibleByNilLiteral {
@inlinable
init() { self.init(wrappedValue: nil) }
}
Loading