ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

OpenDisplay源码精读:InputInjector.swift如何用CGEvent把触摸变成macOS鼠标事件

OpenDisplay源码精读:InputInjector.swift如何用CGEvent把触摸变成macOS鼠标事件 OpenDisplay源码精读InputInjector.swift如何用CGEvent把触摸变成macOS鼠标事件【免费下载链接】opendisplayFree, open-source Sidecar/Duet alternative — use your iPhone, iPad or Mac as a true second monitor for your primary Mac over USB or WiFi. Low latency H.264, Retina HiDPI, touch input.项目地址: https://gitcode.com/GitHub_Trending/op/opendisplayOpenDisplay 是一款免费开源的 Sidecar 替代工具它把 iPhone、iPad 变成 Mac 真正的第二台显示器而用手指在手机上点一下Mac 上就出现鼠标点击这件事正是由一个 290 多行的 Swift 文件完成的——InputInjector.swift。本文带你读懂这段源码一个归一化坐标的 JSON 消息是如何一步步变成 macOS 鼠标事件的。一、先搞清楚触摸输入从哪来OpenDisplay 的工作方式很简单Mac 端创建一个虚拟显示器把桌面画面用 H.264 编码推送到手机上播放反过来手机上的触摸、滚动、手写笔事件通过 TCPWiFi或 usbmuxUSB 线传回 Mac。触摸消息的完整链路是这样的iPhone 端一个透明的全屏 View 捕获 UIKit 触摸把坐标归一化到 0~1按began/moved/ended/cancelled四种相位发出touch消息见 OpenSidecarPhoneApp.swiftMac 端MacSender.swift 收到 JSON 后解析出相位和坐标转交给InputInjector注入InputInjector用 CoreGraphics 的CGEventAPI 造出鼠标事件注入系统事件流——从此 macOS 窗口服务器把它当成一只真实的鼠标。协议层面touch消息的定义在 PROTOCOL.mdx、y是归一化坐标原点在左上角这就是整个注入过程要回答的第一个问题——二、第一步把归一化坐标换算成屏幕坐标手机发来的坐标是 0~1 的浮点数和屏幕毫无关系。handleTouchInputInjector.swift#L75-L114的第一件事就是查目标虚拟显示器的边界用CGDisplayBounds(displayID)拿到显示器在全局坐标系中的位置CG 的 y 轴向下和手机恰好一致不用翻转然后做简单的比例换算point.x origin.x x × widthpoint.y origin.y y × height。这一步保证了无论第二屏被摆放到桌面的哪个位置、是竖屏还是横屏手指落在手机屏幕的哪个比例处Mac 上的光标就出现在虚拟显示器的同一比例处。三、核心映射四种触摸相位 → 四类鼠标事件CGEvent提供了一组鼠标事件类型handleTouch用一段switch完成了关键的语义映射触摸相位CGEvent 事件鼠标语义began手指落下.leftMouseDown左键按下moved有按压.leftMouseDragged拖拽moved无按压.mouseMoved光标移动ended手指抬起.leftMouseUp左键松开cancelled手势被取消.leftMouseUp clickCount0松开但不算点击注意一个精巧的细节moved并不是只映射成一种事件。类里维护了一个isDown布尔量——手指已经落下时移动就是拖拽.leftMouseDragged手指没落下的移动只移动光标.mouseMoved。这正是手机能同时当触摸板点按拖窗口和触屏轻点选择的原因。事件造好后通过event.post(tap: .cghidEventTap)投递到 HID 事件层——这是系统最低的事件注入点所以注入的点击对任何 App 都像真实鼠标一样有效。四、源码里的三个坑都写在注释里这段代码最值得读的不是主流程而是注释里记录的三个踩坑现场。它们解释了为什么非得这么写。1. 为什么clickState必须非零macOS 的菜单追踪对合成事件非常挑剔。源码注释L30-L34写道如果事件的 clickState 是 0、且没有真实事件源菜单能打开但追踪会话会坏掉留下僵尸菜单——画在屏幕流里看得见却点不动。所以InputInjector特意持有两个成员一个真实的CGEventSource状态为hidSystemState按下/松开时 clickState 一律设为 1。2. 为什么cancelled的 clickCount 要设 0两指滚动手势开始时第一根手指的按下其实已经发出。如果第二指加入后只是普通地抬起在按下点上的 up 事件和一个点击完全无法区分——结果就是每次两指滚动都会点开第一根手指下面的东西。CGEvent 没有撤销按下这种事件所以解法是cancelled也发leftMouseUp但把 clickCount 置 0。按钮状态保持诚实该松开就松开同时明确告诉 AppKit 和 WebKit别把这合成点击。3. 双指手势的判定其实在手机端第一指落下时到底是想点击还是想滚动在 Mac 端无法判断。所以 iOS 端OpenSidecarPhoneApp.swift#L790-L893会先扣住began事件 120 毫秒期间移动超过 10pt 就认定为拖拽并提交第二指加入就发cancelled什么都没发生再抬手则补发完整点击。Mac 端的cancelled → clickCount 0机制就是这套握手协议的接收端。五、滚动与手写笔另两条事件通路同一个类还负责两种输入思路完全一致——把手机语义翻译成 CGEvent两指滚动handleScrollL118-L127手机发来的是视频像素单位的位移而 macOS 滚动事件按点计数所以用CGDisplayPixelsWide / bounds.width算出缩放系数换算后再发.scrollWheelEvent2天然滚动的方向符号原样保留用起来和触控板一致Apple PencilhandlePencil走的是tablet 事件而非鼠标事件——先补发tabletProximity进入事件否则 App 不知道有笔再发带压力值pressure × 65535、倾斜角的手写点事件。倾斜角还专门做了坐标变换UIKit 给的是弧度竖直为 π/2CGEvent 要的是 -1~1 的单位向量直接透传会导致 App 报出不可能的角度。由于 tablet 事件拿不到窗口服务器的点击计数代码里还自己实现了一套PenClickSession双击会话连阈值都通过dlopen动态调用 AppKit 的NSDoubleClickDistance()获取——双击判定完全跟随系统偏好设置。六、上手前必做授权辅助功能这一切的前提是 macOS 允许该 App 注入系统事件。InputInjector.ensureAccessibilityPermission()L65-L72在启动时调用AXIsProcessTrustedWithOptions未授权时会直接弹出系统授权对话框并在日志里记录。这也是很多用户反馈触摸没反应时第一个要检查的地方系统设置 → 隐私与安全性 → 辅助功能里要勾选 OpenDisplay。小结290 行代码里的工程智慧回看 InputInjector.swift 的完整设计它的价值不在会调 CGEvent而在对 macOS 事件系统的细节拿捏归一化坐标 显示边界做坐标换算任何摆放/旋转方式都正确一个isDown状态区分点击、拖拽、光标移动三种语义clickState 的 1/0 选择分别解决了菜单追踪崩溃和两指滚动误点击两个真实 bugtablet 事件通路让手写笔的压力、倾斜、双击都以系统级方式工作。配合 Mac 端的消息分发MacSender.swift和 iPhone 端的触摸捕获OpenSidecarPhoneApp.swift输入注入只是 OpenDisplay 四大核心模块之一其余三个分别是虚拟显示器创建、低延迟 H.264 管线和 USB 传输——想继续深读可以从 README.md 的 How it works 一节入手。【免费下载链接】opendisplayFree, open-source Sidecar/Duet alternative — use your iPhone, iPad or Mac as a true second monitor for your primary Mac over USB or WiFi. Low latency H.264, Retina HiDPI, touch input.项目地址: https://gitcode.com/GitHub_Trending/op/opendisplay创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进