From 3b2ef22c88007f69b8c94b455cb3bef5acb21a96 Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Sat, 10 Dec 2022 22:58:11 +0800 Subject: [PATCH] CocoaExtension // Introduce Alexis Bridoux's NSWindowPositioner. --- .../CocoaExtension_NSWindowPositioner.swift | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSWindowPositioner.swift diff --git a/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSWindowPositioner.swift b/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSWindowPositioner.swift new file mode 100644 index 00000000..104521e3 --- /dev/null +++ b/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSWindowPositioner.swift @@ -0,0 +1,158 @@ +// Free to use +// Written by Alexis Bridoux - https://github.com/ABridoux +// Ref: https://gist.github.com/ABridoux/b935c21c7ead92033d39b357fae6366b + +import Cocoa +import SwiftUI + +// MARK: Model + +extension NSWindow { + public struct Position { + public static let defaultPadding: CGFloat = 16 + + public var vertical: Vertical + public var horizontal: Horizontal + public var padding = Self.defaultPadding + } +} + +extension NSWindow.Position { + public enum Horizontal { + case left, center, right + } + + public enum Vertical { + case top, center, bottom + } +} + +// MARK: Logic + +extension NSWindow.Position { + public func value(forWindow windowRect: CGRect, inScreen screenRect: CGRect) -> CGPoint { + let xPosition = horizontal.valueFor( + screenRange: screenRect.minX.., + width: CGFloat, + padding: CGFloat + ) + -> CGFloat + { + switch self { + case .left: return screenRange.lowerBound + padding + case .center: return (screenRange.upperBound + screenRange.lowerBound - width) / 2 + case .right: return screenRange.upperBound - width - padding + } + } +} + +extension NSWindow.Position.Vertical { + public func valueFor( + screenRange: Range, + height: CGFloat, + padding: CGFloat + ) + -> CGFloat + { + switch self { + case .top: return screenRange.upperBound - height - padding + case .center: return (screenRange.upperBound + screenRange.lowerBound - height) / 2 + case .bottom: return screenRange.lowerBound + padding + } + } +} + +// MARK: - AppKit extension + +extension NSWindow { + public func setPosition(_ position: Position, in screen: NSScreen?) { + guard let visibleFrame = (screen ?? self.screen)?.visibleFrame else { return } + let origin = position.value(forWindow: frame, inScreen: visibleFrame) + setFrameOrigin(origin) + } + + public func setPosition( + vertical: Position.Vertical, + horizontal: Position.Horizontal, + padding: CGFloat = Position.defaultPadding, + screen: NSScreen? = nil + ) { + setPosition( + Position(vertical: vertical, horizontal: horizontal, padding: padding), + in: screen + ) + } +} + +// MARK: - SwiftUI modifier + +#if canImport(SwiftUI) + + /// - note: Idea from [LostMoa](https://lostmoa.com/blog/ReadingTheCurrentWindowInANewSwiftUILifecycleApp/) + @available(macOS 10.15, *) + public struct HostingWindowFinder: NSViewRepresentable { + public var callback: (NSWindow?) -> Void + + public func makeNSView(context _: Self.Context) -> NSView { + let view = NSView() + DispatchQueue.main.async { self.callback(view.window) } + return view + } + + public func updateNSView(_ nsView: NSView, context _: Context) { + DispatchQueue.main.async { self.callback(nsView.window) } + } + } + + @available(macOS 10.15, *) + private struct WindowPositionModifier: ViewModifier { + let position: NSWindow.Position + let screen: NSScreen? + + func body(content: Content) -> some View { + content.background( + HostingWindowFinder { + $0?.setPosition(position, in: screen) + } + ) + } + } + + @available(macOS 10.15, *) + extension View { + public func hostingWindowPosition( + vertical: NSWindow.Position.Vertical, + horizontal: NSWindow.Position.Horizontal, + padding: CGFloat = NSWindow.Position.defaultPadding, + screen: NSScreen? = nil + ) -> some View { + modifier( + WindowPositionModifier( + position: NSWindow.Position( + vertical: vertical, + horizontal: horizontal, + padding: padding + ), + screen: screen + ) + ) + } + } +#endif