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

Conversation

shial4
Copy link

@shial4 shial4 commented Jul 31, 2023

Pull Request Description: Add Gestures Functionality

Description

This pull request aims to enhance the Tokamak framework by adding support for handling gestures in SwiftUI-like components. The goal is to enable developers to implement various interactive features in their applications using gestures, such as taps, long presses and drags.

Changes Made

The following changes have been made to the Tokamak framework to support gesture functionality:

  1. Added support for gestures such as TapGesture, LongPressGesture and DragGesture. These gestures are now functional and update the respective state when triggered.

  2. Added helper View modifiers for TapGesture and LongPressGesture aiming to match SwiftUI counterpart.

  3. Implemented TokamakDOM renderer gestures par

  4. Add basic implementation for standard, simultaneous and highPriority gesture handling, without the use of GestureMask.

  5. Add coordinate space

Remaining Work

The following features are yet to be implemented:

  1. Add AnyGesture for type erasure: A generic AnyGesture type is intended to be added to perform type erasure for gestures, allowing for a more flexible and unified approach when handling gestures. WIP, to be fixed.

  2. Add handling of GestureMask.

Example Use Case

To demonstrate the implemented gesture functionality, a sample use case has been provided. It includes examples of tap gestures, double tap gestures, long press gestures, and drag gestures. Certain parts of the code related to simultaneous gesture handling have been commented out, awaiting the implementation of gesture priority and simultaneous gesture support.

Please review and test the changes to ensure they work as expected and are aligned with the Tokamak framework's design and guidelines. Once reviewed and approved, this pull request can be merged into the main repository to enable gesture support in Tokamak.

_WASM_SwiftUI_gestures.mov
Screen.Recording.2023-08-14.at.20.28.32.mov

TestCode

The below code has been tested in SwiftUI and Tokamak to ensure the behavior is the same.

import TokamakDOM
import Foundation

@main
struct TokamakApp: App {
    var body: some Scene {
        WindowGroup("Tokamak App") {
            ContentView()
        }
    }
}

struct ContentView: View {
    @State var count: Int = 0
    @State var countDouble: Int = 0
    @GestureState var isDetectingTap = false
    
    @GestureState var isDetectingLongPress = false
    @State var completedLongPress = false
    @State var countLongpress: Int = 0
    
    @GestureState var dragAmount = CGSize.zero
    @State private var countDragLongPress = 0
    
    var body: some View {
        HStack(alignment: .top, spacing: 8) {
            tapGestures
            longPressGestures
            dragGestures
        }
        .padding()
    }
    
    var dragGestures: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("Drag Gestures")

            HStack {
                Rectangle()
                    .fill(Color.yellow)
                    .frame(width: 100, height: 100)
                    .gesture(DragGesture().updating($dragAmount) { value, state, transaction in
                        state = value.translation
                    }.onEnded { value in
                        print(value)
                    })
                Text("dragAmount: \(dragAmount.width), \(dragAmount.height)")
            }

