| | |
| | | private var dragLayer: CALayer? |
| | | private var normalDragImage: CGImage? |
| | | private var copyDragImage: CGImage? |
| | | private var shredPreviewActive = false |
| | | private var shredSliceLayers: [CALayer] = [] |
| | | private var shredGuideLayers: [CALayer] = [] |
| | | private var currentCopyMode = false |
| | | private var dragWindow: NSWindow? |
| | | private var dragImageSize: NSSize = .zero |
| | |
| | | dragLayer.position = contentPoint |
| | | CATransaction.commit() |
| | | updateHoverTarget(at: screenPoint) |
| | | updateRemoveTagPreview(at: screenPoint) |
| | | hoveredTarget?.appDragLocationChanged(screenPoint: screenPoint, copy: currentCopyMode) |
| | | return |
| | | } |
| | |
| | | ) |
| | | dragWindow.setFrameOrigin(origin) |
| | | updateHoverTarget(at: screenPoint) |
| | | updateRemoveTagPreview(at: screenPoint) |
| | | hoveredTarget?.appDragLocationChanged(screenPoint: screenPoint, copy: currentCopyMode) |
| | | } |
| | | |
| | |
| | | currentCopyMode = copy |
| | | CATransaction.begin() |
| | | CATransaction.setDisableActions(true) |
| | | dragLayer?.contents = copy ? copyDragImage : normalDragImage |
| | | if shredPreviewActive { |
| | | let image = currentDragImage |
| | | shredSliceLayers.forEach { $0.contents = image } |
| | | } else { |
| | | dragLayer?.contents = currentDragImage |
| | | } |
| | | CATransaction.commit() |
| | | } |
| | | |
| | | private func endDragVisuals() { |
| | | stopShredPreview() |
| | | hoveredTarget?.appDragHoverChanged(active: false) |
| | | hoveredTarget = nil |
| | | dragLayer?.removeFromSuperlayer() |
| | |
| | | dragImageSize = .zero |
| | | } |
| | | |
| | | private var currentDragImage: CGImage? { |
| | | currentCopyMode ? copyDragImage : normalDragImage |
| | | } |
| | | |
| | | private func pruneDeadTargets() { |
| | | targets = targets.filter { $0.value.view != nil } |
| | | emptyTargets = emptyTargets.filter { $0.value.view != nil } |
| | |
| | | hoveredTarget?.appDragHoverChanged(active: false) |
| | | nextTarget?.appDragHoverChanged(active: true) |
| | | hoveredTarget = nextTarget |
| | | } |
| | | |
| | | private func updateRemoveTagPreview(at screenPoint: NSPoint) { |
| | | guard hasActiveDrag else { |
| | | setShredPreviewActive(false) |
| | | return |
| | | } |
| | | setShredPreviewActive(removeTagPreviewTarget(at: screenPoint) != nil) |
| | | } |
| | | |
| | | private func removeTagPreviewTarget(at screenPoint: NSPoint) -> AppEmptyDropReceivingView? { |
| | | guard dropTarget(at: screenPoint) == nil else { return nil } |
| | | let parts = activePayload.components(separatedBy: "\n") |
| | | guard let path = parts.first, !path.isEmpty else { return nil } |
| | | let source = parts.dropFirst().first ?? "" |
| | | |
| | | return emptyTargets.values |
| | | .compactMap { target -> (AppEmptyDropReceivingView, CGFloat)? in |
| | | guard let view = target.view, |
| | | let frame = view.screenFrame(), |
| | | frame.contains(screenPoint), |
| | | view.canPreviewEmptyDrop( |
| | | path: path, |
| | | source: source, |
| | | screenPoint: screenPoint, |
| | | copy: currentCopyMode |
| | | ) |
| | | else { return nil } |
| | | return (view, frame.width * frame.height) |
| | | } |
| | | .sorted { $0.1 < $1.1 } |
| | | .first?.0 |
| | | } |
| | | |
| | | private func setShredPreviewActive(_ active: Bool) { |
| | | guard active != shredPreviewActive else { return } |
| | | if active { |
| | | startShredPreview() |
| | | } else { |
| | | stopShredPreview() |
| | | } |
| | | } |
| | | |
| | | private func startShredPreview() { |
| | | guard let dragLayer, |
| | | let image = currentDragImage, |
| | | !shredPreviewActive, |
| | | dragImageSize.width > 0, |
| | | dragImageSize.height > 0 |
| | | else { return } |
| | | |
| | | shredPreviewActive = true |
| | | dragLayer.contents = nil |
| | | shredSliceLayers.removeAll() |
| | | shredGuideLayers.removeAll() |
| | | |
| | | let sliceCount = 7 |
| | | let sliceWidth = dragImageSize.width / CGFloat(sliceCount) |
| | | for index in 0..<sliceCount { |
| | | let x = CGFloat(index) * sliceWidth |
| | | let width = index == sliceCount - 1 |
| | | ? dragImageSize.width - x |
| | | : ceil(sliceWidth) |
| | | let slice = CALayer() |
| | | slice.frame = CGRect(x: x, y: 0, width: width, height: dragImageSize.height) |
| | | slice.contents = image |
| | | slice.contentsGravity = .resize |
| | | slice.contentsScale = NSScreen.main?.backingScaleFactor ?? 2 |
| | | slice.contentsRect = CGRect( |
| | | x: CGFloat(index) / CGFloat(sliceCount), |
| | | y: 0, |
| | | width: 1 / CGFloat(sliceCount), |
| | | height: 1 |
| | | ) |
| | | slice.actions = [ |
| | | "position": NSNull(), |
| | | "opacity": NSNull(), |
| | | "transform": NSNull() |
| | | ] |
| | | dragLayer.addSublayer(slice) |
| | | addShredAnimation(to: slice, index: index) |
| | | shredSliceLayers.append(slice) |
| | | |
| | | if index > 0 { |
| | | let guide = CALayer() |
| | | guide.frame = CGRect( |
| | | x: x - 0.5, |
| | | y: dragImageSize.height * 0.12, |
| | | width: 1, |
| | | height: dragImageSize.height * 0.76 |
| | | ) |
| | | guide.backgroundColor = NSColor.white.withAlphaComponent(0.42).cgColor |
| | | guide.opacity = 0 |
| | | guide.actions = [ |
| | | "opacity": NSNull(), |
| | | "position": NSNull() |
| | | ] |
| | | dragLayer.addSublayer(guide) |
| | | addShredGuideAnimation(to: guide, index: index) |
| | | shredGuideLayers.append(guide) |
| | | } |
| | | } |
| | | } |
| | | |
| | | private func stopShredPreview() { |
| | | guard shredPreviewActive || !shredSliceLayers.isEmpty || !shredGuideLayers.isEmpty else { return } |
| | | shredPreviewActive = false |
| | | shredSliceLayers.forEach { |
| | | $0.removeAllAnimations() |
| | | $0.removeFromSuperlayer() |
| | | } |
| | | shredGuideLayers.forEach { |
| | | $0.removeAllAnimations() |
| | | $0.removeFromSuperlayer() |
| | | } |
| | | shredSliceLayers.removeAll() |
| | | shredGuideLayers.removeAll() |
| | | dragLayer?.contents = currentDragImage |
| | | } |
| | | |
| | | private func addShredAnimation(to layer: CALayer, index: Int) { |
| | | let direction: CGFloat = index.isMultiple(of: 2) ? 1 : -1 |
| | | let travel = min(4.5, max(2.0, dragImageSize.height * 0.06)) |
| | | let drift = min(2.4, max(1.0, dragImageSize.width * 0.025)) * direction |
| | | |
| | | let y = CAKeyframeAnimation(keyPath: "transform.translation.y") |
| | | y.values = [0, -travel, travel * 0.28, -travel * 0.52, 0] |
| | | |
| | | let x = CAKeyframeAnimation(keyPath: "transform.translation.x") |
| | | x.values = [0, drift, -drift * 0.45, drift * 0.25, 0] |
| | | |
| | | let opacity = CAKeyframeAnimation(keyPath: "opacity") |
| | | opacity.values = [1, 0.82, 0.96, 0.88, 1] |
| | | |
| | | let group = CAAnimationGroup() |
| | | group.animations = [x, y, opacity] |
| | | group.duration = 0.46 |
| | | group.repeatCount = .greatestFiniteMagnitude |
| | | group.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) |
| | | group.beginTime = CACurrentMediaTime() + Double(index) * 0.018 |
| | | layer.add(group, forKey: "removeTagShredPreview") |
| | | } |
| | | |
| | | private func addShredGuideAnimation(to layer: CALayer, index: Int) { |
| | | let opacity = CAKeyframeAnimation(keyPath: "opacity") |
| | | opacity.values = [0, 0.42, 0.18, 0.35, 0] |
| | | opacity.duration = 0.46 |
| | | opacity.repeatCount = .greatestFiniteMagnitude |
| | | opacity.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) |
| | | opacity.beginTime = CACurrentMediaTime() + Double(index) * 0.018 |
| | | layer.add(opacity, forKey: "removeTagShredGuide") |
| | | } |
| | | |
| | | private func dropTarget(at screenPoint: NSPoint) -> AppDropTargetReceivingView? { |
| | |
| | | |
| | | protocol AppEmptyDropReceivingView: AnyObject { |
| | | func screenFrame() -> NSRect? |
| | | func canPreviewEmptyDrop(path: String, source: String, screenPoint: NSPoint, copy: Bool) -> Bool |
| | | func performEmptyDrop(path: String, source: String, screenPoint: NSPoint, copy: Bool) |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | extension AppEmptyDropReceivingView { |
| | | func canPreviewEmptyDrop(path: String, source: String, screenPoint: NSPoint, copy: Bool) -> Bool { |
| | | true |
| | | } |
| | | } |
| | | |
| | | struct AppDropTargetView: NSViewRepresentable { |
| | | let targetTag: String |
| | | let onDropApp: (String, String, Bool) -> Void |