feat: added new BarChart and replaced all custom bars with new chart

This commit is contained in:
Serhiy Mytrovtsiy
2026-03-17 18:44:11 +01:00
parent e6bdb2df2b
commit 1c5db2164b
3 changed files with 114 additions and 77 deletions

View File

@@ -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()
}
}
}

View File

@@ -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

View File

@@ -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 {