/* * Atoll (DynamicIsland) * Copyright (C) 2024-2026 Atoll Contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #if os(macOS) import AppKit import SwiftUI import SkyLightWindow import QuartzCore import Defaults import Combine @MainActor final class CircularHUDWindowManager { static let shared = CircularHUDWindowManager() private var windows: [NSScreen: OSDWindow] = [:] private var hideWorkItem: DispatchWorkItem? private let displayDuration: TimeInterval = 2.0 private let animationDuration: TimeInterval = 0.2 private var cancellables = Set() // Layout constants removed in favor of dynamic calculation private init() { setupSizeObserver() } private func setupSizeObserver() { Defaults.publisher(.circularHUDSize, options: []).sink { [weak self] _ in guard let self = self else { return } Task { @MainActor in self.updateWindowLayout() } }.store(in: &cancellables) } private func updateWindowLayout() { guard !windows.isEmpty else { return } for (screen, window) in windows { let screenFrame = screen.frame let hudSize = Defaults[.circularHUDSize] let windowSize = hudSize + 80 // Padding let x = screenFrame.midX - (windowSize / 2) let y = screenFrame.midY - (windowSize / 2) let newFrame = NSRect(x: x, y: y, width: windowSize, height: windowSize) window.nsWindow.setFrame(newFrame, display: true, animate: true) } } func show(type: SneakContentType, value: CGFloat, icon: String = "", onScreen targetScreen: NSScreen? = nil) { guard Defaults[.enableCircularHUD] else { return } let screens = targetScreen.map { [$0] } ?? NSScreen.screens guard !screens.isEmpty else { return } // Show on target screen(s) for screen in screens { let windowContext = ensureWindow(for: type, screen: screen) updateContent(window: windowContext, type: type, value: value, icon: icon) // Ensure visible windowContext.nsWindow.orderFrontRegardless() NSAnimationContext.runAnimationGroup { context in context.duration = animationDuration windowContext.nsWindow.animator().alphaValue = 1 } } scheduleHide() } private func ensureWindow(for type: SneakContentType, screen: NSScreen) -> OSDWindow { if let existing = windows[screen] { return existing } let osdView = CircularHUDView(type: .constant(type), value: .constant(0), icon: .constant("")) let hostingView = NSHostingView(rootView: osdView) let screenFrame = screen.frame let hudSize = Defaults[.circularHUDSize] let windowSize = hudSize + 80 // Padding // Position at center of screen let x = screenFrame.midX - (windowSize / 2) let y = screenFrame.midY - (windowSize / 2) let frame = NSRect(x: x, y: y, width: windowSize, height: windowSize) let win = NSWindow( contentRect: frame, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false ) win.isOpaque = false win.backgroundColor = .clear win.level = .screenSaver win.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle, .fullScreenAuxiliary] win.hasShadow = false win.contentView = hostingView win.alphaValue = 0 SkyLightOperator.shared.delegateWindow(win) let windowStruct = OSDWindow(nsWindow: win, hostingView: hostingView, type: type) windows[screen] = windowStruct return windowStruct } private func updateContent(window: OSDWindow, type: SneakContentType, value: CGFloat, icon: String) { let osdView = CircularHUDView(type: .constant(type), value: .constant(value), icon: .constant(icon)) window.hostingView.rootView = osdView } private func scheduleHide() { hideWorkItem?.cancel() let workItem = DispatchWorkItem { [weak self] in guard let self else { return } Task { @MainActor in self.hideWindow() } } hideWorkItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + displayDuration, execute: workItem) } private func hideWindow() { for window in windows.values { NSAnimationContext.runAnimationGroup { context in context.duration = animationDuration window.nsWindow.animator().alphaValue = 0 } completionHandler: { // Keep window around but hidden for faster subsequent shows } } } // Helper struct private struct OSDWindow { let nsWindow: NSWindow let hostingView: NSHostingView let type: SneakContentType } } #endif