import Foundation import AppKit import Vision import CoreImage import CoreImage.CIFilterBuiltins import CoreML import SwiftUI import Darwin // For utsname system info // MARK: - Background Removal Manager class BackgroundRemover { static let shared = BackgroundRemover() enum ProcessingMethod { case visionFramework case coreMLRMBG14 } private init() {} // MARK: - Public Interface func removeBackground(from image: NSImage, method: ProcessingMethod, completion: @escaping (NSImage?) -> Void) { guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { print("โŒ BackgroundRemover: Could not convert NSImage to CGImage") completion(nil) return } DispatchQueue.global(qos: .userInitiated).async { switch method { case .visionFramework: self.processWithVisionFramework(cgImage: cgImage, completion: completion) case .coreMLRMBG14: self.processWithCoreMLModelWithFallback(cgImage: cgImage, completion: completion) } } } // ๐ŸŽฏ NEW: Process with user's preferred method from settings (with smart fallback) func processWithPreferredMethod(from image: NSImage, completion: @escaping (NSImage?) -> Void) { let preferredMethod = SettingsManager.shared.preferredBackgroundRemovalMethod switch preferredMethod { case .auto: // Auto mode: try RMBG first, fallback to Vision processWithCoreMLModelWithFallback(from: image, completion: completion) case .rmbg: // RMBG only (no fallback) processWithCoreMLRMBGOnly(from: image, completion: completion) case .vision: // Vision Framework only processWithVisionFramework(from: image, completion: completion) } } // ๐ŸŽฏ NEW: Process with CoreML model and fallback to Vision private func processWithCoreMLModelWithFallback(from image: NSImage, completion: @escaping (NSImage?) -> Void) { guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { print("โŒ BackgroundRemover: Could not convert NSImage to CGImage") completion(nil) return } DispatchQueue.global(qos: .userInitiated).async { self.processWithCoreMLModelWithFallback(cgImage: cgImage, completion: completion) } } // ๐ŸŽฏ NEW: Process with Vision Framework only private func processWithVisionFramework(from image: NSImage, completion: @escaping (NSImage?) -> Void) { guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { print("โŒ BackgroundRemover: Could not convert NSImage to CGImage") completion(nil) return } DispatchQueue.global(qos: .userInitiated).async { self.processWithVisionFramework(cgImage: cgImage, completion: completion) } } // ๐ŸŽฏ NEW: Process with RMBG only (no fallback) private func processWithCoreMLRMBGOnly(from image: NSImage, completion: @escaping (NSImage?) -> Void) { guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { print("โŒ BackgroundRemover: Could not convert NSImage to CGImage") completion(nil) return } DispatchQueue.global(qos: .userInitiated).async { self.processWithCoreMLRMBGOnly(cgImage: cgImage, completion: completion) } } // MARK: - Smart E5RT Issue Detection private func shouldSkipRMBGDueToIssues() -> Bool { // Check if we have persistent E5RT cache issues that suggest we should avoid RMBG let issueThreshold = 3 // Skip after 3 attempts with issues let recentFailures = UserDefaults.standard.integer(forKey: "ShotScreen_E5RT_FailureCount") if recentFailures >= issueThreshold { let lastSkip = UserDefaults.standard.double(forKey: "ShotScreen_E5RT_LastSkip") let timeSinceSkip = Date().timeIntervalSince1970 - lastSkip // Reset after 1 hour, in case the issue was temporary if timeSinceSkip > 3600 { UserDefaults.standard.set(0, forKey: "ShotScreen_E5RT_FailureCount") UserDefaults.standard.removeObject(forKey: "ShotScreen_E5RT_LastSkip") return false } return true } return false } private func recordE5RTIssue() { let currentCount = UserDefaults.standard.integer(forKey: "ShotScreen_E5RT_FailureCount") UserDefaults.standard.set(currentCount + 1, forKey: "ShotScreen_E5RT_FailureCount") UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "ShotScreen_E5RT_LastSkip") print("๐Ÿ”ง E5RT Issue recorded: Total count = \(currentCount + 1)") // If we're getting too many E5RT issues, disable Neural Engine more aggressively if currentCount >= 3 { print("๐Ÿ”ง TOO MANY E5RT ISSUES: Disabling Neural Engine for 5 minutes") UserDefaults.standard.set(true, forKey: "ShotScreen_ForceCPU_TempMode") UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "ShotScreen_ForceCPU_StartTime_Extended") } } // ๐ŸŽฏ NEW: RMBG-only processing (no fallback) private func processWithCoreMLRMBGOnly(cgImage: CGImage, completion: @escaping (NSImage?) -> Void) { if tryLoadAndProcessModel(cgImage: cgImage, modelName: "bria-rmbg-coreml", displayName: "RMBG-1.4", completion: completion) { // RMBG-1.4 processing initiated successfully return } // Failed to load RMBG model print("โŒ RMBG-1.4 failed and no fallback allowed by user setting") DispatchQueue.main.async { completion(nil) } } // MARK: - Model Availability private func getModelPath() -> String { let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! let appDirectory = appSupportURL.appendingPathComponent("ShotScreen") return appDirectory.appendingPathComponent("bria-rmbg-coreml.mlpackage").path } func isRMBGModelAvailable() -> Bool { return FileManager.default.fileExists(atPath: getModelPath()) } // MARK: - Public Cache Management func clearCoreMLCachePublic() { print("๐Ÿงน PUBLIC: User requested Core ML cache clearing...") // Do this on background thread to avoid blocking UI DispatchQueue.global(qos: .utility).async { self.clearCoreMLCache() // Reset failure count since user manually cleared cache UserDefaults.standard.set(0, forKey: "ShotScreen_E5RT_FailureCount") UserDefaults.standard.removeObject(forKey: "ShotScreen_E5RT_LastSkip") DispatchQueue.main.async { print("โœ… Cache clearing completed successfully!") } } } // MARK: - Cache Management (ULTRA AGGRESSIVE E5RT fix) private func clearCoreMLCache() { print("๐Ÿงน ULTRA AGGRESSIVE: Nuking ALL Core ML E5RT caches...") // STRATEGY 1: Direct file removal (fastest) ultraFastCacheClear() // STRATEGY 2: Force CPU-only mode temporarily temporarilyForceCPUMode() // STRATEGY 3: Model precompilation with fresh cache forceModelRecompilation() print("๐Ÿงน ULTRA AGGRESSIVE: E5RT cache nuking completed") } private func ultraFastCacheClear() { // Use rm -rf for maximum speed (faster than FileManager) let cachePaths = [ "~/Library/Caches/com.apple.e5rt.e5bundlecache", "~/Library/Caches/com.apple.CoreML", "~/Library/Caches/com.apple.mlcompute", "~/Library/Caches/ShotScreen" ] for path in cachePaths { let expandedPath = NSString(string: path).expandingTildeInPath let command = "rm -rf '\(expandedPath)' 2>/dev/null" let process = Process() process.launchPath = "/bin/sh" process.arguments = ["-c", command] do { try process.run() process.waitUntilExit() print("โœ… NUKED: \(path)") } catch { print("โš ๏ธ Could not nuke: \(path)") } } } private func temporarilyForceCPUMode() { // Set flag to force CPU mode for next few loads UserDefaults.standard.set(true, forKey: "ShotScreen_ForceCPU_TempMode") UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "ShotScreen_ForceCPU_StartTime") print("๐Ÿ”ง TEMPORARY: Forcing CPU-only mode for next 60 seconds") } private func forceModelRecompilation() { // Delete any existing compiled model to force fresh compilation let modelPath = getModelPath() let modelURL = URL(fileURLWithPath: modelPath) // Remove compiled version if it exists do { let compiledURL = try MLModel.compileModel(at: modelURL) if FileManager.default.fileExists(atPath: compiledURL.path) { try FileManager.default.removeItem(at: compiledURL) print("โœ… FORCED: Model recompilation") } } catch { // Ignore errors - we just want to force recompilation } } private func clearWildcardCachePath(_ pattern: String) { // Handle /var/folders/*/com.apple.e5rt.e5bundlecache patterns let components = pattern.components(separatedBy: "*") guard components.count == 2 else { return } let prefix = components[0] let suffix = components[1] do { let prefixURL = URL(fileURLWithPath: prefix) let contents = try FileManager.default.contentsOfDirectory( at: prefixURL, includingPropertiesForKeys: nil ) for item in contents { let targetPath = item.appendingPathComponent(String(suffix.dropFirst())) if FileManager.default.fileExists(atPath: targetPath.path) { try FileManager.default.removeItem(at: targetPath) print("โœ… Cleared system cache: \(targetPath.path)") } } } catch { print("โš ๏ธ Could not clear wildcard cache \(pattern): \(error.localizedDescription)") } } private func clearCompiledModelCache() { // Try to find and clear any compiled versions of our model let modelPath = getModelPath() let modelURL = URL(fileURLWithPath: modelPath) do { // Force recompilation by removing any existing compiled versions let compiledURL = try MLModel.compileModel(at: modelURL) if FileManager.default.fileExists(atPath: compiledURL.path) { try FileManager.default.removeItem(at: compiledURL) print("โœ… Cleared compiled model cache") } } catch { print("โš ๏ธ Could not clear compiled model cache: \(error.localizedDescription)") } } // MARK: - Optimized Model Loading with Proactive Cache Management private func loadModelWithRetry(at url: URL, retryCount: Int = 0) throws -> MLModel { // Proactively clear cache on first attempt if we've seen issues before if retryCount == 0 && hasRecentCacheIssues() { print("๐Ÿงน Proactively clearing E5RT cache due to recent issues...") clearCoreMLCache() Thread.sleep(forTimeInterval: 0.5) } do { let config = MLModelConfiguration() config.computeUnits = getOptimalComputeUnits() // Add cache preferences to avoid E5RT issues if #available(macOS 14.0, *) { config.allowLowPrecisionAccumulationOnGPU = true } return try MLModel(contentsOf: url, configuration: config) } catch let error where retryCount < 2 && (error.localizedDescription.contains("resources.bin") || error.localizedDescription.contains("E5RT")) { print("๐Ÿ”„ E5RT cache error detected, clearing cache and retrying... (attempt \(retryCount + 1)/3)") clearCoreMLCache() // Wait longer for cache to properly clear Thread.sleep(forTimeInterval: 1.5) return try loadModelWithRetry(at: url, retryCount: retryCount + 1) } } // MARK: - Cache Issue Detection private func hasRecentCacheIssues() -> Bool { // Check if we've seen cache issues recently by looking for E5RT cache directories let problemIndicators = [ "~/Library/Caches/ShotScreen/com.apple.e5rt.e5bundlecache", "~/Library/Caches/com.apple.e5rt.e5bundlecache" ] for location in problemIndicators { let expandedPath = NSString(string: location).expandingTildeInPath if FileManager.default.fileExists(atPath: expandedPath) { // Check if directory is non-empty (has cached data that might be corrupt) do { let contents = try FileManager.default.contentsOfDirectory(atPath: expandedPath) if !contents.isEmpty { print("๐Ÿ” Found existing E5RT cache, potential for issues: \(location)") return true } } catch { // If we can't read it, it might be corrupt return true } } } return false } // MARK: - CPU-only Model Loading private func loadModelWithCPUOnly(at url: URL, retryCount: Int = 0) throws -> MLModel { do { let config = MLModelConfiguration() config.computeUnits = .cpuOnly return try MLModel(contentsOf: url, configuration: config) } catch let error where retryCount < 2 && error.localizedDescription.contains("resources.bin") { print("๐Ÿ”„ E5RT cache error in CPU mode, clearing cache and retrying... (attempt \(retryCount + 1)/3)") clearCoreMLCache() // Wait a moment for cache to clear Thread.sleep(forTimeInterval: 1.0) return try loadModelWithCPUOnly(at: url, retryCount: retryCount + 1) } } // MARK: - Architecture Detection & Compute Unit Optimization private func getOptimalComputeUnits() -> MLComputeUnits { // Check if we're in temporary CPU-only mode to avoid E5RT issues if isInTemporaryCPUMode() { print("๐Ÿ”ง TEMPORARY CPU MODE: Using CPU-only to avoid E5RT issues") return .cpuOnly } // Detect CPU architecture for optimal Core ML performance var systemInfo = utsname() uname(&systemInfo) let machine = withUnsafePointer(to: &systemInfo.machine) { $0.withMemoryRebound(to: CChar.self, capacity: 1) { String(validatingUTF8: $0) } } let machineString = machine ?? "unknown" print("๐Ÿ–ฅ๏ธ Detected machine architecture: \(machineString)") // Check for Apple Silicon (M1, M2, M3, etc.) if machineString.contains("arm64") || machineString.hasPrefix("arm") { print("๐Ÿš€ Apple Silicon detected - using all compute units (Neural Engine + GPU + CPU)") return .all } else { // Intel Mac - use CPU and GPU only (no Neural Engine available) print("โšก Intel Mac detected - using CPU and GPU only (no Neural Engine)") return .cpuAndGPU } } private func isInTemporaryCPUMode() -> Bool { guard UserDefaults.standard.bool(forKey: "ShotScreen_ForceCPU_TempMode") else { return false } let startTime = UserDefaults.standard.double(forKey: "ShotScreen_ForceCPU_StartTime") let currentTime = Date().timeIntervalSince1970 let elapsed = currentTime - startTime // Force CPU mode for 60 seconds after cache clear if elapsed > 60 { // Temp mode expired, clear flags UserDefaults.standard.removeObject(forKey: "ShotScreen_ForceCPU_TempMode") UserDefaults.standard.removeObject(forKey: "ShotScreen_ForceCPU_StartTime") print("๐Ÿ”ง TEMPORARY CPU MODE: Expired, returning to normal mode") return false } print("๐Ÿ”ง TEMPORARY CPU MODE: Still active (\(Int(60-elapsed))s remaining)") return true } // MARK: - Vision Framework Processing (OPTIMIZED) private func processWithVisionFramework(cgImage: CGImage, completion: @escaping (NSImage?) -> Void) { guard #available(macOS 14.0, *) else { print("โŒ Vision framework background removal requires macOS 14.0 or higher") DispatchQueue.main.async { completion(nil) } return } print("๐Ÿš€ VISION FAST TRACK: Using optimized Vision Framework") let request = VNGenerateForegroundInstanceMaskRequest { request, error in if let error = error { print("โŒ Vision error: \(error)") DispatchQueue.main.async { completion(nil) } return } guard let results = request.results, !results.isEmpty else { print("โŒ No foreground found with Vision Framework") DispatchQueue.main.async { completion(nil) } return } guard let result = results.first as? VNInstanceMaskObservation else { print("โŒ Invalid result from Vision Framework") DispatchQueue.main.async { completion(nil) } return } print("โœ… VISION FAST: Completed in ~1-2 seconds! (\(results.count) results)") self.applyMask(mask: result, to: cgImage, completion: completion) } // OPTIMIZATION: Configure Vision Framework for maximum speed request.revision = VNGenerateForegroundInstanceMaskRequest.defaultRevision // Create handler with optimized options let handlerOptions: [VNImageOption: Any] = [ .ciContext: CIContext(options: [ .useSoftwareRenderer: false, // Use hardware acceleration .priorityRequestLow: false // High priority processing ]) ] let handler = VNImageRequestHandler(cgImage: cgImage, options: handlerOptions) // Perform on high-priority queue for faster processing DispatchQueue.global(qos: .userInitiated).async { do { try handler.perform([request]) } catch { print("โŒ Vision handler error: \(error)") DispatchQueue.main.async { completion(nil) } } } } @available(macOS 14.0, *) private func applyMask(mask: VNInstanceMaskObservation, to image: CGImage, completion: @escaping (NSImage?) -> Void) { // OPTIMIZATION: Run mask processing on background queue for better performance DispatchQueue.global(qos: .userInitiated).async { do { print("๐Ÿš€ FAST MASK: Starting optimized mask generation...") // BOTTLENECK 1: Optimize mask generation with performance options let imageHandler = VNImageRequestHandler(cgImage: image, options: [ .ciContext: self.getOptimizedCIContext() ]) let maskImage = try mask.generateScaledMaskForImage( forInstances: mask.allInstances, from: imageHandler ) print("๐Ÿš€ FAST MASK: Mask generated, applying to image...") // OPTIMIZATION: Use same optimized CIContext for all operations let optimizedContext = self.getOptimizedCIContext() let ciImage = CIImage(cgImage: image) let ciMask = CIImage(cvPixelBuffer: maskImage) let filter = CIFilter.blendWithMask() filter.inputImage = ciImage filter.backgroundImage = CIImage.empty() filter.maskImage = ciMask guard let outputImage = filter.outputImage else { print("โŒ Error applying mask") DispatchQueue.main.async { completion(nil) } return } // BOTTLENECK 2: Use optimized context instead of creating new one guard let cgResult = optimizedContext.createCGImage(outputImage, from: outputImage.extent) else { print("โŒ Error creating result image") DispatchQueue.main.async { completion(nil) } return } let resultImage = NSImage(cgImage: cgResult, size: NSSize(width: cgResult.width, height: cgResult.height)) DispatchQueue.main.async { print("โœ… VISION OPTIMIZED: Background removed in ~1-2 seconds!") completion(resultImage) } } catch { print("โŒ Error processing mask: \(error.localizedDescription)") DispatchQueue.main.async { completion(nil) } } } } // MARK: - Optimized CIContext (PERFORMANCE CRITICAL) private var _optimizedCIContext: CIContext? private func getOptimizedCIContext() -> CIContext { if let existingContext = _optimizedCIContext { return existingContext } // Create high-performance CIContext with optimized settings let context = CIContext(options: [ .useSoftwareRenderer: false, // Force hardware acceleration .priorityRequestLow: false, // High priority processing .cacheIntermediates: true // Cache for better performance ]) _optimizedCIContext = context print("๐Ÿš€ FAST CONTEXT: Created optimized CIContext for Vision Framework") return context } // MARK: - Core ML Processing with Fallback private func processWithCoreMLModelWithFallback(cgImage: CGImage, completion: @escaping (NSImage?) -> Void) { // Try RMBG-1.4 first, fallback to Vision Framework print("๐Ÿค– Attempting RMBG-1.4 Core ML model...") if tryLoadAndProcessModel(cgImage: cgImage, modelName: "bria-rmbg-coreml", displayName: "RMBG-1.4", completion: completion) { // RMBG-1.4 succeeded return } // Fallback to Vision Framework print("๐Ÿ”„ RMBG-1.4 failed, falling back to Vision Framework...") processWithVisionFramework(cgImage: cgImage, completion: completion) } private func tryLoadAndProcessModel(cgImage: CGImage, modelName: String, displayName: String, completion: @escaping (NSImage?) -> Void) -> Bool { // Check if model is available in Application Support directory let modelPath = getModelPath() guard FileManager.default.fileExists(atPath: modelPath) else { print("โŒ \(displayName) model not found at: \(modelPath)") return false } let finalModelURL = URL(fileURLWithPath: modelPath) do { print("๐Ÿ“ฆ Trying to load \(displayName) model from: \(finalModelURL.lastPathComponent)") // First try to compile the model if it's not compiled let compiledModelURL: URL if finalModelURL.pathExtension == "mlpackage" { print("๐Ÿ”ง Compiling \(displayName) model...") compiledModelURL = try MLModel.compileModel(at: finalModelURL) print("โœ… Model compiled successfully") } else { compiledModelURL = finalModelURL } // Try to load the Core ML model with retry logic for E5RT issues let model = try loadModelWithRetry(at: compiledModelURL) let visionModel = try VNCoreMLModel(for: model) print("โœ… \(displayName) model successfully loaded") // Create the request with E5RT monitoring let request = VNCoreMLRequest(model: visionModel) { request, error in if let error = error { print("โŒ Core ML request error for \(displayName): \(error)") // Check if this is an E5RT related error if error.localizedDescription.contains("resources.bin") || error.localizedDescription.contains("E5RT") { self.recordE5RTIssue() } DispatchQueue.main.async { completion(nil) } return } guard let results = request.results, let pixelBufferObservation = results.first as? VNPixelBufferObservation else { print("โŒ No valid result from \(displayName) model") DispatchQueue.main.async { completion(nil) } return } // Convert the result to NSImage if let resultImage = self.convertPixelBufferToNSImage(pixelBufferObservation.pixelBuffer, originalImage: cgImage) { print("โœ… \(displayName) processing successful") DispatchQueue.main.async { completion(resultImage) } } else { print("โŒ Could not convert \(displayName) result") DispatchQueue.main.async { completion(nil) } } } // Configure request request.imageCropAndScaleOption = .scaleFill // Perform the request let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) try handler.perform([request]) return true // Success } catch { print("โŒ \(displayName) model error: \(error)") let errorMessage = error.localizedDescription if errorMessage.contains("resources.bin") { print("๐Ÿ”ง E5RT cache corruption detected - resources.bin missing") print("๐Ÿš€ FAST FALLBACK: Immediately switching to Vision Framework") // Record this issue and clear cache for next time recordE5RTIssue() clearCoreMLCache() // Instead of retrying RMBG, immediately fall back to Vision DispatchQueue.global(qos: .userInitiated).async { self.processWithVisionFramework(cgImage: cgImage, completion: completion) } return true // We're handling it with Vision fallback } else if errorMessage.contains("MPSGraphExecutable") || errorMessage.contains("E5RT") { print("๐Ÿ”ง Neural Engine compilation problem detected") print("๐Ÿš€ FAST FALLBACK: Immediately switching to Vision Framework") // Same fast fallback strategy recordE5RTIssue() clearCoreMLCache() DispatchQueue.global(qos: .userInitiated).async { self.processWithVisionFramework(cgImage: cgImage, completion: completion) } return true // We're handling it with Vision fallback } else if errorMessage.contains("compute") || errorMessage.contains("Neural") { print("๐Ÿ”ง Intel Mac detected - trying CPU-only fallback") // Try CPU-only as final fallback for Intel Macs return tryLoadModelWithCPUOnly(cgImage: cgImage, modelName: modelName, displayName: displayName, completion: completion) } return false // Failed } } private func convertPixelBufferToNSImage(_ pixelBuffer: CVPixelBuffer, originalImage: CGImage) -> NSImage? { print("๐Ÿš€ FAST RMBG: Starting optimized pixel buffer conversion...") // Convert the grayscale mask to a proper background-removed image let maskCIImage = CIImage(cvPixelBuffer: pixelBuffer) let originalCIImage = CIImage(cgImage: originalImage) // Resize mask to match original image size let scaleX = originalCIImage.extent.width / maskCIImage.extent.width let scaleY = originalCIImage.extent.height / maskCIImage.extent.height let scaledMask = maskCIImage.transformed(by: CGAffineTransform(scaleX: scaleX, y: scaleY)) // Apply the mask to remove background let maskFilter = CIFilter.blendWithMask() maskFilter.inputImage = originalCIImage maskFilter.backgroundImage = CIImage.empty() maskFilter.maskImage = scaledMask guard let outputImage = maskFilter.outputImage else { print("โŒ Mask filter failed") return nil } // OPTIMIZATION: Use same optimized context instead of creating new one let optimizedContext = getOptimizedCIContext() guard let cgResult = optimizedContext.createCGImage(outputImage, from: outputImage.extent) else { print("โŒ CGImage creation failed") return nil } print("โœ… RMBG OPTIMIZED: Pixel buffer converted efficiently!") return NSImage(cgImage: cgResult, size: NSSize(width: cgResult.width, height: cgResult.height)) } // MARK: - Intel Mac CPU-Only Fallback private func tryLoadModelWithCPUOnly(cgImage: CGImage, modelName: String, displayName: String, completion: @escaping (NSImage?) -> Void) -> Bool { let modelPath = getModelPath() guard FileManager.default.fileExists(atPath: modelPath) else { print("โŒ \(displayName) model not found for CPU fallback") return false } let finalModelURL = URL(fileURLWithPath: modelPath) do { print("๐Ÿ”ง Trying CPU-only mode for \(displayName) on Intel Mac...") // Compile model if needed let compiledModelURL: URL if finalModelURL.pathExtension == "mlpackage" { compiledModelURL = try MLModel.compileModel(at: finalModelURL) } else { compiledModelURL = finalModelURL } // CPU-only configuration for Intel Macs with retry logic print("โšก Using CPU-only compute units for Intel Mac compatibility") let model = try loadModelWithCPUOnly(at: compiledModelURL) let visionModel = try VNCoreMLModel(for: model) print("โœ… \(displayName) model loaded successfully in CPU-only mode") // Create the request let request = VNCoreMLRequest(model: visionModel) { request, error in if let error = error { print("โŒ CPU-only Core ML request error for \(displayName): \(error)") DispatchQueue.main.async { completion(nil) } return } guard let results = request.results, let pixelBufferObservation = results.first as? VNPixelBufferObservation else { print("โŒ No valid result from \(displayName) model (CPU-only)") DispatchQueue.main.async { completion(nil) } return } // Convert the result to NSImage if let resultImage = self.convertPixelBufferToNSImage(pixelBufferObservation.pixelBuffer, originalImage: cgImage) { print("โœ… \(displayName) processing successful (CPU-only mode)") DispatchQueue.main.async { completion(resultImage) } } else { print("โŒ Could not convert \(displayName) result (CPU-only)") DispatchQueue.main.async { completion(nil) } } } // Configure request request.imageCropAndScaleOption = .scaleFill // Perform the request let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) try handler.perform([request]) return true // Success } catch { print("โŒ CPU-only fallback failed for \(displayName): \(error)") return false // Failed } } } // MARK: - Drag & Drop Image View class DragDropImageView: NSImageView, NSFilePromiseProviderDelegate, NSDraggingSource { weak var dragDropDelegate: ImageDragDropDelegate? var enableDragOut: Bool = false // Enable dragging images out of this view override func awakeFromNib() { super.awakeFromNib() setupDragDrop() } private func setupDragDrop() { registerForDraggedTypes([.fileURL]) } // MARK: - Drag Out Functionality override func mouseDown(with event: NSEvent) { guard enableDragOut, let image = self.image else { super.mouseDown(with: event) return } // Start drag operation with smaller thumbnail let dragItem = NSDraggingItem(pasteboardWriter: image) // Create smaller drag frame (e.g., 120x120 max) let maxDragSize: CGFloat = 120 let imageSize = image.size let aspectRatio = imageSize.width / imageSize.height var dragWidth: CGFloat var dragHeight: CGFloat if aspectRatio > 1 { // Landscape dragWidth = min(maxDragSize, imageSize.width) dragHeight = dragWidth / aspectRatio } else { // Portrait or square dragHeight = min(maxDragSize, imageSize.height) dragWidth = dragHeight * aspectRatio } // Center the drag frame within the view let dragFrame = NSRect( x: (self.bounds.width - dragWidth) / 2, y: (self.bounds.height - dragHeight) / 2, width: dragWidth, height: dragHeight ) dragItem.setDraggingFrame(dragFrame, contents: image) // Create temporary file for dragging if let tiffData = image.tiffRepresentation, let bitmapRep = NSBitmapImageRep(data: tiffData), let pngData = bitmapRep.representation(using: .png, properties: [:]) { let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("background_removed_\(UUID().uuidString).png") do { try pngData.write(to: tempURL) let filePromise = NSFilePromiseProvider(fileType: "public.png", delegate: self) filePromise.userInfo = ["tempURL": tempURL] let fileDragItem = NSDraggingItem(pasteboardWriter: filePromise) fileDragItem.setDraggingFrame(dragFrame, contents: image) beginDraggingSession(with: [fileDragItem], event: event, source: self) print("๐ŸŽฏ Started dragging background-removed image") } catch { print("โŒ Failed to create temp file for dragging: \(error)") super.mouseDown(with: event) } } else { super.mouseDown(with: event) } } override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { let pasteboard = sender.draggingPasteboard guard let types = pasteboard.types else { return [] } if types.contains(.fileURL) { if let urls = pasteboard.readObjects(forClasses: [NSURL.self], options: nil) as? [URL] { let imageTypes = ["png", "jpg", "jpeg", "gif", "tiff", "bmp", "heic", "heif"] for url in urls { if imageTypes.contains(url.pathExtension.lowercased()) { // Visual feedback - use adaptive color self.layer?.borderWidth = 2 self.layer?.borderColor = ThemeManager.shared.buttonTintColor.cgColor return .copy } } } } return [] } override func draggingExited(_ sender: NSDraggingInfo?) { // Remove visual feedback self.layer?.borderWidth = 0 } override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { let pasteboard = sender.draggingPasteboard guard let urls = pasteboard.readObjects(forClasses: [NSURL.self], options: nil) as? [URL] else { return false } let imageTypes = ["png", "jpg", "jpeg", "gif", "tiff", "bmp", "heic", "heif"] for url in urls { if imageTypes.contains(url.pathExtension.lowercased()) { if let image = NSImage(contentsOf: url) { dragDropDelegate?.didDropImage(image, from: url.path) // Remove visual feedback self.layer?.borderWidth = 0 return true } } } return false } // MARK: - NSFilePromiseProviderDelegate func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String { return "background_removed_\(UUID().uuidString).png" } func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: @escaping (Error?) -> Void) { guard let userInfo = filePromiseProvider.userInfo as? [String: Any], let tempURL = userInfo["tempURL"] as? URL else { completionHandler(NSError(domain: "DragDropImageView", code: 1, userInfo: [NSLocalizedDescriptionKey: "No temp URL found"])) return } do { if FileManager.default.fileExists(atPath: url.path) { try FileManager.default.removeItem(at: url) } try FileManager.default.copyItem(at: tempURL, to: url) completionHandler(nil) print("โœ… Successfully wrote dragged image to: \(url.lastPathComponent)") } catch { completionHandler(error) print("โŒ Failed to write dragged image: \(error)") } } // MARK: - NSDraggingSource func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation { return .copy } func draggingSession(_ session: NSDraggingSession, willBeginAt screenPoint: NSPoint) { print("๐ŸŽฏ Drag session beginning at \(screenPoint)") } func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) { print("๐ŸŽฏ Drag session ended with operation: \(operation.rawValue)") } } protocol ImageDragDropDelegate: AnyObject { func didDropImage(_ image: NSImage, from path: String) } // MARK: - Card Style Button (for method selection) class CardStyleButton: NSView { private let titleLabel: NSTextField = NSTextField(labelWithString: "") private let subtitleLabel: NSTextField = NSTextField(labelWithString: "") private let iconImageView: NSImageView = NSImageView() private var iconBackground: NSView! private var cardBackground: NSView! private var symbolName: String = "" var isSelected: Bool = false { didSet { updateSelectedState(animated: true) } } var isEnabled: Bool = true { didSet { updateEnabledState() } } // Button action properties var target: AnyObject? var action: Selector? init(frame frameRect: NSRect, title: String, subtitle: String, symbolName: String) { self.symbolName = symbolName super.init(frame: frameRect) setupCard(title: title, subtitle: subtitle, symbolName: symbolName) // Setup theme change observer ThemeManager.shared.observeThemeChanges { [weak self] in DispatchQueue.main.async { self?.updateThemeColors() } } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupCard(title: String, subtitle: String, symbolName: String) { wantsLayer = true layer?.cornerRadius = 12 layer?.masksToBounds = false // Mouse tracking updateTrackingAreas() // Card background with blur effect - adaptive colors cardBackground = NSView() cardBackground.wantsLayer = true cardBackground.translatesAutoresizingMaskIntoConstraints = false cardBackground.layer?.cornerRadius = 12 cardBackground.layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.3).cgColor cardBackground.layer?.borderWidth = 1 cardBackground.layer?.borderColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.2).cgColor addSubview(cardBackground) // Icon background - adaptive colors iconBackground = NSView() iconBackground.wantsLayer = true iconBackground.translatesAutoresizingMaskIntoConstraints = false iconBackground.layer?.cornerRadius = 20 iconBackground.layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.cgColor cardBackground.addSubview(iconBackground) // SF Symbol icon let baseImage = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil) let configuredImage = baseImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 20, weight: .semibold)) iconImageView.image = configuredImage iconImageView.contentTintColor = ThemeManager.shared.primaryTextColor iconImageView.translatesAutoresizingMaskIntoConstraints = false iconImageView.imageScaling = .scaleProportionallyDown iconImageView.imageAlignment = .alignCenter iconBackground.addSubview(iconImageView) // Title label - adaptive colors titleLabel.stringValue = title titleLabel.font = NSFont.systemFont(ofSize: 12, weight: .bold) titleLabel.textColor = ThemeManager.shared.primaryTextColor titleLabel.alignment = .left titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.usesSingleLineMode = true cardBackground.addSubview(titleLabel) // Subtitle label - adaptive colors subtitleLabel.stringValue = subtitle subtitleLabel.font = NSFont.systemFont(ofSize: 9, weight: .medium) subtitleLabel.textColor = ThemeManager.shared.secondaryTextColor subtitleLabel.alignment = .left subtitleLabel.translatesAutoresizingMaskIntoConstraints = false subtitleLabel.usesSingleLineMode = true cardBackground.addSubview(subtitleLabel) // Layout constraints NSLayoutConstraint.activate([ // Card background fills the entire view cardBackground.topAnchor.constraint(equalTo: topAnchor), cardBackground.leadingAnchor.constraint(equalTo: leadingAnchor), cardBackground.trailingAnchor.constraint(equalTo: trailingAnchor), cardBackground.bottomAnchor.constraint(equalTo: bottomAnchor), // Icon background iconBackground.leadingAnchor.constraint(equalTo: cardBackground.leadingAnchor, constant: 12), iconBackground.centerYAnchor.constraint(equalTo: cardBackground.centerYAnchor), iconBackground.widthAnchor.constraint(equalToConstant: 40), iconBackground.heightAnchor.constraint(equalToConstant: 40), // Icon image iconImageView.centerXAnchor.constraint(equalTo: iconBackground.centerXAnchor), iconImageView.centerYAnchor.constraint(equalTo: iconBackground.centerYAnchor), iconImageView.widthAnchor.constraint(equalToConstant: 24), iconImageView.heightAnchor.constraint(equalToConstant: 24), // Title label titleLabel.leadingAnchor.constraint(equalTo: iconBackground.trailingAnchor, constant: 12), titleLabel.trailingAnchor.constraint(equalTo: cardBackground.trailingAnchor, constant: -12), titleLabel.topAnchor.constraint(equalTo: cardBackground.centerYAnchor, constant: -12), // Subtitle label subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 2) ]) // Initial shadow - adaptive colors cardBackground.layer?.shadowColor = ThemeManager.shared.shadowColor.cgColor cardBackground.layer?.shadowOffset = CGSize(width: 0, height: 2) cardBackground.layer?.shadowRadius = 8 cardBackground.layer?.shadowOpacity = 0.0 // Set anchor points for smooth scaling cardBackground.layer?.anchorPoint = CGPoint(x: 0.5, y: 0.5) iconBackground.layer?.anchorPoint = CGPoint(x: 0.5, y: 0.5) } override func updateTrackingAreas() { super.updateTrackingAreas() for trackingArea in trackingAreas { removeTrackingArea(trackingArea) } let trackingArea = NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .activeAlways], owner: self, userInfo: nil) addTrackingArea(trackingArea) } private func updateSelectedState(animated: Bool) { if animated { NSAnimationContext.runAnimationGroup { context in context.duration = 0.6 context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) if isSelected { // Selected state: brighter, glowing - adaptive colors cardBackground.animator().layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.6).cgColor cardBackground.animator().layer?.borderColor = ThemeManager.shared.buttonTintColor.withAlphaComponent(0.8).cgColor cardBackground.animator().layer?.shadowOpacity = ThemeManager.shared.shadowOpacity iconBackground.animator().layer?.backgroundColor = ThemeManager.shared.buttonTintColor.withAlphaComponent(0.6).cgColor titleLabel.animator().textColor = ThemeManager.shared.primaryTextColor subtitleLabel.animator().textColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.9) } else { // Unselected state: dimmer - adaptive colors cardBackground.animator().layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.3).cgColor cardBackground.animator().layer?.borderColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.2).cgColor cardBackground.animator().layer?.shadowOpacity = 0.0 iconBackground.animator().layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.cgColor titleLabel.animator().textColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.8) subtitleLabel.animator().textColor = ThemeManager.shared.secondaryTextColor } } // Separate animation for icon bounce if isSelected { let bounceAnimation = CAKeyframeAnimation(keyPath: "transform.scale") bounceAnimation.values = [1.0, 1.2, 1.0] bounceAnimation.keyTimes = [0.0, 0.3, 1.0] bounceAnimation.duration = 0.5 bounceAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) iconBackground.layer?.add(bounceAnimation, forKey: "bounce") } } else { // Immediate update without animation - adaptive colors if isSelected { cardBackground.layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.6).cgColor cardBackground.layer?.borderColor = ThemeManager.shared.buttonTintColor.withAlphaComponent(0.8).cgColor cardBackground.layer?.shadowOpacity = ThemeManager.shared.shadowOpacity iconBackground.layer?.backgroundColor = ThemeManager.shared.buttonTintColor.withAlphaComponent(0.6).cgColor titleLabel.textColor = ThemeManager.shared.primaryTextColor subtitleLabel.textColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.9) } else { cardBackground.layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.3).cgColor cardBackground.layer?.borderColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.2).cgColor cardBackground.layer?.shadowOpacity = 0.0 iconBackground.layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.cgColor titleLabel.textColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.8) subtitleLabel.textColor = ThemeManager.shared.secondaryTextColor } } } private func updateEnabledState() { if isEnabled { alphaValue = 1.0 cardBackground.layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.3).cgColor cardBackground.layer?.borderColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.2).cgColor titleLabel.textColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.8) subtitleLabel.textColor = ThemeManager.shared.secondaryTextColor iconBackground.layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.cgColor iconImageView.contentTintColor = ThemeManager.shared.primaryTextColor } else { alphaValue = 0.5 cardBackground.layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.1).cgColor cardBackground.layer?.borderColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.1).cgColor titleLabel.textColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.4) subtitleLabel.textColor = ThemeManager.shared.secondaryTextColor.withAlphaComponent(0.5) iconBackground.layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.withAlphaComponent(0.5).cgColor iconImageView.contentTintColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.5) } updateTrackingAreas() } override func mouseEntered(with event: NSEvent) { guard isEnabled else { return } NSAnimationContext.runAnimationGroup { context in context.duration = 0.2 context.timingFunction = CAMediaTimingFunction(name: .easeOut) // Hover effect: slight scale and enhanced glow cardBackground.animator().layer?.transform = CATransform3DMakeScale(1.02, 1.02, 1.0) if isSelected { cardBackground.animator().layer?.shadowOpacity = ThemeManager.shared.shadowOpacity * 1.5 } else { cardBackground.animator().layer?.shadowOpacity = ThemeManager.shared.shadowOpacity * 0.8 cardBackground.animator().layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.4).cgColor } } // Icon hover animation let pulseAnimation = CABasicAnimation(keyPath: "transform.scale") pulseAnimation.fromValue = 1.0 pulseAnimation.toValue = 1.1 pulseAnimation.duration = 0.15 pulseAnimation.autoreverses = true iconBackground.layer?.add(pulseAnimation, forKey: "pulse") } override func mouseExited(with event: NSEvent) { guard isEnabled else { return } NSAnimationContext.runAnimationGroup { context in context.duration = 0.2 context.timingFunction = CAMediaTimingFunction(name: .easeOut) // Reset hover effect cardBackground.animator().layer?.transform = CATransform3DIdentity if isSelected { cardBackground.animator().layer?.shadowOpacity = ThemeManager.shared.shadowOpacity } else { cardBackground.animator().layer?.shadowOpacity = 0.0 cardBackground.animator().layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.3).cgColor } } iconBackground.layer?.removeAnimation(forKey: "pulse") } override func mouseDown(with event: NSEvent) { guard isEnabled else { // Show a subtle "disabled" feedback NSAnimationContext.runAnimationGroup({ context in context.duration = 0.1 self.animator().alphaValue = 0.3 }) { NSAnimationContext.runAnimationGroup({ context in context.duration = 0.1 self.animator().alphaValue = 0.5 }) } return } // Click animation NSAnimationContext.runAnimationGroup({ context in context.duration = 0.1 cardBackground.animator().layer?.transform = CATransform3DMakeScale(0.98, 0.98, 1.0) }) { NSAnimationContext.runAnimationGroup({ context in context.duration = 0.1 self.cardBackground.animator().layer?.transform = CATransform3DMakeScale(1.02, 1.02, 1.0) }) } // Handle click action if let target = target, let action = action { _ = target.perform(action, with: self) } } // MARK: - Theme Management private func updateThemeColors() { // Update card background if isSelected { cardBackground.layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.6).cgColor cardBackground.layer?.borderColor = ThemeManager.shared.buttonTintColor.withAlphaComponent(0.8).cgColor iconBackground.layer?.backgroundColor = ThemeManager.shared.buttonTintColor.withAlphaComponent(0.6).cgColor titleLabel.textColor = ThemeManager.shared.primaryTextColor subtitleLabel.textColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.9) } else { cardBackground.layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.3).cgColor cardBackground.layer?.borderColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.2).cgColor iconBackground.layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.cgColor titleLabel.textColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.8) subtitleLabel.textColor = ThemeManager.shared.secondaryTextColor } // Update icon color iconImageView.contentTintColor = ThemeManager.shared.primaryTextColor // Update shadow color cardBackground.layer?.shadowColor = ThemeManager.shared.shadowColor.cgColor // Update enabled state colors if disabled if !isEnabled { updateEnabledState() } } } // MARK: - Action Style Button (matches GridCellView style) class ActionStyleButton: NSView { private let label: NSTextField = NSTextField(labelWithString: "") private let iconImageView: NSImageView = NSImageView() private var iconBackground: NSView! private var iconCenterConstraint: NSLayoutConstraint! private var iconWidthConstraint: NSLayoutConstraint! private var iconHeightConstraint: NSLayoutConstraint! private var iconImageWidthConstraint: NSLayoutConstraint! private var iconImageHeightConstraint: NSLayoutConstraint! private let iconStartOffset: CGFloat = 30 private var isHovered: Bool = false private var symbolName: String = "" var isSmallButton: Bool = false { didSet { updateForSmallButton() } } var isEnabled: Bool = true { didSet { updateEnabledState() } } // Button action properties var target: AnyObject? var action: Selector? init(frame frameRect: NSRect, text: String, symbolName: String) { self.symbolName = symbolName super.init(frame: frameRect) setupButton(text: text, symbolName: symbolName) // Setup theme change observer ThemeManager.shared.observeThemeChanges { [weak self] in DispatchQueue.main.async { self?.updateThemeColors() } } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupButton(text: String, symbolName: String) { wantsLayer = true // layer?.backgroundColor = NSColor.red.withAlphaComponent(0.3).cgColor // DEBUG BG // Mouse tracking for hover effects - will be updated in updateTrackingAreas updateTrackingAreas() layer?.cornerRadius = 0 layer?.masksToBounds = false // Label setup - adaptive colors label.stringValue = text label.font = NSFont.systemFont(ofSize: 12, weight: .semibold) label.textColor = ThemeManager.shared.primaryTextColor label.alignment = .left label.alphaValue = text.isEmpty ? 0 : 0.3 // Hidden for small buttons label.translatesAutoresizingMaskIntoConstraints = false label.usesSingleLineMode = true label.lineBreakMode = .byTruncatingTail addSubview(label) // Circular icon background - adaptive colors let iconBackground = NSView() iconBackground.wantsLayer = true iconBackground.translatesAutoresizingMaskIntoConstraints = false iconBackground.layer?.cornerRadius = 18 // 36ร—36 circle (will be adjusted for small buttons) iconBackground.layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.cgColor addSubview(iconBackground) // SF Symbol icon let baseImage = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil) // Adjusted pointSize for normal state for better proportion within the 18x18 constraint let configuredImage = baseImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)) iconImageView.image = configuredImage iconImageView.contentTintColor = ThemeManager.shared.primaryTextColor iconImageView.translatesAutoresizingMaskIntoConstraints = false iconImageView.imageScaling = .scaleProportionallyDown iconImageView.imageAlignment = .alignCenter iconImageView.wantsLayer = true iconBackground.addSubview(iconImageView) // Store reference to icon background for small button adjustments self.iconBackground = iconBackground // Layout constraints iconCenterConstraint = iconBackground.centerXAnchor.constraint(equalTo: centerXAnchor, constant: iconStartOffset) iconWidthConstraint = iconBackground.widthAnchor.constraint(equalToConstant: 36) iconHeightConstraint = iconBackground.heightAnchor.constraint(equalTo: iconBackground.widthAnchor) // Constraints for the iconImageView itself, to ensure it's centered and sized within iconBackground iconImageWidthConstraint = iconImageView.widthAnchor.constraint(equalToConstant: 18) // Initial size for normal buttons iconImageHeightConstraint = iconImageView.heightAnchor.constraint(equalToConstant: 18) // Initial size for normal buttons NSLayoutConstraint.activate([ iconCenterConstraint, iconBackground.centerYAnchor.constraint(equalTo: centerYAnchor), iconWidthConstraint, iconHeightConstraint, iconImageView.centerXAnchor.constraint(equalTo: iconBackground.centerXAnchor), iconImageView.centerYAnchor.constraint(equalTo: iconBackground.centerYAnchor), iconImageWidthConstraint, // Activate new constraint iconImageHeightConstraint, // Activate new constraint label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12), label.trailingAnchor.constraint(equalTo: iconBackground.leadingAnchor, constant: -8), label.centerYAnchor.constraint(equalTo: iconBackground.centerYAnchor) ]) // Initial transform layer?.anchorPoint = CGPoint(x: 0.5, y: 0.5) layer?.transform = CATransform3DIdentity // Set anchor point for icon background to center for proper scaling // This must be done BEFORE adding constraints to prevent position jumping iconBackground.layer?.anchorPoint = CGPoint(x: 0.5, y: 0.5) // Add subtle glow to label - adaptive colors let shadow = NSShadow() shadow.shadowColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.4) shadow.shadowBlurRadius = 4 shadow.shadowOffset = NSSize(width: 0, height: -1) label.shadow = shadow } override func updateTrackingAreas() { super.updateTrackingAreas() // Remove existing tracking areas for trackingArea in trackingAreas { removeTrackingArea(trackingArea) } // Add tracking area covering the entire button bounds with some extra margin for easier hovering let expandedRect = bounds.insetBy(dx: -5, dy: -5) let trackingArea = NSTrackingArea(rect: expandedRect, options: [.mouseEnteredAndExited, .activeAlways], owner: self, userInfo: nil) addTrackingArea(trackingArea) } private func updateForSmallButton() { if isSmallButton { // For small buttons (info, close), center the icon and make it smaller iconCenterConstraint.constant = 0 label.alphaValue = 0 iconWidthConstraint.constant = 24 // iconBackground size iconBackground.layer?.cornerRadius = 12 iconBackground.alphaValue = 0.8 // Smaller icon for small buttons let baseImage = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil) // pointSize 10 for 12x12 constraint seems reasonable. let configuredImage = baseImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 10, weight: .semibold)) iconImageView.image = configuredImage iconImageWidthConstraint.constant = 12 // Update constraint for icon image iconImageHeightConstraint.constant = 12 // Update constraint for icon image } else { // This is the NORMAL/ENABLED state for the 5 action buttons iconCenterConstraint.constant = iconStartOffset label.alphaValue = label.stringValue.isEmpty ? 0 : 0.3 iconWidthConstraint.constant = 36 // iconBackground size iconBackground.layer?.cornerRadius = 18 iconBackground.alphaValue = 1.0 // Reset icon for normal buttons let baseImage = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil) // Adjusted pointSize for normal state for better proportion let configuredImage = baseImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)) iconImageView.image = configuredImage iconImageWidthConstraint.constant = 18 // Reset constraint for icon image iconImageHeightConstraint.constant = 18 // Reset constraint for icon image } // Update tracking areas when button type changes updateTrackingAreas() } private func updateEnabledState() { // Update tracking areas based on enabled state updateTrackingAreas() } func setIconScale(_ scale: CGFloat) { // Scale the icon background (which contains the icon) with animation NSAnimationContext.runAnimationGroup { context in context.duration = 2.0 // <<<< INCREASED DURATION HERE context.timingFunction = CAMediaTimingFunction(name: .easeOut) self.iconBackground.animator().layer?.transform = CATransform3DMakeScale(scale, scale, 1.0) } } func setIconScaleImmediate(_ scale: CGFloat) { // Scale the icon background immediately without animation self.iconBackground.layer?.transform = CATransform3DMakeScale(scale, scale, 1.0) } override func mouseEntered(with event: NSEvent) { guard isEnabled else { return } if isSmallButton { // Zoom effect for small buttons (info, close) - anchor point already set in setupButton // Create scale animation let scaleAnimation = CABasicAnimation(keyPath: "transform.scale") scaleAnimation.fromValue = 1.0 scaleAnimation.toValue = 1.2 scaleAnimation.duration = 0.2 scaleAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut) scaleAnimation.fillMode = .forwards scaleAnimation.isRemovedOnCompletion = false iconBackground.layer?.add(scaleAnimation, forKey: "scaleUp") iconBackground.layer?.transform = CATransform3DMakeScale(1.2, 1.2, 1.0) // Alpha animation NSAnimationContext.runAnimationGroup { ctx in ctx.duration = 0.2 ctx.timingFunction = CAMediaTimingFunction(name: .easeOut) self.iconBackground.animator().alphaValue = 1.0 } } else { // Slide effect for regular buttons let shift = (self.bounds.width / 2) - 18 - 2 NSAnimationContext.runAnimationGroup { ctx in ctx.duration = 0.15 ctx.timingFunction = CAMediaTimingFunction(name: .easeOut) self.iconCenterConstraint.animator().constant = shift self.label.animator().alphaValue = 1.0 } } } override func mouseExited(with event: NSEvent) { guard isEnabled else { return } if isSmallButton { // Reset zoom for small buttons iconBackground.layer?.removeAnimation(forKey: "scaleUp") let scaleAnimation = CABasicAnimation(keyPath: "transform.scale") scaleAnimation.fromValue = 1.2 scaleAnimation.toValue = 1.0 scaleAnimation.duration = 0.2 scaleAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut) scaleAnimation.fillMode = .forwards scaleAnimation.isRemovedOnCompletion = false iconBackground.layer?.add(scaleAnimation, forKey: "scaleDown") iconBackground.layer?.transform = CATransform3DIdentity // Alpha animation NSAnimationContext.runAnimationGroup { context in context.duration = 0.2 context.timingFunction = CAMediaTimingFunction(name: .easeOut) self.iconBackground.animator().alphaValue = 0.8 } } else { // Reset slide for regular buttons NSAnimationContext.runAnimationGroup { context in context.duration = 0.15 context.timingFunction = CAMediaTimingFunction(name: .easeOut) self.iconCenterConstraint.animator().constant = self.iconStartOffset self.label.animator().alphaValue = 0.3 } } } override func mouseDown(with event: NSEvent) { guard isEnabled else { return } // Visual feedback for click NSAnimationContext.runAnimationGroup({ context in context.duration = 0.1 if isSmallButton { self.iconBackground.layer?.transform = CATransform3DMakeScale(0.9, 0.9, 1.0) } else { self.animator().alphaValue = 0.8 } }) { NSAnimationContext.runAnimationGroup({ context in context.duration = 0.1 if self.isSmallButton { self.iconBackground.layer?.transform = CATransform3DMakeScale(1.2, 1.2, 1.0) } else { self.animator().alphaValue = 1.0 } }) } // Handle click action if let target = target, let action = action { print("๐Ÿ”˜ ActionStyleButton clicked: \(symbolName)") _ = target.perform(action, with: self) } } func setIconDisabledLook(disabled: Bool) { if disabled { iconImageView.alphaValue = 0.5 // Make the SF Symbol itself more transparent } else { iconImageView.alphaValue = 1.0 // Normal visibility for the icon } } // MARK: - Theme Management private func updateThemeColors() { // Update label color label.textColor = ThemeManager.shared.primaryTextColor // Update icon background color iconBackground.layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.cgColor // Update icon tint color iconImageView.contentTintColor = ThemeManager.shared.primaryTextColor // Update label shadow let shadow = NSShadow() shadow.shadowColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.4) shadow.shadowBlurRadius = 4 shadow.shadowOffset = NSSize(width: 0, height: -1) label.shadow = shadow // Update enabled state if disabled if !isEnabled { updateEnabledState() } } } // MARK: - Legacy code removed - use thumbnail-based BGR workflow instead