// // 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 Foundation /// Represents the UI state for a single window struct VMWindowState: Identifiable { enum Device: Identifiable, Hashable { case display(CSDisplay, Int) case serial(CSPort, Int) var configIndex: Int { switch self { case .display(_, let index): return index case .serial(_, let index): return index } } var id: Self { self } } let id: VMSessionState.WindowID var device: Device? private var shouldViewportChange: Bool { !(displayScale == 1.0 && displayOrigin == .zero) } var displayScale: CGFloat = 1.0 { didSet { isViewportChanged = shouldViewportChange } } var displayOrigin: CGPoint = CGPoint(x: 0, y: 0) { didSet { isViewportChanged = shouldViewportChange } } var displayViewSize: CGSize = .zero var isDisplayZoomLocked: Bool = false var isKeyboardRequested: Bool = false var isKeyboardShown: Bool = false var isViewportChanged: Bool = false var isUserInteracting: Bool = false var isBusy: Bool = false var isRunning: Bool = false var alert: Alert? var isDynamicResolutionSupported: Bool = false } // MARK: - VM action alerts extension VMWindowState { enum Alert: Identifiable { var id: Int { switch self { case .powerDown: return 0 case .terminateApp: return 1 case .restart: return 2 #if WITH_USB case .deviceConnected(_): return 3 #endif case .nonfatalError(_): return 4 case .fatalError(_): return 5 case .memoryWarning: return 6 } } case powerDown case terminateApp case restart #if WITH_USB case deviceConnected(CSUSBDevice) #endif case nonfatalError(String) case fatalError(String) case memoryWarning } } // MARK: - Resizing display extension VMWindowState { private var kVMDefaultResizeCmd: String { "stty cols $COLS rows $ROWS\\n" } mutating func resizeDisplayToFit(_ display: CSDisplay, size: CGSize = .zero) { let viewSize = displayViewSize let displaySize = size == .zero ? display.displaySize : size let scaled = CGSize(width: viewSize.width / displaySize.width, height: viewSize.height / displaySize.height) let viewportScale = min(scaled.width, scaled.height) // persist this change in viewState displayScale = viewportScale displayOrigin = .zero } private mutating func resetDisplay(_ display: CSDisplay) { // persist this change in viewState displayScale = 1.0 displayOrigin = .zero } private mutating func resetConsole(_ serial: CSPort, command: String? = nil) { let cols = Int(displayViewSize.width) let rows = Int(displayViewSize.height) let template = command ?? kVMDefaultResizeCmd let cmd = template .replacingOccurrences(of: "$COLS", with: String(cols)) .replacingOccurrences(of: "$ROWS", with: String(rows)) .replacingOccurrences(of: "\\n", with: "\n") serial.write(cmd.data(using: .nonLossyASCII)!) } mutating func toggleDisplayResize(command: String? = nil) { if case let .display(display, _) = device { if isViewportChanged { isDisplayZoomLocked = false resetDisplay(display) } else { isDisplayZoomLocked = true resizeDisplayToFit(display) } } else if case let .serial(serial, _) = device { resetConsole(serial) isViewportChanged = false isDisplayZoomLocked = false } } } // MARK: - Persist changes @MainActor extension VMWindowState { func saveWindow(to registryEntry: UTMRegistryEntry, device: Device?) { guard case let .display(_, id) = device else { return } var window = UTMRegistryEntry.Window() window.scale = displayScale #if !os(visionOS) window.origin = displayOrigin window.isDisplayZoomLocked = isDisplayZoomLocked #endif window.isKeyboardVisible = isKeyboardShown registryEntry.windowSettings[id] = window } mutating func restoreWindow(from registryEntry: UTMRegistryEntry, device: Device?) { guard case let .display(_, id) = device else { return } let window = registryEntry.windowSettings[id] ?? UTMRegistryEntry.Window() displayScale = window.scale #if os(visionOS) isDisplayZoomLocked = true #else displayOrigin = window.origin isDisplayZoomLocked = window.isDisplayZoomLocked isKeyboardRequested = window.isKeyboardVisible #endif } }