210 lines
6.9 KiB
Swift
210 lines
6.9 KiB
Swift
//
|
|
// Copyright © 2022 osy. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
import Combine
|
|
import SwiftUI
|
|
|
|
struct VMDisplayHostedView: UIViewControllerRepresentable {
|
|
internal class Coordinator: VMDisplayViewControllerDelegate {
|
|
let vm: any UTMSpiceVirtualMachine
|
|
let device: VMWindowState.Device
|
|
@Binding var state: VMWindowState
|
|
var vmStateCancellable: AnyCancellable?
|
|
|
|
var vmState: UTMVirtualMachineState {
|
|
vm.state
|
|
}
|
|
|
|
var vmConfig: UTMQemuConfiguration {
|
|
vm.config
|
|
}
|
|
|
|
@MainActor var qemuInputLegacy: Bool {
|
|
vmConfig.input.usbBusSupport == .disabled
|
|
}
|
|
|
|
@MainActor var qemuDisplayUpscaler: MTLSamplerMinMagFilter {
|
|
vmConfig.displays[state.device!.configIndex].upscalingFilter.metalSamplerMinMagFilter
|
|
}
|
|
|
|
@MainActor var qemuDisplayDownscaler: MTLSamplerMinMagFilter {
|
|
vmConfig.displays[state.device!.configIndex].downscalingFilter.metalSamplerMinMagFilter
|
|
}
|
|
|
|
@MainActor var qemuDisplayIsDynamicResolution: Bool {
|
|
vmConfig.displays[state.device!.configIndex].isDynamicResolution
|
|
}
|
|
|
|
@MainActor var qemuDisplayIsNativeResolution: Bool {
|
|
vmConfig.displays[state.device!.configIndex].isNativeResolution
|
|
}
|
|
|
|
@MainActor var qemuHasClipboardSharing: Bool {
|
|
vmConfig.sharing.hasClipboardSharing
|
|
}
|
|
|
|
@MainActor var qemuConsoleResizeCommand: String? {
|
|
vmConfig.serials[state.device!.configIndex].terminal?.resizeCommand
|
|
}
|
|
|
|
var isViewportChanged: Bool {
|
|
get {
|
|
state.isViewportChanged
|
|
}
|
|
|
|
set {
|
|
state.isViewportChanged = newValue
|
|
}
|
|
}
|
|
|
|
var displayOrigin: CGPoint {
|
|
get {
|
|
state.displayOrigin
|
|
}
|
|
|
|
set {
|
|
state.displayOrigin = newValue
|
|
}
|
|
}
|
|
|
|
var displayScale: CGFloat {
|
|
get {
|
|
state.displayScale
|
|
}
|
|
|
|
set {
|
|
state.displayScale = newValue
|
|
}
|
|
}
|
|
|
|
var displayViewSize: CGSize {
|
|
get {
|
|
state.displayViewSize
|
|
}
|
|
|
|
set {
|
|
state.displayViewSize = newValue
|
|
}
|
|
}
|
|
|
|
init(with vm: any UTMSpiceVirtualMachine, device: VMWindowState.Device, state: Binding<VMWindowState>) {
|
|
self.vm = vm
|
|
self.device = device
|
|
self._state = state
|
|
}
|
|
|
|
func displayDidAssertUserInteraction() {
|
|
state.isUserInteracting.toggle()
|
|
}
|
|
|
|
func displayDidAppear() {
|
|
if vm.state == .stopped {
|
|
vm.requestVmStart()
|
|
}
|
|
}
|
|
|
|
func display(_ display: CSDisplay, didResizeTo size: CGSize) {
|
|
if state.isDisplayZoomLocked {
|
|
state.resizeDisplayToFit(display, size: size)
|
|
}
|
|
}
|
|
|
|
func serialDidError(_ error: String) {
|
|
state.alert = .nonfatalError(error)
|
|
}
|
|
|
|
func requestInputTablet(_ tablet: Bool) {
|
|
vm.requestInputTablet(tablet)
|
|
}
|
|
}
|
|
|
|
let vm: any UTMSpiceVirtualMachine
|
|
let device: VMWindowState.Device
|
|
|
|
@Binding var state: VMWindowState
|
|
|
|
@EnvironmentObject private var session: VMSessionState
|
|
|
|
func makeUIViewController(context: Context) -> VMDisplayViewController {
|
|
let vc: VMDisplayViewController
|
|
switch device {
|
|
case .display(let display, _):
|
|
let mvc = VMDisplayMetalViewController(display: display, input: session.primaryInput)
|
|
mvc.delegate = context.coordinator
|
|
mvc.setDisplayScaling(state.displayScale, origin: state.displayOrigin)
|
|
vc = mvc
|
|
case .serial(let serial, let id):
|
|
let style = vm.config.serials[id].terminal
|
|
vc = VMDisplayTerminalViewController(port: serial, style: style)
|
|
vc.delegate = context.coordinator
|
|
}
|
|
context.coordinator.vmStateCancellable = session.$vmState.sink { vmState in
|
|
switch vmState {
|
|
case .stopped, .paused:
|
|
vc.enterSuspended(isBusy: false)
|
|
case .pausing, .stopping, .starting, .resuming, .saving, .restoring:
|
|
vc.enterSuspended(isBusy: true)
|
|
case .started:
|
|
vc.enterLive()
|
|
}
|
|
}
|
|
return vc
|
|
}
|
|
|
|
func updateUIViewController(_ uiViewController: VMDisplayViewController, context: Context) {
|
|
if let vc = uiViewController as? VMDisplayMetalViewController {
|
|
vc.vmInput = session.primaryInput
|
|
}
|
|
if state.isKeyboardShown != state.isKeyboardRequested {
|
|
DispatchQueue.main.async {
|
|
if state.isKeyboardRequested {
|
|
uiViewController.showKeyboard()
|
|
} else {
|
|
uiViewController.hideKeyboard()
|
|
}
|
|
#if os(visionOS)
|
|
// UIKeyboardDidShowNotification is never posted on visionOS
|
|
// so we cannot determine the keyboard state
|
|
state.isKeyboardRequested = false
|
|
#endif
|
|
}
|
|
}
|
|
switch state.device {
|
|
case .display(let display, _):
|
|
if let vc = uiViewController as? VMDisplayMetalViewController {
|
|
if vc.vmDisplay != display {
|
|
vc.vmDisplay = display
|
|
}
|
|
// some obscure SwiftUI error means we cannot refer to Coordinator's state binding
|
|
vc.setDisplayScaling(state.displayScale, origin: state.displayOrigin)
|
|
vc.isDynamicResolutionSupported = state.isDynamicResolutionSupported
|
|
}
|
|
case .serial(let serial, _):
|
|
if let vc = uiViewController as? VMDisplayTerminalViewController {
|
|
if vc.vmSerialPort != serial {
|
|
vc.vmSerialPort = serial
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func makeCoordinator() -> Coordinator {
|
|
Coordinator(with: vm, device: device, state: $state)
|
|
}
|
|
}
|