2
0

VMWindowState.swift 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. //
  2. // Copyright © 2022 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import Foundation
  17. /// Represents the UI state for a single window
  18. struct VMWindowState: Identifiable {
  19. enum Device: Identifiable, Hashable {
  20. case display(CSDisplay, Int)
  21. case serial(CSPort, Int)
  22. var configIndex: Int {
  23. switch self {
  24. case .display(_, let index): return index
  25. case .serial(_, let index): return index
  26. }
  27. }
  28. var id: Self {
  29. self
  30. }
  31. }
  32. let id: VMSessionState.WindowID
  33. var device: Device?
  34. private var shouldViewportChange: Bool {
  35. !(displayScale == 1.0 && displayOrigin == .zero)
  36. }
  37. var displayScale: CGFloat = 1.0 {
  38. didSet {
  39. isViewportChanged = shouldViewportChange
  40. }
  41. }
  42. var displayOrigin: CGPoint = CGPoint(x: 0, y: 0) {
  43. didSet {
  44. isViewportChanged = shouldViewportChange
  45. }
  46. }
  47. var displayViewSize: CGSize = .zero
  48. var isDisplayZoomLocked: Bool = false
  49. var isKeyboardRequested: Bool = false
  50. var isKeyboardShown: Bool = false
  51. var isViewportChanged: Bool = false
  52. var isUserInteracting: Bool = false
  53. var isBusy: Bool = false
  54. var isRunning: Bool = false
  55. var alert: Alert?
  56. var isDynamicResolutionSupported: Bool = false
  57. }
  58. // MARK: - VM action alerts
  59. extension VMWindowState {
  60. enum Alert: Identifiable {
  61. var id: Int {
  62. switch self {
  63. case .powerDown: return 0
  64. case .terminateApp: return 1
  65. case .restart: return 2
  66. #if WITH_USB
  67. case .deviceConnected(_): return 3
  68. #endif
  69. case .nonfatalError(_): return 4
  70. case .fatalError(_): return 5
  71. case .memoryWarning: return 6
  72. }
  73. }
  74. case powerDown
  75. case terminateApp
  76. case restart
  77. #if WITH_USB
  78. case deviceConnected(CSUSBDevice)
  79. #endif
  80. case nonfatalError(String)
  81. case fatalError(String)
  82. case memoryWarning
  83. }
  84. }
  85. // MARK: - Resizing display
  86. extension VMWindowState {
  87. private var kVMDefaultResizeCmd: String {
  88. "stty cols $COLS rows $ROWS\\n"
  89. }
  90. mutating func resizeDisplayToFit(_ display: CSDisplay, size: CGSize = .zero) {
  91. let viewSize = displayViewSize
  92. let displaySize = size == .zero ? display.displaySize : size
  93. let scaled = CGSize(width: viewSize.width / displaySize.width, height: viewSize.height / displaySize.height)
  94. let viewportScale = min(scaled.width, scaled.height)
  95. // persist this change in viewState
  96. displayScale = viewportScale
  97. displayOrigin = .zero
  98. }
  99. private mutating func resetDisplay(_ display: CSDisplay) {
  100. // persist this change in viewState
  101. displayScale = 1.0
  102. displayOrigin = .zero
  103. }
  104. private mutating func resetConsole(_ serial: CSPort, command: String? = nil) {
  105. let cols = Int(displayViewSize.width)
  106. let rows = Int(displayViewSize.height)
  107. let template = command ?? kVMDefaultResizeCmd
  108. let cmd = template
  109. .replacingOccurrences(of: "$COLS", with: String(cols))
  110. .replacingOccurrences(of: "$ROWS", with: String(rows))
  111. .replacingOccurrences(of: "\\n", with: "\n")
  112. serial.write(cmd.data(using: .nonLossyASCII)!)
  113. }
  114. mutating func toggleDisplayResize(command: String? = nil) {
  115. if case let .display(display, _) = device {
  116. if isViewportChanged {
  117. isDisplayZoomLocked = false
  118. resetDisplay(display)
  119. } else {
  120. isDisplayZoomLocked = true
  121. resizeDisplayToFit(display)
  122. }
  123. } else if case let .serial(serial, _) = device {
  124. resetConsole(serial)
  125. isViewportChanged = false
  126. isDisplayZoomLocked = false
  127. }
  128. }
  129. }
  130. // MARK: - Persist changes
  131. @MainActor extension VMWindowState {
  132. func saveWindow(to registryEntry: UTMRegistryEntry, device: Device?) {
  133. guard case let .display(_, id) = device else {
  134. return
  135. }
  136. var window = UTMRegistryEntry.Window()
  137. window.scale = displayScale
  138. #if !os(visionOS)
  139. window.origin = displayOrigin
  140. window.isDisplayZoomLocked = isDisplayZoomLocked
  141. #endif
  142. window.isKeyboardVisible = isKeyboardShown
  143. registryEntry.windowSettings[id] = window
  144. }
  145. mutating func restoreWindow(from registryEntry: UTMRegistryEntry, device: Device?) {
  146. guard case let .display(_, id) = device else {
  147. return
  148. }
  149. let window = registryEntry.windowSettings[id] ?? UTMRegistryEntry.Window()
  150. displayScale = window.scale
  151. #if os(visionOS)
  152. isDisplayZoomLocked = true
  153. #else
  154. displayOrigin = window.origin
  155. isDisplayZoomLocked = window.isDisplayZoomLocked
  156. isKeyboardRequested = window.isKeyboardVisible
  157. #endif
  158. }
  159. }