diff --git a/Modules/CPU/popup.swift b/Modules/CPU/popup.swift index 65fbbb0a..11f389ca 100644 --- a/Modules/CPU/popup.swift +++ b/Modules/CPU/popup.swift @@ -18,7 +18,8 @@ internal class Popup: NSView { private var title: String private let dashboardHeight: CGFloat = 90 - private let detailsHeight: CGFloat = 66 // -26 + private let chartHeight: CGFloat = 90 + private let detailsHeight: CGFloat = 88 private let processesHeight: CGFloat = 22*5 private var loadField: NSTextField? = nil @@ -28,6 +29,7 @@ internal class Popup: NSView { private var userField: NSTextField? = nil private var idleField: NSTextField? = nil + private var circle: CircleGraphView? = nil private var chart: LineChartView? = nil private var ready: Bool = false @@ -37,9 +39,15 @@ internal class Popup: NSView { self.store = store self.title = title - super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: dashboardHeight + (Constants.Popup.separatorHeight*2) + detailsHeight + processesHeight)) + super.init(frame: NSRect( + x: 0, + y: 0, + width: Constants.Popup.width, + height: dashboardHeight + chartHeight + detailsHeight + processesHeight + (Constants.Popup.separatorHeight*3) + )) initDashboard() + initChart() initDetails() initProcesses() } @@ -53,33 +61,43 @@ internal class Popup: NSView { } private func initDashboard() { - let rightWidth: CGFloat = 110 let view: NSView = NSView(frame: NSRect(x: 0, y: self.frame.height - self.dashboardHeight, width: self.frame.width, height: self.dashboardHeight)) - let leftPanel = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width - rightWidth - Constants.Popup.margins, height: view.frame.height)) + let container: NSView = NSView(frame: NSRect(x: 0, y: 20/2, width: view.frame.width, height: self.dashboardHeight-20)) + let circle: CircleGraphView = CircleGraphView(frame: NSRect(x: 0, y: 0, width: container.frame.width, height: container.frame.height), segments: []) + self.circle = circle + container.addSubview(circle) + view.addSubview(container) - self.chart = LineChartView(frame: NSRect(x: 4, y: 3, width: leftPanel.frame.width, height: leftPanel.frame.height), num: 120) - leftPanel.addSubview(self.chart!) + self.addSubview(view) + } + + private func initChart() { + let y: CGFloat = self.frame.height - self.dashboardHeight - Constants.Popup.separatorHeight + let separator = SeparatorView("History", origin: NSPoint(x: 0, y: y), width: self.frame.width) + self.addSubview(separator) - let rightPanel: NSView = NSView(frame: NSRect(x: view.frame.width - rightWidth, y: 0, width: rightWidth, height: view.frame.height)) - self.loadField = addFirstRow(mView: rightPanel, y: ((rightPanel.frame.height - 16)/2)+9, title: "Load:", value: "") - self.temperatureField = addFirstRow(mView: rightPanel, y: ((rightPanel.frame.height - 16)/2)-9, title: "Temperature:", value: "") + let view: NSView = NSView(frame: NSRect(x: 0, y: y - self.chartHeight, width: self.frame.width, height: self.chartHeight)) + view.wantsLayer = true + view.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.1).cgColor + view.layer?.cornerRadius = 3 + self.chart = LineChartView(frame: NSRect(x: 1, y: 0, width: view.frame.width, height: view.frame.height), num: 120) + view.addSubview(self.chart!) - view.addSubview(leftPanel) - view.addSubview(rightPanel) self.addSubview(view) } private func initDetails() { - let y: CGFloat = self.frame.height - self.dashboardHeight - Constants.Popup.separatorHeight + let y: CGFloat = self.frame.height - self.dashboardHeight - self.chartHeight - (Constants.Popup.separatorHeight*2) let separator = SeparatorView("Details", origin: NSPoint(x: 0, y: y), width: self.frame.width) self.addSubview(separator) let view: NSView = NSView(frame: NSRect(x: 0, y: separator.frame.origin.y - self.detailsHeight, width: self.frame.width, height: self.detailsHeight)) - self.systemField = PopupRow(view, n: 2, title: "System:", value: "") - self.userField = PopupRow(view, n: 1, title: "User:", value: "") - self.idleField = PopupRow(view, n: 0, title: "Idle:", value: "") + self.systemField = PopupWithColorRow(view, color: NSColor.systemRed, n: 3, title: "System:", value: "") + self.userField = PopupWithColorRow(view, color: NSColor.systemBlue, n: 2, title: "User:", value: "") + self.idleField = PopupWithColorRow(view, color: NSColor.lightGray.withAlphaComponent(0.5), n: 1, title: "Idle:", value: "") + self.temperatureField = PopupRow(view, n: 0, title: "Temperature:", value: "") self.addSubview(view) } @@ -144,6 +162,11 @@ internal class Popup: NSView { self.ready = true } + self.circle?.setValue(value.totalUsage) + self.circle?.setSegments([ + circle_segment(value: value.systemLoad, color: NSColor.systemRed), + circle_segment(value: value.userLoad, color: NSColor.systemBlue), + ]) self.chart?.addValue(value.totalUsage) }) } diff --git a/StatsKit/Charts.swift b/StatsKit/Charts.swift index d824cc8f..30754a45 100644 --- a/StatsKit/Charts.swift +++ b/StatsKit/Charts.swift @@ -20,6 +20,16 @@ public enum chart_t: Int { } } +public struct circle_segment { + let value: Double + let color: NSColor + + public init(value: Double, color: NSColor) { + self.value = value + self.color = color + } +} + public class LineChartView: NSView { public var points: [Double]? = nil public var transparent: Bool = true @@ -48,7 +58,7 @@ public class LineChartView: NSView { gradientColor = self.color.withAlphaComponent(0.8) } - let context = NSGraphicsContext.current!.cgContext + guard let context = NSGraphicsContext.current?.cgContext else { return } context.setShouldAntialias(true) let height: CGFloat = self.frame.size.height - self.frame.origin.y - 0.5 let xRatio: CGFloat = self.frame.size.width / CGFloat(self.points!.count) @@ -98,3 +108,78 @@ public class LineChartView: NSView { } } } + +public class CircleGraphView: NSView { + private var value: Double? = nil + private var segments: [circle_segment] = [] + + public init(frame: NSRect, segments: [circle_segment]) { + self.segments = segments + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func draw(_ rect: CGRect) { + let arcWidth: CGFloat = 7.0 + let fullCircle = 2 * CGFloat.pi + var segments = self.segments + let totalAmount = segments.reduce(0) { $0 + $1.value } + if totalAmount < 1 { + segments.append(circle_segment(value: Double(1-totalAmount), color: NSColor.lightGray.withAlphaComponent(0.5))) + } + + let centerPoint = CGPoint(x: rect.midX, y: rect.midY) + let radius = (min(rect.width, rect.height) - arcWidth) / 2 + + guard let context = NSGraphicsContext.current?.cgContext else { return } + context.setShouldAntialias(true) + + context.setLineWidth(arcWidth) + context.setLineCap(.butt) + + let startAngle: CGFloat = CGFloat.pi/2 + var previousAngle = startAngle + + for segment in segments.reversed() { + let currentAngle: CGFloat = previousAngle + (CGFloat(segment.value) * fullCircle) + + context.setStrokeColor(segment.color.cgColor) + context.addArc(center: centerPoint, radius: radius, startAngle: previousAngle, endAngle: currentAngle, clockwise: false) + context.strokePath() + + previousAngle = currentAngle + } + + if let value = self.value { + let stringAttributes = [ + NSAttributedString.Key.font: NSFont.systemFont(ofSize: 15, weight: .regular), + NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor, + NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle() + ] + + let percentage = "\(Int(value*100))%" + let width: CGFloat = percentage.widthOfString(usingFont: NSFont.systemFont(ofSize: 15)) + let rect = CGRect(x: (self.frame.width-width)/2, y: (self.frame.height-12)/2, width: width, height: 12) + + let str = NSAttributedString.init(string: percentage, attributes: stringAttributes) + str.draw(with: rect) + } + } + + public func setValue(_ value: Double) { + self.value = value + if self.window?.isVisible ?? true { + self.display() + } + } + + public func setSegments(_ segments: [circle_segment]) { + self.segments = segments + if self.window?.isVisible ?? true { + self.display() + } + } +} diff --git a/StatsKit/extensions.swift b/StatsKit/extensions.swift index f2666ea9..f381821d 100644 --- a/StatsKit/extensions.swift +++ b/StatsKit/extensions.swift @@ -620,6 +620,25 @@ public func PopupRow(_ view: NSView, n: CGFloat, title: String, value: String) - return valueView } +public func PopupWithColorRow(_ view: NSView, color: NSColor, n: CGFloat, title: String, value: String) -> ValueField { + let rowView: NSView = NSView(frame: NSRect(x: 0, y: 22*n, width: view.frame.width, height: 22)) + + let colorView: NSView = NSView(frame: NSRect(x: 2, y: 5, width: 12, height: 12)) + colorView.wantsLayer = true + colorView.layer?.backgroundColor = color.cgColor + colorView.layer?.cornerRadius = 2 + let labelWidth = title.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .regular)) + 5 + let labelView: LabelField = LabelField(frame: NSRect(x: 18, y: (22-15)/2, width: labelWidth, height: 15), title) + let valueView: ValueField = ValueField(frame: NSRect(x: 18 + labelWidth, y: (22-16)/2, width: rowView.frame.width - labelWidth - 18, height: 16), value) + + rowView.addSubview(colorView) + rowView.addSubview(labelView) + rowView.addSubview(valueView) + view.addSubview(rowView) + + return valueView +} + public extension Array where Element : Equatable { func allEqual() -> Bool { if let firstElem = first {