VMWindowState.swift 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  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. }
  57. // MARK: - VM action alerts
  58. extension VMWindowState {
  59. enum Alert: Identifiable {
  60. var id: Int {
  61. switch self {
  62. case .powerDown: return 0
  63. case .terminateApp: return 1
  64. case .restart: return 2
  65. #if WITH_USB
  66. case .deviceConnected(_): return 3
  67. #endif
  68. case .nonfatalError(_): return 4
  69. case .fatalError(_): return 5
  70. case .memoryWarning: return 6
  71. }
  72. }
  73. case powerDown
  74. case terminateApp
  75. case restart
  76. #if WITH_USB
  77. case deviceConnected(CSUSBDevice)
  78. #endif
  79. case nonfatalError(String)
  80. case fatalError(String)
  81. case memoryWarning
  82. }
  83. }
  84. // MARK: - Resizing display
  85. extension VMWindowState {
  86. private var kVMDefaultResizeCmd: String {
  87. "stty cols $COLS rows $ROWS\\n"
  88. }
  89. mutating func resizeDisplayToFit(_ display: CSDisplay, size: CGSize = .zero) {
  90. let viewSize = displayViewSize
  91. let displaySize = size == .zero ? display.displaySize : size
  92. let scaled = CGSize(width: viewSize.width / displaySize.width, height: viewSize.height / displaySize.height)
  93. let viewportScale = min(scaled.width, scaled.height)
  94. // persist this change in viewState
  95. displayScale = viewportScale
  96. displayOrigin = .zero
  97. }
  98. private mutating func resetDisplay(_ display: CSDisplay) {
  99. // persist this change in viewState
  100. displayScale = 1.0
  101. displayOrigin = .zero
  102. }
  103. private mutating func resetConsole(_ serial: CSPort, command: String? = nil) {
  104. let cols = Int(displayViewSize.width)
  105. let rows = Int(displayViewSize.height)
  106. let template = command ?? kVMDefaultResizeCmd
  107. let cmd = template
  108. .replacingOccurrences(of: "$COLS", with: String(cols))
  109. .replacingOccurrences(of: "$ROWS", with: String(rows))
  110. .replacingOccurrences(of: "\\n", with: "\n")
  111. serial.write(cmd.data(using: .nonLossyASCII)!)
  112. }
  113. mutating func toggleDisplayResize(command: String? = nil) {
  114. if case let .display(display, _) = device {
  115. if isViewportChanged {
  116. isDisplayZoomLocked = false
  117. resetDisplay(display)
  118. } else {
  119. isDisplayZoomLocked = true
  120. resizeDisplayToFit(display)
  121. }
  122. } else if case let .serial(serial, _) = device {
  123. resetConsole(serial)
  124. isViewportChanged = false
  125. isDisplayZoomLocked = false
  126. }
  127. }
  128. }
  129. // MARK: - Persist changes
  130. @MainActor extension VMWindowState {
  131. func saveWindow(to registryEntry: UTMRegistryEntry, device: Device?) {
  132. guard case let .display(_, id) = device else {
  133. return
  134. }
  135. var window = UTMRegistryEntry.Window()
  136. window.scale = displayScale
  137. #if !os(visionOS)
  138. window.origin = displayOrigin
  139. window.isDisplayZoomLocked = isDisplayZoomLocked
  140. #endif
  141. window.isKeyboardVisible = isKeyboardShown
  142. registryEntry.windowSettings[id] = window
  143. }
  144. mutating func restoreWindow(from registryEntry: UTMRegistryEntry, device: Device?) {
  145. guard case let .display(_, id) = device else {
  146. return
  147. }
  148. let window = registryEntry.windowSettings[id] ?? UTMRegistryEntry.Window()
  149. displayScale = window.scale
  150. #if os(visionOS)
  151. isDisplayZoomLocked = true
  152. #else
  153. displayOrigin = window.origin
  154. isDisplayZoomLocked = window.isDisplayZoomLocked
  155. isKeyboardRequested = window.isKeyboardVisible
  156. #endif
  157. }
  158. }