WindowFullScreenObserver.swift 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import AppKit
  2. import SwiftUI
  3. struct WindowFullScreenObserver: NSViewRepresentable {
  4. @Binding var isFullScreen: Bool
  5. func makeCoordinator() -> Coordinator {
  6. Coordinator(isFullScreen: $isFullScreen)
  7. }
  8. func makeNSView(context: Context) -> WindowObservationView {
  9. let view = WindowObservationView()
  10. view.onWindowChange = { [weak coordinator = context.coordinator] window in
  11. coordinator?.observe(window: window)
  12. }
  13. return view
  14. }
  15. func updateNSView(_ nsView: WindowObservationView, context: Context) {
  16. context.coordinator.observe(window: nsView.window)
  17. }
  18. final class Coordinator {
  19. @Binding private var isFullScreen: Bool
  20. private var observers: [NSObjectProtocol] = []
  21. private weak var observedWindow: NSWindow?
  22. init(isFullScreen: Binding<Bool>) {
  23. _isFullScreen = isFullScreen
  24. }
  25. func observe(window: NSWindow?) {
  26. guard window !== observedWindow else {
  27. if let window {
  28. updateFullScreenState(for: window)
  29. }
  30. return
  31. }
  32. removeObservers()
  33. observedWindow = window
  34. guard let window else {
  35. isFullScreen = false
  36. return
  37. }
  38. updateFullScreenState(for: window)
  39. let center = NotificationCenter.default
  40. observers = [
  41. center.addObserver(
  42. forName: NSWindow.didEnterFullScreenNotification,
  43. object: window,
  44. queue: .main
  45. ) { [weak self] _ in
  46. self?.updateFullScreenState(for: window)
  47. },
  48. center.addObserver(
  49. forName: NSWindow.didExitFullScreenNotification,
  50. object: window,
  51. queue: .main
  52. ) { [weak self] _ in
  53. self?.updateFullScreenState(for: window)
  54. },
  55. ]
  56. }
  57. private func updateFullScreenState(for window: NSWindow) {
  58. isFullScreen = window.styleMask.contains(.fullScreen)
  59. }
  60. private func removeObservers() {
  61. observers.forEach { NotificationCenter.default.removeObserver($0) }
  62. observers.removeAll()
  63. }
  64. deinit {
  65. removeObservers()
  66. }
  67. }
  68. }
  69. final class WindowObservationView: NSView {
  70. var onWindowChange: ((NSWindow?) -> Void)?
  71. override func viewDidMoveToWindow() {
  72. super.viewDidMoveToWindow()
  73. onWindowChange?(window)
  74. }
  75. override func hitTest(_ point: NSPoint) -> NSView? {
  76. nil
  77. }
  78. }
  79. private struct FullScreenLayoutModifier: ViewModifier {
  80. @State private var isFullScreen = false
  81. func body(content: Content) -> some View {
  82. VStack(spacing: 0) {
  83. if isFullScreen {
  84. AppTheme.sidebarBackground
  85. .frame(height: AppWindow.fullScreenTopInset)
  86. .frame(maxWidth: .infinity)
  87. .allowsHitTesting(false)
  88. }
  89. content
  90. .frame(maxWidth: .infinity, maxHeight: .infinity)
  91. }
  92. .frame(maxWidth: .infinity, maxHeight: .infinity)
  93. .background {
  94. WindowFullScreenObserver(isFullScreen: $isFullScreen)
  95. }
  96. }
  97. }
  98. extension View {
  99. func fullScreenLayoutWhenNeeded() -> some View {
  100. modifier(FullScreenLayoutModifier())
  101. }
  102. }