Skip to content
Closed
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
1 change: 1 addition & 0 deletions .checkpoint
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
05d6c65 | preserve combined branch while split PR work uses local worktrees
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ Temporary Items

## User settings
xcuserdata/

# Local agent worktrees
.worktree/
4 changes: 4 additions & 0 deletions OpenParsec.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
17CD1E032BF07BC3003D2102 /* ViewContainerPatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CD1E022BF07BC3003D2102 /* ViewContainerPatch.swift */; };
17EA35F72BF712A8001BE0DF /* ParsecSDKBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17EA35F62BF712A8001BE0DF /* ParsecSDKBridge.swift */; };
17EEB91C2BEE62BA00502A3A /* KeyBoardTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17EEB91B2BEE62BA00502A3A /* KeyBoardTest.swift */; };
A1B2C3D42F8E000100000002 /* InputMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F8E000100000001 /* InputMapping.swift */; };
271D14F9292E90EB00D7F1D6 /* ParsecView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271D14F8292E90EB00D7F1D6 /* ParsecView.swift */; };
271D14FC292EAA3600D7F1D6 /* ParsecGLKRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271D14FA292EAA3600D7F1D6 /* ParsecGLKRenderer.swift */; };
271D14FD292EAA3600D7F1D6 /* ParsecGLKViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271D14FB292EAA3600D7F1D6 /* ParsecGLKViewController.swift */; };
Expand Down Expand Up @@ -65,6 +66,7 @@
17CD1E022BF07BC3003D2102 /* ViewContainerPatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewContainerPatch.swift; sourceTree = "<group>"; };
17EA35F62BF712A8001BE0DF /* ParsecSDKBridge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParsecSDKBridge.swift; sourceTree = "<group>"; };
17EEB91B2BEE62BA00502A3A /* KeyBoardTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBoardTest.swift; sourceTree = "<group>"; };
A1B2C3D42F8E000100000001 /* InputMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputMapping.swift; sourceTree = "<group>"; };
271D14F8292E90EB00D7F1D6 /* ParsecView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsecView.swift; sourceTree = "<group>"; };
271D14FA292EAA3600D7F1D6 /* ParsecGLKRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParsecGLKRenderer.swift; sourceTree = "<group>"; };
271D14FB292EAA3600D7F1D6 /* ParsecGLKViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParsecGLKViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -164,6 +166,7 @@
271D14FB292EAA3600D7F1D6 /* ParsecGLKViewController.swift */,
27B23A212B1B979C00B52F14 /* ParsecMetalRenderer.swift */,
27B23A232B1B98EF00B52F14 /* ParsecMetalViewController.swift */,
A1B2C3D42F8E000100000001 /* InputMapping.swift */,
17EA35F62BF712A8001BE0DF /* ParsecSDKBridge.swift */,
271D14F8292E90EB00D7F1D6 /* ParsecView.swift */,
170E0A762BEF5AFF00B0EA32 /* ParsecViewController.swift */,
Expand Down Expand Up @@ -305,6 +308,7 @@
27899AC7292FE2A9001ACA33 /* audio.c in Sources */,
27E61AA62929817700FF6563 /* LoginView.swift in Sources */,
27B8D564292DC7A000A324AD /* CParsec.swift in Sources */,
A1B2C3D42F8E000100000002 /* InputMapping.swift in Sources */,
27A923E229E8FEE900F54BDA /* UIViewControllerWrapper.swift in Sources */,
170E0A772BEF5AFF00B0EA32 /* ParsecViewController.swift in Sources */,
27B23A222B1B979C00B52F14 /* ParsecMetalRenderer.swift in Sources */,
Expand Down
43 changes: 35 additions & 8 deletions OpenParsec/GameController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ class GamepadController {
private(set) var mice = Set<GCMouse>()
// private var panRecognizer: UIPanGestureRecognizer!
weak var delegate: InputManagerDelegate?
var pointerInputStatusProvider: (() -> PointerInputStatus?)?
var pointerButtonTouchSupported = false
var gcmouseScrollHandler: ((GCMouseScrollAxis, Float) -> Void)?

public func viewDidLoad() {

Expand Down Expand Up @@ -82,32 +85,56 @@ class GamepadController {
func registerMouseHandler() {
for mouse in GCMouse.mice() {
mice.insert(mouse)
mouse.mouseInput?.leftButton.pressedChangedHandler = {(_: GCControllerButtonInput, _: Float, pressed: Bool) in
mouse.mouseInput?.leftButton.pressedChangedHandler = {[weak self] (_: GCControllerButtonInput, _: Float, pressed: Bool) in
guard GCMousePointerMapper.shouldSendButton(pointerButtonTouchSupported: self?.pointerButtonTouchSupported == true) else { return }
guard ParsecBackgroundManager.shared.hasActiveConnection else { return }
CParsec.sendMouseClickMessage(MOUSE_L, pressed)
}
mouse.mouseInput?.rightButton?.pressedChangedHandler = {(_: GCControllerButtonInput, _: Float, pressed: Bool) in
mouse.mouseInput?.rightButton?.pressedChangedHandler = {[weak self] (_: GCControllerButtonInput, _: Float, pressed: Bool) in
guard GCMousePointerMapper.shouldSendButton(pointerButtonTouchSupported: self?.pointerButtonTouchSupported == true) else { return }
// pointer-lock toggles on the connect/disconnect view swap can synthesize a button edge
// with no real input — dont forward it unless a session is actually live
guard ParsecBackgroundManager.shared.hasActiveConnection else { return }
CParsec.sendMouseClickMessage(MOUSE_R, pressed)
}
mouse.mouseInput?.middleButton?.pressedChangedHandler = {(_: GCControllerButtonInput, _: Float, pressed: Bool) in
mouse.mouseInput?.middleButton?.pressedChangedHandler = {[weak self] (_: GCControllerButtonInput, _: Float, pressed: Bool) in
guard GCMousePointerMapper.shouldSendButton(pointerButtonTouchSupported: self?.pointerButtonTouchSupported == true) else { return }
guard ParsecBackgroundManager.shared.hasActiveConnection else { return }
CParsec.sendMouseClickMessage(MOUSE_MIDDLE, pressed)
}
mouse.mouseInput?.mouseMovedHandler={(_: GCMouseInput, v: Float, v2: Float) in
mouse.mouseInput?.mouseMovedHandler={[weak self] (_: GCMouseInput, v: Float, v2: Float) in
guard let status = self?.pointerInputStatus() else {
return
}
let pointerHoverSupported: Bool
if #available(iOS 13.4, *) {
pointerHoverSupported = true
} else {
pointerHoverSupported = false
}
guard GCMousePointerMapper.shouldSendRelativeMove(pointerHoverSupported: pointerHoverSupported) else {
return
}
guard status.isActive else {
return
}
CParsec.sendMouseDelta(Int32(v/1.25 * Float(SettingsHandler.mouseSensitivity)), Int32(-v2/1.25 * Float(SettingsHandler.mouseSensitivity)))
}
mouse.mouseInput?.scroll.yAxis.valueChangedHandler = {(_: GCControllerAxisInput, value: Float) in
CParsec.sendWheelMsg(x: Int32(value), y: 0)
mouse.mouseInput?.scroll.yAxis.valueChangedHandler = {[weak self] (_: GCControllerAxisInput, value: Float) in
ScrollInputGate.recordGCMouseScroll()
self?.gcmouseScrollHandler?(.y, value)
}
mouse.mouseInput?.scroll.xAxis.valueChangedHandler = {(_: GCControllerAxisInput, value: Float) in
CParsec.sendWheelMsg(x: 0, y: Int32(value))
mouse.mouseInput?.scroll.xAxis.valueChangedHandler = {[weak self] (_: GCControllerAxisInput, value: Float) in
ScrollInputGate.recordGCMouseScroll()
self?.gcmouseScrollHandler?(.x, value)
}
}
}

private func pointerInputStatus() -> PointerInputStatus? {
return pointerInputStatusProvider?()
}

@objc func didMouseConnectController(_ notification: Notification) {
self.registerMouseHandler()
}
Expand Down
237 changes: 237 additions & 0 deletions OpenParsec/InputMapping.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import Foundation
import CoreGraphics

enum ZoomViewportPolicy {
private static let originalScaleTolerance: CGFloat = 0.01

static func shouldConstrain(
zoomEnabled: Bool,
zoomScale: CGFloat,
minimumZoomScale: CGFloat
) -> Bool {
return zoomEnabled && zoomScale > minimumZoomScale + originalScaleTolerance
}

static func shouldRepositionForCursor(
zoomEnabled: Bool,
zoomScale: CGFloat,
minimumZoomScale: CGFloat,
directMouseMode: Bool
) -> Bool {
return !directMouseMode && shouldConstrain(
zoomEnabled: zoomEnabled,
zoomScale: zoomScale,
minimumZoomScale: minimumZoomScale
)
}
}

enum ScrollWheelMapper {
private static let wheelDivisor: Float = 20.0

static func wheelScale(sensitivity: Float, naturalScrolling: Bool) -> Float {
let direction: Float = naturalScrolling ? -1.0 : 1.0
return sensitivity * direction / wheelDivisor
}

static func wheelUnits(rawDelta: Float, sensitivity: Float, naturalScrolling: Bool) -> Int32 {
return Int32(rawDelta * wheelScale(sensitivity: sensitivity, naturalScrolling: naturalScrolling))
}
}

struct ScrollWheel {
let x: Int32
let y: Int32
}

enum GCMouseScrollAxis {
case x
case y
}

enum GCMouseScrollMapper {
static func adjustedDelta(axis: GCMouseScrollAxis, rawValue: Float, naturalScrolling: Bool) -> (x: Float, y: Float) {
let direction: Float = naturalScrolling ? -1.0 : 1.0
let adjusted = rawValue * direction
switch axis {
case .x:
return (x: 0, y: adjusted)
case .y:
return (x: adjusted, y: 0)
}
}
}

struct GCMouseScrollMotion {
private var accumulatedX: Float = 0
private var accumulatedY: Float = 0
private(set) var velocityX: Float = 0
private(set) var velocityY: Float = 0
private(set) var lastEventTime: TimeInterval?

mutating func consume(
axis: GCMouseScrollAxis,
rawValue: Float,
naturalScrolling: Bool,
at time: TimeInterval
) -> ScrollWheel {
let delta = GCMouseScrollMapper.adjustedDelta(
axis: axis,
rawValue: rawValue,
naturalScrolling: naturalScrolling
)

if let lastEventTime {
let elapsed = Float(time - lastEventTime)
if elapsed > 0.0005 && elapsed < 0.1 {
switch axis {
case .x:
velocityY = velocityY * 0.4 + (delta.y / elapsed) * 0.6
case .y:
velocityX = velocityX * 0.4 + (delta.x / elapsed) * 0.6
}
} else if elapsed >= 0.1 {
velocityX = 0
velocityY = 0
}
}
lastEventTime = time

return accumulate(deltaX: delta.x, deltaY: delta.y)
}

mutating func momentumWheel(deltaTime: Float, decayPerSecond: Double) -> ScrollWheel {
let decay = Float(pow(decayPerSecond, Double(deltaTime)))
velocityX *= decay
velocityY *= decay
return accumulate(deltaX: velocityX * deltaTime, deltaY: velocityY * deltaTime)
}

func shouldStartMomentum(minimumSpeed: Float) -> Bool {
return speed >= minimumSpeed
}

func shouldStopMomentum(maximumSpeed: Float) -> Bool {
return speed < maximumSpeed
}

mutating func reset() {
accumulatedX = 0
accumulatedY = 0
velocityX = 0
velocityY = 0
lastEventTime = nil
}

private var speed: Float {
return sqrt(velocityX * velocityX + velocityY * velocityY)
}

private mutating func accumulate(deltaX: Float, deltaY: Float) -> ScrollWheel {
accumulatedX += deltaX
accumulatedY += deltaY
let wheel = ScrollWheel(x: Int32(accumulatedX), y: Int32(accumulatedY))
accumulatedX -= Float(wheel.x)
accumulatedY -= Float(wheel.y)
return wheel
}
}

enum ScrollInputGate {
private static let trackpadSuppressionWindow: TimeInterval = 0.20
private static var lastGCMouseScrollTime: TimeInterval?

static func recordGCMouseScroll(at time: TimeInterval = Date().timeIntervalSinceReferenceDate) {
lastGCMouseScrollTime = time
}

static func shouldSendTrackpadScroll(at time: TimeInterval = Date().timeIntervalSinceReferenceDate) -> Bool {
guard let lastGCMouseScrollTime else {
return true
}

return time - lastGCMouseScrollTime >= trackpadSuppressionWindow
}

static func reset() {
lastGCMouseScrollTime = nil
}
}

enum PointerInputGate {
// Absolute hover input needs an unlocked system pointer.
static let prefersPointerLocked = false

static func status(
windowIsFocused: Bool,
appIsActive: Bool,
sceneIsForegroundActive: Bool
) -> PointerInputStatus {
return PointerInputStatus(
windowIsFocused: windowIsFocused,
appIsActive: appIsActive,
sceneIsForegroundActive: sceneIsForegroundActive
)
}

static func isActive(
windowIsFocused: Bool,
appIsActive: Bool,
sceneIsForegroundActive: Bool
) -> Bool {
return status(
windowIsFocused: windowIsFocused,
appIsActive: appIsActive,
sceneIsForegroundActive: sceneIsForegroundActive
).isActive
}
}

struct PointerInputStatus {
let windowIsFocused: Bool
let appIsActive: Bool
let sceneIsForegroundActive: Bool

var isActive: Bool {
return windowIsFocused && appIsActive && sceneIsForegroundActive
}
}

enum GCMousePointerMapper {
static func shouldSendRelativeMove(pointerHoverSupported: Bool) -> Bool {
return !pointerHoverSupported
}

static func shouldSendButton(pointerButtonTouchSupported: Bool) -> Bool {
return !pointerButtonTouchSupported
}
}

enum PointerPositionMapper {
static func hostPosition(
pointerLocation: CGPoint,
visibleFrame: CGRect,
contentSize: CGSize,
windowIsFocused: Bool = true,
appIsActive: Bool = true,
sceneIsForegroundActive: Bool = true
) -> CGPoint? {
guard PointerInputGate.isActive(
windowIsFocused: windowIsFocused,
appIsActive: appIsActive,
sceneIsForegroundActive: sceneIsForegroundActive
) else {
return nil
}
guard visibleFrame.width > 0, visibleFrame.height > 0, contentSize.width > 0, contentSize.height > 0 else {
return nil
}
guard visibleFrame.contains(pointerLocation) else {
return nil
}

let relativeX = (pointerLocation.x - visibleFrame.minX) / visibleFrame.width
let relativeY = (pointerLocation.y - visibleFrame.minY) / visibleFrame.height
return CGPoint(x: relativeX * contentSize.width, y: relativeY * contentSize.height)
}
}
14 changes: 12 additions & 2 deletions OpenParsec/ParsecView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ struct ParsecView: View {

let save = SettingsHandler.saveSessionSettings
_muted = State(initialValue: SettingsHandler.startMuted || (save && SettingsHandler.savedMuted))
_zoomEnabled = State(initialValue: save ? SettingsHandler.savedZoomEnabled : false)
_zoomEnabled = State(initialValue: SettingsHandler.pinchZoomEnabled)
_constantFps = State(initialValue: save ? SettingsHandler.savedConstantFps : false)
_resolutions = State(initialValue: ParsecResolution.resolutions)
_bitrates = State(initialValue: ParsecResolution.bitrates)
Expand Down Expand Up @@ -413,6 +413,7 @@ struct ParsecView: View {
CParsec.pause()
ParsecBackgroundManager.shared.isPaused = true
} else {
parsecViewController.restorePointerInput()
if ParsecBackgroundManager.shared.isReconnecting {
return
}
Expand Down Expand Up @@ -459,6 +460,15 @@ struct ParsecView: View {
parsecViewController.onKeyboardVisibilityChanged = { visible in
showKeyboard = visible
}
let menuVisibility = $showMenu
parsecViewController.onMenuEdgeSwipe = {
DispatchQueue.main.async {
menuVisibility.wrappedValue = true
let data = Data()
CParsec.sendUserData(type: .getVideoConfig, message: data)
CParsec.sendUserData(type: .getAdapterInfo, message: data)
}
}

parsecViewController.setKeyboardVisible(showKeyboard)
}
Expand Down Expand Up @@ -565,7 +575,7 @@ struct ParsecView: View {
DispatchQueue.main.async {
zoomEnabled.toggle()
parsecViewController.setZoomEnabled(zoomEnabled)
if SettingsHandler.saveSessionSettings { SettingsHandler.savedZoomEnabled = zoomEnabled }
SettingsHandler.pinchZoomEnabled = zoomEnabled
}
}

Expand Down
Loading