Graph size fixed

This commit is contained in:
Hariharan Mudaliar 2025-09-28 00:57:04 +05:30
commit cc53e73cd1
2 changed files with 378 additions and 89 deletions

View file

@ -47,6 +47,10 @@
}, },
"%lld%%" : { "%lld%%" : {
},
"•" : {
"comment" : "A period used to separate two related items.",
"isCommentAutoGenerated" : true
}, },
"• Bug fixes" : { "• Bug fixes" : {
@ -567,6 +571,10 @@
}, },
"Enable global keyboard shortcuts above to customize your shortcuts." : { "Enable global keyboard shortcuts above to customize your shortcuts." : {
},
"Enable graph visibility in Settings → Stats to view performance data." : {
"comment" : "A message displayed when no graphs are enabled in the Stats tab.",
"isCommentAutoGenerated" : true
}, },
"Enable haptics" : { "Enable haptics" : {
"extractionState" : "stale", "extractionState" : "stale",
@ -940,6 +948,10 @@
}, },
"No favorites" : { "No favorites" : {
},
"No Graphs Enabled" : {
"comment" : "A label displayed when no graphs are enabled in the Stats tab.",
"isCommentAutoGenerated" : true
}, },
"No results" : { "No results" : {

View file

@ -31,104 +31,209 @@ struct SingleGraphData: GraphData {
let type: GraphType = .single let type: GraphType = .single
} }
// Dual value graph data (for network/disk)
struct DualGraphData: GraphData {
let title: String
let positiveValue: String
let negativeValue: String
let positiveData: [Double]
let negativeData: [Double]
let positiveColor: Color
let negativeColor: Color
let color: Color // Primary color for the component
let icon: String
let type: GraphType = .dual
}
struct NotchStatsView: View { struct NotchStatsView: View {
@ObservedObject var statsManager = StatsManager.shared @ObservedObject var statsManager = StatsManager.shared
@Default(.enableStatsFeature) var enableStatsFeature @Default(.enableStatsFeature) var enableStatsFeature
@Default(.showCpuGraph) var showCpuGraph
@Default(.showMemoryGraph) var showMemoryGraph
@Default(.showGpuGraph) var showGpuGraph
@Default(.showNetworkGraph) var showNetworkGraph
@Default(.showDiskGraph) var showDiskGraph
@State private var showingCPUPopover = false @State private var showingCPUPopover = false
@State private var showingMemoryPopover = false @State private var showingMemoryPopover = false
@State private var showingGPUPopover = false @State private var showingGPUPopover = false
@State private var isHoveringCPUPopover = false @State private var isHoveringCPUPopover = false
@State private var isHoveringMemoryPopover = false @State private var isHoveringMemoryPopover = false
@State private var isHoveringGPUPopover = false @State private var isHoveringGPUPopover = false
@State private var isResizingForStats = false
@EnvironmentObject var vm: DynamicIslandViewModel @EnvironmentObject var vm: DynamicIslandViewModel
var availableGraphs: [GraphData] { var availableGraphs: [GraphData] {
var graphs: [GraphData] = [] var graphs: [GraphData] = []
// Only CPU, Memory, and GPU as in boring.notch - no network/disk if showCpuGraph {
graphs.append(SingleGraphData( graphs.append(SingleGraphData(
title: "CPU", title: "CPU",
value: statsManager.cpuUsageString, value: statsManager.cpuUsageString,
data: statsManager.cpuHistory, data: statsManager.cpuHistory,
color: .blue, color: .blue,
icon: "cpu" icon: "cpu"
)) ))
}
graphs.append(SingleGraphData( if showMemoryGraph {
title: "Memory", graphs.append(SingleGraphData(
value: statsManager.memoryUsageString, title: "Memory",
data: statsManager.memoryHistory, value: statsManager.memoryUsageString,
color: .green, data: statsManager.memoryHistory,
icon: "memorychip" color: .green,
)) icon: "memorychip"
))
}
graphs.append(SingleGraphData( if showGpuGraph {
title: "GPU", graphs.append(SingleGraphData(
value: statsManager.gpuUsageString, title: "GPU",
data: statsManager.gpuHistory, value: statsManager.gpuUsageString,
color: .purple, data: statsManager.gpuHistory,
icon: "display" color: .purple,
)) icon: "display"
))
}
if showNetworkGraph {
graphs.append(DualGraphData(
title: "Network",
positiveValue: "↓" + statsManager.networkDownloadString,
negativeValue: "↑" + statsManager.networkUploadString,
positiveData: statsManager.networkDownloadHistory,
negativeData: statsManager.networkUploadHistory,
positiveColor: .orange,
negativeColor: .red,
color: .orange,
icon: "network"
))
}
if showDiskGraph {
graphs.append(DualGraphData(
title: "Disk",
positiveValue: "R " + statsManager.diskReadString,
negativeValue: "W " + statsManager.diskWriteString,
positiveData: statsManager.diskReadHistory,
negativeData: statsManager.diskWriteHistory,
positiveColor: .cyan,
negativeColor: .yellow,
color: .cyan,
icon: "internaldrive"
))
}
return graphs return graphs
} }
// Restored original 3-graph layout from boring.notch // Smart grid layout system for different graph counts
@ViewBuilder @ViewBuilder
var statsGridLayout: some View { var statsGridLayout: some View {
// 3 graphs: Single row with proper spacing - matches boring.notch exactly let graphCount = availableGraphs.count
LazyVGrid(
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3),
spacing: 8
) {
ForEach(0..<availableGraphs.count, id: \.self) { index in
let graphData = availableGraphs[index]
Button(action: { if graphCount <= 3 {
handleGraphClick(for: graphData) // 1-3 graphs: Single row with equal spacing
}) { LazyVGrid(
UnifiedStatsCard(graphData: graphData) columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: graphCount),
spacing: 12
) {
ForEach(0..<graphCount, id: \.self) { index in
graphViewForIndex(index)
} }
.buttonStyle(PlainButtonStyle()) }
.popover(isPresented: bindingForGraph(graphData)) { } else if graphCount == 4 {
RankedProcessPopover( // 4 graphs: 2x2 grid
rankingType: rankingTypeForGraph(graphData), LazyVGrid(
onHoverChange: { hovering in columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
switch graphData.title { spacing: 12
case "CPU": ) {
isHoveringCPUPopover = hovering ForEach(0..<graphCount, id: \.self) { index in
case "Memory": graphViewForIndex(index)
isHoveringMemoryPopover = hovering }
case "GPU": }
isHoveringGPUPopover = hovering } else {
default: // 5 graphs: First row 3 graphs, second row 2 graphs (half-width each)
break VStack(spacing: 12) {
} // First row: 3 graphs
} LazyVGrid(
) columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3),
.onDisappear { spacing: 8
// Reset hover states when popover disappears ) {
ForEach(0..<3, id: \.self) { index in
graphViewForIndex(index)
}
}
// Second row: 2 graphs (half-width each)
LazyVGrid(
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
spacing: 8
) {
ForEach(3..<graphCount, id: \.self) { index in
graphViewForIndex(index)
}
}
}
}
}
@ViewBuilder
private func graphViewForIndex(_ index: Int) -> some View {
let graphData = availableGraphs[index]
// Only make CPU, Memory, GPU clickable for process popovers
if graphData.title == "CPU" || graphData.title == "Memory" || graphData.title == "GPU" {
Button(action: {
handleGraphClick(for: graphData)
}) {
UnifiedStatsCard(graphData: graphData)
}
.buttonStyle(PlainButtonStyle())
.popover(isPresented: bindingForGraph(graphData)) {
RankedProcessPopover(
rankingType: rankingTypeForGraph(graphData),
onHoverChange: { hovering in
switch graphData.title { switch graphData.title {
case "CPU": case "CPU":
isHoveringCPUPopover = false isHoveringCPUPopover = hovering
case "Memory": case "Memory":
isHoveringMemoryPopover = false isHoveringMemoryPopover = hovering
case "GPU": case "GPU":
isHoveringGPUPopover = false isHoveringGPUPopover = hovering
default: default:
break break
} }
// Ensure popover state is updated when popover disappears }
DispatchQueue.main.async { )
updateStatsPopoverState() .onDisappear {
} // Reset hover states when popover disappears
switch graphData.title {
case "CPU":
isHoveringCPUPopover = false
case "Memory":
isHoveringMemoryPopover = false
case "GPU":
isHoveringGPUPopover = false
default:
break
}
// Ensure popover state is updated when popover disappears
DispatchQueue.main.async {
updateStatsPopoverState()
} }
} }
}
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
))
} else {
// Network and Disk graphs are not clickable
UnifiedStatsCard(graphData: graphData)
.transition(.asymmetric( .transition(.asymmetric(
insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)), insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)) removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
)) ))
}
} }
} }
@ -198,8 +303,27 @@ struct NotchStatsView: View {
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.padding() .padding()
} else if availableGraphs.isEmpty {
// No graphs enabled state
VStack(spacing: 12) {
Image(systemName: "chart.xyaxis.line")
.font(.system(size: 40))
.foregroundColor(.secondary)
Text("No Graphs Enabled")
.font(.headline)
.foregroundColor(.primary)
Text("Enable graph visibility in Settings → Stats to view performance data.")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
} else { } else {
// Stats content - restored to original boring.notch 3-graph layout // Stats content with smart grid layout
VStack(spacing: 8) { VStack(spacing: 8) {
statsGridLayout statsGridLayout
} }
@ -222,6 +346,13 @@ struct NotchStatsView: View {
} }
.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) { _, newCount in
// Protect against hover interference during dynamic sizing
isResizingForStats = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
isResizingForStats = false
}
}
.onChange(of: showingCPUPopover) { _, _ in .onChange(of: showingCPUPopover) { _, _ in
updateStatsPopoverState() updateStatsPopoverState()
} }
@ -244,9 +375,11 @@ struct NotchStatsView: View {
private func updateStatsPopoverState() { private func updateStatsPopoverState() {
// Use the same logic as battery popover: active only when shown AND hovered // Use the same logic as battery popover: active only when shown AND hovered
// Also consider resize protection to prevent closing during layout changes
let newState = (showingCPUPopover && isHoveringCPUPopover) || let newState = (showingCPUPopover && isHoveringCPUPopover) ||
(showingMemoryPopover && isHoveringMemoryPopover) || (showingMemoryPopover && isHoveringMemoryPopover) ||
(showingGPUPopover && isHoveringGPUPopover) (showingGPUPopover && isHoveringGPUPopover) ||
isResizingForStats
if vm.isStatsPopoverActive != newState { if vm.isStatsPopoverActive != newState {
vm.isStatsPopoverActive = newState vm.isStatsPopoverActive = newState
#if DEBUG #if DEBUG
@ -254,62 +387,89 @@ struct NotchStatsView: View {
print(" CPU: shown=\(showingCPUPopover), hovering=\(isHoveringCPUPopover)") print(" CPU: shown=\(showingCPUPopover), hovering=\(isHoveringCPUPopover)")
print(" Memory: shown=\(showingMemoryPopover), hovering=\(isHoveringMemoryPopover)") print(" Memory: shown=\(showingMemoryPopover), hovering=\(isHoveringMemoryPopover)")
print(" GPU: shown=\(showingGPUPopover), hovering=\(isHoveringGPUPopover)") print(" GPU: shown=\(showingGPUPopover), hovering=\(isHoveringGPUPopover)")
print(" Resizing: \(isResizingForStats)")
#endif #endif
} }
} }
} }
// Unified Stats Card Component - now clickable for popovers, matches boring.notch sizing // Unified Stats Card Component - handles both single and dual data types, matches boring.notch sizing
struct UnifiedStatsCard: View { struct UnifiedStatsCard: View {
let graphData: GraphData let graphData: GraphData
@State private var isHovered = false @State private var isHovered = false
var body: some View { var body: some View {
VStack(spacing: 3) { VStack(spacing: 3) { // Match boring.notch spacing
// Header with title on left and percentage on right - matches boring.notch layout // Header - consistent across all card types
HStack(spacing: 4) { HStack(spacing: 4) {
// Title and icon on left Image(systemName: graphData.icon)
HStack(spacing: 3) { .foregroundColor(graphData.color)
Image(systemName: graphData.icon) .font(.caption) // Match boring.notch font size
.foregroundColor(graphData.color)
.font(.caption)
Text(graphData.title) Text(graphData.title)
.font(.caption) .font(.caption) // Match boring.notch font size
.fontWeight(.medium) .fontWeight(.medium)
.foregroundColor(.secondary) .foregroundColor(.secondary)
}
Spacer() Spacer()
// Percentage value on right // Show value on right for single graphs like boring.notch
if let singleData = graphData as? SingleGraphData { if let singleData = graphData as? SingleGraphData {
Text(singleData.value) Text(singleData.value)
.font(.caption) .font(.caption) // Match boring.notch font size
.fontWeight(.bold) .fontWeight(.bold)
.foregroundColor(.primary) .foregroundColor(.primary)
} }
} }
// Graph section - full height since no separate value section, matches boring.notch // Values section for dual graphs only
if let dualData = graphData as? DualGraphData {
HStack(spacing: 6) {
Text(dualData.positiveValue)
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(dualData.positiveColor)
Text("•")
.font(.caption2)
.foregroundColor(.secondary)
Text(dualData.negativeValue)
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(dualData.negativeColor)
}
.frame(height: 18) // Smaller for dual graphs
}
// Graph section - adapts based on graph type
Group { Group {
if let singleData = graphData as? SingleGraphData { if let singleData = graphData as? SingleGraphData {
MiniGraph(data: singleData.data, color: singleData.color) MiniGraph(data: singleData.data, color: singleData.color)
} else if let dualData = graphData as? DualGraphData {
DualQuadrantGraph(
positiveData: dualData.positiveData,
negativeData: dualData.negativeData,
positiveColor: dualData.positiveColor,
negativeColor: dualData.negativeColor
)
} }
} }
.frame(height: 36) // Matches boring.notch exactly - reduced from 50px .frame(height: 36) // Match boring.notch exactly - reduced from 50
// Click hint // Click hint - only for CPU, Memory, GPU
Text("Click for details") if graphData.title == "CPU" || graphData.title == "Memory" || graphData.title == "GPU" {
.font(.caption2) Text("Click for details")
.foregroundColor(.secondary) .font(.caption2)
.opacity(isHovered ? 1.0 : 0.0) .foregroundColor(.secondary)
.animation(.easeInOut(duration: 0.2), value: isHovered) .opacity(isHovered ? 1.0 : 0.0)
.animation(.easeInOut(duration: 0.2), value: isHovered)
}
} }
.padding(8) // Reduced padding to match boring.notch .padding(8) // Match boring.notch padding - reduced from 10
.background( .background(
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.fill(Color(NSColor.controlBackgroundColor).opacity(0.7)) .fill(Color(NSColor.controlBackgroundColor).opacity(0.7)) // Match boring.notch opacity
.overlay( .overlay(
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.stroke(graphData.color.opacity(isHovered ? 0.5 : 0.2), lineWidth: 1) .stroke(graphData.color.opacity(isHovered ? 0.5 : 0.2), lineWidth: 1)
@ -381,6 +541,123 @@ struct MiniGraph: View {
struct DualQuadrantGraph: View {
let positiveData: [Double]
let negativeData: [Double]
let positiveColor: Color
let negativeColor: Color
var body: some View {
GeometryReader { geometry in
let maxPositive = positiveData.max() ?? 1.0
let maxNegative = negativeData.max() ?? 1.0
let maxValue = max(maxPositive, maxNegative)
let normalizedPositive = maxValue > 0 ? positiveData.map { $0 / maxValue } : positiveData
let normalizedNegative = maxValue > 0 ? negativeData.map { $0 / maxValue } : negativeData
let centerY = geometry.size.height / 2
ZStack {
// Center dividing line
Path { path in
path.move(to: CGPoint(x: 0, y: centerY))
path.addLine(to: CGPoint(x: geometry.size.width, y: centerY))
}
.stroke(Color.secondary.opacity(0.3), lineWidth: 1)
// Positive quadrant (upper half)
Path { path in
guard !normalizedPositive.isEmpty else { return }
let stepX = geometry.size.width / CGFloat(normalizedPositive.count - 1)
for (index, value) in normalizedPositive.enumerated() {
let x = CGFloat(index) * stepX
let y = centerY - (centerY * CGFloat(value)) // Above center
if index == 0 {
path.move(to: CGPoint(x: x, y: y))
} else {
path.addLine(to: CGPoint(x: x, y: y))
}
}
}
.stroke(positiveColor, lineWidth: 2)
// Positive fill
Path { path in
guard !normalizedPositive.isEmpty else { return }
let stepX = geometry.size.width / CGFloat(normalizedPositive.count - 1)
path.move(to: CGPoint(x: 0, y: centerY))
for (index, value) in normalizedPositive.enumerated() {
let x = CGFloat(index) * stepX
let y = centerY - (centerY * CGFloat(value))
path.addLine(to: CGPoint(x: x, y: y))
}
path.addLine(to: CGPoint(x: geometry.size.width, y: centerY))
path.closeSubpath()
}
.fill(
LinearGradient(
gradient: Gradient(colors: [positiveColor.opacity(0.3), positiveColor.opacity(0.1)]),
startPoint: .top,
endPoint: .center
)
)
// Negative quadrant (lower half)
Path { path in
guard !normalizedNegative.isEmpty else { return }
let stepX = geometry.size.width / CGFloat(normalizedNegative.count - 1)
for (index, value) in normalizedNegative.enumerated() {
let x = CGFloat(index) * stepX
let y = centerY + (centerY * CGFloat(value)) // Below center
if index == 0 {
path.move(to: CGPoint(x: x, y: y))
} else {
path.addLine(to: CGPoint(x: x, y: y))
}
}
}
.stroke(negativeColor, lineWidth: 2)
// Negative fill
Path { path in
guard !normalizedNegative.isEmpty else { return }
let stepX = geometry.size.width / CGFloat(normalizedNegative.count - 1)
path.move(to: CGPoint(x: 0, y: centerY))
for (index, value) in normalizedNegative.enumerated() {
let x = CGFloat(index) * stepX
let y = centerY + (centerY * CGFloat(value))
path.addLine(to: CGPoint(x: x, y: y))
}
path.addLine(to: CGPoint(x: geometry.size.width, y: centerY))
path.closeSubpath()
}
.fill(
LinearGradient(
gradient: Gradient(colors: [negativeColor.opacity(0.3), negativeColor.opacity(0.1)]),
startPoint: .bottom,
endPoint: .center
)
)
}
}
}
}
#Preview { #Preview {
NotchStatsView() NotchStatsView()
.frame(width: 400, height: 300) .frame(width: 400, height: 300)