Skip to content

Commit 13e876c

Browse files
committed
Cleaned up the emphasize highlights appearance. Added black text layer.
1 parent b9c8c1f commit 13e876c

File tree

3 files changed

+130
-17
lines changed

3 files changed

+130
-17
lines changed

Sources/CodeEditTextView/EmphasizeAPI/EmphasizeAPI.swift

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Public.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ extension TextLayoutManager {
234234

235235
// Close the path
236236
if let firstPoint = points.first {
237-
return NSBezierPath.smoothPath(points + [firstPoint], radius: 2)
237+
return NSBezierPath.smoothPath(points + [firstPoint], radius: 4)
238238
}
239239

240240
return nil

Sources/CodeEditTextView/TextView/TextView.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,34 @@ public class TextView: NSView, NSTextContent {
213213
}
214214
}
215215

216+
/// Whether the text view should use a custom background color
217+
private var useCustomBackground: Bool = false
218+
219+
/// The background color to use when useCustomBackground is true
220+
private var customBackgroundColor: NSColor = .textBackgroundColor {
221+
didSet {
222+
updateBackgroundColor()
223+
}
224+
}
225+
226+
/// Sets the background color of the text view
227+
/// - Parameters:
228+
/// - color: The color to use for the background
229+
/// - useCustom: Whether to use the custom color (true) or system background (false)
230+
public func setBackgroundColor(_ color: NSColor, useCustom: Bool) {
231+
useCustomBackground = useCustom
232+
customBackgroundColor = color
233+
updateBackgroundColor()
234+
}
235+
236+
private func updateBackgroundColor() {
237+
if useCustomBackground {
238+
layer?.backgroundColor = customBackgroundColor.cgColor
239+
} else {
240+
layer?.backgroundColor = NSColor.textBackgroundColor.cgColor
241+
}
242+
}
243+
216244
/// The attributes used to render marked text.
217245
/// Defaults to a single underline.
218246
public var markedTextAttributes: [NSAttributedString.Key: Any] {
@@ -323,6 +351,7 @@ public class TextView: NSView, NSTextContent {
323351

324352
layoutManager.layoutLines()
325353
setUpDragGesture()
354+
layer?.backgroundColor = .clear
326355
}
327356

328357
required init?(coder: NSCoder) {

0 commit comments

Comments
 (0)