diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 12b845f..9995446 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -162,10 +162,7 @@ struct ContentView: View { enqueueMusicControlWindowSync(forceRefresh: forceRefresh, delay: delay) } private var dynamicNotchResizeAnimation: Animation? { - if enableMinimalisticUI && reminderManager.activeWindowReminders.isEmpty == false { - return nil - } - return .easeInOut(duration: 0.4) + nil } private let zeroHeightHoverPadding: CGFloat = 10 @@ -206,14 +203,27 @@ struct ContentView: View { var body: some View { let interactionsEnabled = !lockScreenManager.isLocked + let notchHorizontalPadding: CGFloat = { + guard vm.notchState == .open else { + return activeCornerRadiusInsets.closed.bottom + } + if Defaults[.cornerRadiusScaling] { + return activeCornerRadiusInsets.opened.top - 5 + } + return activeCornerRadiusInsets.opened.bottom - 5 + }() + let hoverAreaPadding: CGFloat = { + if vm.notchState == .open && Defaults[.extendHoverArea] { + return 0 + } + return vm.effectiveClosedNotchHeight == 0 ? zeroHeightHoverPadding : 0 + }() + let notchBottomPadding = currentShadowPadding + hoverAreaPadding ZStack(alignment: .top) { let mainLayout = NotchLayout() .frame(alignment: .top) - .padding(.horizontal, vm.notchState == .open - ? Defaults[.cornerRadiusScaling] ? (activeCornerRadiusInsets.opened.top - 5) : (activeCornerRadiusInsets.opened.bottom - 5) - : activeCornerRadiusInsets.closed.bottom - ) + .padding(.horizontal, notchHorizontalPadding) .padding([.horizontal, .bottom], vm.notchState == .open ? 12 : 0) .background(.black) .clipShape(currentNotchShape) @@ -224,23 +234,15 @@ struct ContentView: View { : .clear, radius: Defaults[.cornerRadiusScaling] ? 10 : 5 ) - .padding(.bottom, - currentShadowPadding + ( - vm.notchState == .open && Defaults[.extendHoverArea] - ? 0 - : (vm.effectiveClosedNotchHeight == 0 ? zeroHeightHoverPadding : 0) - ) - ) + .padding(.bottom, notchBottomPadding) mainLayout .conditionalModifier(!useModernCloseAnimation) { view in let hoverAnimation = Animation.bouncy.speed(1.2) let notchStateAnimation = Animation.spring.speed(1.2) - let viewTransitionAnimation = Animation.easeInOut(duration: 0.4) return view .animation(hoverAnimation, value: isHovering) .animation(notchStateAnimation, value: vm.notchState) - .animation(viewTransitionAnimation, value: coordinator.currentView) .animation(.smooth, value: gestureProgress) .transition(.blurReplace.animation(.interactiveSpring(dampingFraction: 1.2))) } @@ -248,12 +250,10 @@ struct ContentView: View { let hoverAnimation = Animation.bouncy.speed(1.2) let openAnimation = Animation.spring(response: 0.42, dampingFraction: 0.8, blendDuration: 0) let closeAnimation = Animation.spring(response: 0.45, dampingFraction: 1.0, blendDuration: 0) - let viewTransitionAnimation = Animation.easeInOut(duration: 0.4) let notchAnimation = vm.notchState == .open ? openAnimation : closeAnimation return view .animation(hoverAnimation, value: isHovering) .animation(notchAnimation, value: vm.notchState) - .animation(viewTransitionAnimation, value: coordinator.currentView) .animation(.smooth, value: gestureProgress) } .conditionalModifier(interactionsEnabled) { view in @@ -382,13 +382,10 @@ struct ContentView: View { maxHeight: dynamicNotchSize.height + currentShadowPadding, alignment: .top ) - .animation(dynamicNotchResizeAnimation, value: dynamicNotchSize) - .animation(.easeInOut(duration: 0.4), value: coordinator.currentView) .environmentObject(privacyManager) .onChange(of: dynamicNotchSize) { oldSize, newSize in guard oldSize != newSize else { return } - let delay: TimeInterval = dynamicNotchResizeAnimation == nil ? 0.25 : 1.0 - runAfter(delay) { + runAfter(0.1) { vm.shouldRecheckHover.toggle() } } @@ -738,14 +735,9 @@ struct ContentView: View { } } } - .transition(.asymmetric( - insertion: .scale(scale: 0.8).combined(with: .opacity).animation(.easeInOut(duration: 0.4)), - removal: .scale(scale: 0.8).combined(with: .opacity).animation(.easeInOut(duration: 0.4)) - )) .id(coordinator.currentView) // Force SwiftUI to treat each view as unique } } - .animation(.easeInOut(duration: 0.4), value: coordinator.currentView) .zIndex(1) .allowsHitTesting(vm.notchState == .open) .blur(radius: abs(gestureProgress) > 0.3 ? min(abs(gestureProgress), 8) : 0) diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 118ccd2..c70629a 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -209,6 +209,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { backing: .buffered, defer: false ) + + window.animationBehavior = .none window.contentView = NSHostingView( rootView: ContentView() @@ -253,6 +255,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { let animateResize = shouldAnimateResize(for: requiredSize) resizeWindows(to: requiredSize, animated: animateResize, force: false) } + + private func updateWindowSizeForTabSwitch() { + let requiredSize = calculateRequiredNotchSize() + resizeWindows(to: requiredSize, animated: false, force: true) + } private func calculateRequiredNotchSize() -> CGSize { // Check if inline sneak peek is showing and notch is closed @@ -321,16 +328,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { let newY = screenFrame.origin.y + screenFrame.height - size.height let targetFrame = NSRect(x: newX, y: newY, width: size.width, height: size.height) - if animated { - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.25 - context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) - context.allowsImplicitAnimation = true - window.animator().setFrame(targetFrame, display: true) - } - } else { - window.setFrame(targetFrame, display: true) - } + window.setFrame(targetFrame, display: true) } private func shouldAnimateResize(for newSize: CGSize) -> Bool { @@ -370,9 +368,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Setup Privacy Indicator Manager (camera and microphone monitoring) PrivacyIndicatorManager.shared.startMonitoring() - // Observe tab changes - use immediate updates to prevent clipping - coordinator.$currentView.sink { [weak self] newView in - self?.updateWindowSizeIfNeeded() + // Observe tab changes - use immediate resize to keep the notch pinned + coordinator.$currentView.sink { [weak self] _ in + self?.updateWindowSizeForTabSwitch() }.store(in: &cancellables) coordinator.$notesLayoutState diff --git a/DynamicIsland/DynamicIslandViewCoordinator.swift b/DynamicIsland/DynamicIslandViewCoordinator.swift index 331a256..52b9f26 100644 --- a/DynamicIsland/DynamicIslandViewCoordinator.swift +++ b/DynamicIsland/DynamicIslandViewCoordinator.swift @@ -102,9 +102,6 @@ class DynamicIslandViewCoordinator: ObservableObject { } @Published var statsSecondRowExpansion: CGFloat = 1 - private var statsSecondRowWorkItem: DispatchWorkItem? - private let statsSecondRowRevealDelay: TimeInterval = 0.5 - private let statsSecondRowAnimationDuration: TimeInterval = 0.3 @Published var notesLayoutState: NotesLayoutState = .list @Published var selectedExtensionExperienceID: String? @@ -206,22 +203,8 @@ class DynamicIslandViewCoordinator: ObservableObject { private func handleStatsTabTransition(from oldValue: NotchViews, to newValue: NotchViews) { guard oldValue != newValue else { return } - statsSecondRowWorkItem?.cancel() if newValue == .stats && Defaults[.enableStatsFeature] { - statsSecondRowExpansion = 0 - let workItem = DispatchWorkItem { [weak self] in - guard let self else { return } - withAnimation(.easeInOut(duration: self.statsSecondRowAnimationDuration)) { - self.statsSecondRowExpansion = 1 - } - self.statsSecondRowWorkItem = nil - } - statsSecondRowWorkItem = workItem - DispatchQueue.main.asyncAfter(deadline: .now() + statsSecondRowRevealDelay, execute: workItem) - } else { - withAnimation(.easeInOut(duration: 0.2)) { - statsSecondRowExpansion = 0 - } + statsSecondRowExpansion = 1 } } diff --git a/DynamicIsland/components/Notch/NotchStatsView.swift b/DynamicIsland/components/Notch/NotchStatsView.swift index f309f57..f8c5896 100644 --- a/DynamicIsland/components/Notch/NotchStatsView.swift +++ b/DynamicIsland/components/Notch/NotchStatsView.swift @@ -63,9 +63,6 @@ struct NotchStatsView: View { @State private var isHoveringGPUPopover = false @State private var isHoveringNetworkPopover = false @State private var isHoveringDiskPopover = false - @State private var isResizingForStats = false - @State private var statsHoverGraceActive = false - @State private var statsHoverGraceWorkItem: DispatchWorkItem? @EnvironmentObject var vm: DynamicIslandViewModel @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @@ -149,8 +146,8 @@ struct NotchStatsView: View { } } } else if graphCount == 4 { - // 4 graphs: second row scales from zero height on tab open - VStack(spacing: 0) { + // 4 graphs: two rows (2x2) without a collapsible expansion + VStack(spacing: statsGridSpacingHeight) { LazyVGrid( columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2), spacing: 12 @@ -159,26 +156,18 @@ struct NotchStatsView: View { graphViewForIndex(index) } } - - StatsCollapsibleRow( - expansion: coordinator.statsSecondRowExpansion, - height: statsSecondRowContentHeight + statsGridSpacingHeight + LazyVGrid( + columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2), + spacing: 12 ) { - LazyVGrid( - columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2), - spacing: 12 - ) { - ForEach(2..= 4 && newValue < 0.999 - if shouldHold { - isResizingForStats = true - } else { - DispatchQueue.main.async { - isResizingForStats = false - } - } - } } - - -private struct StatsCollapsibleRow: View { - let expansion: CGFloat - let height: CGFloat - let content: () -> Content - private let minVisibleHeight: CGFloat = 0.5 - - init(expansion: CGFloat, height: CGFloat, @ViewBuilder content: @escaping () -> Content) { - self.expansion = expansion - self.height = height - self.content = content - } - - var body: some View { - let clamped = max(0, min(expansion, 1)) - let targetHeight = clamped == 0 ? 0 : max(height * clamped, minVisibleHeight) - - return Group { - if targetHeight == 0 { - Color.clear.frame(height: 0) - } else { - content() - .frame(height: height, alignment: .top) - .clipped() - .mask( - Rectangle() - .frame(height: targetHeight) - .frame(maxWidth: .infinity, alignment: .top) - ) - .frame(height: targetHeight, alignment: .top) - } - } - .animation(.easeInOut(duration: 0.3), value: clamped) - } -} - private func activateStatsHoverGrace(duration: TimeInterval = 0.45) { - statsHoverGraceWorkItem?.cancel() - statsHoverGraceActive = true - let workItem = DispatchWorkItem { - statsHoverGraceActive = false - statsHoverGraceWorkItem = nil - updateStatsPopoverState() - } - statsHoverGraceWorkItem = workItem - DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: workItem) - } - - private func deactivateStatsHoverGrace() { - statsHoverGraceWorkItem?.cancel() - statsHoverGraceWorkItem = nil - if statsHoverGraceActive { - statsHoverGraceActive = false - } - } - private func updateStatsPopoverState() { let anyPopoverOpen = showingCPUPopover || showingMemoryPopover || showingGPUPopover || showingNetworkPopover || showingDiskPopover - let newState = anyPopoverOpen || isResizingForStats || statsHoverGraceActive + let newState = anyPopoverOpen if vm.isStatsPopoverActive != newState { vm.isStatsPopoverActive = newState #if DEBUG @@ -540,8 +414,6 @@ private struct StatsCollapsibleRow: View { print(" GPU open=\(showingGPUPopover)") print(" Network open=\(showingNetworkPopover)") print(" Disk open=\(showingDiskPopover)") - print(" Resizing: \(isResizingForStats)") - print(" Hover grace: \(statsHoverGraceActive)") #endif } }