From 1c5db2164bbc4244fd3f3c73953dca6acd7d2a5b Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Tue, 17 Mar 2026 18:44:11 +0100 Subject: [PATCH] feat: added new BarChart and replaced all custom bars with new chart --- Kit/plugins/Charts.swift | 95 ++++++++++++++++++++++++++++++++++++- Modules/Disk/popup.swift | 68 +++++--------------------- Modules/Sensors/popup.swift | 28 ++++------- 3 files changed, 114 insertions(+), 77 deletions(-) diff --git a/Kit/plugins/Charts.swift b/Kit/plugins/Charts.swift index 6e4cd0a7..613cc056 100644 --- a/Kit/plugins/Charts.swift +++ b/Kit/plugins/Charts.swift @@ -142,7 +142,7 @@ public class LineChartView: NSView { private var cursor: NSPoint? = nil private var stop: Bool = false - public init(frame: NSRect, num: Int, suffix: String = "%", color: NSColor = .controlAccentColor, scale: Scale = .none, fixedScale: Double = 1, zeroValue: Double = 0.01) { + public init(frame: NSRect = .zero, num: Int, suffix: String = "%", color: NSColor = .controlAccentColor, scale: Scale = .none, fixedScale: Double = 1, zeroValue: Double = 0.01) { self.points = Array(repeating: nil, count: max(num, 1)) self.suffix = suffix self.color = color @@ -233,7 +233,7 @@ public class LineChartView: NSView { } let point = CGPoint( - x: (CGFloat(i) * xRatio) + dirtyRect.origin.x, + x: CGFloat(i) * xRatio, y: y ) line.append(point) @@ -1017,3 +1017,94 @@ public class GridChartView: NSView { } } } + +public class BarChartView: NSView { + private var values: [ColorValue] = [] + private var cursor: CGPoint? = nil + private var queue: DispatchQueue = DispatchQueue(label: "eu.exelban.Stats.charts.bar") + + private var size: CGFloat? + private var horizontal: Bool + + public init(frame: NSRect = NSRect.zero, size: CGFloat? = nil, horizontal: Bool = false) { + self.size = size + self.horizontal = horizontal + + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func draw(_ dirtyRect: NSRect) { + var widthHeight: CGFloat? = nil + var isHorizontal: Bool = false + var values: [ColorValue] = [] + self.queue.sync { + widthHeight = self.size + isHorizontal = self.horizontal + values = self.values + } + + guard !values.isEmpty else { return } + + let totalValue = values.reduce(0) { $0 + $1.value } + if totalValue < 1 { + values.append(ColorValue(1 - totalValue, color: NSColor.lightGray.withAlphaComponent(0.25))) + } + + let barSize = widthHeight ?? (isHorizontal ? self.frame.height : self.frame.width) + let adjustedTotal = values.reduce(0) { $0 + $1.value } + guard adjustedTotal > 0 else { return } + + guard let context = NSGraphicsContext.current?.cgContext else { return } + + let barRect: NSRect = isHorizontal + ? NSRect(x: 0, y: (self.frame.height - barSize) / 2, width: self.frame.width, height: barSize) + : NSRect(x: (self.frame.width - barSize) / 2, y: 0, width: barSize, height: self.frame.height) + let clipPath = NSBezierPath(roundedRect: barRect, xRadius: 3, yRadius: 3) + + context.saveGState() + clipPath.addClip() + + var list: [(value: Double, path: NSBezierPath)] = [] + var offset: CGFloat = 0 + + for value in values { + let color = value.color ?? .controlAccentColor + let segmentLength = CGFloat(value.value / adjustedTotal) * (isHorizontal ? self.frame.width : self.frame.height) + + let rect: NSRect = isHorizontal + ? NSRect(x: offset, y: (self.frame.height - barSize) / 2, width: segmentLength, height: barSize) + : NSRect(x: (self.frame.width - barSize) / 2, y: offset, width: barSize, height: segmentLength) + + let path = NSBezierPath(rect: rect) + color.setFill() + path.fill() + + list.append((value: value.value, path: path)) + offset += segmentLength + } + + context.restoreGState() + } + + public func setValue(_ values: ColorValue) { + self.queue.async(flags: .barrier) { + self.values = [values] + } + if self.window?.isVisible ?? false { + self.display() + } + } + + public func setValues(_ values: [ColorValue]) { + self.queue.async(flags: .barrier) { + self.values = values + } + if self.window?.isVisible ?? false { + self.display() + } + } +} diff --git a/Modules/Disk/popup.swift b/Modules/Disk/popup.swift index bbe87279..ab89cd9e 100644 --- a/Modules/Disk/popup.swift +++ b/Modules/Disk/popup.swift @@ -279,10 +279,11 @@ internal class DiskView: NSStackView { public var name: String public var uuid: String private let width: CGFloat + private let size: Int64 private var nameView: NameView private var chartView: ChartView - private var barView: BarView + private var barView: BarChartView private var legendView: LegendView private var detailsView: DetailsView @@ -297,10 +298,16 @@ internal class DiskView: NSStackView { self.uuid = uuid self.name = name self.width = width + self.size = size let innerWidth: CGFloat = width - (Constants.Popup.margins * 2) self.nameView = NameView(width: innerWidth, name: name, size: size, free: free, path: path) self.chartView = ChartView(width: innerWidth) - self.barView = BarView(width: innerWidth, size: size, free: free) + self.barView = BarChartView(frame: NSRect(x: 0, y: 0, width: innerWidth, height: 10), horizontal: true) + self.barView.widthAnchor.constraint(equalToConstant: innerWidth).isActive = true + self.barView.heightAnchor.constraint(equalToConstant: 10).isActive = true + if size != 0 { + self.barView.setValue(ColorValue(Double(size - free) / Double(size))) + } self.legendView = LegendView(width: innerWidth, id: "\(name)_\(path?.absoluteString ?? "")", size: size, free: free) self.detailsView = DetailsView(width: innerWidth, id: "\(name)_\(path?.absoluteString ?? "")", smart: smart) @@ -344,7 +351,9 @@ internal class DiskView: NSStackView { public func update(free: Int64, smart: smart_t?) { self.nameView.update(free: free, read: nil, write: nil) self.legendView.update(free: free) - self.barView.update(free: free) + if size != 0 { + self.barView.setValue(ColorValue(Double(self.size - free) / Double(self.size))) + } self.detailsView.update(smart: smart) } @@ -560,59 +569,6 @@ internal class ChartView: NSStackView { } } -internal class BarView: NSView { - private let size: Int64 - private var usedBarSpace: NSView? = nil - private var ready: Bool = false - - private var background: NSView? = nil - - public init(width: CGFloat, size: Int64, free: Int64) { - self.size = size - - super.init(frame: NSRect(x: 0, y: 0, width: width, height: 10)) - - let view: NSView = NSView(frame: NSRect(x: 1, y: 0, width: self.frame.width - 2, height: self.frame.height)) - view.wantsLayer = true - view.layer?.borderColor = NSColor.secondaryLabelColor.cgColor - view.layer?.borderWidth = 0.25 - view.layer?.cornerRadius = 3 - self.background = view - - let percentage = CGFloat(size - free) / CGFloat(size) - let width: CGFloat = (view.frame.width * (percentage < 0 ? 0 : percentage)) / 1 - self.usedBarSpace = NSView(frame: NSRect(x: 0, y: 0, width: width, height: view.frame.height)) - self.usedBarSpace?.wantsLayer = true - self.usedBarSpace?.layer?.backgroundColor = NSColor.controlAccentColor.cgColor - - view.addSubview(self.usedBarSpace!) - self.addSubview(view) - - self.widthAnchor.constraint(equalToConstant: self.frame.width).isActive = true - self.heightAnchor.constraint(equalToConstant: self.frame.height).isActive = true - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func updateLayer() { - self.background?.layer?.backgroundColor = self.isDarkMode ? NSColor.lightGray.withAlphaComponent(0.1).cgColor : NSColor.white.cgColor - } - - public func update(free: Int64?) { - if (self.window?.isVisible ?? false) || !self.ready { - if let free = free, self.usedBarSpace != nil { - let percentage = CGFloat(self.size - free) / CGFloat(self.size) - let width: CGFloat = ((self.frame.width - 2) * (percentage < 0 ? 0 : percentage)) / 1 - self.usedBarSpace?.setFrameSize(NSSize(width: width, height: self.usedBarSpace!.frame.height)) - } - - self.ready = true - } - } -} - internal class LegendView: NSView { private let size: Int64 private var free: Int64 diff --git a/Modules/Sensors/popup.swift b/Modules/Sensors/popup.swift index 77710020..28d1382f 100644 --- a/Modules/Sensors/popup.swift +++ b/Modules/Sensors/popup.swift @@ -485,7 +485,7 @@ internal class FanView: NSStackView { private var modeButtons: ModeButtons? = nil private var debouncer: DispatchWorkItem? = nil - private var barView: NSView? = nil + private var barView: BarChartView? = nil private var minBtn: NSButton? = nil private var maxBtn: NSButton? = nil @@ -593,28 +593,18 @@ internal class FanView: NSStackView { valueField.stringValue = self.fanValue == .percentage ? "\(self.fan.percentage)%" : self.fan.formattedValue valueField.toolTip = "\(value)" - let bar: NSView = NSView(frame: NSRect(x: 0, y: 0, width: 80, height: 8)) - bar.widthAnchor.constraint(equalToConstant: bar.bounds.width).isActive = true - bar.heightAnchor.constraint(equalToConstant: bar.bounds.height).isActive = true - bar.wantsLayer = true - bar.layer?.backgroundColor = NSColor.textBackgroundColor.cgColor - bar.layer?.borderColor = NSColor.quaternaryLabelColor.cgColor - bar.layer?.borderWidth = 1 - bar.layer?.cornerRadius = 2 - - let width: CGFloat = (bar.frame.width * CGFloat(self.fan.percentage < 0 ? 0 : self.fan.percentage)) / 100 - let barInner = NSView(frame: NSRect(x: 0, y: 0, width: width, height: bar.frame.height)) - barInner.wantsLayer = true - barInner.layer?.backgroundColor = NSColor.controlAccentColor.cgColor - - bar.addSubview(barInner) + let bar = BarChartView(frame: NSRect(x: 0, y: 0, width: 80, height: 8), horizontal: true) + bar.widthAnchor.constraint(equalToConstant: 80).isActive = true + bar.heightAnchor.constraint(equalToConstant: 8).isActive = true + let percentage = self.fan.percentage < 0 ? 0 : self.fan.percentage + bar.setValue(ColorValue(Double(percentage) / 100)) row.addArrangedSubview(nameField) row.addArrangedSubview(bar) row.addArrangedSubview(valueField) self.valueField = valueField - self.barView = barInner + self.barView = bar self.addArrangedSubview(row) } @@ -921,8 +911,8 @@ internal class FanView: NSStackView { self.valueField?.toolTip = value.formattedValue if let v = self.barView { - let width: CGFloat = (80 * CGFloat(value.percentage < 0 ? 0 : value.percentage)) / 100 - v.setFrameSize(NSSize(width: width, height: v.frame.height)) + let percentage = value.percentage < 0 ? 0 : value.percentage + v.setValue(ColorValue(Double(percentage) / 100)) } if self.resetModeAfterSleep && !value.mode.isAutomatic {