            HStack {
                Rectangle()
                    .fill(Color.red)
                    .frame(width: 100, height: 100)
                    .gesture(DragGesture(minimumDistance: 0)
                        .onChanged { _ in
                            self.countDragLongPress += 1
                        })
                Text("Drag Count: \(countDragLongPress)")
            }
        }
    }
    
    var longPressGestures: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("LongPress Gestures")

            HStack {
                Rectangle()
                    .fill(self.isDetectingLongPress ? Color.pink : (self.completedLongPress ? Color.purple : Color.gray))
                    .frame(width: 100, height: 100)
                    .gesture(LongPressGesture(minimumDuration: 2)
                        .updating($isDetectingLongPress) { currentState, gestureState, transaction in
                            gestureState = currentState
                            transaction.animation = Animation.easeIn(duration: 2.0)
                        }
                        .onEnded { finished in
                            self.completedLongPress = finished
                        })
                Text(self.isDetectingLongPress ? "detecting" : (self.completedLongPress ? "completed" : "unknow"))
            }

            HStack {
                Rectangle()
                    .fill(Color.orange)
                    .frame(width: 100, height: 100)
                    .onLongPressGesture(minimumDuration: 0) {
                        countLongpress += 1
                    }
                    .onTapGesture() {
                        fatalError("onTapGesture, should not be called")
                    }
                Text("Long Pressed: \(countLongpress)")
            }
        }
    }
    
    var tapGestures: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("Tap Gestures")
            HStack {
                Rectangle()
                    .fill(Color.white)
                    .frame(width: 100, height: 100)
                    .onTapGesture {
                        count += 1
                        print("⚪️ gesture")
                    }
                Text("Tap: \(count)")
            }
            HStack {
                Rectangle()
                    .fill(Color.green)
                    .frame(width: 100, height: 100)
                    .onTapGesture(count: 2) {
                        countDouble += 1
                        print("🟢 double gesture")
                    }
                Text("double tap: \(countDouble)")
            }
            HStack {
                Rectangle()
                    .fill(Color.blue)
                    .frame(width: 100, height: 100)
                    .onTapGesture() {
                        print("🔵 1st gesture")
                    }
                    .onTapGesture() {
                        fatalError("should not be called")
                    }
                Text("1st tap gesture")
            }
            HStack {
                Rectangle()
                    .fill(Color.pink)
                    .frame(width: 100, height: 100)
                    .simultaneousGesture(
                        TapGesture()
                            .onEnded({ _ in
                                print("🩷 simultaneousGesture gesture")
                            })
                    )
                    .onTapGesture() {
                        fatalError("should not be called")
                    }
                    .onTapGesture() {
                        fatalError("should not be called")
                    }
                    .simultaneousGesture(
                        TapGesture()
                            .onEnded({ _ in
                                print("🩷 simultaneousGesture 2 gesture")
                            })
                    )
                Text("simultaneousGesture")
            }
            HStack {
                Rectangle()
                    .fill(Color.purple)
                    .frame(width: 100, height: 100)
                    .simultaneousGesture(
                        TapGesture()
                            .onEnded({ _ in
                                fatalError("should not be called")
                            })
                    )
                    .onTapGesture() {
                        fatalError("should not be called")
                    }
                    .highPriorityGesture(
                        TapGesture()
                            .onEnded({ _ in
                                fatalError("should not be called")
                            })
                    )
                    .highPriorityGesture(
                        TapGesture()
                            .onEnded({ _ in
                                print("🟣 highPriorityGesture 3 gesture")
                            })
                    )
                Text("highPriorityGesture")
            }
        }
    }
}

Basic implementation of standard, simultaneous and highPriority gesture handling has been tested and compared against SwiftUI playgrounds with the below code

var tapGestures: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("Tap Gestures")

            HStack {
                Rectangle()
                    .fill(Color.blue)
                    .frame(width: 100, height: 100)
                    .simultaneousGesture(
                        TapGesture().onEnded({ _ in
                            print("🔵 simultaneousGesture gesture")
                        })
                    )
                    .onTapGesture() {
                        print("🔵 1st gesture")
                    }
                    .onTapGesture() {
                        print("🔵 2st gesture")
                    }
                    .simultaneousGesture(
                        TapGesture().onEnded({ _ in
                            print("🔵 simultaneousGesture 2 gesture")
                        })
                    )
                Text("simultaneousGesture")
            }
            HStack {
                Rectangle()
                    .fill(Color.purple)
                    .frame(width: 100, height: 100)
                    .simultaneousGesture(
                        TapGesture().onEnded({ _ in
                            print("🟢 simultaneousGesture gesture")
                        })
                    )
                    .onTapGesture() {
                        print("🟢 1st gesture")
                    }
                    .highPriorityGesture(
                        TapGesture().onEnded({ _ in
                            print("🟢 highPriorityGesture 2 gesture")
                        })
                    )
                    .highPriorityGesture(
                        TapGesture().onEnded({ _ in
                            print("🟢 highPriorityGesture 3 gesture")
                        })
                    )
                Text("highPriorityGesture")
            }
        }
    }

