diff --git a/ClashX.xcodeproj/project.pbxproj b/ClashX.xcodeproj/project.pbxproj index 676db55f0..2d8b756a6 100644 --- a/ClashX.xcodeproj/project.pbxproj +++ b/ClashX.xcodeproj/project.pbxproj @@ -79,6 +79,7 @@ 01F335D92AD10D0B0048AF77 /* ConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F977FAAD23669D6400C17F1F /* ConnectionManager.swift */; }; 01F335DA2AD10D0B0048AF77 /* StatusItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 495340B220DE68C300B0D3FF /* StatusItemView.swift */; }; 01F335DB2AD10D0B0048AF77 /* SystemProxyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F935B2FB23085515009E4D33 /* SystemProxyManager.swift */; }; + AA1234562BF5A00000000001 /* CustomTextLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1234552BF5A00000000001 /* CustomTextLabel.swift */; }; 01F335DC2AD10D0B0048AF77 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 495A44D220D267D000888A0A /* LaunchAtLogin.swift */; }; 01F335DD2AD10D0B0048AF77 /* Combine+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4991D2312A565E6A00978143 /* Combine+Ext.swift */; }; 01F335DE2AD10D0B0048AF77 /* DebugSettingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49281C7F2A1F01FA00F60935 /* DebugSettingViewController.swift */; }; @@ -302,6 +303,7 @@ 495340AF20DE5F7200B0D3FF /* StatusItemView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = StatusItemView.xib; path = ClashX/Views/StatusItem/StatusItemView.xib; sourceTree = SOURCE_ROOT; }; 495340B220DE68C300B0D3FF /* StatusItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemView.swift; sourceTree = ""; }; 495A44D220D267D000888A0A /* LaunchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLogin.swift; sourceTree = ""; }; + AA1234552BF5A00000000001 /* CustomTextLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextLabel.swift; sourceTree = ""; }; 495BFB8721919B9800C8779D /* RemoteConfigManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteConfigManager.swift; sourceTree = ""; }; 496322212AA5D89E00854231 /* UpdateExternalResourceAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateExternalResourceAction.swift; sourceTree = ""; }; 496BDEDF21196F1E00C5207F /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; @@ -763,6 +765,7 @@ children = ( 495340AF20DE5F7200B0D3FF /* StatusItemView.xib */, 495340B220DE68C300B0D3FF /* StatusItemView.swift */, + AA1234552BF5A00000000001 /* CustomTextLabel.swift */, 49D6A45129AEEC15006487EF /* StatusItemTool.swift */, 49D6A45529AEEC55006487EF /* StatusItemViewProtocol.swift */, ); @@ -1024,6 +1027,7 @@ 01F335D82AD10D0B0048AF77 /* ClashConnection.swift in Sources */, 01F335D92AD10D0B0048AF77 /* ConnectionManager.swift in Sources */, 01F335DA2AD10D0B0048AF77 /* StatusItemView.swift in Sources */, + AA1234562BF5A00000000001 /* CustomTextLabel.swift in Sources */, 01F335DB2AD10D0B0048AF77 /* SystemProxyManager.swift in Sources */, 01F335DC2AD10D0B0048AF77 /* LaunchAtLogin.swift in Sources */, 01F335DD2AD10D0B0048AF77 /* Combine+Ext.swift in Sources */, diff --git a/ClashX/Views/StatusItem/CustomTextLabel.swift b/ClashX/Views/StatusItem/CustomTextLabel.swift new file mode 100644 index 000000000..2709b5907 --- /dev/null +++ b/ClashX/Views/StatusItem/CustomTextLabel.swift @@ -0,0 +1,106 @@ +// +// CustomTextLabel.swift +// ClashX +// +// Created to fix high CPU usage issue on macOS 26.1+ +// Replaces NSTextField with custom drawing to avoid infinite draw loop +// + +import AppKit +import Foundation + +class CustomTextLabel: NSView { + var text: String = "" { + didSet { + if text != oldValue { + needsDisplay = true + } + } + } + + var font: NSFont = NSFont.systemFont(ofSize: 8) { + didSet { + needsDisplay = true + } + } + + var textColor: NSColor = NSColor.labelColor { + didSet { + needsDisplay = true + } + } + + var alignment: NSTextAlignment = .right { + didSet { + needsDisplay = true + } + } + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + setupView() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupView() + } + + private func setupView() { + wantsLayer = true + } + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + guard !text.isEmpty else { return } + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = alignment + + let attributes: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: textColor, + .paragraphStyle: paragraphStyle + ] + + let attributedString = NSAttributedString(string: text, attributes: attributes) + let size = attributedString.size() + + // Calculate position based on alignment + let rect: NSRect + switch alignment { + case .right: + rect = NSRect( + x: bounds.width - size.width, + y: (bounds.height - size.height) / 2, + width: size.width, + height: size.height + ) + case .left: + rect = NSRect( + x: 0, + y: (bounds.height - size.height) / 2, + width: size.width, + height: size.height + ) + case .center: + rect = NSRect( + x: (bounds.width - size.width) / 2, + y: (bounds.height - size.height) / 2, + width: size.width, + height: size.height + ) + default: + rect = NSRect( + x: 0, + y: (bounds.height - size.height) / 2, + width: bounds.width, + height: size.height + ) + } + + attributedString.draw(in: rect) + } +} + diff --git a/ClashX/Views/StatusItem/StatusItemView.swift b/ClashX/Views/StatusItem/StatusItemView.swift index a3bef1452..28f88bb33 100644 --- a/ClashX/Views/StatusItem/StatusItemView.swift +++ b/ClashX/Views/StatusItem/StatusItemView.swift @@ -14,8 +14,8 @@ import RxSwift class StatusItemView: NSView, StatusItemViewProtocol { @IBOutlet var imageView: NSImageView! - @IBOutlet var uploadSpeedLabel: NSTextField! - @IBOutlet var downloadSpeedLabel: NSTextField! + @IBOutlet var uploadSpeedLabel: CustomTextLabel! + @IBOutlet var downloadSpeedLabel: CustomTextLabel! @IBOutlet var speedContainerView: NSView! var up: Int = 0 @@ -47,6 +47,13 @@ class StatusItemView: NSView, StatusItemViewProtocol { uploadSpeedLabel.textColor = NSColor.labelColor downloadSpeedLabel.textColor = NSColor.labelColor + + uploadSpeedLabel.alignment = .right + downloadSpeedLabel.alignment = .right + + // Show initial speed text so the label is not blank before the first update + uploadSpeedLabel.text = SpeedUtils.getSpeedString(for: up) + downloadSpeedLabel.text = SpeedUtils.getSpeedString(for: down) } func updateSize(width: CGFloat) { @@ -64,11 +71,11 @@ class StatusItemView: NSView, StatusItemViewProtocol { func updateSpeedLabel(up: Int, down: Int) { guard !speedContainerView.isHidden else { return } if up != self.up { - uploadSpeedLabel.stringValue = SpeedUtils.getSpeedString(for: up) + uploadSpeedLabel.text = SpeedUtils.getSpeedString(for: up) self.up = up } if down != self.down { - downloadSpeedLabel.stringValue = SpeedUtils.getSpeedString(for: down) + downloadSpeedLabel.text = SpeedUtils.getSpeedString(for: down) self.down = down } } diff --git a/ClashX/Views/StatusItem/StatusItemView.xib b/ClashX/Views/StatusItem/StatusItemView.xib index 0d23a25f7..f17e84d0d 100644 --- a/ClashX/Views/StatusItem/StatusItemView.xib +++ b/ClashX/Views/StatusItem/StatusItemView.xib @@ -24,29 +24,22 @@ - + - - - - - - - + + - - - - - - + + + +