mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Tried to fix the detach animation
This commit is contained in:
parent
f425b38a81
commit
a1678afe0a
4 changed files with 46 additions and 201 deletions
|
|
@ -162,10 +162,7 @@ struct ContentView: View {
|
||||||
enqueueMusicControlWindowSync(forceRefresh: forceRefresh, delay: delay)
|
enqueueMusicControlWindowSync(forceRefresh: forceRefresh, delay: delay)
|
||||||
}
|
}
|
||||||
private var dynamicNotchResizeAnimation: Animation? {
|
private var dynamicNotchResizeAnimation: Animation? {
|
||||||
if enableMinimalisticUI && reminderManager.activeWindowReminders.isEmpty == false {
|
nil
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return .easeInOut(duration: 0.4)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private let zeroHeightHoverPadding: CGFloat = 10
|
private let zeroHeightHoverPadding: CGFloat = 10
|
||||||
|
|
@ -206,14 +203,27 @@ struct ContentView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let interactionsEnabled = !lockScreenManager.isLocked
|
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) {
|
ZStack(alignment: .top) {
|
||||||
let mainLayout = NotchLayout()
|
let mainLayout = NotchLayout()
|
||||||
.frame(alignment: .top)
|
.frame(alignment: .top)
|
||||||
.padding(.horizontal, vm.notchState == .open
|
.padding(.horizontal, notchHorizontalPadding)
|
||||||
? Defaults[.cornerRadiusScaling] ? (activeCornerRadiusInsets.opened.top - 5) : (activeCornerRadiusInsets.opened.bottom - 5)
|
|
||||||
: activeCornerRadiusInsets.closed.bottom
|
|
||||||
)
|
|
||||||
.padding([.horizontal, .bottom], vm.notchState == .open ? 12 : 0)
|
.padding([.horizontal, .bottom], vm.notchState == .open ? 12 : 0)
|
||||||
.background(.black)
|
.background(.black)
|
||||||
.clipShape(currentNotchShape)
|
.clipShape(currentNotchShape)
|
||||||
|
|
@ -224,23 +234,15 @@ struct ContentView: View {
|
||||||
: .clear,
|
: .clear,
|
||||||
radius: Defaults[.cornerRadiusScaling] ? 10 : 5
|
radius: Defaults[.cornerRadiusScaling] ? 10 : 5
|
||||||
)
|
)
|
||||||
.padding(.bottom,
|
.padding(.bottom, notchBottomPadding)
|
||||||
currentShadowPadding + (
|
|
||||||
vm.notchState == .open && Defaults[.extendHoverArea]
|
|
||||||
? 0
|
|
||||||
: (vm.effectiveClosedNotchHeight == 0 ? zeroHeightHoverPadding : 0)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
mainLayout
|
mainLayout
|
||||||
.conditionalModifier(!useModernCloseAnimation) { view in
|
.conditionalModifier(!useModernCloseAnimation) { view in
|
||||||
let hoverAnimation = Animation.bouncy.speed(1.2)
|
let hoverAnimation = Animation.bouncy.speed(1.2)
|
||||||
let notchStateAnimation = Animation.spring.speed(1.2)
|
let notchStateAnimation = Animation.spring.speed(1.2)
|
||||||
let viewTransitionAnimation = Animation.easeInOut(duration: 0.4)
|
|
||||||
return view
|
return view
|
||||||
.animation(hoverAnimation, value: isHovering)
|
.animation(hoverAnimation, value: isHovering)
|
||||||
.animation(notchStateAnimation, value: vm.notchState)
|
.animation(notchStateAnimation, value: vm.notchState)
|
||||||
.animation(viewTransitionAnimation, value: coordinator.currentView)
|
|
||||||
.animation(.smooth, value: gestureProgress)
|
.animation(.smooth, value: gestureProgress)
|
||||||
.transition(.blurReplace.animation(.interactiveSpring(dampingFraction: 1.2)))
|
.transition(.blurReplace.animation(.interactiveSpring(dampingFraction: 1.2)))
|
||||||
}
|
}
|
||||||
|
|
@ -248,12 +250,10 @@ struct ContentView: View {
|
||||||
let hoverAnimation = Animation.bouncy.speed(1.2)
|
let hoverAnimation = Animation.bouncy.speed(1.2)
|
||||||
let openAnimation = Animation.spring(response: 0.42, dampingFraction: 0.8, blendDuration: 0)
|
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 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
|
let notchAnimation = vm.notchState == .open ? openAnimation : closeAnimation
|
||||||
return view
|
return view
|
||||||
.animation(hoverAnimation, value: isHovering)
|
.animation(hoverAnimation, value: isHovering)
|
||||||
.animation(notchAnimation, value: vm.notchState)
|
.animation(notchAnimation, value: vm.notchState)
|
||||||
.animation(viewTransitionAnimation, value: coordinator.currentView)
|
|
||||||
.animation(.smooth, value: gestureProgress)
|
.animation(.smooth, value: gestureProgress)
|
||||||
}
|
}
|
||||||
.conditionalModifier(interactionsEnabled) { view in
|
.conditionalModifier(interactionsEnabled) { view in
|
||||||
|
|
@ -382,13 +382,10 @@ struct ContentView: View {
|
||||||
maxHeight: dynamicNotchSize.height + currentShadowPadding,
|
maxHeight: dynamicNotchSize.height + currentShadowPadding,
|
||||||
alignment: .top
|
alignment: .top
|
||||||
)
|
)
|
||||||
.animation(dynamicNotchResizeAnimation, value: dynamicNotchSize)
|
|
||||||
.animation(.easeInOut(duration: 0.4), value: coordinator.currentView)
|
|
||||||
.environmentObject(privacyManager)
|
.environmentObject(privacyManager)
|
||||||
.onChange(of: dynamicNotchSize) { oldSize, newSize in
|
.onChange(of: dynamicNotchSize) { oldSize, newSize in
|
||||||
guard oldSize != newSize else { return }
|
guard oldSize != newSize else { return }
|
||||||
let delay: TimeInterval = dynamicNotchResizeAnimation == nil ? 0.25 : 1.0
|
runAfter(0.1) {
|
||||||
runAfter(delay) {
|
|
||||||
vm.shouldRecheckHover.toggle()
|
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
|
.id(coordinator.currentView) // Force SwiftUI to treat each view as unique
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.easeInOut(duration: 0.4), value: coordinator.currentView)
|
|
||||||
.zIndex(1)
|
.zIndex(1)
|
||||||
.allowsHitTesting(vm.notchState == .open)
|
.allowsHitTesting(vm.notchState == .open)
|
||||||
.blur(radius: abs(gestureProgress) > 0.3 ? min(abs(gestureProgress), 8) : 0)
|
.blur(radius: abs(gestureProgress) > 0.3 ? min(abs(gestureProgress), 8) : 0)
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
backing: .buffered,
|
backing: .buffered,
|
||||||
defer: false
|
defer: false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
window.animationBehavior = .none
|
||||||
|
|
||||||
window.contentView = NSHostingView(
|
window.contentView = NSHostingView(
|
||||||
rootView: ContentView()
|
rootView: ContentView()
|
||||||
|
|
@ -253,6 +255,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
let animateResize = shouldAnimateResize(for: requiredSize)
|
let animateResize = shouldAnimateResize(for: requiredSize)
|
||||||
resizeWindows(to: requiredSize, animated: animateResize, force: false)
|
resizeWindows(to: requiredSize, animated: animateResize, force: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateWindowSizeForTabSwitch() {
|
||||||
|
let requiredSize = calculateRequiredNotchSize()
|
||||||
|
resizeWindows(to: requiredSize, animated: false, force: true)
|
||||||
|
}
|
||||||
|
|
||||||
private func calculateRequiredNotchSize() -> CGSize {
|
private func calculateRequiredNotchSize() -> CGSize {
|
||||||
// Check if inline sneak peek is showing and notch is closed
|
// 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 newY = screenFrame.origin.y + screenFrame.height - size.height
|
||||||
let targetFrame = NSRect(x: newX, y: newY, width: size.width, height: size.height)
|
let targetFrame = NSRect(x: newX, y: newY, width: size.width, height: size.height)
|
||||||
|
|
||||||
if animated {
|
window.setFrame(targetFrame, display: true)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func shouldAnimateResize(for newSize: CGSize) -> Bool {
|
private func shouldAnimateResize(for newSize: CGSize) -> Bool {
|
||||||
|
|
@ -370,9 +368,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
// Setup Privacy Indicator Manager (camera and microphone monitoring)
|
// Setup Privacy Indicator Manager (camera and microphone monitoring)
|
||||||
PrivacyIndicatorManager.shared.startMonitoring()
|
PrivacyIndicatorManager.shared.startMonitoring()
|
||||||
|
|
||||||
// Observe tab changes - use immediate updates to prevent clipping
|
// Observe tab changes - use immediate resize to keep the notch pinned
|
||||||
coordinator.$currentView.sink { [weak self] newView in
|
coordinator.$currentView.sink { [weak self] _ in
|
||||||
self?.updateWindowSizeIfNeeded()
|
self?.updateWindowSizeForTabSwitch()
|
||||||
}.store(in: &cancellables)
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
coordinator.$notesLayoutState
|
coordinator.$notesLayoutState
|
||||||
|
|
|
||||||
|
|
@ -102,9 +102,6 @@ class DynamicIslandViewCoordinator: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var statsSecondRowExpansion: CGFloat = 1
|
@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 notesLayoutState: NotesLayoutState = .list
|
||||||
@Published var selectedExtensionExperienceID: String?
|
@Published var selectedExtensionExperienceID: String?
|
||||||
|
|
||||||
|
|
@ -206,22 +203,8 @@ class DynamicIslandViewCoordinator: ObservableObject {
|
||||||
|
|
||||||
private func handleStatsTabTransition(from oldValue: NotchViews, to newValue: NotchViews) {
|
private func handleStatsTabTransition(from oldValue: NotchViews, to newValue: NotchViews) {
|
||||||
guard oldValue != newValue else { return }
|
guard oldValue != newValue else { return }
|
||||||
statsSecondRowWorkItem?.cancel()
|
|
||||||
if newValue == .stats && Defaults[.enableStatsFeature] {
|
if newValue == .stats && Defaults[.enableStatsFeature] {
|
||||||
statsSecondRowExpansion = 0
|
statsSecondRowExpansion = 1
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,6 @@ struct NotchStatsView: View {
|
||||||
@State private var isHoveringGPUPopover = false
|
@State private var isHoveringGPUPopover = false
|
||||||
@State private var isHoveringNetworkPopover = false
|
@State private var isHoveringNetworkPopover = false
|
||||||
@State private var isHoveringDiskPopover = 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
|
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||||
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
||||||
|
|
||||||
|
|
@ -149,8 +146,8 @@ struct NotchStatsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if graphCount == 4 {
|
} else if graphCount == 4 {
|
||||||
// 4 graphs: second row scales from zero height on tab open
|
// 4 graphs: two rows (2x2) without a collapsible expansion
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: statsGridSpacingHeight) {
|
||||||
LazyVGrid(
|
LazyVGrid(
|
||||||
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
|
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
@ -159,26 +156,18 @@ struct NotchStatsView: View {
|
||||||
graphViewForIndex(index)
|
graphViewForIndex(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LazyVGrid(
|
||||||
StatsCollapsibleRow(
|
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
|
||||||
expansion: coordinator.statsSecondRowExpansion,
|
spacing: 12
|
||||||
height: statsSecondRowContentHeight + statsGridSpacingHeight
|
|
||||||
) {
|
) {
|
||||||
LazyVGrid(
|
ForEach(2..<graphCount, id: \.self) { index in
|
||||||
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
|
graphViewForIndex(index)
|
||||||
spacing: 12
|
|
||||||
) {
|
|
||||||
ForEach(2..<graphCount, id: \.self) { index in
|
|
||||||
graphViewForIndex(index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(.top, statsGridSpacingHeight)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 5 graphs: First row 3 graphs, second row 2 graphs (half-width each)
|
// 5 graphs: First row 3 graphs, second row 2 graphs (half-width each)
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: statsGridSpacingHeight) {
|
||||||
// First row: 3 graphs
|
|
||||||
LazyVGrid(
|
LazyVGrid(
|
||||||
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3),
|
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3),
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
@ -187,20 +176,13 @@ struct NotchStatsView: View {
|
||||||
graphViewForIndex(index)
|
graphViewForIndex(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Second row: 2 graphs (half-width each)
|
LazyVGrid(
|
||||||
StatsCollapsibleRow(
|
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
|
||||||
expansion: coordinator.statsSecondRowExpansion,
|
spacing: 8
|
||||||
height: statsSecondRowContentHeight + statsGridSpacingHeight
|
|
||||||
) {
|
) {
|
||||||
LazyVGrid(
|
ForEach(3..<graphCount, id: \.self) { index in
|
||||||
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
|
graphViewForIndex(index)
|
||||||
spacing: 8
|
|
||||||
) {
|
|
||||||
ForEach(3..<graphCount, id: \.self) { index in
|
|
||||||
graphViewForIndex(index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(.top, statsGridSpacingHeight)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -382,68 +364,26 @@ struct NotchStatsView: View {
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
// Note: Smart monitoring will handle starting/stopping based on notch state and current view
|
// Note: Smart monitoring will handle starting/stopping based on notch state and current view
|
||||||
// Protect against hover interference during view transition
|
|
||||||
isResizingForStats = true
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
|
||||||
isResizingForStats = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
statsHoverGraceWorkItem?.cancel()
|
|
||||||
statsHoverGraceWorkItem = nil
|
|
||||||
if statsHoverGraceActive {
|
|
||||||
statsHoverGraceActive = false
|
|
||||||
}
|
|
||||||
// Keep monitoring running when tab is not visible
|
// Keep monitoring running when tab is not visible
|
||||||
updateStatsPopoverState()
|
updateStatsPopoverState()
|
||||||
}
|
}
|
||||||
.animation(.easeInOut(duration: 0.4), value: enableStatsFeature)
|
.animation(.easeInOut(duration: 0.4), value: enableStatsFeature)
|
||||||
.animation(.easeInOut(duration: 0.4), value: availableGraphs.count)
|
.animation(.easeInOut(duration: 0.4), value: availableGraphs.count)
|
||||||
.onChange(of: availableGraphs.count) { _, _ in
|
|
||||||
// Protect against hover interference during dynamic sizing
|
|
||||||
isResizingForStats = true
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
|
||||||
isResizingForStats = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: showingCPUPopover) { _, newValue in
|
.onChange(of: showingCPUPopover) { _, newValue in
|
||||||
if newValue {
|
|
||||||
activateStatsHoverGrace()
|
|
||||||
} else {
|
|
||||||
deactivateStatsHoverGrace()
|
|
||||||
}
|
|
||||||
updateStatsPopoverState()
|
updateStatsPopoverState()
|
||||||
}
|
}
|
||||||
.onChange(of: showingMemoryPopover) { _, newValue in
|
.onChange(of: showingMemoryPopover) { _, newValue in
|
||||||
if newValue {
|
|
||||||
activateStatsHoverGrace()
|
|
||||||
} else {
|
|
||||||
deactivateStatsHoverGrace()
|
|
||||||
}
|
|
||||||
updateStatsPopoverState()
|
updateStatsPopoverState()
|
||||||
}
|
}
|
||||||
.onChange(of: showingGPUPopover) { _, newValue in
|
.onChange(of: showingGPUPopover) { _, newValue in
|
||||||
if newValue {
|
|
||||||
activateStatsHoverGrace()
|
|
||||||
} else {
|
|
||||||
deactivateStatsHoverGrace()
|
|
||||||
}
|
|
||||||
updateStatsPopoverState()
|
updateStatsPopoverState()
|
||||||
}
|
}
|
||||||
.onChange(of: showingNetworkPopover) { _, newValue in
|
.onChange(of: showingNetworkPopover) { _, newValue in
|
||||||
if newValue {
|
|
||||||
activateStatsHoverGrace()
|
|
||||||
} else {
|
|
||||||
deactivateStatsHoverGrace()
|
|
||||||
}
|
|
||||||
updateStatsPopoverState()
|
updateStatsPopoverState()
|
||||||
}
|
}
|
||||||
.onChange(of: showingDiskPopover) { _, newValue in
|
.onChange(of: showingDiskPopover) { _, newValue in
|
||||||
if newValue {
|
|
||||||
activateStatsHoverGrace()
|
|
||||||
} else {
|
|
||||||
deactivateStatsHoverGrace()
|
|
||||||
}
|
|
||||||
updateStatsPopoverState()
|
updateStatsPopoverState()
|
||||||
}
|
}
|
||||||
.onChange(of: isHoveringCPUPopover) { _, _ in
|
.onChange(of: isHoveringCPUPopover) { _, _ in
|
||||||
|
|
@ -461,76 +401,10 @@ struct NotchStatsView: View {
|
||||||
.onChange(of: isHoveringDiskPopover) { _, _ in
|
.onChange(of: isHoveringDiskPopover) { _, _ in
|
||||||
updateStatsPopoverState()
|
updateStatsPopoverState()
|
||||||
}
|
}
|
||||||
.onChange(of: coordinator.statsSecondRowExpansion) { _, newValue in
|
|
||||||
let shouldHold = availableGraphs.count >= 4 && newValue < 0.999
|
|
||||||
if shouldHold {
|
|
||||||
isResizingForStats = true
|
|
||||||
} else {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
isResizingForStats = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private struct StatsCollapsibleRow<Content: View>: 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() {
|
private func updateStatsPopoverState() {
|
||||||
let anyPopoverOpen = showingCPUPopover || showingMemoryPopover || showingGPUPopover || showingNetworkPopover || showingDiskPopover
|
let anyPopoverOpen = showingCPUPopover || showingMemoryPopover || showingGPUPopover || showingNetworkPopover || showingDiskPopover
|
||||||
let newState = anyPopoverOpen || isResizingForStats || statsHoverGraceActive
|
let newState = anyPopoverOpen
|
||||||
if vm.isStatsPopoverActive != newState {
|
if vm.isStatsPopoverActive != newState {
|
||||||
vm.isStatsPopoverActive = newState
|
vm.isStatsPopoverActive = newState
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|
@ -540,8 +414,6 @@ private struct StatsCollapsibleRow<Content: View>: View {
|
||||||
print(" GPU open=\(showingGPUPopover)")
|
print(" GPU open=\(showingGPUPopover)")
|
||||||
print(" Network open=\(showingNetworkPopover)")
|
print(" Network open=\(showingNetworkPopover)")
|
||||||
print(" Disk open=\(showingDiskPopover)")
|
print(" Disk open=\(showingDiskPopover)")
|
||||||
print(" Resizing: \(isResizingForStats)")
|
|
||||||
print(" Hover grace: \(statsHoverGraceActive)")
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue