[fix] prevent iOS relative cursor jumps after movement

This commit is contained in:
dijunkun
2026-09-07 21:11:27 +08:00
parent 5d97d5a393
commit 2d3f1bb475
3 changed files with 21 additions and 15 deletions
@@ -58,6 +58,7 @@ struct RemoteTouchInputView: UIViewRepresentable {
let videoSize: CGSize let videoSize: CGSize
let controlMode: MouseControlMode let controlMode: MouseControlMode
let remoteCursorPosition: CGPoint? let remoteCursorPosition: CGPoint?
let remoteCursorPositionRevision: UInt64
let viewportScale: CGFloat let viewportScale: CGFloat
let viewportOffset: CGSize let viewportOffset: CGSize
let viewportChanged: (CGFloat, CGSize) -> Void let viewportChanged: (CGFloat, CGSize) -> Void
@@ -81,7 +82,8 @@ struct RemoteTouchInputView: UIViewRepresentable {
view.onLeftUp = leftUp view.onLeftUp = leftUp
view.onRightClick = rightClick view.onRightClick = rightClick
view.onScroll = scroll view.onScroll = scroll
view.synchronizeRemoteCursor(remoteCursorPosition) view.synchronizeRemoteCursor(remoteCursorPosition,
revision: remoteCursorPositionRevision)
} }
} }
@@ -108,6 +110,7 @@ final class RemoteTouchSurface: UIView, UIGestureRecognizerDelegate {
private var relativeCursorPoint: (Float, Float) = (0.5, 0.5) private var relativeCursorPoint: (Float, Float) = (0.5, 0.5)
private var lastRemoteCursorPoint: (Float, Float)? private var lastRemoteCursorPoint: (Float, Float)?
private var lastRemoteCursorRevision: UInt64?
private var relativePanActive = false private var relativePanActive = false
private var heldDragPoint: (Float, Float)? private var heldDragPoint: (Float, Float)?
private var heldDragLastLocation: CGPoint? private var heldDragLastLocation: CGPoint?
@@ -184,7 +187,12 @@ final class RemoteTouchSurface: UIView, UIGestureRecognizerDelegate {
viewportOffset = constrainedViewportOffset(offset, scale: nextScale) viewportOffset = constrainedViewportOffset(offset, scale: nextScale)
} }
func synchronizeRemoteCursor(_ position: CGPoint?) { func synchronizeRemoteCursor(_ position: CGPoint?, revision: UInt64) {
// SwiftUI refreshes this view for video frames and shape-only feedback
// too. Consume each position update once, even when it arrives during
// local movement, so a later refresh cannot restore an old coordinate.
guard revision != lastRemoteCursorRevision else { return }
lastRemoteCursorRevision = revision
guard let position else { guard let position else {
lastRemoteCursorPoint = nil lastRemoteCursorPoint = nil
return return
@@ -194,7 +202,11 @@ final class RemoteTouchSurface: UIView, UIGestureRecognizerDelegate {
Float(min(max(position.y, 0), 1)) Float(min(max(position.y, 0), 1))
) )
lastRemoteCursorPoint = point lastRemoteCursorPoint = point
applyRemoteCursorIfIdle() guard controlMode == .relative,
!relativePanActive,
heldDragPoint == nil,
hoverLastLocation == nil else { return }
relativeCursorPoint = point
} }
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
@@ -400,7 +412,6 @@ final class RemoteTouchSurface: UIView, UIGestureRecognizerDelegate {
} }
default: default:
relativePanActive = false relativePanActive = false
applyRemoteCursorIfIdle()
} }
} }
@@ -429,7 +440,6 @@ final class RemoteTouchSurface: UIView, UIGestureRecognizerDelegate {
} }
heldDragPoint = nil heldDragPoint = nil
heldDragLastLocation = nil heldDragLastLocation = nil
applyRemoteCursorIfIdle()
default: default:
break break
} }
@@ -457,14 +467,6 @@ final class RemoteTouchSurface: UIView, UIGestureRecognizerDelegate {
return relativeCursorPoint return relativeCursorPoint
} }
private func applyRemoteCursorIfIdle() {
guard controlMode == .relative,
!relativePanActive,
heldDragPoint == nil,
let point = lastRemoteCursorPoint else { return }
relativeCursorPoint = point
}
private func normalized(_ point: CGPoint) -> (Float, Float)? { private func normalized(_ point: CGPoint) -> (Float, Float)? {
guard let renderRect = renderRect(), renderRect.contains(point) else { return nil } guard let renderRect = renderRect(), renderRect.contains(point) else { return nil }
return (Float((point.x - renderRect.minX) / renderRect.width), return (Float((point.x - renderRect.minX) / renderRect.width),
@@ -381,6 +381,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
@Published private(set) var remoteCursorVisible = true @Published private(set) var remoteCursorVisible = true
@Published private(set) var remoteCursorShape = 0 @Published private(set) var remoteCursorShape = 0
@Published private(set) var remoteCursorPosition: CGPoint? @Published private(set) var remoteCursorPosition: CGPoint?
@Published private(set) var remoteCursorPositionRevision: UInt64 = 0
@Published private(set) var remoteCursorVisualOffset = CGPoint.zero @Published private(set) var remoteCursorVisualOffset = CGPoint.zero
@Published private(set) var hasRemoteCursorState = false @Published private(set) var hasRemoteCursorState = false
@Published private(set) var bitrate: UInt = 0 @Published private(set) var bitrate: UInt = 0
@@ -775,6 +776,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
remoteCursorVisible = true remoteCursorVisible = true
remoteCursorShape = 0 remoteCursorShape = 0
remoteCursorPosition = nil remoteCursorPosition = nil
remoteCursorPositionRevision &+= 1
remoteCursorVisualOffset = .zero remoteCursorVisualOffset = .zero
hasRemoteCursorState = false hasRemoteCursorState = false
remoteCursorSequence = nil remoteCursorSequence = nil
@@ -991,6 +993,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
} else { } else {
remoteCursorPosition = nil remoteCursorPosition = nil
} }
remoteCursorPositionRevision &+= 1
} }
hasRemoteCursorState = true hasRemoteCursorState = true
} }
@@ -79,6 +79,7 @@ struct RemoteSessionView: View {
videoSize: session.displayGeometrySize, videoSize: session.displayGeometrySize,
controlMode: session.mouseControlMode, controlMode: session.mouseControlMode,
remoteCursorPosition: session.remoteCursorPosition, remoteCursorPosition: session.remoteCursorPosition,
remoteCursorPositionRevision: session.remoteCursorPositionRevision,
viewportScale: viewport.scale, viewportScale: viewport.scale,
viewportOffset: viewport.offset, viewportOffset: viewport.offset,
viewportChanged: { scale, offset in viewportChanged: { scale, offset in
@@ -161,8 +162,8 @@ struct RemoteSessionView: View {
.onChange(of: session.mouseControlMode) { _ in .onChange(of: session.mouseControlMode) { _ in
cursorPosition = session.remoteCursorPosition cursorPosition = session.remoteCursorPosition
} }
.onChange(of: session.remoteCursorPosition) { position in .onChange(of: session.remoteCursorPositionRevision) { _ in
guard let position else { return } guard let position = session.remoteCursorPosition else { return }
cursorPosition = position cursorPosition = position
} }
.fileImporter(isPresented: $showingFileImporter, .fileImporter(isPresented: $showingFileImporter,