소스 검색

Fix text editor caret blink artifact by hiding the system insertion indicator.

Use TextKit 1 caret drawing with proper invalidation so the thin custom caret blinks cleanly without a stray dot.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 개월 전
부모
커밋
5509e72fbe
1개의 변경된 파일44개의 추가작업 그리고 11개의 파일을 삭제
  1. 44 11
      gramora/Views/Components/ThinCaretTextEditor.swift

+ 44 - 11
gramora/Views/Components/ThinCaretTextEditor.swift

@@ -46,8 +46,6 @@ private final class TextEditorScrollView: NSScrollView {
 
 private final class ThinCaretTextView: NSTextView {
     private let caretWidth: CGFloat = 1
-    private let caretHeightBoost: CGFloat = 2
-    private let caretVerticalOffset: CGFloat = 1
 
     var placeholder: String = "" {
         didSet {
@@ -58,23 +56,39 @@ private final class ThinCaretTextView: NSTextView {
 
     var placeholderColor: NSColor = .placeholderTextColor
 
+    override func viewDidMoveToWindow() {
+        super.viewDidMoveToWindow()
+        // Opt into TextKit 1 so custom caret drawing is honored on modern macOS.
+        _ = layoutManager
+        hideSystemInsertionIndicator()
+    }
+
+    override func becomeFirstResponder() -> Bool {
+        let becameFirstResponder = super.becomeFirstResponder()
+        hideSystemInsertionIndicator()
+        return becameFirstResponder
+    }
+
+    override func layout() {
+        super.layout()
+        hideSystemInsertionIndicator()
+    }
+
     override func drawInsertionPoint(in rect: NSRect, color: NSColor, turnedOn flag: Bool) {
         var caretRect = rect
         caretRect.size.width = caretWidth
-        caretRect.origin.x += (rect.width - caretWidth) / 2
-
-        if let font {
-            let caretHeight = font.ascender + abs(font.descender) + caretHeightBoost
-            caretRect.size.height = caretHeight
-            caretRect.origin.y = rect.origin.y + (rect.height - caretHeight) / 2 + caretVerticalOffset
-        }
-
         super.drawInsertionPoint(in: caretRect, color: color, turnedOn: flag)
     }
 
+    override func setNeedsDisplay(_ rect: NSRect, avoidAdditionalLayout flag: Bool) {
+        var invalidRect = rect
+        invalidRect.size.width += caretWidth - 1
+        super.setNeedsDisplay(invalidRect, avoidAdditionalLayout: flag)
+    }
+
     override func draw(_ dirtyRect: NSRect) {
-        super.draw(dirtyRect)
         drawPlaceholderIfNeeded()
+        super.draw(dirtyRect)
     }
 
     override func didChangeText() {
@@ -82,6 +96,20 @@ private final class ThinCaretTextView: NSTextView {
         needsDisplay = true
     }
 
+    private func hideSystemInsertionIndicator() {
+        hideInsertionIndicators(in: self)
+    }
+
+    private func hideInsertionIndicators(in view: NSView) {
+        for subview in view.subviews {
+            if #available(macOS 14.0, *),
+               let indicator = subview as? NSTextInsertionIndicator {
+                indicator.displayMode = .hidden
+            }
+            hideInsertionIndicators(in: subview)
+        }
+    }
+
     private func drawPlaceholderIfNeeded() {
         guard string.isEmpty, !placeholder.isEmpty else { return }
 
@@ -131,6 +159,7 @@ struct ThinCaretTextEditor: NSViewRepresentable {
         scrollView.autohidesScrollers = true
         scrollView.borderType = .noBorder
         scrollView.drawsBackground = false
+        scrollView.focusRingType = .none
 
         let textView = ThinCaretTextView()
         textView.isEditable = isEditable
@@ -139,6 +168,7 @@ struct ThinCaretTextEditor: NSViewRepresentable {
         textView.allowsUndo = true
         textView.drawsBackground = false
         textView.backgroundColor = .clear
+        textView.focusRingType = .none
         textView.isVerticallyResizable = true
         textView.isHorizontallyResizable = false
         textView.autoresizingMask = [.width]
@@ -158,6 +188,7 @@ struct ThinCaretTextEditor: NSViewRepresentable {
         textView.isAutomaticTextReplacementEnabled = false
         textView.font = NSFont.systemFont(ofSize: 14)
         textView.textColor = NSColor(AppTheme.textPrimary)
+        textView.insertionPointColor = NSColor(AppTheme.textPrimary)
         textView.placeholder = placeholder
         textView.placeholderColor = NSColor(AppTheme.textMuted)
         textView.delegate = context.coordinator
@@ -185,6 +216,7 @@ struct ThinCaretTextEditor: NSViewRepresentable {
         }
 
         textView.textColor = NSColor(AppTheme.textPrimary)
+        textView.insertionPointColor = NSColor(AppTheme.textPrimary)
         textView.font = NSFont.systemFont(ofSize: 14)
 
         if let scrollView = scrollView as? TextEditorScrollView {
@@ -214,6 +246,7 @@ struct ThinCaretTextEditor: NSViewRepresentable {
         @objc private func applyThemeColors() {
             guard let textView = textView as? ThinCaretTextView else { return }
             textView.textColor = NSColor(AppTheme.textPrimary)
+            textView.insertionPointColor = NSColor(AppTheme.textPrimary)
             textView.placeholderColor = NSColor(AppTheme.textMuted)
             textView.needsDisplay = true
         }