Files
luma/Luma/VirtualMachines/VirtualMachinePointerCapture.swift
Ole André Vadla Ravnås 0e933206fb Boot virtual machines from the target picker
Add a machine abstraction to LumaCore, with QEMU as its first
backend: templates mirroring frida-core's test guests, a QMP client,
and a display that maps QEMU's surface over D-Bus rather than
copying frames.

Leave readiness to the user: drive the guest, mark it ready, and come
back to that snapshot later. Keep the machine in the project, so
booting it again resumes where it was marked. Hand the stub to
addBareboneDevice, so it joins the picker beside the other devices.

Show the display while booting, and afterwards in a Machines panel,
which the side panel now models as an enum to share its slot.
2026-08-20 14:52:52 +02:00

238 lines
7.9 KiB
Swift

import LumaCore
import SwiftUI
#if canImport(AppKit)
import AppKit
/// A guest shown in a panel waits to be clicked before it takes the pointer;
/// one filling the screen has already been asked for.
enum PointerCapturePolicy {
case onClick
case onAppear
}
/// The guest's pointer, driven by the real thing: where it is, which button
/// went down, and how far the wheel turned.
struct VirtualMachinePointerCapture: PlatformViewRepresentable {
let placement: CGRect
let guestSize: CGSize?
let isAbsolute: Bool
let capturing: PointerCapturePolicy
let send: (VirtualMachineInputEvent) -> Void
let captureChanged: (Bool) -> Void
let hoverChanged: (Bool) -> Void
func makeNSView(context: Context) -> PointerView {
PointerView()
}
func updateNSView(_ view: PointerView, context: Context) {
view.placement = placement
view.guestSize = guestSize
view.isAbsolute = isAbsolute
view.capturing = capturing
view.send = send
view.captureChanged = captureChanged
view.hoverChanged = hoverChanged
}
static func dismantleNSView(_ view: PointerView, coordinator: ()) {
view.releasePointer()
}
final class PointerView: NSView {
var placement: CGRect = .zero
var guestSize: CGSize?
var isAbsolute = true
var capturing: PointerCapturePolicy = .onClick
var send: ((VirtualMachineInputEvent) -> Void)?
var captureChanged: ((Bool) -> Void)?
var hoverChanged: ((Bool) -> Void)?
private var pointer: PointerMode = .free
private var releaseChord: Any?
private var windowResignation: Any?
private var tracking: NSTrackingArea?
/// A guest left with the PC's own mouse only hears how far the pointer
/// moved, so the host's has to stop moving: it is parked and hidden
/// while the guest's is being driven, leaving one pointer on screen
/// instead of two drifting apart.
private enum PointerMode {
case free
case captured(returningTo: CGPoint)
}
override var acceptsFirstResponder: Bool { true }
override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
releasePointer()
if let windowResignation {
NotificationCenter.default.removeObserver(windowResignation)
self.windowResignation = nil
}
guard let window else { return }
if capturing == .onAppear, !isAbsolute {
capturePointer()
}
windowResignation = NotificationCenter.default.addObserver(
forName: NSWindow.didResignKeyNotification,
object: window,
queue: .main
) { [weak self] _ in
MainActor.assumeIsolated { self?.releasePointer() }
}
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
if let tracking {
removeTrackingArea(tracking)
}
let area = NSTrackingArea(
rect: bounds,
options: [.activeInKeyWindow, .mouseMoved, .mouseEnteredAndExited, .inVisibleRect],
owner: self
)
addTrackingArea(area)
tracking = area
}
override func mouseEntered(with event: NSEvent) {
hoverChanged?(true)
}
override func mouseExited(with event: NSEvent) {
hoverChanged?(false)
}
override func mouseMoved(with event: NSEvent) {
reportMove(event)
}
override func mouseDragged(with event: NSEvent) {
reportMove(event)
}
override func rightMouseDragged(with event: NSEvent) {
reportMove(event)
}
override func otherMouseDragged(with event: NSEvent) {
reportMove(event)
}
override func mouseDown(with event: NSEvent) {
if !isAbsolute, case .free = pointer {
capturePointer()
return
}
reportMove(event)
send?(.pointerButtonDown(button: .left))
}
override func mouseUp(with event: NSEvent) {
reportMove(event)
send?(.pointerButtonUp(button: .left))
}
override func rightMouseDown(with event: NSEvent) {
reportMove(event)
send?(.pointerButtonDown(button: .right))
}
override func rightMouseUp(with event: NSEvent) {
reportMove(event)
send?(.pointerButtonUp(button: .right))
}
override func otherMouseDown(with event: NSEvent) {
reportMove(event)
send?(.pointerButtonDown(button: .middle))
}
override func otherMouseUp(with event: NSEvent) {
reportMove(event)
send?(.pointerButtonUp(button: .middle))
}
override func scrollWheel(with event: NSEvent) {
guard event.scrollingDeltaY != 0 || event.scrollingDeltaX != 0 else { return }
send?(.scrolled(deltaX: event.scrollingDeltaX, deltaY: event.scrollingDeltaY))
}
private func capturePointer() {
pointer = .captured(returningTo: NSEvent.mouseLocation)
CGAssociateMouseAndMouseCursorPosition(0)
NSCursor.hide()
releaseChord = NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
if event.modifierFlags.isSuperset(of: [.control, .option]) {
MainActor.assumeIsolated { self?.releasePointer() }
}
return event
}
captureChanged?(true)
}
func releasePointer() {
guard case .captured(let origin) = pointer else { return }
pointer = .free
CGAssociateMouseAndMouseCursorPosition(1)
CGWarpMouseCursorPosition(CGPoint(x: origin.x, y: NSScreen.screens[0].frame.height - origin.y))
NSCursor.unhide()
if let releaseChord {
NSEvent.removeMonitor(releaseChord)
self.releaseChord = nil
}
captureChanged?(false)
}
/// A guest with a tablet is told where the pointer is; one left with
/// the PC's own mouse is told how far it moved, in its own pixels
/// rather than the points the screen happens to be drawn at.
private func reportMove(_ event: NSEvent) {
guard isAbsolute else {
guard case .captured = pointer else { return }
let scale = guestScale
send?(.pointerMovedBy(dx: event.deltaX * scale.width, dy: event.deltaY * scale.height))
return
}
guard let position = guestPosition(of: convert(event.locationInWindow, from: nil)) else { return }
send?(.pointerMoved(x: position.x, y: position.y))
}
private var guestScale: CGSize {
guard let guestSize, placement.width > 0, placement.height > 0 else { return CGSize(width: 1, height: 1) }
return CGSize(width: guestSize.width / placement.width, height: guestSize.height / placement.height)
}
/// The view's own coordinates run from the bottom left, the guest's
/// from the top left of where its frame was drawn.
private func guestPosition(of point: CGPoint) -> CGPoint? {
guard let guestSize, placement.width > 0, placement.height > 0 else { return nil }
let x = (point.x - placement.minX) / placement.width * guestSize.width
let y = (bounds.height - point.y - placement.minY) / placement.height * guestSize.height
guard x >= 0, y >= 0, x < guestSize.width, y < guestSize.height else { return nil }
return CGPoint(x: x, y: y)
}
}
}
#endif