Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ struct InstallProgressStepView: View {
VirtualBuddyMonoProgressView(progress: progress, status: status, style: style)
.textSelection(.enabled)
} else if let virtualMachine = viewModel.virtualMachine {
SwiftUIVMView(controllerState: .constant(.running(virtualMachine)), captureSystemKeys: false, isDFUModeVM: false, automaticallyReconfiguresDisplay: .constant(false))
.virtualMachineInteractionDisabled()
SwiftUIVMView(controllerState: .constant(.running(virtualMachine)), captureSystemKeysEnabled: false, isDFUModeVM: false, automaticallyReconfiguresDisplay: .constant(false))
.virtualMachineEventDeliveryMask(.none)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
VirtualBuddyMonoProgressView(progress: progress, status: Text(""), style: style)
Expand Down
220 changes: 170 additions & 50 deletions VirtualUI/Source/Session/Components/SwiftUIVMView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,38 @@

import SwiftUI
import Cocoa
import BuddyUI
import Virtualization
import VirtualCore

/// Controls the input events that are delivered to the virtual machine.
struct VMEventDeliveryMask: OptionSet, Sendable, CustomStringConvertible {
let rawValue: UInt32

static let keyboard = VMEventDeliveryMask(rawValue: 1 << 0)
static let mouse = VMEventDeliveryMask(rawValue: 1 << 1)

static let all: VMEventDeliveryMask = [.keyboard, .mouse]
static let none: VMEventDeliveryMask = []

var description: String {
guard !isEmpty else { return "<empty>" }

var elements = [String]()
if contains(.mouse) { elements.append("mouse") }
if contains(.keyboard) { elements.append("keyboard") }
return elements.joined(separator: "|")
}
}


private extension EnvironmentValues {
@Entry var virtualMachineInteractionDisabled = false
@Entry var virtualMachineEventDeliveryMask = VMEventDeliveryMask.all
}

extension View {
func virtualMachineInteractionDisabled(_ disabled: Bool = true) -> some View {
environment(\.virtualMachineInteractionDisabled, disabled)
func virtualMachineEventDeliveryMask(_ mask: VMEventDeliveryMask) -> some View {
environment(\.virtualMachineEventDeliveryMask, mask)
}
}

Expand All @@ -25,7 +47,7 @@ struct SwiftUIVMView: NSViewControllerRepresentable {
typealias NSViewControllerType = VMViewController

@Binding var controllerState: VMController.State
let captureSystemKeys: Bool
let captureSystemKeysEnabled: Bool
var isDFUModeVM: Bool
var vmECID: UInt64?
@Binding var automaticallyReconfiguresDisplay: Bool
Expand All @@ -34,7 +56,7 @@ struct SwiftUIVMView: NSViewControllerRepresentable {
let controller = VMViewController()
controller.vmECID = vmECID
controller.isDFUModeVM = isDFUModeVM
controller.captureSystemKeys = captureSystemKeys
controller.captureSystemKeysEnabled = captureSystemKeysEnabled
controller.automaticallyReconfiguresDisplay = automaticallyReconfiguresDisplay
return controller
}
Expand All @@ -44,15 +66,15 @@ struct SwiftUIVMView: NSViewControllerRepresentable {

nsViewController.vmECID = vmECID
nsViewController.isDFUModeVM = isDFUModeVM
nsViewController.interactionDisabled = context.environment.virtualMachineInteractionDisabled
nsViewController.eventDeliveryMask = context.environment.virtualMachineEventDeliveryMask

if case .running(let vm) = controllerState {
nsViewController.virtualMachine = vm
} else {
nsViewController.virtualMachine = nil
}
}

}

final class VMViewController: NSViewController {
Expand All @@ -74,13 +96,6 @@ final class VMViewController: NSViewController {
}
}

var captureSystemKeys: Bool = false {
didSet {
guard captureSystemKeys != oldValue, isViewLoaded else { return }
vmView.capturesSystemKeys = captureSystemKeys
}
}

var automaticallyReconfiguresDisplay: Bool = true {
didSet {
guard #available(macOS 14.0, *) else { return }
Expand All @@ -102,9 +117,14 @@ final class VMViewController: NSViewController {
#endif
}

var interactionDisabled: Bool {
get { vmView.isViewOnly }
set { vmView.isViewOnly = newValue }
var captureSystemKeysEnabled: Bool {
get { vmView.capturesSystemKeysEnabled }
set { vmView.capturesSystemKeysEnabled = newValue }
}

var eventDeliveryMask: VMEventDeliveryMask {
get { vmView.eventDeliveryMask }
set { vmView.eventDeliveryMask = newValue }
}

private lazy var vmView: VirtualBuddyVMView = {
Expand All @@ -116,8 +136,6 @@ final class VMViewController: NSViewController {
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.black.cgColor

vmView.capturesSystemKeys = captureSystemKeys

if #available(macOS 14.0, *) {
vmView.automaticallyReconfiguresDisplay = automaticallyReconfiguresDisplay
}
Expand Down Expand Up @@ -231,70 +249,172 @@ struct DFUStatusView: View {
}
}

extension VMEventDeliveryMask {
var allowsMouseEvents: Bool { contains(.mouse) }
var allowsKeyboardEvents: Bool { contains(.keyboard) }
}

final class VirtualBuddyVMView: VZVirtualMachineView {
var isViewOnly = false
var eventDeliveryMask = VMEventDeliveryMask.all {
didSet {
guard eventDeliveryMask != oldValue else { return }

override func hitTest(_ point: NSPoint) -> NSView? {
guard !isViewOnly else { return nil }
return super.hitTest(point)
if oldValue.allowsKeyboardEvents != eventDeliveryMask.allowsKeyboardEvents {
if eventDeliveryMask.allowsKeyboardEvents {
window?.makeFirstResponder(self)
} else {
window?.makeFirstResponder(nextResponder)
}

updateCaptureSystemKeysState()
}

if oldValue.allowsMouseEvents != eventDeliveryMask.allowsMouseEvents {
window?.invalidateCursorRects(for: self)
}
}
}

override func isMousePoint(_ point: NSPoint, in rect: NSRect) -> Bool {
guard !isViewOnly else { return false }
return super.isMousePoint(point, in: rect)
var capturesSystemKeysEnabled: Bool = false {
didSet {
guard capturesSystemKeysEnabled != oldValue else { return }
updateCaptureSystemKeysState()
}
}

private func updateCaptureSystemKeysState() {
/// We only want to enable capture system keys when the explicit setting is enabled and when keyboard event capture is also enabled.
capturesSystemKeys = capturesSystemKeysEnabled && eventDeliveryMask.contains(.keyboard)

UILog("eventDeliveryMask = \(eventDeliveryMask); capturesSystemKeysEnabled = \(capturesSystemKeysEnabled); capturesSystemKeys = \(capturesSystemKeys)")
}

override var acceptsFirstResponder: Bool {
guard eventDeliveryMask.allowsKeyboardEvents else { return false }
return super.acceptsFirstResponder
}

override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
guard !isViewOnly else { return false }
guard eventDeliveryMask.allowsMouseEvents else { return false }
return super.acceptsFirstMouse(for: event)
}

override func hitTest(_ point: NSPoint) -> NSView? {
guard eventDeliveryMask.allowsMouseEvents else { return nil }
return super.hitTest(point)
}

override func isMousePoint(_ point: NSPoint, in rect: NSRect) -> Bool {
guard eventDeliveryMask.allowsMouseEvents else { return false }
return super.isMousePoint(point, in: rect)
}

override func mouseDown(with event: NSEvent) {
guard !isViewOnly else { return }
guard eventDeliveryMask.allowsMouseEvents else { return }
super.mouseDown(with: event)
}

override var acceptsFirstResponder: Bool {
guard !isViewOnly else { return false }
return super.acceptsFirstResponder
override func mouseDragged(with event: NSEvent) {
guard eventDeliveryMask.allowsMouseEvents else { return }
super.mouseDragged(with: event)
}

override func updateTrackingAreas() {
guard !isViewOnly else { return }
super.updateTrackingAreas()
override func mouseEntered(with event: NSEvent) {
guard eventDeliveryMask.allowsMouseEvents else { return }
super.mouseEntered(with: event)
}

override func cursorUpdate(with event: NSEvent) {
guard !isViewOnly else { return }
super.cursorUpdate(with: event)
override func mouseExited(with event: NSEvent) {
guard eventDeliveryMask.allowsMouseEvents else { return }
super.mouseExited(with: event)
}

override func mouseMoved(with event: NSEvent) {
guard eventDeliveryMask.allowsMouseEvents else { return }
super.mouseMoved(with: event)
}

override func mouseUp(with event: NSEvent) {
guard eventDeliveryMask.allowsMouseEvents else { return }
super.mouseUp(with: event)
}

override func rightMouseDown(with event: NSEvent) {
guard eventDeliveryMask.allowsMouseEvents else { return }
super.rightMouseDown(with: event)
}

override func rightMouseDragged(with event: NSEvent) {
guard eventDeliveryMask.allowsMouseEvents else { return }
super.rightMouseDragged(with: event)
}

override func rightMouseUp(with event: NSEvent) {
guard eventDeliveryMask.allowsMouseEvents else { return }
super.rightMouseUp(with: event)
}

override func otherMouseDown(with event: NSEvent) {
guard eventDeliveryMask.allowsMouseEvents else { return }
super.otherMouseDown(with: event)
}

override func resetCursorRects() {
guard !isViewOnly else { return }
super.resetCursorRects()
override func otherMouseDragged(with event: NSEvent) {
guard eventDeliveryMask.allowsMouseEvents else { return }
super.otherMouseDragged(with: event)
}

override func otherMouseUp(with event: NSEvent) {
guard eventDeliveryMask.allowsMouseEvents else { return }
super.otherMouseUp(with: event)
}

override func scrollWheel(with event: NSEvent) {
guard eventDeliveryMask.allowsMouseEvents else { return }
super.scrollWheel(with: event)
}

override func magnify(with event: NSEvent) {
guard eventDeliveryMask.allowsMouseEvents else { return }
super.magnify(with: event)
}

override func smartMagnify(with event: NSEvent) {
guard eventDeliveryMask.allowsMouseEvents else { return }
super.smartMagnify(with: event)
}

override func rotate(with event: NSEvent) {
guard eventDeliveryMask.allowsMouseEvents else { return }
super.rotate(with: event)
}

override func cursorUpdate(with event: NSEvent) {
guard eventDeliveryMask.allowsMouseEvents else { return }
super.cursorUpdate(with: event)
}

override func discardCursorRects() {
guard !isViewOnly else { return }
super.discardCursorRects()
override func keyDown(with event: NSEvent) {
guard eventDeliveryMask.allowsKeyboardEvents else { return }
super.keyDown(with: event)
}

override func addCursorRect(_ rect: NSRect, cursor object: NSCursor) {
guard !isViewOnly else { return }
super.addCursorRect(rect, cursor: object)
override func keyUp(with event: NSEvent) {
guard eventDeliveryMask.allowsKeyboardEvents else { return }
super.keyUp(with: event)
}

override func removeCursorRect(_ rect: NSRect, cursor object: NSCursor) {
guard !isViewOnly else { return }
super.removeCursorRect(rect, cursor: object)
override func flagsChanged(with event: NSEvent) {
guard eventDeliveryMask.allowsKeyboardEvents else { return }
super.flagsChanged(with: event)
}
}

#if DEBUG
#Preview("VM View - DFU") {
SwiftUIVMView(
controllerState: .constant(.starting(nil)),
captureSystemKeys: false,
captureSystemKeysEnabled: false,
isDFUModeVM: true,
vmECID: 7788022887768653863,
automaticallyReconfiguresDisplay: .constant(false)
Expand Down
Loading