120 lines
3.8 KiB
Swift
120 lines
3.8 KiB
Swift
import AppKit
|
||
import SwiftUI
|
||
|
||
/// 悬浮窗口控制器:管理毛玻璃 + SwiftUI 视图嵌入 + 窗口显隐
|
||
final class FloatingWindowController: NSWindowController {
|
||
private var hostingView: NSHostingView<ContentView>?
|
||
private var visualEffect: NSVisualEffectView!
|
||
|
||
convenience init() {
|
||
let window = FloatingWindow()
|
||
window.isReleasedWhenClosed = false
|
||
self.init(window: window)
|
||
setupVisualEffect()
|
||
}
|
||
|
||
// MARK: - 毛玻璃(只做合成锚点,alpha 始终=1)
|
||
|
||
private func setupVisualEffect() {
|
||
guard let window = window else { return }
|
||
|
||
let ve = NSVisualEffectView()
|
||
ve.wantsLayer = true
|
||
ve.material = .hudWindow
|
||
ve.blendingMode = .behindWindow
|
||
ve.state = .active
|
||
ve.alphaValue = 1.0 // 始终不透明,维持窗口合成通道
|
||
ve.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
ve.layer?.cornerRadius = 16
|
||
ve.layer?.masksToBounds = true
|
||
ve.layer?.borderWidth = 0
|
||
ve.layer?.borderColor = NSColor.clear.cgColor
|
||
ve.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.35).cgColor
|
||
|
||
self.visualEffect = ve
|
||
|
||
guard let contentView = window.contentView else { return }
|
||
contentView.addSubview(ve, positioned: .below, relativeTo: contentView.subviews.first)
|
||
|
||
NSLayoutConstraint.activate([
|
||
ve.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||
ve.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||
ve.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||
ve.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||
])
|
||
}
|
||
|
||
// MARK: - SwiftUI 嵌入
|
||
|
||
func embed(rootView: ContentView) {
|
||
guard let window = window, let contentView = window.contentView else { return }
|
||
|
||
let hv = NSHostingView(rootView: rootView)
|
||
hv.wantsLayer = true
|
||
hv.layer?.cornerRadius = 16
|
||
hv.layer?.masksToBounds = true
|
||
hv.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
contentView.addSubview(hv)
|
||
|
||
NSLayoutConstraint.activate([
|
||
hv.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||
hv.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||
hv.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||
hv.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||
])
|
||
|
||
self.hostingView = hv
|
||
}
|
||
|
||
// MARK: - 显隐
|
||
|
||
func toggleVisible() {
|
||
guard let window = window else { return }
|
||
if window.isVisible {
|
||
window.orderOut(nil)
|
||
} else {
|
||
positionNearMenuBar()
|
||
window.makeKeyAndOrderFront(nil)
|
||
NSApp.activate(ignoringOtherApps: false)
|
||
}
|
||
}
|
||
|
||
func show() {
|
||
guard let window = window else { return }
|
||
if !window.isVisible {
|
||
positionNearMenuBar()
|
||
}
|
||
window.makeKeyAndOrderFront(nil)
|
||
NSApp.activate(ignoringOtherApps: false)
|
||
}
|
||
|
||
func hide() {
|
||
window?.orderOut(nil)
|
||
}
|
||
|
||
var isVisible: Bool {
|
||
window?.isVisible ?? false
|
||
}
|
||
|
||
// MARK: - 尺寸 & 位置
|
||
|
||
func resize(width: CGFloat, height: CGFloat) {
|
||
guard let window = window else { return }
|
||
var frame = window.frame
|
||
frame.size.width = width
|
||
frame.size.height = height
|
||
window.setFrame(frame, display: true, animate: true)
|
||
}
|
||
|
||
private func positionNearMenuBar() {
|
||
guard let window = window, let screen = NSScreen.main else { return }
|
||
let screenFrame = screen.visibleFrame
|
||
window.setFrameOrigin(NSPoint(
|
||
x: screenFrame.maxX - window.frame.width - 24,
|
||
y: screenFrame.maxY - window.frame.height - 24
|
||
))
|
||
}
|
||
}
|