Working stuff

This commit is contained in:
Hariharan Mudaliar 2025-10-19 08:43:58 +05:30
commit 7a1945dd21
5 changed files with 162 additions and 30 deletions

View file

@ -694,7 +694,7 @@ struct ContentView: View {
self.isHovering = false self.isHovering = false
} }
if self.vm.notchState == .open && !self.vm.isBatteryPopoverActive { if self.vm.notchState == .open && !self.hasAnyActivePopovers() {
self.vm.close() self.vm.close()
} }
} }

View file

@ -64,6 +64,8 @@ struct NotchStatsView: View {
@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 isResizingForStats = false
@State private var statsHoverGraceActive = false
@State private var statsHoverGraceWorkItem: DispatchWorkItem?
@EnvironmentObject var vm: DynamicIslandViewModel @EnvironmentObject var vm: DynamicIslandViewModel
var availableGraphs: [GraphData] { var availableGraphs: [GraphData] {
@ -364,7 +366,13 @@ struct NotchStatsView: View {
} }
} }
.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()
} }
.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)
@ -375,19 +383,44 @@ struct NotchStatsView: View {
isResizingForStats = false isResizingForStats = false
} }
} }
.onChange(of: showingCPUPopover) { _, _ in .onChange(of: showingCPUPopover) { _, newValue in
if newValue {
activateStatsHoverGrace()
} else {
deactivateStatsHoverGrace()
}
updateStatsPopoverState() updateStatsPopoverState()
} }
.onChange(of: showingMemoryPopover) { _, _ in .onChange(of: showingMemoryPopover) { _, newValue in
if newValue {
activateStatsHoverGrace()
} else {
deactivateStatsHoverGrace()
}
updateStatsPopoverState() updateStatsPopoverState()
} }
.onChange(of: showingGPUPopover) { _, _ in .onChange(of: showingGPUPopover) { _, newValue in
if newValue {
activateStatsHoverGrace()
} else {
deactivateStatsHoverGrace()
}
updateStatsPopoverState() updateStatsPopoverState()
} }
.onChange(of: showingNetworkPopover) { _, _ in .onChange(of: showingNetworkPopover) { _, newValue in
if newValue {
activateStatsHoverGrace()
} else {
deactivateStatsHoverGrace()
}
updateStatsPopoverState() updateStatsPopoverState()
} }
.onChange(of: showingDiskPopover) { _, _ in .onChange(of: showingDiskPopover) { _, newValue in
if newValue {
activateStatsHoverGrace()
} else {
deactivateStatsHoverGrace()
}
updateStatsPopoverState() updateStatsPopoverState()
} }
.onChange(of: isHoveringCPUPopover) { _, _ in .onChange(of: isHoveringCPUPopover) { _, _ in
@ -406,26 +439,41 @@ struct NotchStatsView: View {
updateStatsPopoverState() updateStatsPopoverState()
} }
} }
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() {
// Use the same logic as battery popover: active only when shown AND hovered let anyPopoverOpen = showingCPUPopover || showingMemoryPopover || showingGPUPopover || showingNetworkPopover || showingDiskPopover
// Also consider resize protection to prevent closing during layout changes let newState = anyPopoverOpen || isResizingForStats || statsHoverGraceActive
let newState = (showingCPUPopover && isHoveringCPUPopover) ||
(showingMemoryPopover && isHoveringMemoryPopover) ||
(showingGPUPopover && isHoveringGPUPopover) ||
(showingNetworkPopover && isHoveringNetworkPopover) ||
(showingDiskPopover && isHoveringDiskPopover) ||
isResizingForStats
if vm.isStatsPopoverActive != newState { if vm.isStatsPopoverActive != newState {
vm.isStatsPopoverActive = newState vm.isStatsPopoverActive = newState
#if DEBUG #if DEBUG
print("📊 Stats popover state updated: \(newState)") print("📊 Stats popover state updated: \(newState)")
print(" CPU: shown=\(showingCPUPopover), hovering=\(isHoveringCPUPopover)") print(" CPU open=\(showingCPUPopover)")
print(" Memory: shown=\(showingMemoryPopover), hovering=\(isHoveringMemoryPopover)") print(" Memory open=\(showingMemoryPopover)")
print(" GPU: shown=\(showingGPUPopover), hovering=\(isHoveringGPUPopover)") print(" GPU open=\(showingGPUPopover)")
print(" Network: shown=\(showingNetworkPopover), hovering=\(isHoveringNetworkPopover)") print(" Network open=\(showingNetworkPopover)")
print(" Disk: shown=\(showingDiskPopover), hovering=\(isHoveringDiskPopover)") print(" Disk open=\(showingDiskPopover)")
print(" Resizing: \(isResizingForStats)") print(" Resizing: \(isResizingForStats)")
print(" Hover grace: \(statsHoverGraceActive)")
#endif #endif
} }
} }

View file

