Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ClashX.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 = "<group>"; };
495A44D220D267D000888A0A /* LaunchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLogin.swift; sourceTree = "<group>"; };
AA1234552BF5A00000000001 /* CustomTextLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextLabel.swift; sourceTree = "<group>"; };
495BFB8721919B9800C8779D /* RemoteConfigManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteConfigManager.swift; sourceTree = "<group>"; };
496322212AA5D89E00854231 /* UpdateExternalResourceAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateExternalResourceAction.swift; sourceTree = "<group>"; };
496BDEDF21196F1E00C5207F /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -763,6 +765,7 @@
children = (
495340AF20DE5F7200B0D3FF /* StatusItemView.xib */,
495340B220DE68C300B0D3FF /* StatusItemView.swift */,
AA1234552BF5A00000000001 /* CustomTextLabel.swift */,
49D6A45129AEEC15006487EF /* StatusItemTool.swift */,
49D6A45529AEEC55006487EF /* StatusItemViewProtocol.swift */,
);
Expand Down Expand Up @@ -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 */,
Expand Down
106 changes: 106 additions & 0 deletions ClashX/Views/StatusItem/CustomTextLabel.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}

15 changes: 11 additions & 4 deletions ClashX/Views/StatusItem/StatusItemView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
}
Expand Down
21 changes: 7 additions & 14 deletions ClashX/Views/StatusItem/StatusItemView.xib
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,22 @@
<customView translatesAutoresizingMaskIntoConstraints="NO" id="x4I-nn-92U">
<rect key="frame" x="38" y="0.0" width="24" height="22"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Hlw-Vg-wLD">
<customView translatesAutoresizingMaskIntoConstraints="NO" id="Hlw-Vg-wLD" customClass="CustomTextLabel" customModule="ClashX_Meta" customModuleProvider="target">
<rect key="frame" x="-2" y="1" width="28" height="10"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="0KB/s" id="Cy5-d1-ldi">
<font key="font" metaFont="system" size="8"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="srp-1R-o7n">
</customView>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="srp-1R-o7n" customClass="CustomTextLabel" customModule="ClashX_Meta" customModuleProvider="target">
<rect key="frame" x="-2" y="11" width="28" height="10"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="0KB/s" id="oCt-YG-3AI">
<font key="font" metaFont="system" size="8"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</customView>
</subviews>
<constraints>
<constraint firstItem="srp-1R-o7n" firstAttribute="top" secondItem="x4I-nn-92U" secondAttribute="top" constant="1" id="1zQ-0p-x99"/>
<constraint firstAttribute="trailing" secondItem="srp-1R-o7n" secondAttribute="trailing" id="Ijz-ec-WpJ"/>
<constraint firstItem="srp-1R-o7n" firstAttribute="height" constant="10" id="hgt-srp-10"/>
<constraint firstItem="srp-1R-o7n" firstAttribute="bottom" secondItem="Hlw-Vg-wLD" secondAttribute="top" constant="0" id="gap-srp-hlw"/>
<constraint firstAttribute="bottom" secondItem="Hlw-Vg-wLD" secondAttribute="bottom" constant="1" id="a9D-MM-89v"/>
<constraint firstItem="Hlw-Vg-wLD" firstAttribute="leading" secondItem="x4I-nn-92U" secondAttribute="leading" id="i79-Gi-Tfm"/>
<constraint firstAttribute="trailing" secondItem="Hlw-Vg-wLD" secondAttribute="trailing" id="maG-ty-KTU"/>
<constraint firstItem="Hlw-Vg-wLD" firstAttribute="height" constant="10" id="hgt-hlw-10"/>
<constraint firstItem="srp-1R-o7n" firstAttribute="leading" secondItem="x4I-nn-92U" secondAttribute="leading" id="pgT-Fj-TYZ"/>
</constraints>
</customView>
Expand Down