import AppKit import QuartzCore final class HoverChromeContainerView: NSView { private let content: NSView private let chrome: CanvasChromeOverlayView private var tracking: NSTrackingArea? var onClose: (() -> Void)? init(containing content: NSView) { self.content = content self.chrome = CanvasChromeOverlayView(frame: .zero) super.init(frame: .zero) self.wantsLayer = true self.layer?.cornerRadius = 12 self.layer?.masksToBounds = true self.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor self.content.translatesAutoresizingMaskIntoConstraints = false self.addSubview(self.content) self.chrome.translatesAutoresizingMaskIntoConstraints = false self.chrome.alphaValue = 0 self.chrome.onClose = { [weak self] in self?.onClose?() } self.addSubview(self.chrome) NSLayoutConstraint.activate([ self.content.leadingAnchor.constraint(equalTo: self.leadingAnchor), self.content.trailingAnchor.constraint(equalTo: self.trailingAnchor), self.content.topAnchor.constraint(equalTo: self.topAnchor), self.content.bottomAnchor.constraint(equalTo: self.bottomAnchor), self.chrome.leadingAnchor.constraint(equalTo: self.leadingAnchor), self.chrome.trailingAnchor.constraint(equalTo: self.trailingAnchor), self.chrome.topAnchor.constraint(equalTo: self.topAnchor), self.chrome.bottomAnchor.constraint(equalTo: self.bottomAnchor), ]) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } override func updateTrackingAreas() { super.updateTrackingAreas() if let tracking { self.removeTrackingArea(tracking) } let area = NSTrackingArea( rect: self.bounds, options: [.activeAlways, .mouseEnteredAndExited, .inVisibleRect], owner: self, userInfo: nil) self.addTrackingArea(area) self.tracking = area } private final class CanvasDragHandleView: NSView { override func mouseDown(with event: NSEvent) { self.window?.performDrag(with: event) } override func acceptsFirstMouse(for _: NSEvent?) -> Bool { true } } private final class CanvasResizeHandleView: NSView { private var startPoint: NSPoint = .zero private var startFrame: NSRect = .zero override func acceptsFirstMouse(for _: NSEvent?) -> Bool { true } override func mouseDown(with event: NSEvent) { guard let window else { return } _ = window.makeFirstResponder(self) self.startPoint = NSEvent.mouseLocation self.startFrame = window.frame super.mouseDown(with: event) } override func mouseDragged(with _: NSEvent) { guard let window else { return } let current = NSEvent.mouseLocation let dx = current.x - self.startPoint.x let dy = current.y - self.startPoint.y var frame = self.startFrame frame.size.width = max(CanvasLayout.minPanelSize.width, frame.size.width + dx) frame.origin.y += dy frame.size.height = max(CanvasLayout.minPanelSize.height, frame.size.height - dy) if let screen = window.screen { frame = CanvasWindowController.constrainFrame(frame, toVisibleFrame: screen.visibleFrame) } window.setFrame(frame, display: true) } } private final class CanvasChromeOverlayView: NSView { var onClose: (() -> Void)? private let dragHandle = CanvasDragHandleView(frame: .zero) private let resizeHandle = CanvasResizeHandleView(frame: .zero) private final class PassthroughVisualEffectView: NSVisualEffectView { override func hitTest(_: NSPoint) -> NSView? { nil } } private let closeBackground: NSVisualEffectView = { let v = PassthroughVisualEffectView(frame: .zero) v.material = .hudWindow v.blendingMode = .withinWindow v.state = .active v.appearance = NSAppearance(named: .vibrantDark) v.wantsLayer = true v.layer?.cornerRadius = 10 v.layer?.masksToBounds = true v.layer?.borderWidth = 1 v.layer?.borderColor = NSColor.white.withAlphaComponent(0.22).cgColor v.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.28).cgColor v.layer?.shadowColor = NSColor.black.withAlphaComponent(0.35).cgColor v.layer?.shadowOpacity = 0.35 v.layer?.shadowRadius = 8 v.layer?.shadowOffset = .zero return v }() private let closeButton: NSButton = { let cfg = NSImage.SymbolConfiguration(pointSize: 8, weight: .semibold) let img = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Close")? .withSymbolConfiguration(cfg) ?? NSImage(size: NSSize(width: 18, height: 18)) let btn = NSButton(image: img, target: nil, action: nil) btn.isBordered = false btn.bezelStyle = .regularSquare btn.imageScaling = .scaleProportionallyDown btn.contentTintColor = NSColor.white.withAlphaComponent(0.92) btn.toolTip = "Close" return btn }() override init(frame frameRect: NSRect) { super.init(frame: frameRect) self.wantsLayer = true self.layer?.cornerRadius = 12 self.layer?.masksToBounds = true self.layer?.borderWidth = 1 self.layer?.borderColor = NSColor.black.withAlphaComponent(0.18).cgColor self.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.02).cgColor self.dragHandle.translatesAutoresizingMaskIntoConstraints = false self.dragHandle.wantsLayer = true self.dragHandle.layer?.backgroundColor = NSColor.clear.cgColor self.addSubview(self.dragHandle) self.resizeHandle.translatesAutoresizingMaskIntoConstraints = false self.resizeHandle.wantsLayer = true self.resizeHandle.layer?.backgroundColor = NSColor.clear.cgColor self.addSubview(self.resizeHandle) self.closeBackground.translatesAutoresizingMaskIntoConstraints = false self.addSubview(self.closeBackground) self.closeButton.translatesAutoresizingMaskIntoConstraints = false self.closeButton.target = self self.closeButton.action = #selector(self.handleClose) self.addSubview(self.closeButton) NSLayoutConstraint.activate([ self.dragHandle.leadingAnchor.constraint(equalTo: self.leadingAnchor), self.dragHandle.trailingAnchor.constraint(equalTo: self.trailingAnchor), self.dragHandle.topAnchor.constraint(equalTo: self.topAnchor), self.dragHandle.heightAnchor.constraint(equalToConstant: 30), self.closeBackground.centerXAnchor.constraint(equalTo: self.closeButton.centerXAnchor), self.closeBackground.centerYAnchor.constraint(equalTo: self.closeButton.centerYAnchor), self.closeBackground.widthAnchor.constraint(equalToConstant: 20), self.closeBackground.heightAnchor.constraint(equalToConstant: 20), self.closeButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -8), self.closeButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 8), self.closeButton.widthAnchor.constraint(equalToConstant: 16), self.closeButton.heightAnchor.constraint(equalToConstant: 16), self.resizeHandle.trailingAnchor.constraint(equalTo: self.trailingAnchor), self.resizeHandle.bottomAnchor.constraint(equalTo: self.bottomAnchor), self.resizeHandle.widthAnchor.constraint(equalToConstant: 18), self.resizeHandle.heightAnchor.constraint(equalToConstant: 18), ]) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } override func hitTest(_ point: NSPoint) -> NSView? { // When the chrome is hidden, do not intercept any mouse events (let the WKWebView receive them). guard self.alphaValue > 0.02 else { return nil } if self.closeButton.frame.contains(point) { return self.closeButton } if self.dragHandle.frame.contains(point) { return self.dragHandle } if self.resizeHandle.frame.contains(point) { return self.resizeHandle } return nil } @objc private func handleClose() { self.onClose?() } } override func mouseEntered(with _: NSEvent) { NSAnimationContext.runAnimationGroup { ctx in ctx.duration = 0.12 ctx.timingFunction = CAMediaTimingFunction(name: .easeOut) self.chrome.animator().alphaValue = 1 } } override func mouseExited(with _: NSEvent) { NSAnimationContext.runAnimationGroup { ctx in ctx.duration = 0.16 ctx.timingFunction = CAMediaTimingFunction(name: .easeOut) self.chrome.animator().alphaValue = 0 } } }