import AppKit import SwiftUI final class FloatingWidgetWindow: NSPanel { private let margin: CGFloat = 16 private let bottomOffset: CGFloat = 200 init() { super.init( contentRect: NSRect(x: 0, y: 0, width: 1, height: 1), styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false ) isFloatingPanel = true level = .floating collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] isOpaque = false backgroundColor = .clear hasShadow = true isMovableByWindowBackground = true hidesOnDeactivate = false animationBehavior = .none // Place the 1x1 placeholder in the bottom-right corner so the first // content measurement anchors to the screen's right edge. if let screen = NSScreen.main { let screenFrame = screen.visibleFrame setFrameOrigin(NSPoint( x: screenFrame.maxX - 1 - margin, y: screenFrame.minY + bottomOffset )) } let rootView = FloatingWidgetView { [weak self] size in self?.applyContentSize(size) } let hostingView = NSHostingView(rootView: rootView) contentView = hostingView // Size to the content's intrinsic size up front to avoid a 1x1 flash // before the first SwiftUI layout pass reports a measurement. applyContentSize(hostingView.fittingSize) } override var canBecomeKey: Bool { false } override var canBecomeMain: Bool { false } /// Resizes the window to fit its content while keeping the bottom-right /// corner anchored, so a growing bug count extends leftward instead of /// spilling off-screen. Clamped to the visible screen frame as a safety /// net (e.g. when the user has dragged the widget toward an edge). private func applyContentSize(_ size: CGSize) { guard size.width > 0, size.height > 0 else { return } // Skip no-op resizes to avoid a layout feedback loop with SwiftUI. guard abs(size.width - frame.width) > 0.5 || abs(size.height - frame.height) > 0.5 else { return } var newFrame = frame // Keep the right edge: grow leftward as the count widens. newFrame.origin.x = newFrame.maxX - size.width // Keep the bottom edge: grow upward when the priority row appears. newFrame.origin.y = newFrame.minY newFrame.size = size if let screen = NSScreen.main { let visible = screen.visibleFrame newFrame.origin.x = max(newFrame.origin.x, visible.minX) newFrame.origin.y = min( max(newFrame.origin.y, visible.minY), visible.maxY - size.height ) } setFrame(newFrame, display: true) } } struct FloatingWidgetView: View { @State private var bugStore = BugStore.shared let onSizeChange: (CGSize) -> Void private var priorityCounts: [(BugPriority, Int)] { let active = bugStore.activeBugs return BugPriority.allCases .filter { $0 != .unknown } .map { priority in (priority, active.filter { $0.priority == priority }.count) } .filter { $0.1 > 0 } } var body: some View { Button { AppDelegate.shared?.togglePopover() } label: { VStack(spacing: 4) { HStack(spacing: 4) { Image(systemName: "ant.fill") .font(.caption) Text("\(bugStore.activeBugs.count)") .font(.title3) .fontWeight(.bold) .monospacedDigit() } if !priorityCounts.isEmpty { HStack(spacing: 6) { ForEach(priorityCounts, id: \.0) { priority, count in HStack(spacing: 2) { Circle() .fill(priority.color) .frame(width: 6, height: 6) Text("\(count)") .font(.caption2) .fontWeight(.medium) .monospacedDigit() } } } } } .padding(.horizontal, 14) .padding(.vertical, 10) .background( RoundedRectangle(cornerRadius: 14) .fill(.ultraThinMaterial) .shadow(color: .black.opacity(0.15), radius: 8, x: 0, y: 4) ) } .buttonStyle(.plain) // Let the button take its intrinsic size so the window can fit it // exactly, regardless of the (initially 1x1) hosting container. .fixedSize() .background( GeometryReader { proxy in Color.clear .onAppear { onSizeChange(proxy.size) } .onChange(of: proxy.size) { _, newSize in onSizeChange(newSize) } } ) } }