output

🟢 highPriorityGesture 3 gesture
🔵 simultaneousGesture 2 gesture
🔵 simultaneousGesture gesture

The main difference in simultaneousGesture is that Tokamak has different order where simultaneousGesture 2 is after simultaneousGesture

Screen.Recording.2023-08-13.at.20.46.33.mov

With follow-up test

Rectangle()
 .fill(Color.blue)
 .frame(width: 100, height: 100)
.onTapGesture() {
    print("🔵 1st gesture")
}
.onTapGesture() {
    print("🔵 2st gesture")
}

🔵 1st gesture

Screen.Recording.2023-08-13.at.20.45.57.mov

Added Fibre support for gestures

Screen_Recording_2023-08-25_at_19.26.10.mov

Tested with the below code

struct GestureDemo: View {
    let rows = 16
    let columns = 16
    
    struct Rect: Hashable {
        let row: Int
        let column: Int
    }
    
    @State private var selectedRects: Set<Rect> = []
    
    var body: some View {
        VStack(spacing: 0) {
            ForEach(0..<rows, id: \.self) { row in
                HStack(spacing: 0) {
                    ForEach(0..<columns, id: \.self) { column in
                        Rectangle()
                            .fill(isSelected(row: row, column: column) ? Color.blue : Color.gray)
                            .frame(width: 50, height: 50)
                            .overlay(
                                Text("\(row):\(column)")
                                    .foregroundColor(.white)
                            )
                    }
                }
            }
        }
        .border(Color.red)
        .coordinateSpace(name: "MyView")
        .gesture(
            TapGesture()
                .onEnded {
                    print("🔵")
                    selectedRects.removeAll()
                }
        )
        .gesture(
            DragGesture(coordinateSpace: .named("MyView"))
                .onChanged { value in
                    let location = value.location
                    let row = Int(location.y / 50)
                    let column = Int(location.x / 50)
                    print("🟢", row, column, location)
                    if !isSelected(row: row, column: column) {
                        selectedRects.insert(Rect(row: row, column: column))
                    }
                }
        )
    }
    
    func isSelected(row: Int, column: Int) -> Bool {
        selectedRects.contains(Rect(row: row, column: column))
    }
}

Szymon Lorenz added 3 commits July 31, 2023 18:55
Add tap, lonPress and Drag gestures.
@j-f1 j-f1 requested a review from carson-katri July 31, 2023 14:02
@j-f1
Copy link
Member

j-f1 commented Jul 31, 2023

This is incredible! I am unfortunately about to hop on a bus to start a job at Apple so I can’t give this a thorough review 🥲

But hopefully @carson-katri can take a look at some point.

Copy link
Member

@carson-katri carson-katri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is amazing, you for working on this! Left a few notes.

Sources/TokamakCore/Gestures/GestureMask.swift Outdated Show resolved Hide resolved
Sources/TokamakCore/Gestures/GesturePhase.swift Outdated Show resolved Hide resolved
Sources/TokamakCore/Gestures/Gesture.swift Outdated Show resolved Hide resolved
Sources/TokamakCore/Gestures/Recognizers/DragGesture.swift Outdated Show resolved Hide resolved
Sources/TokamakDOM/Views/Gestures/GestureView.swift Outdated Show resolved Hide resolved
@carson-katri carson-katri added the SwiftUI compatibility Tokamak API differences with SwiftUI label Aug 5, 2023
@shial4 shial4 requested a review from carson-katri August 9, 2023 09:53
Copy link
Author

@shial4 shial4 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, I've addressed PR feedback. Have some questions where I tagged you. Could use some input

