import AppKit import Foundation import PDFKit import QuickLookThumbnailing import SwiftUI /// "Float": a borderless preview that hovers over other windows, the same /// gesture Tangerine offers from the Control wheel. final class FloatPanel: NSObject { static let shared = FloatPanel() private var window: NSWindow? private var urls: [URL] = [] private var index = 0 private override init() { super.init() } @MainActor func show(urls: [URL]) { self.urls = urls index = 0 let window = ensureWindow() if let screen = NSScreen.screens.first(where: { $0.frame.contains(NSEvent.mouseLocation) }) ?? NSScreen.main { let frame = screen.visibleFrame let size = CGSize(width: 420, height: 320) window.setContentSize(size) window.setFrameOrigin(CGPoint( x: min(max(frame.minX + 16, NSEvent.mouseLocation.x - size.width / 2), frame.maxX - size.width - 16), y: min(max(frame.minY + 16, NSEvent.mouseLocation.y - size.height - 24), frame.maxY - size.height - 16) )) } window.alphaValue = 0 window.orderFrontRegardless() NSAnimationContext.runAnimationGroup { context in context.duration = 0.18 window.animator().alphaValue = 1 } render() } @MainActor private func ensureWindow() -> NSWindow { if let window { return window } let window = OverlayWindow( contentRect: NSRect(x: 0, y: 0, width: 420, height: 320), styleMask: [.borderless], backing: .buffered, defer: false ) window.isOpaque = false window.backgroundColor = .clear window.hasShadow = true window.level = .floating window.isMovableByWindowBackground = true window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] window.contentView = NSHostingView(rootView: FloatView(controller: self)) self.window = window return window } @MainActor func render() { guard let window, let content = window.contentView as? NSHostingView else { return } content.rootView = FloatView(controller: self) } @MainActor func close() { guard let window else { return } NSAnimationContext.runAnimationGroup { context in context.duration = 0.15 window.animator().alphaValue = 0 } completionHandler: { window.orderOut(nil) } urls = [] } @MainActor func step(_ delta: Int) { guard !urls.isEmpty else { return } index = ((index + delta) % urls.count + urls.count) % urls.count render() } var currentURL: URL? { urls.indices.contains(index) ? urls[index] : nil } var counter: String? { urls.count > 1 ? "\(index + 1) of \(urls.count)" : nil } @MainActor func openInDefaultApp() { guard let url = currentURL else { return } NSWorkspace.shared.open(url) } @MainActor func reveal() { guard let url = currentURL else { return } NSWorkspace.shared.activateFileViewerSelecting([url]) } } struct FloatView: View { let controller: FloatPanel @State private var image: NSImage? @State private var failure: String? @State private var isLoading = true var body: some View { VStack(spacing: 0) { Group { if let image { Image(nsImage: image) .resizable() .aspectRatio(contentMode: .fit) .padding(10) } else if let failure { VStack(spacing: 6) { Image(systemName: "eye.slash") .font(.system(size: 22)) Text(failure) .font(.system(size: 11)) .multilineTextAlignment(.center) } .foregroundStyle(.secondary) .padding() } else { ProgressView() } } .frame(maxWidth: .infinity, maxHeight: .infinity) Divider().opacity(0.35) HStack(spacing: 10) { Image(systemName: "doc") .font(.system(size: 11)) Text(controller.currentURL?.lastPathComponent ?? "") .font(.system(size: 11, weight: .medium)) .lineLimit(1) .truncationMode(.middle) if let counter = controller.counter { Text(counter) .font(.system(size: 10)) .foregroundStyle(.secondary) } Spacer() Button { controller.step(-1) } label: { Image(systemName: "chevron.left") } .buttonStyle(.plain) Button { controller.step(1) } label: { Image(systemName: "chevron.right") } .buttonStyle(.plain) Button("Open") { controller.openInDefaultApp() } .buttonStyle(.plain) .font(.system(size: 11, weight: .medium)) .foregroundStyle(Color.accentColor) Button { controller.close() } label: { Image(systemName: "xmark") } .buttonStyle(.plain) } .padding(.horizontal, 12) .padding(.vertical, 8) } .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: 14, style: .continuous) .strokeBorder(Color.primary.opacity(0.1)) ) .task(id: controller.currentURL) { await load() } } private func load() async { guard let url = controller.currentURL else { image = nil failure = "Nothing to preview" isLoading = false return } isLoading = true failure = nil // Images and PDFs get a real render; everything else falls back to a // Quick Look thumbnail. if FileKind.category(of: url) == .image, let loaded = NSImage(contentsOf: url) { image = loaded isLoading = false return } if url.pathExtension.lowercased() == "pdf", let page = PDFKitPreview.image(for: url) { image = page isLoading = false return } let request = QLThumbnailGenerator.Request( fileAt: url, size: CGSize(width: 900, height: 700), scale: NSScreen.main?.backingScaleFactor ?? 2, representationTypes: .all ) let representation = try? await QLThumbnailGenerator.shared.generateBestRepresentation(for: request) if let cgImage = representation?.cgImage { image = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width / 2, height: cgImage.height / 2)) } else { image = nil failure = "No preview available for .\(url.pathExtension)" } isLoading = false } } enum PDFKitPreview { static func image(for url: URL) -> NSImage? { guard let document = PDFDocument(url: url), let page = document.page(at: 0) else { return nil } let bounds = page.bounds(for: .mediaBox) let scale: CGFloat = 1.6 let size = CGSize(width: bounds.width * scale, height: bounds.height * scale) let thumbnail = page.thumbnail(of: size, for: .mediaBox) return thumbnail } }