Skip to content

Commit 46cc903

Browse files
committed
Pause cursor blinking when the terminal loses focus
The cursor now blinks only while the terminal has focus when focus tracking is enabled. This applies to both native caret views and the Metal renderer on macOS and iOS. Steady cursor styles remain unchanged. Focus changes, key-window changes, and changes to focus tracking now refresh the cursor animation and Metal display. This keeps the cursor visible and updates its appearance without waiting for a later redraw. The unfocused Metal cursor uses a narrower outline. This matches the visible inner half of the centered Core Graphics stroke after clipping. When debugging cursor state, check terminal focus, focus tracking, and whether the native or Metal renderer is active. A cursor that stops blinking while unfocused is expected behavior.
1 parent 6899ad3 commit 46cc903

5 files changed

Lines changed: 58 additions & 24 deletions

File tree

Sources/SwiftTerm/Apple/Metal/MetalTerminalRenderer.swift

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,9 @@ final class MetalTerminalRenderer: NSObject, MTKViewDelegate {
343343
#endif
344344
view.drawableSize = CGSize(width: view.bounds.width * scale, height: view.bounds.height * scale)
345345
let cursorStyle = terminalView.terminal.options.cursorStyle
346-
let shouldBlink = isBlinkStyle(cursorStyle) && !terminalView.terminal.cursorHidden
346+
let shouldBlink = isBlinkStyle(cursorStyle)
347+
&& !terminalView.terminal.cursorHidden
348+
&& cursorHasFocus(in: terminalView)
347349
updateCursorBlinkTimer(shouldBlink: shouldBlink)
348350

349351
#if canImport(os)
@@ -2285,7 +2287,8 @@ final class MetalTerminalRenderer: NSObject, MTKViewDelegate {
22852287
return ([], [], [])
22862288
}
22872289
let cursorStyle = terminalView.terminal.options.cursorStyle
2288-
if isBlinkStyle(cursorStyle) && !cursorBlinkOn {
2290+
let hasFocus = cursorHasFocus(in: terminalView)
2291+
if hasFocus && isBlinkStyle(cursorStyle) && !cursorBlinkOn {
22892292
return ([], [], [])
22902293
}
22912294
let lineOffset = cellHeight * CGFloat(cursorRow - yDisp + 1)
@@ -2304,19 +2307,16 @@ final class MetalTerminalRenderer: NSObject, MTKViewDelegate {
23042307
let x1 = x0 + cellWidthPx * doublePosition * cursorColumnWidth
23052308
let y1 = y0 + cellHeightPx
23062309

2307-
#if os(macOS)
2308-
let hasFocus = terminalView.caretViewTracksFocus ? terminalView.hasFocus : true
2309-
#else
2310-
let hasFocus = terminalView.caretViewTracksFocus ? terminalView.isFirstResponder : true
2311-
#endif
23122310
let cursorColor = colorToSIMD(terminalView.caretColor)
23132311
let cursorClip = ClipRect(minX: Float(x0), minY: Float(y0), maxX: Float(x1), maxY: Float(y1))
23142312
var colorVertices: [ColorVertex] = []
23152313
var glyphVerticesGray: [GlyphVertex] = []
23162314
var glyphVerticesColor: [GlyphVertex] = []
23172315

23182316
if !hasFocus {
2319-
let stroke = max(1, 3 * scale)
2317+
// Core Graphics centers its 3-point stroke on the cell boundary.
2318+
// Clipping keeps only the inner half of that stroke.
2319+
let stroke = max(1, 1.5 * scale)
23202320
colorVertices.append(contentsOf: quadVertices(x0: CGFloat(x0),
23212321
y0: CGFloat(y0),
23222322
x1: CGFloat(x1),
@@ -2873,6 +2873,17 @@ final class MetalTerminalRenderer: NSObject, MTKViewDelegate {
28732873
}
28742874
}
28752875

2876+
private func cursorHasFocus(in terminalView: TerminalView) -> Bool {
2877+
guard terminalView.caretViewTracksFocus else {
2878+
return true
2879+
}
2880+
#if os(macOS)
2881+
return terminalView.hasFocus
2882+
#else
2883+
return terminalView.isFirstResponder
2884+
#endif
2885+
}
2886+
28762887
private func updateCursorBlinkTimer(shouldBlink: Bool) {
28772888
if shouldBlink {
28782889
if cursorBlinkTimer == nil {

Sources/SwiftTerm/Mac/MacCaretView.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ class CaretView: NSView, CALayerDelegate {
2323
var glyphColumnWidth: Int = 1
2424
var powerlineCodePoint: UInt32?
2525
var bgColor: CGColor
26-
var tracksFocus = true
26+
var tracksFocus = true {
27+
didSet {
28+
updateCursorStyle()
29+
}
30+
}
2731

2832
public init (frame: CGRect, cursorStyle: CursorStyle, terminal: TerminalView)
2933
{
@@ -67,9 +71,10 @@ class CaretView: NSView, CALayerDelegate {
6771
}
6872

6973
func updateCursorStyle () {
74+
let canBlink = !tracksFocus || (terminal?.hasFocus ?? true)
7075
switch style {
7176
case .blinkUnderline, .blinkBlock, .blinkBar:
72-
updateAnimation(to: true)
77+
updateAnimation(to: canBlink)
7378
case .steadyBar, .steadyBlock, .steadyUnderline:
7479
updateAnimation(to: false)
7580
}
@@ -114,7 +119,7 @@ class CaretView: NSView, CALayerDelegate {
114119

115120
public var focused: Bool = false {
116121
didSet {
117-
updateView()
122+
updateCursorStyle()
118123
}
119124
}
120125

Sources/SwiftTerm/Mac/MacTerminalView.swift

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations,
194194
}
195195
set {
196196
caretView.tracksFocus = newValue
197+
#if canImport(MetalKit)
198+
queueMetalDisplay()
199+
#endif
197200
}
198201
}
199202

@@ -601,27 +604,26 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations,
601604
// Not used on Mac
602605
}
603606

604-
var becomeMainObserver, resignMainObserver: NSObjectProtocol?
607+
var becomeKeyObserver, resignKeyObserver: NSObjectProtocol?
605608

606609
deinit {
607610
stopWindowMouseMovedFallback()
608-
if let becomeMainObserver {
609-
NotificationCenter.default.removeObserver (becomeMainObserver)
611+
if let becomeKeyObserver {
612+
NotificationCenter.default.removeObserver (becomeKeyObserver)
610613
}
611-
if let resignMainObserver {
612-
NotificationCenter.default.removeObserver (resignMainObserver)
614+
if let resignKeyObserver {
615+
NotificationCenter.default.removeObserver (resignKeyObserver)
613616
}
614617
progressReportTimer?.invalidate()
615618
}
616619

617620
func setupFocusNotification() {
618-
becomeMainObserver = NotificationCenter.default.addObserver(forName: .init("NSWindowDidBecomeMainNotification"), object: nil, queue: nil) { [unowned self] notification in
621+
becomeKeyObserver = NotificationCenter.default.addObserver(forName: NSWindow.didBecomeKeyNotification, object: nil, queue: nil) { [unowned self] notification in
619622
self.caretView.updateCursorStyle()
620623
self.queueMetalDisplay()
621624
}
622-
resignMainObserver = NotificationCenter.default.addObserver(forName: .init("NSWindowDidResignMainNotification"), object: nil, queue: nil) { [unowned self] notification in
623-
self.caretView.disableAnimations()
624-
self.caretView.updateView()
625+
resignKeyObserver = NotificationCenter.default.addObserver(forName: NSWindow.didResignKeyNotification, object: nil, queue: nil) { [unowned self] notification in
626+
self.caretView.updateCursorStyle()
625627
self.queueMetalDisplay()
626628
}
627629
}
@@ -1076,6 +1078,9 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations,
10761078
set {
10771079
_hasFocus = newValue
10781080
caretView.focused = newValue
1081+
#if canImport(MetalKit)
1082+
queueMetalDisplay()
1083+
#endif
10791084
}
10801085
}
10811086

Sources/SwiftTerm/iOS/iOSCaretView.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ class CaretView: UIView {
2121
var glyphColumnWidth: Int = 1
2222
var powerlineCodePoint: UInt32?
2323
var bgColor: CGColor
24-
var tracksFocus = true
24+
var tracksFocus = true {
25+
didSet {
26+
updateCursorStyle()
27+
}
28+
}
2529

2630
public init (frame: CGRect, cursorStyle: CursorStyle, terminal: TerminalView)
2731
{
@@ -93,9 +97,10 @@ class CaretView: UIView {
9397
}
9498

9599
func updateCursorStyle () {
100+
let canBlink = !tracksFocus || (superview?.isFirstResponder ?? true)
96101
switch style {
97102
case .blinkUnderline, .blinkBlock, .blinkBar:
98-
updateAnimation(to: true)
103+
updateAnimation(to: canBlink)
99104
case .steadyBar, .steadyBlock, .steadyUnderline:
100105
updateAnimation(to: false)
101106
}

Sources/SwiftTerm/iOS/iOSTerminalView.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi
191191
}
192192
set {
193193
caretView?.tracksFocus = newValue
194+
#if canImport(MetalKit)
195+
queueMetalDisplay()
196+
#endif
194197
}
195198
}
196199
var accessibility: AccessibilityService = AccessibilityService()
@@ -2584,6 +2587,9 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi
25842587
if response {
25852588
caretView?.updateCursorStyle()
25862589
terminal.setTerminalFocus(true)
2590+
#if canImport(MetalKit)
2591+
queueMetalDisplay()
2592+
#endif
25872593
}
25882594
return response
25892595
}
@@ -2593,8 +2599,10 @@ open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollVi
25932599

25942600
if code {
25952601
terminal.setTerminalFocus(false)
2596-
caretView?.disableAnimations()
2597-
caretView?.updateView()
2602+
caretView?.updateCursorStyle()
2603+
#if canImport(MetalKit)
2604+
queueMetalDisplay()
2605+
#endif
25982606
keyRepeat?.invalidate()
25992607
keyRepeat = nil
26002608

0 commit comments

Comments
 (0)