WindowReference.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. /// Holds the view graph and window handle for a single window.
  2. @MainActor
  3. final class WindowReference<SceneType: WindowingScene> {
  4. /// The scene.
  5. private var scene: SceneType
  6. /// The view graph of the window's root view.
  7. private let viewGraph: ViewGraph<SceneType.Content>
  8. /// The window being rendered in.
  9. let window: Any
  10. /// `false` after the first scene update.
  11. private var isFirstUpdate = true
  12. /// The cached window size. Nil on first run or after a window is resized.
  13. private var cachedWindowSize: SIMD2<Int>?
  14. /// The environment most recently provided by this node's parent scene.
  15. private var parentEnvironment: EnvironmentValues
  16. /// The container used to center the root view in the window.
  17. private let containerWidget: AnyWidget
  18. /// The window's preferred color scheme, cached from the last update.
  19. private var preferredColorScheme: ColorScheme?
  20. /// - Parameters:
  21. /// - closeHandler: The action to perform when the window is closed. Should
  22. /// dispose of the scene's reference to this `WindowReference`.
  23. /// - id: A unique id to use when restoring the window's frame from disk (if present).
  24. init<Backend: BaseAppBackend>(
  25. scene: SceneType,
  26. backend: Backend,
  27. environment: EnvironmentValues,
  28. onClose closeHandler: @escaping @Sendable @MainActor () -> Void,
  29. id: String
  30. ) {
  31. self.scene = scene
  32. let window = backend.createWindow(
  33. withDefaultSize: environment.defaultWindowSize,
  34. id: id
  35. )
  36. viewGraph = ViewGraph(
  37. for: scene.content(),
  38. backend: backend,
  39. environment: environment.with(\.window, window)
  40. )
  41. let rootWidget = viewGraph.rootNode.concreteNode(for: Backend.self).widget
  42. let container = backend.createContainer()
  43. backend.insert(rootWidget, into: container, at: 0)
  44. self.containerWidget = AnyWidget(container)
  45. backend.setChild(ofWindow: window, to: container)
  46. backend.setTitle(ofWindow: window, to: scene.title)
  47. self.window = window
  48. parentEnvironment = environment
  49. if let backend = backend as? any BackendFeatures.WindowClosing {
  50. func setCloseHandler<NewBackend: BackendFeatures.WindowClosing>(backend: NewBackend) {
  51. backend.setCloseHandler(ofWindow: window as! NewBackend.Window, to: closeHandler)
  52. }
  53. setCloseHandler(backend: backend)
  54. }
  55. backend.setResizeHandler(ofWindow: window) { [weak self] newSize in
  56. guard let self else { return }
  57. self.update(
  58. self.scene,
  59. proposedWindowSize: newSize,
  60. needsWindowSizeCommit: false,
  61. backend: backend,
  62. environment: self.parentEnvironment,
  63. windowSizeIsFinal: !backend.isWindowProgrammaticallyResizable(window)
  64. )
  65. }
  66. backend.setWindowEnvironmentChangeHandler(of: window) { [weak self] in
  67. guard let self else { return }
  68. self.update(
  69. self.scene,
  70. proposedWindowSize: backend.size(ofWindow: window),
  71. needsWindowSizeCommit: false,
  72. backend: backend,
  73. environment: self.parentEnvironment,
  74. windowSizeIsFinal: !backend.isWindowProgrammaticallyResizable(window)
  75. )
  76. }
  77. }
  78. func update<Backend: BaseAppBackend>(
  79. _ newScene: SceneType?,
  80. backend: Backend,
  81. environment: EnvironmentValues
  82. ) {
  83. guard let window = window as? Backend.Window else {
  84. fatalError("Scene updated with a backend incompatible with the window it was given")
  85. }
  86. let isProgramaticallyResizable =
  87. backend.isWindowProgrammaticallyResizable(window)
  88. let proposedWindowSize: SIMD2<Int>
  89. let usedDefaultSize: Bool
  90. if isFirstUpdate && isProgramaticallyResizable && !backend.restoresWindowFrames {
  91. proposedWindowSize = environment.defaultWindowSize
  92. usedDefaultSize = true
  93. } else {
  94. proposedWindowSize = cachedWindowSize ?? backend.size(ofWindow: window)
  95. usedDefaultSize = false
  96. }
  97. update(
  98. newScene,
  99. proposedWindowSize: proposedWindowSize,
  100. needsWindowSizeCommit: usedDefaultSize,
  101. backend: backend,
  102. environment: environment,
  103. windowSizeIsFinal: !isProgramaticallyResizable
  104. )
  105. }
  106. /// Updates the `WindowReference`.
  107. /// - Parameters:
  108. /// - newScene: The scene. `nil` if reusing previous scene value.
  109. /// - proposedWindowSize: The proposed window size.
  110. /// - needsWindowSizeCommit: Whether the proposed window size matches the
  111. /// windows current size (or imminent size in the case of a window
  112. /// resize). We use this parameter instead of comparing to the window's
  113. /// current size to the proposed size, because some backends (such as
  114. /// AppKitBackend) trigger window resize handlers *before* the underlying
  115. /// window gets assigned its new size (allowing us to pre-emptively update the
  116. /// window's content to match the new size).
  117. /// - backend: The backend to use.
  118. /// - environment: The current environment.
  119. /// - windowSizeIsFinal: If true, no further resizes can/will be made. This
  120. /// is true on platforms that don't support programmatic window resizing,
  121. /// and when a window is full screen.
  122. private func update<Backend: BaseAppBackend>(
  123. _ newScene: SceneType?,
  124. proposedWindowSize: SIMD2<Int>,
  125. needsWindowSizeCommit: Bool,
  126. backend: Backend,
  127. environment: EnvironmentValues,
  128. windowSizeIsFinal: Bool = false
  129. ) {
  130. guard let window = window as? Backend.Window else {
  131. fatalError("Scene updated with a backend incompatible with the window it was given")
  132. }
  133. parentEnvironment = environment
  134. if let newScene {
  135. // Don't set default size even if it has changed. We only set that once
  136. // at window creation since some backends don't have a concept of
  137. // 'default' size which would mean that setting the default size every time
  138. // the default size changed would resize the window (which is incorrect
  139. // behaviour).
  140. backend.setTitle(ofWindow: window, to: newScene.title)
  141. scene = newScene
  142. }
  143. var environment =
  144. backend.computeWindowEnvironment(
  145. window: window,
  146. rootEnvironment: environment.with(\.window, window)
  147. )
  148. .with(\.onResize) { [weak self] _ in
  149. guard let self else { return }
  150. self.cachedWindowSize = nil
  151. // TODO: Figure out whether this would still work if we didn't recompute the
  152. // scene's body. I have a vague feeling that it wouldn't work in all cases?
  153. // But I don't have the time to come up with a counterexample right now.
  154. self.update(
  155. self.scene,
  156. proposedWindowSize: backend.size(ofWindow: window),
  157. needsWindowSizeCommit: false,
  158. backend: backend,
  159. environment: environment
  160. )
  161. }
  162. let outerColorScheme = environment.colorScheme
  163. // Update environment with latest cached value before first update to
  164. // minimise toggling between outer color scheme and preferred color
  165. // scheme where possible (could confuse people when logging the color
  166. // scheme or debugging things)
  167. if let preferredColorScheme {
  168. environment.colorScheme = preferredColorScheme
  169. }
  170. let probingResult = viewGraph.computeLayout(
  171. with: newScene?.content(),
  172. proposedSize: .zero,
  173. environment: environment
  174. .with(\.allowLayoutCaching, true)
  175. )
  176. let minimumWindowSize = probingResult.size
  177. updateEnvironment(
  178. &environment,
  179. viewLayoutResult: probingResult,
  180. outerColorScheme: outerColorScheme,
  181. backend: backend
  182. )
  183. // With `.contentSize`, the window's maximum size is the maximum size of its
  184. // content. With `.contentMinSize` (and `.automatic`), there is no maximum
  185. // size.
  186. let maximumWindowSize: ViewSize?
  187. switch environment.windowResizability {
  188. case .contentSize:
  189. let result = viewGraph.computeLayout(
  190. with: newScene?.content(),
  191. proposedSize: .infinity,
  192. environment: environment.with(\.allowLayoutCaching, true)
  193. )
  194. updateEnvironment(
  195. &environment,
  196. viewLayoutResult: result,
  197. outerColorScheme: outerColorScheme,
  198. backend: backend
  199. )
  200. maximumWindowSize = result.size
  201. case .automatic, .contentMinSize:
  202. maximumWindowSize = nil
  203. }
  204. let clampedWindowSize = ViewSize(
  205. min(
  206. maximumWindowSize?.width ?? .infinity,
  207. max(minimumWindowSize.width, Double(proposedWindowSize.x))
  208. ),
  209. min(
  210. maximumWindowSize?.height ?? .infinity,
  211. max(minimumWindowSize.height, Double(proposedWindowSize.y))
  212. )
  213. )
  214. if clampedWindowSize.vector != proposedWindowSize && !windowSizeIsFinal {
  215. // Restart the window update if the content has caused the window to
  216. // change size.
  217. return update(
  218. scene,
  219. proposedWindowSize: clampedWindowSize.vector,
  220. needsWindowSizeCommit: true,
  221. backend: backend,
  222. environment: environment,
  223. windowSizeIsFinal: true
  224. )
  225. }
  226. // Set these even if the window isn't programmatically resizable
  227. // because the window may still be user resizable.
  228. backend.setSizeLimits(
  229. ofWindow: window,
  230. minimum: minimumWindowSize.vector,
  231. maximum: maximumWindowSize?.vector
  232. )
  233. let finalContentResult = viewGraph.computeLayout(
  234. proposedSize: ProposedViewSize(proposedWindowSize),
  235. environment: environment
  236. )
  237. updateEnvironment(
  238. &environment,
  239. viewLayoutResult: finalContentResult,
  240. outerColorScheme: outerColorScheme,
  241. backend: backend
  242. )
  243. backend.setPosition(
  244. ofChildAt: 0,
  245. in: containerWidget.into(),
  246. to: (proposedWindowSize &- finalContentResult.size.vector) / 2
  247. )
  248. if needsWindowSizeCommit {
  249. backend.setSize(ofWindow: window, to: proposedWindowSize)
  250. }
  251. cachedWindowSize = proposedWindowSize
  252. if let backend = backend as? any BackendFeatures.WindowBehaviors {
  253. func setBehaviors<NewBackend: BackendFeatures.WindowBehaviors>(backend: NewBackend) {
  254. backend.setBehaviors(
  255. ofWindow: window as! NewBackend.Window,
  256. closable: finalContentResult.preferences.windowDismissBehavior?
  257. .isEnabled ?? true,
  258. minimizable: finalContentResult.preferences.preferredWindowMinimizeBehavior?
  259. .isEnabled ?? true,
  260. resizable: finalContentResult.preferences.windowResizeBehavior?
  261. .isEnabled ?? true
  262. )
  263. }
  264. setBehaviors(backend: backend)
  265. }
  266. // Generally just used to update the window color scheme
  267. backend.updateWindow(window, environment: environment)
  268. // Delay committing the view graph so that the View.inspectWindow(_:)
  269. // modifiers can be used to overwrite certain SwiftCrossUI behaviors
  270. viewGraph.commit()
  271. if isFirstUpdate {
  272. backend.show(window: window)
  273. isFirstUpdate = false
  274. }
  275. }
  276. func activate<Backend: BaseAppBackend>(backend: Backend) {
  277. guard let window = window as? Backend.Window else {
  278. fatalError("Scene updated with a backend incompatible with the window it was given")
  279. }
  280. backend.activate(window: window)
  281. }
  282. private func updateEnvironment<Backend: BaseAppBackend>(
  283. _ environment: inout EnvironmentValues,
  284. viewLayoutResult: ViewLayoutResult,
  285. outerColorScheme: ColorScheme,
  286. backend: Backend
  287. ) {
  288. preferredColorScheme = viewLayoutResult.preferences.preferredColorScheme
  289. // Update environment with preferred color scheme if provided
  290. if let preferredColorScheme, backend.canOverrideWindowColorScheme {
  291. environment.colorScheme = preferredColorScheme
  292. } else {
  293. // If the preferred color scheme just changed to nil, then we must
  294. // reset the environment's color scheme to the outer color scheme
  295. // provided by a higher scene or the system.
  296. environment.colorScheme = outerColorScheme
  297. }
  298. }
  299. }