VMWindowView.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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 SwiftUI
  17. import SwiftUIVisualEffects
  18. struct VMWindowView: View {
  19. let id: VMSessionState.WindowID
  20. @State var isInteractive = true
  21. @State private var state: VMWindowState
  22. @EnvironmentObject private var session: VMSessionState
  23. @Environment(\.scenePhase) private var scenePhase
  24. private let keyboardDidShowNotification = NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)
  25. private let keyboardDidHideNotification = NotificationCenter.default.publisher(for: UIResponder.keyboardDidHideNotification)
  26. private let didReceiveMemoryWarningNotification = NotificationCenter.default.publisher(for: UIApplication.didReceiveMemoryWarningNotification)
  27. init(id: VMSessionState.WindowID, isInteractive: Bool = true) {
  28. self.id = id
  29. self._isInteractive = State<Bool>(initialValue: isInteractive)
  30. self._state = State<VMWindowState>(initialValue: VMWindowState(id: id))
  31. }
  32. private func withOptionalAnimation<Result>(_ animation: Animation? = .default, _ body: () throws -> Result) rethrows -> Result {
  33. if UIAccessibility.isReduceMotionEnabled {
  34. return try body()
  35. } else {
  36. return try withAnimation(animation, body)
  37. }
  38. }
  39. var body: some View {
  40. ZStack {
  41. ZStack {
  42. if let device = state.device {
  43. switch device {
  44. case .display(_, _):
  45. VMDisplayHostedView(vm: session.vm, device: device, state: $state)
  46. case .serial(_, _):
  47. VMDisplayHostedView(vm: session.vm, device: device, state: $state)
  48. }
  49. } else if !state.isBusy && state.isRunning {
  50. // headless
  51. HeadlessView()
  52. }
  53. if state.isBusy || !state.isRunning {
  54. BlurEffect().blurEffectStyle(.light)
  55. VStack {
  56. Spacer()
  57. HStack {
  58. Spacer()
  59. if state.isBusy {
  60. Spinner(size: .large)
  61. } else if session.vmState == .paused {
  62. Button {
  63. session.vm.requestVmResume()
  64. } label: {
  65. if #available(iOS 16, *) {
  66. Label("Resume", systemImage: "playpause.circle.fill")
  67. } else {
  68. Label("Resume", systemImage: "play.circle.fill")
  69. }
  70. }
  71. } else {
  72. Button {
  73. session.vm.requestVmStart()
  74. } label: {
  75. Label("Start", systemImage: "play.circle.fill")
  76. }
  77. }
  78. Spacer()
  79. }
  80. Spacer()
  81. }.labelStyle(.iconOnly)
  82. .font(.system(size: 128))
  83. .vibrancyEffect()
  84. .vibrancyEffectStyle(.label)
  85. }
  86. }.background(Color.black)
  87. .ignoresSafeArea()
  88. #if !os(visionOS)
  89. if isInteractive && state.isRunning {
  90. VMToolbarView(state: $state)
  91. }
  92. #endif
  93. }
  94. .modifier(VMToolbarOrnamentModifier(state: $state))
  95. .statusBarHidden(true)
  96. .alert(item: $state.alert, content: { type in
  97. switch type {
  98. case .powerDown:
  99. return Alert(title: Text("Are you sure you want to stop this VM and exit? Any unsaved changes will be lost."), primaryButton: .destructive(Text("Yes")) {
  100. session.powerDown()
  101. }, secondaryButton: .cancel(Text("No")))
  102. case .terminateApp:
  103. return Alert(title: Text("Are you sure you want to exit UTM?"), primaryButton: .destructive(Text("Yes")) {
  104. session.stop()
  105. }, secondaryButton: .cancel(Text("No")))
  106. case .restart:
  107. return Alert(title: Text("Are you sure you want to reset this VM? Any unsaved changes will be lost."), primaryButton: .destructive(Text("Yes")) {
  108. session.reset()
  109. }, secondaryButton: .cancel(Text("No")))
  110. #if WITH_USB
  111. case .deviceConnected(let device):
  112. return Alert(title: Text("Would you like to connect '\(device.name ?? device.description)' to this virtual machine?"), primaryButton: .default(Text("Yes")) {
  113. session.mostRecentConnectedDevice = nil
  114. session.connectDevice(device)
  115. }, secondaryButton: .cancel(Text("No")) {
  116. session.mostRecentConnectedDevice = nil
  117. })
  118. #endif
  119. case .nonfatalError(let message), .fatalError(let message):
  120. return Alert(title: Text(message), dismissButton: .cancel(Text("OK")) {
  121. if case .fatalError(_) = type {
  122. session.stop()
  123. } else {
  124. session.nonfatalError = nil
  125. }
  126. })
  127. case .memoryWarning:
  128. return Alert(title: Text("Running low on memory! UTM might soon be killed by iOS. You can prevent this by decreasing the amount of memory and/or JIT cache assigned to this VM"), dismissButton: .cancel(Text("OK")) {
  129. session.didReceiveMemoryWarning()
  130. })
  131. }
  132. })
  133. .onChange(of: session.windowDeviceMap) { windowDeviceMap in
  134. if let device = windowDeviceMap[state.id] {
  135. state.device = device
  136. } else {
  137. state.device = nil
  138. }
  139. }
  140. .onChange(of: state.device) { [oldDevice = state.device] newDevice in
  141. if session.windowDeviceMap[state.id] != newDevice {
  142. session.windowDeviceMap[state.id] = newDevice
  143. }
  144. state.saveWindow(to: session.vm.registryEntry, device: oldDevice)
  145. state.restoreWindow(from: session.vm.registryEntry, device: newDevice)
  146. }
  147. #if WITH_USB
  148. .onChange(of: session.mostRecentConnectedDevice) { newValue in
  149. if session.activeWindow == state.id, let device = newValue {
  150. state.alert = .deviceConnected(device)
  151. }
  152. }
  153. #endif
  154. .onChange(of: session.nonfatalError) { newValue in
  155. if session.activeWindow == state.id, let message = newValue {
  156. state.alert = .nonfatalError(message)
  157. }
  158. }
  159. .onChange(of: session.fatalError) { newValue in
  160. if session.activeWindow == state.id, let message = newValue {
  161. state.alert = .fatalError(message)
  162. }
  163. }
  164. .onChange(of: session.vmState) { [oldValue = session.vmState] newValue in
  165. vmStateUpdated(from: oldValue, to: newValue)
  166. }
  167. .onReceive(keyboardDidShowNotification) { _ in
  168. state.isKeyboardShown = true
  169. state.isKeyboardRequested = true
  170. }
  171. .onReceive(keyboardDidHideNotification) { _ in
  172. state.isKeyboardShown = false
  173. state.isKeyboardRequested = false
  174. }
  175. .onReceive(didReceiveMemoryWarningNotification) { _ in
  176. if session.activeWindow == state.id && !session.hasShownMemoryWarning {
  177. session.hasShownMemoryWarning = true
  178. state.alert = .memoryWarning
  179. }
  180. }
  181. .onChange(of: scenePhase) { newValue in
  182. guard session.activeWindow == state.id else {
  183. return
  184. }
  185. if newValue == .background {
  186. saveWindow()
  187. session.didEnterBackground()
  188. } else if newValue == .active {
  189. session.didEnterForeground()
  190. }
  191. }
  192. .onAppear {
  193. vmStateUpdated(from: nil, to: session.vmState)
  194. session.registerWindow(state.id, isExternal: !isInteractive)
  195. if !isInteractive {
  196. session.externalWindowBinding = $state
  197. }
  198. }
  199. .onDisappear {
  200. session.removeWindow(state.id)
  201. if !isInteractive {
  202. session.externalWindowBinding = nil
  203. }
  204. }
  205. }
  206. private func vmStateUpdated(from oldState: UTMVirtualMachineState?, to vmState: UTMVirtualMachineState) {
  207. if oldState == .started {
  208. saveWindow()
  209. }
  210. switch vmState {
  211. case .stopped, .paused:
  212. withOptionalAnimation {
  213. state.isBusy = false
  214. state.isRunning = false
  215. }
  216. DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
  217. if session.vmState == .stopped && session.fatalError == nil {
  218. session.stop()
  219. }
  220. }
  221. case .pausing, .stopping, .starting, .resuming, .saving, .restoring:
  222. withOptionalAnimation {
  223. state.isBusy = true
  224. state.isRunning = false
  225. }
  226. case .started:
  227. withOptionalAnimation {
  228. state.isBusy = false
  229. state.isRunning = true
  230. }
  231. }
  232. }
  233. private func saveWindow() {
  234. state.saveWindow(to: session.vm.registryEntry, device: state.device)
  235. }
  236. }
  237. private struct HeadlessView: View {
  238. var body: some View {
  239. ZStack {
  240. BlurEffect().blurEffectStyle(.dark)
  241. VStack {
  242. Image(systemName: "rectangle.dashed")
  243. .font(.title)
  244. Text("No output device is selected for this window.")
  245. .foregroundColor(.white)
  246. .font(.headline)
  247. .multilineTextAlignment(.center)
  248. }.padding()
  249. .frame(width: 200, height: 200, alignment: .center)
  250. .foregroundColor(.white)
  251. .background(Color.gray.opacity(0.5))
  252. .clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous))
  253. .vibrancyEffect()
  254. .vibrancyEffectStyle(.label)
  255. }
  256. }
  257. }
  258. #if !os(visionOS)
  259. /// Stub for non-Vision platforms
  260. fileprivate struct VMToolbarOrnamentModifier: ViewModifier {
  261. @Binding var state: VMWindowState
  262. func body(content: Content) -> some View {
  263. content
  264. }
  265. }
  266. #endif