123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190 |
- //
- // 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
- }
- }
|