@@ -15,6 +15,8 @@ public class EmphasizeAPI {
1515 public private( set) var emphasizedRangeIndex : Int ?
1616 private let activeColor : NSColor = NSColor ( hex: 0xFFFB00 , alpha: 1 )
1717 private let inactiveColor : NSColor = NSColor . lightGray. withAlphaComponent ( 0.4 )
18+ private var activeTextLayer : CATextLayer ?
19+ private var originalSelectionColor : NSColor ?
1820
1921 weak var textView : TextView ?
2022
@@ -26,42 +28,57 @@ public class EmphasizeAPI {
2628 public struct EmphasizedRange {
2729 public var range : NSRange
2830 var layer : CAShapeLayer
31+ var textLayer : CATextLayer ?
2932 }
3033
3134 // MARK: - Public Methods
3235
33- /// Emphasises multiple ranges, with one optionally marked as active (highlighted usually in yellow).
36+ /// Emphasises multiple ranges, with one optionally marked as active (highlighted in yellow with black text ).
3437 ///
3538 /// - Parameters:
3639 /// - ranges: An array of ranges to highlight.
37- /// - activeIndex: The index of the range to highlight in yellow . Defaults to `nil`.
40+ /// - activeIndex: The index of the range to highlight. Defaults to `nil`.
3841 /// - clearPrevious: Removes previous emphasised ranges. Defaults to `true`.
3942 public func emphasizeRanges( ranges: [ NSRange ] , activeIndex: Int ? = nil , clearPrevious: Bool = true ) {
4043 if clearPrevious {
41- removeEmphasizeLayers ( ) // Clear all existing highlights
44+ removeEmphasizeLayers ( )
4245 }
4346
47+ // Store the current selection background color if not already stored
48+ if originalSelectionColor == nil {
49+ originalSelectionColor = textView? . selectionManager. selectionBackgroundColor ?? . selectedTextBackgroundColor
50+ }
51+ // Temporarily disable selection highlighting
52+ textView? . selectionManager. selectionBackgroundColor = . clear
53+
4454 ranges. enumerated ( ) . forEach { index, range in
4555 let isActive = ( index == activeIndex)
4656 emphasizeRange ( range: range, active: isActive)
4757
4858 if isActive {
4959 emphasizedRangeIndex = activeIndex
60+ setTextColorForRange ( range, active: true )
5061 }
5162 }
5263 }
5364
5465 /// Emphasises a single range.
5566 /// - Parameters:
5667 /// - range: The text range to highlight.
57- /// - active: Whether the range should be highlighted as active (usually in yellow ). Defaults to `false`.
68+ /// - active: Whether the range should be highlighted as active (black text ). Defaults to `false`.
5869 public func emphasizeRange( range: NSRange , active: Bool = false ) {
5970 guard let shapePath = textView? . layoutManager? . roundedPathForRange ( range) else { return }
6071
6172 let layer = createEmphasizeLayer ( shapePath: shapePath, active: active)
6273 textView? . layer? . insertSublayer ( layer, at: 1 )
63-
64- emphasizedRanges. append ( EmphasizedRange ( range: range, layer: layer) )
74+
75+ // Create and add text layer
76+ if let textLayer = createTextLayer ( for: range, active: active) {
77+ textView? . layer? . addSublayer ( textLayer)
78+ emphasizedRanges. append ( EmphasizedRange ( range: range, layer: layer, textLayer: textLayer) )
79+ } else {
80+ emphasizedRanges. append ( EmphasizedRange ( range: range, layer: layer, textLayer: nil ) )
81+ }
6582 }
6683
6784 /// Removes the highlight for a specific range.
@@ -71,16 +88,18 @@ public class EmphasizeAPI {
7188
7289 let removedLayer = emphasizedRanges [ index] . layer
7390 removedLayer. removeFromSuperlayer ( )
91+
92+ // Remove text layer
93+ emphasizedRanges [ index] . textLayer? . removeFromSuperlayer ( )
7494
7595 emphasizedRanges. remove ( at: index)
7696
7797 // Adjust the active highlight index
7898 if let currentIndex = emphasizedRangeIndex {
7999 if currentIndex == index {
80- // TODO: What is the desired behaviour here?
81- emphasizedRangeIndex = nil // Reset if the active highlight is removed
100+ emphasizedRangeIndex = nil
82101 } else if currentIndex > index {
83- emphasizedRangeIndex = currentIndex - 1 // Shift if the removed index was before the active index
102+ emphasizedRangeIndex = currentIndex - 1
84103 }
85104 }
86105 }
@@ -105,22 +124,34 @@ public class EmphasizeAPI {
105124
106125 /// Removes all emphasised ranges.
107126 public func removeEmphasizeLayers( ) {
108- emphasizedRanges. forEach { $0. layer. removeFromSuperlayer ( ) }
127+ emphasizedRanges. forEach { range in
128+ range. layer. removeFromSuperlayer ( )
129+ range. textLayer? . removeFromSuperlayer ( )
130+ }
109131 emphasizedRanges. removeAll ( )
110132 emphasizedRangeIndex = nil
133+
134+ // Restore original selection highlighting
135+ if let originalColor = originalSelectionColor {
136+ textView? . selectionManager. selectionBackgroundColor = originalColor
137+ }
138+
139+ // Force a redraw to ensure colors update
140+ textView? . needsDisplay = true
111141 }
112142
113143 // MARK: - Private Methods
114144
115145 private func createEmphasizeLayer( shapePath: NSBezierPath , active: Bool ) -> CAShapeLayer {
116146 let layer = CAShapeLayer ( )
117- layer. cornerRadius = 3 .0
147+ layer. cornerRadius = 4 .0
118148 layer. fillColor = ( active ? activeColor : inactiveColor) . cgColor
119149 layer. shadowColor = . black
120- layer. shadowOpacity = active ? 0.3 : 0.0
121- layer. shadowOffset = CGSize ( width: 0 , height: 1 )
122- layer. shadowRadius = 3.0
150+ layer. shadowOpacity = active ? 0.5 : 0.0
151+ layer. shadowOffset = CGSize ( width: 0 , height: 1.5 )
152+ layer. shadowRadius = 1.5
123153 layer. opacity = 1.0
154+ layer. zPosition = active ? 1 : 0
124155
125156 if #available( macOS 14 . 0 , * ) {
126157 layer. path = shapePath. cgPath
@@ -154,17 +185,19 @@ public class EmphasizeAPI {
154185
155186 guard currentIndex < emphasizedRanges. count else { return nil }
156187
157- // Reset the previously active layer
188+ // Reset the previously active layer and text color
158189 if let currentIndex = emphasizedRangeIndex {
159190 let previousLayer = emphasizedRanges [ currentIndex] . layer
160191 previousLayer. fillColor = inactiveColor. cgColor
161192 previousLayer. shadowOpacity = 0.0
193+ setTextColorForRange ( emphasizedRanges [ currentIndex] . range, active: false )
162194 }
163195
164- // Set the new active layer
196+ // Set the new active layer and text color
165197 let newLayer = emphasizedRanges [ currentIndex] . layer
166198 newLayer. fillColor = activeColor. cgColor
167199 newLayer. shadowOpacity = 0.3
200+ setTextColorForRange ( emphasizedRanges [ currentIndex] . range, active: true )
168201
169202 applyPopAnimation ( to: newLayer)
170203 emphasizedRangeIndex = currentIndex
@@ -181,4 +214,55 @@ public class EmphasizeAPI {
181214
182215 layer. add ( scaleAnimation, forKey: " popAnimation " )
183216 }
217+
218+ private func getInactiveTextColor( ) -> NSColor {
219+ if textView? . effectiveAppearance. name == . darkAqua {
220+ return . white
221+ }
222+ return . black
223+ }
224+
225+ private func createTextLayer( for range: NSRange , active: Bool ) -> CATextLayer ? {
226+ guard let textView = textView,
227+ let layoutManager = textView. layoutManager,
228+ let shapePath = layoutManager. roundedPathForRange ( range) ,
229+ let originalString = textView. textStorage? . attributedSubstring ( from: range) else { return nil }
230+
231+ var bounds = shapePath. bounds
232+ bounds. origin. y += 1 // Move down by 1 pixel
233+
234+ // Create text layer
235+ let textLayer = CATextLayer ( )
236+ textLayer. frame = bounds
237+ textLayer. backgroundColor = NSColor . clear. cgColor
238+ textLayer. contentsScale = textView. window? . screen? . backingScaleFactor ?? 2.0
239+ textLayer. allowsFontSubpixelQuantization = true
240+ textLayer. zPosition = 2
241+
242+ // Get the font from the attributed string
243+ if let font = originalString. attribute ( . font, at: 0 , effectiveRange: nil ) as? NSFont {
244+ textLayer. font = font
245+ } else {
246+ textLayer. font = NSFont . systemFont ( ofSize: NSFont . systemFontSize)
247+ }
248+
249+ updateTextLayer ( textLayer, with: originalString, active: active)
250+ return textLayer
251+ }
252+
253+ private func updateTextLayer( _ textLayer: CATextLayer , with originalString: NSAttributedString , active: Bool ) {
254+ let text = NSMutableAttributedString ( attributedString: originalString)
255+ text. addAttribute ( . foregroundColor,
256+ value: active ? NSColor . black : getInactiveTextColor ( ) ,
257+ range: NSRange ( location: 0 , length: text. length) )
258+ textLayer. string = text
259+ }
260+
261+ private func setTextColorForRange( _ range: NSRange , active: Bool ) {
262+ guard let index = emphasizedRanges. firstIndex ( where: { $0. range == range } ) ,
263+ let textLayer = emphasizedRanges [ index] . textLayer,
264+ let originalString = textView? . textStorage? . attributedSubstring ( from: range) else { return }
265+
266+ updateTextLayer ( textLayer, with: originalString, active: active)
267+ }
184268}
0 commit comments