@ -73,11 +73,11 @@ private struct NetworkOverview: View {
Text("Download") Text("Download")
.font(.headline) .font(.headline)
.foregroundColor(downloadColor) .foregroundColor(downloadColor)
Text(StatsFormatting.mbPerSecond(download)) Text(StatsFormatting.throughput(download))
.font(.system(size: 26, weight: .bold, design: .rounded)) .font(.system(size: 26, weight: .bold, design: .rounded))
.foregroundColor(.primary) .foregroundColor(.primary)
DetailRow(color: downloadColor.opacity(0.6), label: "Average", value: StatsFormatting.mbPerSecond(avgDownload)) DetailRow(color: downloadColor.opacity(0.6), label: "Average", value: StatsFormatting.throughput(avgDownload))
DetailRow(color: downloadColor.opacity(0.45), label: "Peak", value: StatsFormatting.mbPerSecond(peakDownload)) DetailRow(color: downloadColor.opacity(0.45), label: "Peak", value: StatsFormatting.throughput(peakDownload))
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
@ -89,11 +89,11 @@ private struct NetworkOverview: View {
Text("Upload") Text("Upload")
.font(.headline) .font(.headline)
.foregroundColor(uploadColor) .foregroundColor(uploadColor)
Text(StatsFormatting.mbPerSecond(upload)) Text(StatsFormatting.throughput(upload))
.font(.system(size: 26, weight: .bold, design: .rounded)) .font(.system(size: 26, weight: .bold, design: .rounded))
.foregroundColor(.primary) .foregroundColor(.primary)
DetailRow(color: uploadColor.opacity(0.6), label: "Average", value: StatsFormatting.mbPerSecond(avgUpload)) DetailRow(color: uploadColor.opacity(0.6), label: "Average", value: StatsFormatting.throughput(avgUpload))
DetailRow(color: uploadColor.opacity(0.45), label: "Peak", value: StatsFormatting.mbPerSecond(peakUpload)) DetailRow(color: uploadColor.opacity(0.45), label: "Peak", value: StatsFormatting.throughput(peakUpload))
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
@ -213,8 +213,8 @@ private struct NetworkInterfaceRow: View {
Spacer() Spacer()
VStack(alignment: .trailing, spacing: 4) { VStack(alignment: .trailing, spacing: 4) {
metricRow(label: "Down", value: StatsFormatting.mbPerSecond(interface.currentDownload), color: downloadColor) metricRow(label: "Down", value: StatsFormatting.throughput(interface.currentDownload), color: downloadColor)
metricRow(label: "Up", value: StatsFormatting.mbPerSecond(interface.currentUpload), color: uploadColor) metricRow(label: "Up", value: StatsFormatting.throughput(interface.currentUpload), color: uploadColor)
} }
} }
} }

View file

@ -6,6 +6,9 @@
// Adapted from boring.notch implementation // Adapted from boring.notch implementation
import SwiftUI import SwiftUI
#if os(macOS)
import AppKit
#endif
struct RankedProcessPopover: View { struct RankedProcessPopover: View {
let rankingType: ProcessRankingType let rankingType: ProcessRankingType
@ -44,8 +47,12 @@ struct RankedProcessPopover: View {
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
} }
.onHover { hovering in .overlay(alignment: .center) {
onHoverChange?(hovering) PopoverHoverSensor { hovering in
onHoverChange?(hovering)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.allowsHitTesting(false)
} }
} }
@ -64,4 +71,51 @@ struct RankedProcessPopover: View {
DiskStatsDetailView() DiskStatsDetailView()
} }
} }
} }
#if os(macOS)
private struct PopoverHoverSensor: NSViewRepresentable {
var onHoverChange: ((Bool) -> Void)?
func makeNSView(context: Context) -> TrackingView {
let view = TrackingView()
view.onHoverChange = onHoverChange
return view
}
func updateNSView(_ nsView: TrackingView, context: Context) {
nsView.onHoverChange = onHoverChange
}
final class TrackingView: NSView {
var onHoverChange: ((Bool) -> Void)?
private var trackingArea: NSTrackingArea?
override func updateTrackingAreas() {
super.updateTrackingAreas()
if let trackingArea {
removeTrackingArea(trackingArea)
}
let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways, .inVisibleRect]
let area = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
addTrackingArea(area)
trackingArea = area
}
override func mouseEntered(with event: NSEvent) {
onHoverChange?(true)
}
override func mouseExited(with event: NSEvent) {
onHoverChange?(false)
}
deinit {
onHoverChange?(false)
if let trackingArea {
removeTrackingArea(trackingArea)
}
}
}
}
#endif

View file

@ -26,6 +26,36 @@ enum StatsFormatting {
let formatted = formatter.string(from: number) ?? String(format: "%.2f", value) let formatted = formatter.string(from: number) ?? String(format: "%.2f", value)
return "\(formatted) MB/s" return "\(formatted) MB/s"
} }
static func throughput(_ valueInMegabytesPerSecond: Double) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
if valueInMegabytesPerSecond >= 1 {
formatter.maximumFractionDigits = valueInMegabytesPerSecond >= 10 ? 1 : 2
let formatted = formatter.string(from: NSNumber(value: valueInMegabytesPerSecond))
?? String(format: "%.2f", valueInMegabytesPerSecond)
return "\(formatted) MB/s"
}
let valueInKilobytesPerSecond = valueInMegabytesPerSecond * 1024
if valueInKilobytesPerSecond >= 1 {
formatter.maximumFractionDigits = valueInKilobytesPerSecond >= 10 ? 0 : 1
let formatted = formatter.string(from: NSNumber(value: valueInKilobytesPerSecond))
?? String(format: valueInKilobytesPerSecond >= 10 ? "%.0f" : "%.1f", valueInKilobytesPerSecond)
return "\(formatted) KB/s"
}
let valueInBytesPerSecond = valueInMegabytesPerSecond * 1_048_576
if valueInBytesPerSecond >= 1 {
formatter.maximumFractionDigits = 0
let formatted = formatter.string(from: NSNumber(value: valueInBytesPerSecond))
?? String(format: "%.0f", valueInBytesPerSecond)
return "\(formatted) B/s"
}
return "0 B/s"
}
static func percentage(_ value: Double) -> String { static func percentage(_ value: Double) -> String {
String(format: "%.1f%%", value) String(format: "%.1f%%", value)