226 lines
9.4 KiB
Swift
226 lines
9.4 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|