mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 23:24:32 +00:00
264 lines
8.9 KiB
Swift
264 lines
8.9 KiB
Swift
#if os(macOS)
|
|
import AppKit
|
|
import SwiftUI
|
|
import SkyLightWindow
|
|
import QuartzCore
|
|
import Defaults
|
|
|
|
/// Manages custom OSD windows for volume, brightness, and keyboard backlight controls
|
|
/// Mimics macOS native OSD behavior with custom styling
|
|
@MainActor
|
|
final class CustomOSDWindowManager {
|
|
static let shared = CustomOSDWindowManager()
|
|
|
|
private var volumeWindows: [NSScreen: OSDWindow] = [:]
|
|
private var brightnessWindows: [NSScreen: OSDWindow] = [:]
|
|
private var backlightWindows: [NSScreen: OSDWindow] = [:]
|
|
|
|
private var hideWorkItem: DispatchWorkItem?
|
|
private let displayDuration: TimeInterval = 2.0
|
|
private let animationDuration: TimeInterval = 0.3
|
|
private var isInitialized = false
|
|
|
|
// Standard macOS OSD dimensions (approximate)
|
|
private let osdWidth: CGFloat = 200
|
|
private let osdHeight: CGFloat = 200
|
|
|
|
private init() {}
|
|
|
|
// MARK: - Public API
|
|
|
|
func showVolume(value: CGFloat, isMuted: Bool = false, icon: String = "") {
|
|
guard Defaults[.enableCustomOSD], isInitialized else { return }
|
|
show(type: .volume, value: value, icon: icon)
|
|
}
|
|
|
|
func showBrightness(value: CGFloat) {
|
|
guard Defaults[.enableCustomOSD], isInitialized else { return }
|
|
show(type: .brightness, value: value, icon: "")
|
|
}
|
|
|
|
func showBacklight(value: CGFloat) {
|
|
guard Defaults[.enableCustomOSD], isInitialized else { return }
|
|
show(type: .backlight, value: value, icon: "")
|
|
}
|
|
|
|
func initialize() {
|
|
isInitialized = true
|
|
}
|
|
|
|
// MARK: - Private Implementation
|
|
|
|
private func show(type: SneakContentType, value: CGFloat, icon: String) {
|
|
let screens = NSScreen.screens
|
|
guard !screens.isEmpty else { return }
|
|
|
|
// Close other windows first
|
|
hideAllWindowsExcept(type: type)
|
|
|
|
// Show on all screens
|
|
for screen in screens {
|
|
let window = ensureWindow(for: type, screen: screen)
|
|
updateContent(window: window, type: type, value: value, icon: icon)
|
|
|
|
let targetFrame = calculateFrame(for: screen)
|
|
|
|
if window.nsWindow.alphaValue <= 0.01 {
|
|
// Initial presentation
|
|
presentWindow(window, targetFrame: targetFrame)
|
|
} else {
|
|
// Update existing window
|
|
window.nsWindow.setFrame(targetFrame, display: true)
|
|
window.nsWindow.orderFrontRegardless()
|
|
}
|
|
}
|
|
|
|
scheduleHide(for: type)
|
|
}
|
|
|
|
private func ensureWindow(for type: SneakContentType, screen: NSScreen) -> OSDWindow {
|
|
switch type {
|
|
case .volume:
|
|
if let existing = volumeWindows[screen] {
|
|
return existing
|
|
}
|
|
let window = createWindow(for: type, screen: screen)
|
|
volumeWindows[screen] = window
|
|
return window
|
|
case .brightness:
|
|
if let existing = brightnessWindows[screen] {
|
|
return existing
|
|
}
|
|
let window = createWindow(for: type, screen: screen)
|
|
brightnessWindows[screen] = window
|
|
return window
|
|
case .backlight:
|
|
if let existing = backlightWindows[screen] {
|
|
return existing
|
|
}
|
|
let window = createWindow(for: type, screen: screen)
|
|
backlightWindows[screen] = window
|
|
return window
|
|
default:
|
|
fatalError("Unsupported OSD type: \(type)")
|
|
}
|
|
}
|
|
|
|
private func createWindow(for type: SneakContentType, screen: NSScreen) -> OSDWindow {
|
|
let osdView = CustomOSDView(type: .constant(type), value: .constant(0), icon: .constant(""))
|
|
let hostingView = NSHostingView(rootView: osdView)
|
|
|
|
let frame = calculateFrame(for: screen)
|
|
let window = NSWindow(
|
|
contentRect: frame,
|
|
styleMask: [.borderless, .nonactivatingPanel],
|
|
backing: .buffered,
|
|
defer: false
|
|
)
|
|
|
|
window.isOpaque = false
|
|
window.backgroundColor = .clear
|
|
window.level = .statusBar + 1
|
|
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle, .fullScreenAuxiliary]
|
|
window.hasShadow = false
|
|
window.contentView = hostingView
|
|
window.alphaValue = 0
|
|
|
|
// Delegate to SkyLight for proper rendering
|
|
SkyLightOperator.shared.delegateWindow(window)
|
|
|
|
return OSDWindow(nsWindow: window, hostingView: hostingView, type: type)
|
|
}
|
|
|
|
private func updateContent(window: OSDWindow, type: SneakContentType, value: CGFloat, icon: String) {
|
|
let osdView = CustomOSDView(type: .constant(type), value: .constant(value), icon: .constant(icon))
|
|
window.hostingView.rootView = osdView
|
|
}
|
|
|
|
private func calculateFrame(for screen: NSScreen) -> NSRect {
|
|
let screenFrame = screen.frame
|
|
let centerX = screenFrame.midX - (osdWidth / 2)
|
|
// Position at bottom center, similar to native macOS OSD
|
|
let bottomOffset: CGFloat = 120 // Distance from bottom of screen
|
|
let centerY = screenFrame.minY + bottomOffset
|
|
|
|
return NSRect(x: centerX, y: centerY, width: osdWidth, height: osdHeight)
|
|
}
|
|
|
|
private func presentWindow(_ window: OSDWindow, targetFrame: NSRect) {
|
|
let startFrame = targetFrame.offsetBy(dx: 0, dy: -20)
|
|
window.nsWindow.setFrame(startFrame, display: true)
|
|
window.nsWindow.alphaValue = 0
|
|
window.nsWindow.orderFrontRegardless()
|
|
|
|
NSAnimationContext.runAnimationGroup { context in
|
|
context.duration = animationDuration
|
|
context.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
|
window.nsWindow.animator().setFrame(targetFrame, display: true)
|
|
window.nsWindow.animator().alphaValue = 1
|
|
}
|
|
}
|
|
|
|
private func hideAllWindowsExcept(type: SneakContentType) {
|
|
if type != .volume {
|
|
for window in volumeWindows.values where window.nsWindow.alphaValue > 0.01 {
|
|
hideWindowImmediately(window)
|
|
}
|
|
}
|
|
if type != .brightness {
|
|
for window in brightnessWindows.values where window.nsWindow.alphaValue > 0.01 {
|
|
hideWindowImmediately(window)
|
|
}
|
|
}
|
|
if type != .backlight {
|
|
for window in backlightWindows.values where window.nsWindow.alphaValue > 0.01 {
|
|
hideWindowImmediately(window)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func hideWindowImmediately(_ window: OSDWindow) {
|
|
NSAnimationContext.runAnimationGroup { context in
|
|
context.duration = 0.15
|
|
context.timingFunction = CAMediaTimingFunction(name: .easeIn)
|
|
window.nsWindow.animator().alphaValue = 0
|
|
} completionHandler: {
|
|
window.nsWindow.orderOut(nil)
|
|
}
|
|
}
|
|
|
|
private func scheduleHide(for type: SneakContentType) {
|
|
hideWorkItem?.cancel()
|
|
|
|
let workItem = DispatchWorkItem { [weak self] in
|
|
guard let self else { return }
|
|
Task { @MainActor in
|
|
self.hideWindow(for: type)
|
|
}
|
|
}
|
|
|
|
hideWorkItem = workItem
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + displayDuration, execute: workItem)
|
|
}
|
|
|
|
private func hideWindow(for type: SneakContentType) {
|
|
let windows: [OSDWindow]
|
|
|
|
switch type {
|
|
case .volume:
|
|
windows = Array(volumeWindows.values)
|
|
case .brightness:
|
|
windows = Array(brightnessWindows.values)
|
|
case .backlight:
|
|
windows = Array(backlightWindows.values)
|
|
default:
|
|
return
|
|
}
|
|
|
|
for window in windows {
|
|
|
|
let currentFrame = window.nsWindow.frame
|
|
let hideFrame = currentFrame.offsetBy(dx: 0, dy: -20)
|
|
|
|
NSAnimationContext.runAnimationGroup { context in
|
|
context.duration = animationDuration
|
|
context.timingFunction = CAMediaTimingFunction(name: .easeIn)
|
|
window.nsWindow.animator().setFrame(hideFrame, display: true)
|
|
window.nsWindow.animator().alphaValue = 0
|
|
} completionHandler: {
|
|
window.nsWindow.orderOut(nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Cleanup
|
|
|
|
func tearDown() {
|
|
hideWorkItem?.cancel()
|
|
hideWorkItem = nil
|
|
|
|
for window in volumeWindows.values {
|
|
window.nsWindow.orderOut(nil)
|
|
}
|
|
for window in brightnessWindows.values {
|
|
window.nsWindow.orderOut(nil)
|
|
}
|
|
for window in backlightWindows.values {
|
|
window.nsWindow.orderOut(nil)
|
|
}
|
|
|
|
volumeWindows.removeAll()
|
|
brightnessWindows.removeAll()
|
|
backlightWindows.removeAll()
|
|
}
|
|
}
|
|
|
|
// MARK: - OSD Window Container
|
|
|
|
private struct OSDWindow {
|
|
let nsWindow: NSWindow
|
|
let hostingView: NSHostingView<CustomOSDView>
|
|
let type: SneakContentType
|
|
}
|
|
|
|
#endif
|