diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 9a5565d..708d01e 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -38,13 +38,17 @@ struct ContentView: View { let enabledGraphsCount = [showCpuGraph, showMemoryGraph, showGpuGraph, showNetworkGraph, showDiskGraph].filter { $0 }.count - // If 4+ graphs are enabled, increase width + // Calculate height based on layout: 1-3 graphs = single row, 4+ graphs = two rows + var requiredHeight = openNotchSize.height + if enabledGraphsCount >= 4 { - let extraWidth: CGFloat = CGFloat(enabledGraphsCount - 3) * 120 - return CGSize(width: openNotchSize.width + extraWidth, height: openNotchSize.height) + // Two rows needed - add height for second row plus spacing + let extraHeight: CGFloat = 120 + 12 // Graph height + spacing + requiredHeight = openNotchSize.height + extraHeight } - return openNotchSize + // Width stays constant - no horizontal expansion + return CGSize(width: openNotchSize.width, height: requiredHeight) } diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index b4970d4..102ff7a 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -86,12 +86,31 @@ class AppDelegate: NSObject, NSApplicationDelegate { private var previousScreens: [NSScreen]? private var onboardingWindowController: NSWindowController? private var cancellables = Set() + + // Debouncing mechanism for window size updates + private var windowSizeUpdateWorkItem: DispatchWorkItem? + + private func debouncedUpdateWindowSize() { + // Cancel any existing work item + windowSizeUpdateWorkItem?.cancel() + + // Create new work item with delay + let workItem = DispatchWorkItem { [weak self] in + self?.updateWindowSizeIfNeeded() + } + + // Store reference and schedule + windowSizeUpdateWorkItem = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15, execute: workItem) + } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return false } func applicationWillTerminate(_ notification: Notification) { + // Cancel any pending window size updates + windowSizeUpdateWorkItem?.cancel() NotificationCenter.default.removeObserver(self) } @@ -126,9 +145,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { private func createDynamicIslandWindow(for screen: NSScreen, with viewModel: DynamicIslandViewModel) -> NSWindow { + // Use the current required size instead of always using openNotchSize + let requiredSize = calculateRequiredNotchSize() + let window = DynamicIslandWindow( contentRect: NSRect( - x: 0, y: 0, width: openNotchSize.width, height: openNotchSize.height), + x: 0, y: 0, width: requiredSize.width, height: requiredSize.height), styleMask: [.borderless, .nonactivatingPanel, .utilityWindow, .hudWindow], backing: .buffered, defer: false @@ -151,14 +173,20 @@ class AppDelegate: NSObject, NSApplicationDelegate { window.alphaValue = 0 } - DispatchQueue.main.async { [weak window] in - guard let window = window else { return } - let screenFrame = screen.frame - window.setFrameOrigin( - NSPoint( - x: screenFrame.origin.x + (screenFrame.width / 2) - window.frame.width / 2, - y: screenFrame.origin.y + screenFrame.height - window.frame.height - )) + // Use the same centering logic as updateWindowSizeIfNeeded() + let screenFrame = screen.frame + let centerX = screenFrame.origin.x + (screenFrame.width / 2) + let newX = centerX - (window.frame.width / 2) + let newY = screenFrame.origin.y + screenFrame.height - window.frame.height + + window.setFrame(NSRect( + x: newX, + y: newY, + width: window.frame.width, + height: window.frame.height + ), display: false) + + if changeAlpha { window.alphaValue = 1 } } @@ -167,22 +195,65 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Calculate required size based on current state let requiredSize = calculateRequiredNotchSize() - // Update all windows if size has changed - for (screen, window) in windows { - if window.frame.size != requiredSize { - let screenFrame = screen.frame - let newFrame = NSRect( - x: screenFrame.origin.x + (screenFrame.width / 2) - requiredSize.width / 2, - y: screenFrame.origin.y + screenFrame.height - requiredSize.height, - width: requiredSize.width, - height: requiredSize.height - ) + if Defaults[.showOnAllDisplays] { + // Update all windows if size has changed (multi-display mode) + for (screen, window) in windows { + if window.frame.size != requiredSize { + // Calculate center position BEFORE any changes + let screenFrame = screen.frame + let centerX = screenFrame.origin.x + (screenFrame.width / 2) + let newX = centerX - (requiredSize.width / 2) + let newY = screenFrame.origin.y + screenFrame.height - requiredSize.height + + // Stop any existing animations first + NSAnimationContext.runAnimationGroup { _ in + window.animator().setFrame(window.frame, display: false) + } + + // Animate window resize smoothly + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.25 + context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + context.allowsImplicitAnimation = true + window.animator().setFrame(NSRect( + x: newX, + y: newY, + width: requiredSize.width, + height: requiredSize.height + ), display: true) + } + } + } + } else { + // Update single window if size has changed (single display mode) + if let window = window, window.frame.size != requiredSize { + // Find the screen this window is on + let currentScreen = NSScreen.screens.first { screen in + screen.frame.intersects(window.frame) + } ?? NSScreen.main ?? NSScreen.screens.first! - // Animate the frame change for smooth transition + // Calculate center position BEFORE any changes + let screenFrame = currentScreen.frame + let centerX = screenFrame.origin.x + (screenFrame.width / 2) + let newX = centerX - (requiredSize.width / 2) + let newY = screenFrame.origin.y + screenFrame.height - requiredSize.height + + // Stop any existing animations first + NSAnimationContext.runAnimationGroup { _ in + window.animator().setFrame(window.frame, display: false) + } + + // Animate window resize smoothly NSAnimationContext.runAnimationGroup { context in - context.duration = 0.3 + context.duration = 0.25 + context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) context.allowsImplicitAnimation = true - window.setFrame(newFrame, display: true, animate: true) + window.animator().setFrame(NSRect( + x: newX, + y: newY, + width: requiredSize.width, + height: requiredSize.height + ), display: true) } } } @@ -202,61 +273,51 @@ class AppDelegate: NSObject, NSApplicationDelegate { Defaults[.showDiskGraph] ].filter { $0 }.count - // If 4+ graphs are enabled, increase width + // Calculate height based on layout: 1-3 graphs = single row, 4+ graphs = two rows + var requiredHeight = openNotchSize.height + if enabledGraphsCount >= 4 { - let extraWidth: CGFloat = CGFloat(enabledGraphsCount - 3) * 120 - return CGSize(width: openNotchSize.width + extraWidth, height: openNotchSize.height) + // Two rows needed - add height for second row plus spacing + let extraHeight: CGFloat = 120 + 12 // Graph height + spacing + requiredHeight = openNotchSize.height + extraHeight } - return openNotchSize + // Width stays constant - no horizontal expansion + return CGSize(width: openNotchSize.width, height: requiredHeight) } func applicationDidFinishLaunching(_ notification: Notification) { coordinator.setupWorkersNotificationObservers() - // Observe tab changes to update window size dynamically - coordinator.objectWillChange.sink { [weak self] in - DispatchQueue.main.async { - self?.updateWindowSizeIfNeeded() - } + // Observe tab changes - use debounced updates + coordinator.$currentView.sink { [weak self] newView in + self?.debouncedUpdateWindowSize() }.store(in: &cancellables) - // Observe stats settings changes + // Observe stats settings changes - use debounced updates Defaults.publisher(.enableStatsFeature, options: []).sink { [weak self] _ in - DispatchQueue.main.async { - self?.updateWindowSizeIfNeeded() - } + self?.debouncedUpdateWindowSize() }.store(in: &cancellables) Defaults.publisher(.showCpuGraph, options: []).sink { [weak self] _ in - DispatchQueue.main.async { - self?.updateWindowSizeIfNeeded() - } + self?.debouncedUpdateWindowSize() }.store(in: &cancellables) Defaults.publisher(.showMemoryGraph, options: []).sink { [weak self] _ in - DispatchQueue.main.async { - self?.updateWindowSizeIfNeeded() - } + self?.debouncedUpdateWindowSize() }.store(in: &cancellables) Defaults.publisher(.showGpuGraph, options: []).sink { [weak self] _ in - DispatchQueue.main.async { - self?.updateWindowSizeIfNeeded() - } + self?.debouncedUpdateWindowSize() }.store(in: &cancellables) Defaults.publisher(.showNetworkGraph, options: []).sink { [weak self] _ in - DispatchQueue.main.async { - self?.updateWindowSizeIfNeeded() - } + self?.debouncedUpdateWindowSize() }.store(in: &cancellables) Defaults.publisher(.showDiskGraph, options: []).sink { [weak self] _ in - DispatchQueue.main.async { - self?.updateWindowSizeIfNeeded() - } + self?.debouncedUpdateWindowSize() }.store(in: &cancellables) NotificationCenter.default.addObserver( diff --git a/DynamicIsland/components/Notch/NotchStatsView.swift b/DynamicIsland/components/Notch/NotchStatsView.swift index 556a96f..8c2ec38 100644 --- a/DynamicIsland/components/Notch/NotchStatsView.swift +++ b/DynamicIsland/components/Notch/NotchStatsView.swift @@ -118,18 +118,144 @@ struct NotchStatsView: View { return graphs } - var gridColumns: [GridItem] { + // New vertical expansion layout system + @ViewBuilder + var statsGridLayout: some View { let graphCount = availableGraphs.count - if graphCount <= 3 { - // Horizontal layout for 1-3 graphs - return Array(repeating: GridItem(.flexible(), spacing: 12), count: graphCount) - } else { - // Compressed grid layout for 4+ graphs - return Array(repeating: GridItem(.flexible(), spacing: 8), count: graphCount) + switch graphCount { + case 1...3: + // Single row for 1-3 graphs + HStack(spacing: 12) { + ForEach(0.. some View { + if let singleData = graphData as? SingleGraphData { + StatCard( + title: singleData.title, + value: singleData.value, + data: singleData.data, + color: singleData.color, + icon: singleData.icon + ) + } else if let dualData = graphData as? DualGraphData { + DualStatCard( + title: dualData.title, + positiveValue: dualData.positiveValue, + negativeValue: dualData.negativeValue, + positiveData: dualData.positiveData, + negativeData: dualData.negativeData, + positiveColor: dualData.positiveColor, + negativeColor: dualData.negativeColor, + icon: dualData.icon + ) + } + } + var body: some View { VStack(spacing: 0) { if !enableStatsFeature { @@ -171,39 +297,18 @@ struct NotchStatsView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() } else { - // Stats content with dynamic layout + // Stats content with vertical expansion layout ZStack(alignment: .topTrailing) { VStack(spacing: 12) { - // Dynamic Stats Grid - LazyVGrid(columns: gridColumns, spacing: 12) { - ForEach(0..