@shial4
Copy link
Author

shial4 commented Aug 13, 2023

updated PR & its description
#bump up

🚀

1. **Gesture Coordinate Space**: A new gesture coordinate space has been added to provide better support for gesture-based interactions.

2. **Geometry Reader Update**: The geometry reader has been updated to offer improved functionality and performance.

3. **Geometry Proxy Preparation**: A geometry proxy has been prepared to streamline and optimize geometric operations.

4. **Coordinate Space Enhancement**: The coordinate space has been enhanced to support new use cases and scenarios.
Sources/TokamakCore/Gestures/AnyGesture.swift Outdated Show resolved Hide resolved
Sources/TokamakCore/Gestures/GestureEnvironmentKey.swift Outdated Show resolved Hide resolved
Sources/TokamakCore/Gestures/GestureEnvironmentKey.swift Outdated Show resolved Hide resolved
Sources/TokamakCore/Gestures/GestureMask.swift Outdated Show resolved Hide resolved
Sources/TokamakCore/Gestures/GestureMask.swift Outdated Show resolved Hide resolved
Sources/TokamakDOM/Views/Gestures/GestureView.swift Outdated Show resolved Hide resolved
Sources/TokamakCore/Views/View+DataChanges.swift Outdated Show resolved Hide resolved
Sources/TokamakCore/Views/View+DataChanges.swift Outdated Show resolved Hide resolved
Sources/TokamakCore/Views/View+DataChanges.swift Outdated Show resolved Hide resolved
Szymon Lorenz added 3 commits August 20, 2023 09:12
…dinate-space

# Conflicts:
#	Sources/TokamakCore/Gestures/GestureEnvironmentKey.swift
#	Sources/TokamakCore/Modifiers/OnChangeModifier.swift
@shial4 shial4 requested a review from carson-katri August 20, 2023 01:10
@shial4
Copy link
Author

shial4 commented Aug 23, 2023

fixed
#545

@shial4
Copy link
Author

shial4 commented Aug 26, 2023

coordinate space broken for custom spaces while using fiber reconciler.
ticket here: #547

commit 6ad4a06a6d11a461d993cda8697a6fa8d9ebbdc6
Author: Szymon Lorenz <[email protected]>
Date:   Sat Aug 26 13:32:06 2023 +1000

    Add demos
@shial4
Copy link
Author

shial4 commented Aug 26, 2023

Add content shape modifier. Verify gesture start against the shape fill area.
#548

Copy link
Member

@carson-katri carson-katri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like formatting was changed in many of the files. Could you run swift-lint or check if you have the pre-commit hook installed?
https://github.com/TokamakUI/Tokamak/blob/main/CONTRIBUTING.md#coding-style

Sources/TokamakCore/Gestures/View+HitTesting.swift Outdated Show resolved Hide resolved
import TokamakCore


enum GestureEventsObserver {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice refactor 👍

@shial4 shial4 requested a review from carson-katri September 4, 2023 08:06
@shial4
Copy link
Author

shial4 commented Sep 7, 2023

updated view change modifier, not to relay on _onChange or _onUnmount events
due to this comment
#542 (comment)

@carson-katri ready for the review too :)

@shial4
Copy link
Author

shial4 commented Sep 15, 2023

Tests for onChange & onReceive will pass after Fiber fixes attempt 2 will be merge

@aehlke
Copy link

aehlke commented Oct 11, 2023

is this in a good state to use? (sorry to bother)

@shial4
Copy link
Author

shial4 commented Oct 12, 2023

@aehlke

is this in a good state to use? (sorry to bother)

I'm building my apps of top of this branch. However would love to have fibre working with gesture, but for that we will need the fibre PR merged in + some other additions and improvements

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
SwiftUI compatibility Tokamak API differences with SwiftUI
Development

Successfully merging this pull request may close these issues.

4 participants