From e4545f186a8c8cec4bb7d8899247909cb3080ff6 Mon Sep 17 00:00:00 2001 From: Guilherme Rambo Date: Fri, 28 Aug 2026 10:06:20 -0300 Subject: [PATCH 1/3] Add toggles to enable/disable mouse and keyboard input to virtual machine --- .../Steps/InstallProgressStepView.swift | 4 +- .../Session/Components/SwiftUIVMView.swift | 197 ++++++++++++++---- ...wift => VirtualMachineStateControls.swift} | 88 +------- .../VirtualMachineUserControls.swift | 51 +++++ .../Session/VirtualMachineSessionUI.swift | 25 ++- .../Session/VirtualMachineSessionView.swift | 16 +- 6 files changed, 252 insertions(+), 129 deletions(-) rename VirtualUI/Source/Session/Components/{VirtualMachineControls.swift => VirtualMachineStateControls.swift} (69%) create mode 100644 VirtualUI/Source/Session/Components/VirtualMachineUserControls.swift diff --git a/VirtualUI/Source/Installer/Steps/InstallProgressStepView.swift b/VirtualUI/Source/Installer/Steps/InstallProgressStepView.swift index 50017c62..614c8580 100644 --- a/VirtualUI/Source/Installer/Steps/InstallProgressStepView.swift +++ b/VirtualUI/Source/Installer/Steps/InstallProgressStepView.swift @@ -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) diff --git a/VirtualUI/Source/Session/Components/SwiftUIVMView.swift b/VirtualUI/Source/Session/Components/SwiftUIVMView.swift index 41c8987f..b11f581a 100644 --- a/VirtualUI/Source/Session/Components/SwiftUIVMView.swift +++ b/VirtualUI/Source/Session/Components/SwiftUIVMView.swift @@ -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 "" } + + 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) } } @@ -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 @@ -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 } @@ -44,7 +66,7 @@ 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 @@ -52,7 +74,7 @@ struct SwiftUIVMView: NSViewControllerRepresentable { nsViewController.virtualMachine = nil } } - + } final class VMViewController: NSViewController { @@ -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 } @@ -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 = { @@ -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 } @@ -231,70 +249,173 @@ 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() + } + } } - 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 mouseEntered(with event: NSEvent) { + guard eventDeliveryMask.allowsMouseEvents else { return } + super.mouseEntered(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 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 updateTrackingAreas() { - guard !isViewOnly else { return } + guard eventDeliveryMask.allowsMouseEvents else { return } super.updateTrackingAreas() } override func cursorUpdate(with event: NSEvent) { - guard !isViewOnly else { return } + guard eventDeliveryMask.allowsMouseEvents else { return } super.cursorUpdate(with: event) } override func resetCursorRects() { - guard !isViewOnly else { return } + guard eventDeliveryMask.allowsMouseEvents else { return } super.resetCursorRects() } override func discardCursorRects() { - guard !isViewOnly else { return } + guard eventDeliveryMask.allowsMouseEvents else { return } super.discardCursorRects() } override func addCursorRect(_ rect: NSRect, cursor object: NSCursor) { - guard !isViewOnly else { return } + guard eventDeliveryMask.allowsMouseEvents else { return } super.addCursorRect(rect, cursor: object) } override func removeCursorRect(_ rect: NSRect, cursor object: NSCursor) { - guard !isViewOnly else { return } + guard eventDeliveryMask.allowsMouseEvents else { return } super.removeCursorRect(rect, cursor: object) } + + override func keyDown(with event: NSEvent) { + guard eventDeliveryMask.allowsKeyboardEvents else { return } + super.keyDown(with: event) + } + + override func keyUp(with event: NSEvent) { + guard eventDeliveryMask.allowsKeyboardEvents else { return } + super.keyUp(with: event) + } + + 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) diff --git a/VirtualUI/Source/Session/Components/VirtualMachineControls.swift b/VirtualUI/Source/Session/Components/VirtualMachineStateControls.swift similarity index 69% rename from VirtualUI/Source/Session/Components/VirtualMachineControls.swift rename to VirtualUI/Source/Session/Components/VirtualMachineStateControls.swift index 607ce3ee..8bc324ac 100644 --- a/VirtualUI/Source/Session/Components/VirtualMachineControls.swift +++ b/VirtualUI/Source/Session/Components/VirtualMachineStateControls.swift @@ -1,5 +1,5 @@ // -// VirtualMachineControls.swift +// VirtualMachineStateControls.swift // VirtualUI // // Created by Guilherme Rambo on 24/10/23. @@ -8,26 +8,8 @@ import SwiftUI import VirtualCore -@MainActor -protocol VirtualMachineStateController: ObservableObject { - var state: VMState { get } - - func start() async throws - func stop() async throws - func pause() async throws - func resume() async throws - - @available(macOS 14.0, *) - func saveState(snapshotName: String) async throws - - var virtualMachineModel: VBVirtualMachine { get } -} - -extension VMController: VirtualMachineStateController { } - -@available(macOS 14.0, *) -struct VirtualMachineControls: View { - @EnvironmentObject private var controller: Controller +struct VirtualMachineStateControls: View { + @EnvironmentObject private var controller: VMController @State private var actionTask: Task? @State private var isPopoverPresented = false @@ -50,7 +32,7 @@ struct VirtualMachineControls: View { } .disabled(controller.state.isSavingState || controller.state.isRestoringState || controller.state.isResizingDisk) case .starting, .running: - if #available(macOS 14.0, *), controller.virtualMachineModel.supportsStateRestoration { + if controller.virtualMachineModel.supportsStateRestoration { Button { /** Ability to save new states has been temporarily disabled in version 2 due to issues with its implementation. @@ -158,65 +140,3 @@ private extension DateFormatter { return f }() } - - -#if DEBUG -private final class PreviewVirtualMachineStateController: VirtualMachineStateController { - @MainActor - @Published var state: VMState = .idle - - @Published var virtualMachineModel = VBVirtualMachine.preview - - @MainActor - func start() async throws { - state = .starting(nil) - - try await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC) - - state = .running(.preview) - } - - @MainActor - func stop() async throws { - try await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC) - - state = .stopped(nil) - } - - @MainActor - func pause() async throws { - state = .paused(.preview) - } - - @MainActor - func resume() async throws { - state = .running(.preview) - } - - @available(macOS 14.0, *) - @MainActor - func saveState(snapshotName: String) async throws { - state = .paused(.preview) - } - -} - -@available(macOS 14.0, *) -private struct _VirtualMachineControlsPreview: View { - var body: some View { - Text("Preview") - .frame(minWidth: 200, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity) - .toolbar { - ToolbarItemGroup(placement: .primaryAction) { - VirtualMachineControls() - .environmentObject(PreviewVirtualMachineStateController()) - } - } - } -} - -@available(macOS 14.0, *) -#Preview { - _VirtualMachineControlsPreview() -} -#endif diff --git a/VirtualUI/Source/Session/Components/VirtualMachineUserControls.swift b/VirtualUI/Source/Session/Components/VirtualMachineUserControls.swift new file mode 100644 index 00000000..b9885f94 --- /dev/null +++ b/VirtualUI/Source/Session/Components/VirtualMachineUserControls.swift @@ -0,0 +1,51 @@ +import SwiftUI +import VirtualCore + +struct VirtualMachineUserControls: View { + @EnvironmentObject private var ui: VirtualMachineSessionUI + + var body: some View { + Group { + Toggle(isOn: $ui.captureKeyboardEvents) { + Label("Capture keyboard", systemImage: ui.virtualMachine.keyboardDeviceSFSymbol) + .labelStyle(.iconOnly) + } + .help(ui.captureKeyboardEvents ? "Click to disconnect keyboard from virtual machine" : "Click to connect keyboard to virtual machine") + + Toggle(isOn: $ui.captureMouseEvents) { + Label("Capture mouse", systemImage: ui.virtualMachine.pointingDeviceSFSymbol) + .labelStyle(.iconOnly) + } + .help(ui.captureMouseEvents ? "Click to disconnect \(ui.virtualMachine.pointingDeviceName) from virtual machine" : "Click to connect \(ui.virtualMachine.pointingDeviceName) to virtual machine") + } + } +} + +// MARK: - Helpers + +extension VBVirtualMachine { + var pointingDeviceName: String { configuration.pointingDeviceName } + var pointingDeviceSFSymbol: String { configuration.pointingDeviceSFSymbol } + var keyboardDeviceSFSymbol: String { configuration.keyboardDeviceSFSymbol } +} + +extension VBMacConfiguration { + var pointingDeviceName: String { hardware.pointingDevice.kind.name.lowercased() } + + var pointingDeviceSFSymbol: String { + switch systemType { + case .mac: + switch hardware.pointingDevice.kind { + case .mouse: "magicmouse" + case .trackpad: "rectangle.and.hand.point.up.left.filled" + } + case .linux: + switch hardware.pointingDevice.kind { + case .mouse: "computermouse" + case .trackpad: "rectangle.and.hand.point.up.left.filled" + } + } + } + + var keyboardDeviceSFSymbol: String { "keyboard" } +} diff --git a/VirtualUI/Source/Session/VirtualMachineSessionUI.swift b/VirtualUI/Source/Session/VirtualMachineSessionUI.swift index 2c286c4f..edc6f806 100644 --- a/VirtualUI/Source/Session/VirtualMachineSessionUI.swift +++ b/VirtualUI/Source/Session/VirtualMachineSessionUI.swift @@ -13,6 +13,11 @@ public final class VirtualMachineSessionUI: ObservableObject { @Published public var lockProportions = false + @Published private(set) var eventDeliveryMask = VMEventDeliveryMask.all + + @Published var captureMouseEvents = true + @Published var captureKeyboardEvents = true + let setWindowAspectRatio = PassthroughSubject() let resizeWindow = PassthroughSubject() let makeWindowKey = PassthroughSubject() @@ -20,7 +25,7 @@ public final class VirtualMachineSessionUI: ObservableObject { public let controller: VMController public let virtualMachine: VBVirtualMachine - private lazy var cancellables = Set() + private var cancellables = Set() @MainActor public convenience init(with virtualMachine: VBVirtualMachine, library: VMLibraryController, options: VMSessionOptions?) { @@ -45,6 +50,24 @@ public final class VirtualMachineSessionUI: ObservableObject { self.setWindowAspectRatio.send(ratio) } .store(in: &cancellables) + + $captureMouseEvents.sink { [weak self] captureMouse in + if captureMouse { + self?.eventDeliveryMask.insert(.mouse) + } else { + self?.eventDeliveryMask.remove(.mouse) + } + } + .store(in: &cancellables) + + $captureKeyboardEvents.sink { [weak self] captureKeyboard in + if captureKeyboard { + self?.eventDeliveryMask.insert(.keyboard) + } else { + self?.eventDeliveryMask.remove(.keyboard) + } + } + .store(in: &cancellables) } @MainActor diff --git a/VirtualUI/Source/Session/VirtualMachineSessionView.swift b/VirtualUI/Source/Session/VirtualMachineSessionView.swift index c13f2a97..744fb2e4 100644 --- a/VirtualUI/Source/Session/VirtualMachineSessionView.swift +++ b/VirtualUI/Source/Session/VirtualMachineSessionView.swift @@ -74,9 +74,16 @@ public struct VirtualMachineSessionView: View { } } .toolbar { - if #available(macOS 14.0, *) { - VirtualMachineControls() - .environmentObject(controller) + ToolbarItemGroup(placement: .primaryAction) { + VirtualMachineUserControls() + } + + if #available(macOS 26, *) { + ToolbarSpacer(.fixed) + } + + ToolbarItemGroup(placement: .primaryAction) { + VirtualMachineStateControls() } } } @@ -123,11 +130,12 @@ public struct VirtualMachineSessionView: View { private func vmView(with vm: VZVirtualMachine) -> some View { SwiftUIVMView( controllerState: .constant(.running(vm)), - captureSystemKeys: controller.virtualMachineModel.configuration.captureSystemKeys, + captureSystemKeysEnabled: controller.virtualMachineModel.configuration.captureSystemKeys, isDFUModeVM: controller.options.bootInDFUMode, vmECID: controller.virtualMachineModel.ECID, automaticallyReconfiguresDisplay: .constant(controller.virtualMachineModel.configuration.hardware.displayDevices.count > 0 ? controller.virtualMachineModel.configuration.hardware.displayDevices[0].automaticallyReconfiguresDisplay : false) ) + .virtualMachineEventDeliveryMask(ui.eventDeliveryMask) } @ViewBuilder From cff168a8242e520a418721a44b6045b9763aa66c Mon Sep 17 00:00:00 2001 From: Guilherme Rambo Date: Fri, 28 Aug 2026 10:45:13 -0300 Subject: [PATCH 2/3] Filter additional mouse events, allow cursor updates even when mouse is disabled --- .../Session/Components/SwiftUIVMView.swift | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/VirtualUI/Source/Session/Components/SwiftUIVMView.swift b/VirtualUI/Source/Session/Components/SwiftUIVMView.swift index b11f581a..12afedbc 100644 --- a/VirtualUI/Source/Session/Components/SwiftUIVMView.swift +++ b/VirtualUI/Source/Session/Components/SwiftUIVMView.swift @@ -268,6 +268,10 @@ final class VirtualBuddyVMView: VZVirtualMachineView { updateCaptureSystemKeysState() } + + if oldValue.allowsMouseEvents != eventDeliveryMask.allowsMouseEvents { + window?.invalidateCursorRects(for: self) + } } } @@ -365,34 +369,29 @@ final class VirtualBuddyVMView: VZVirtualMachineView { super.otherMouseUp(with: event) } - override func updateTrackingAreas() { + override func scrollWheel(with event: NSEvent) { guard eventDeliveryMask.allowsMouseEvents else { return } - super.updateTrackingAreas() + super.scrollWheel(with: event) } - override func cursorUpdate(with event: NSEvent) { + override func magnify(with event: NSEvent) { guard eventDeliveryMask.allowsMouseEvents else { return } - super.cursorUpdate(with: event) + super.magnify(with: event) } - override func resetCursorRects() { + override func smartMagnify(with event: NSEvent) { guard eventDeliveryMask.allowsMouseEvents else { return } - super.resetCursorRects() + super.smartMagnify(with: event) } - override func discardCursorRects() { + override func rotate(with event: NSEvent) { guard eventDeliveryMask.allowsMouseEvents else { return } - super.discardCursorRects() + super.rotate(with: event) } - override func addCursorRect(_ rect: NSRect, cursor object: NSCursor) { - guard eventDeliveryMask.allowsMouseEvents else { return } - super.addCursorRect(rect, cursor: object) - } - - override func removeCursorRect(_ rect: NSRect, cursor object: NSCursor) { + override func cursorUpdate(with event: NSEvent) { guard eventDeliveryMask.allowsMouseEvents else { return } - super.removeCursorRect(rect, cursor: object) + super.cursorUpdate(with: event) } override func keyDown(with event: NSEvent) { From d9a2bd7c943cec3197bb65aa896992c9839846ee Mon Sep 17 00:00:00 2001 From: Guilherme Rambo Date: Fri, 28 Aug 2026 17:14:05 -0300 Subject: [PATCH 3/3] Hide input controls when VM not running --- VirtualUI/Source/Session/VirtualMachineSessionView.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/VirtualUI/Source/Session/VirtualMachineSessionView.swift b/VirtualUI/Source/Session/VirtualMachineSessionView.swift index 744fb2e4..0df65898 100644 --- a/VirtualUI/Source/Session/VirtualMachineSessionView.swift +++ b/VirtualUI/Source/Session/VirtualMachineSessionView.swift @@ -74,8 +74,10 @@ public struct VirtualMachineSessionView: View { } } .toolbar { - ToolbarItemGroup(placement: .primaryAction) { - VirtualMachineUserControls() + if controller.isRunning { + ToolbarItemGroup(placement: .primaryAction) { + VirtualMachineUserControls() + } } if #available(macOS 26, *) {