Skip to content

Commit 0d31c2e

Browse files
Merge pull request #289 from migueldeicaza/cursorRenderContentsOnLayer
The caret will now render the character underneath it.
2 parents 5991b97 + 64e3b8d commit 0d31c2e

File tree

9 files changed

+275
-64
lines changed

9 files changed

+275
-64
lines changed

Sources/SwiftTerm/Apple/AppleTerminalView.swift

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ extension TerminalView {
9292

9393
// Install carret view
9494
if caretView == nil {
95-
caretView = CaretView(frame: CGRect(origin: .zero, size: CGSize(width: cellDimension.width, height: cellDimension.height)), cursorStyle: terminal.options.cursorStyle)
95+
caretView = CaretView(frame: CGRect(origin: .zero, size: CGSize(width: cellDimension.width, height: cellDimension.height)), cursorStyle: terminal.options.cursorStyle, terminal: self)
9696
addSubview(caretView)
9797
} else {
9898
updateCaretView ()
@@ -238,14 +238,62 @@ extension TerminalView {
238238
colorsChanged()
239239
}
240240

241-
public func setCursorColor(source: Terminal, color: Color?) {
241+
/// Sets the color for the cursor block, and the text when it is under that cursor in block mode
242+
public func setCursorColor(source: Terminal, color: Color?, textColor: Color?) {
242243
if let setColor = color {
243244
caretColor = TTColor.make (color: setColor)
244245
} else {
245246
caretColor = caretView.defaultCaretColor
246247
}
248+
if let setColor = textColor {
249+
caretTextColor = TTColor.make (color: setColor)
250+
} else {
251+
caretTextColor = caretView.defaultCaretTextColor
252+
}
247253
}
248254

255+
func getAttributedValue (_ attribute: Attribute, usingFg: TTColor, andBg: TTColor) -> [NSAttributedString.Key:Any]?
256+
{
257+
guard let terminal else {
258+
return nil
259+
}
260+
let flags = attribute.style
261+
var bg = andBg
262+
var fg = usingFg
263+
264+
if flags.contains (.inverse) {
265+
swap (&bg, &fg)
266+
}
267+
268+
var tf: TTFont
269+
let isBold = flags.contains(.bold)
270+
if isBold {
271+
if flags.contains (.italic) {
272+
tf = fontSet.boldItalic
273+
} else {
274+
tf = fontSet.bold
275+
}
276+
} else if flags.contains (.italic) {
277+
tf = fontSet.italic
278+
} else {
279+
tf = fontSet.normal
280+
}
281+
282+
var nsattr: [NSAttributedString.Key:Any] = [
283+
.font: tf,
284+
.foregroundColor: fg,
285+
.backgroundColor: bg
286+
]
287+
if flags.contains (.underline) {
288+
nsattr [.underlineColor] = fg
289+
nsattr [.underlineStyle] = NSUnderlineStyle.single.rawValue
290+
}
291+
if flags.contains (.crossedOut) {
292+
nsattr [.strikethroughColor] = fg
293+
nsattr [.strikethroughStyle] = NSUnderlineStyle.single.rawValue
294+
}
295+
return nsattr
296+
}
249297

250298
//
251299
// Given a vt100 attribute, return the NSAttributedString attributes used to render it
@@ -488,7 +536,8 @@ extension TerminalView {
488536
{
489537
let lineDescent = CTFontGetDescent(fontSet.normal)
490538
let lineLeading = CTFontGetLeading(fontSet.normal)
491-
539+
let yOffset = ceil(lineDescent+lineLeading)
540+
492541
func calcLineOffset (forRow: Int) -> CGFloat {
493542
cellDimension.height * CGFloat (forRow-bufferOffset+1) + offset
494543
}
@@ -582,7 +631,7 @@ extension TerminalView {
582631
}
583632

584633
var positions = runGlyphs.enumerated().map { (i: Int, glyph: CGGlyph) -> CGPoint in
585-
CGPoint(x: lineOrigin.x + (cellDimension.width * CGFloat(col + i)), y: lineOrigin.y + ceil(lineLeading + lineDescent))
634+
CGPoint(x: lineOrigin.x + (cellDimension.width * CGFloat(col + i)), y: lineOrigin.y + yOffset)
586635
}
587636

588637
var backgroundColor: TTColor?
@@ -803,6 +852,7 @@ extension TerminalView {
803852
let lineOrigin = CGPoint(x: 0, y: frame.height - offset)
804853
#endif
805854
caretView.frame.origin = CGPoint(x: lineOrigin.x + (cellDimension.width * doublePosition * CGFloat(buffer.x)), y: lineOrigin.y)
855+
caretView.setText (ch: buffer.lines [vy][buffer.x])
806856
}
807857

808858
// Does not use a default argument and merge, because it is called back
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Miguel de Icaza on 4/16/23.
6+
//
7+
8+
import Foundation
9+
import CoreText
10+
11+
extension CaretView {
12+
func drawCursor (in context: CGContext, hasFocus: Bool) {
13+
guard let ctline else {
14+
return
15+
}
16+
guard let terminal else {
17+
return
18+
}
19+
context.setFillColor(TTColor.clear.cgColor)
20+
context.fill ([bounds])
21+
22+
if !hasFocus {
23+
context.setStrokeColor(bgColor)
24+
context.setLineWidth(2)
25+
context.stroke(bounds)
26+
return
27+
}
28+
context.setFillColor(bgColor)
29+
let region: CGRect
30+
switch style {
31+
case .blinkBar, .steadyBar:
32+
region = CGRect (x: 0, y: 0, width: bounds.width, height: 2)
33+
case .blinkBlock, .steadyBlock:
34+
region = bounds
35+
case .blinkUnderline, .steadyUnderline:
36+
region = CGRect (x: 0, y: 0, width: bounds.width, height: 2)
37+
}
38+
context.fill([region])
39+
40+
let lineDescent = CTFontGetDescent(terminal.fontSet.normal)
41+
let lineLeading = CTFontGetLeading(terminal.fontSet.normal)
42+
let yOffset = ceil(lineDescent+lineLeading)
43+
44+
guard style == .steadyBlock || style == .blinkBlock else {
45+
return
46+
}
47+
let caretFG = caretTextColor ?? terminal.nativeForegroundColor
48+
context.setFillColor(TTColor.black.cgColor)
49+
for run in CTLineGetGlyphRuns(ctline) as? [CTRun] ?? [] {
50+
let runGlyphsCount = CTRunGetGlyphCount(run)
51+
let runAttributes = CTRunGetAttributes(run) as? [NSAttributedString.Key: Any] ?? [:]
52+
let runFont = runAttributes[.font] as! TTFont
53+
54+
let runGlyphs = [CGGlyph](unsafeUninitializedCapacity: runGlyphsCount) { (bufferPointer, count) in
55+
CTRunGetGlyphs(run, CFRange(), bufferPointer.baseAddress!)
56+
count = runGlyphsCount
57+
}
58+
59+
var positions = runGlyphs.enumerated().map { (i: Int, glyph: CGGlyph) -> CGPoint in
60+
CGPoint(x: 0, y: yOffset)
61+
}
62+
CTFontDrawGlyphs(runFont, runGlyphs, &positions, positions.count, context)
63+
}
64+
}
65+
}

Sources/SwiftTerm/Mac/MacCaretView.swift

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// MacCaretView.swift
33
//
44
// Implements the caret in the Mac caret view
5+
// TODO: looks like I can kill sub now. unless it can be used to draw a border when out of focus
56
//
67
// Created by Miguel de Icaza on 3/20/20.
78
//
@@ -11,18 +12,21 @@ import Foundation
1112
import AppKit
1213
import CoreText
1314
import CoreGraphics
15+
import CoreText
1416

1517
// The CaretView is used to show the cursor
16-
class CaretView: NSView {
17-
var sub: CALayer
18+
class CaretView: NSView, CALayerDelegate {
19+
weak var terminal: TerminalView?
20+
var ctline: CTLine?
21+
var bgColor: CGColor
1822

19-
public init (frame: CGRect, cursorStyle: CursorStyle)
23+
public init (frame: CGRect, cursorStyle: CursorStyle, terminal: TerminalView)
2024
{
25+
self.terminal = terminal
2126
style = cursorStyle
22-
sub = CALayer ()
27+
bgColor = caretColor.cgColor
2328
super.init(frame: frame)
2429
wantsLayer = true
25-
layer?.addSublayer(sub)
2630

2731
updateView()
2832
}
@@ -31,6 +35,15 @@ class CaretView: NSView {
3135
fatalError("init(coder:) has not been implemented")
3236
}
3337

38+
func setText (ch: CharData) {
39+
let res = NSAttributedString (
40+
string: String (ch.getCharacter()),
41+
attributes: terminal?.getAttributedValue(ch.attribute, usingFg: caretColor, andBg: caretTextColor ?? terminal?.nativeForegroundColor ?? NSColor.black))
42+
ctline = CTLineCreateWithAttributedString(res)
43+
44+
setNeedsDisplay(bounds)
45+
}
46+
3447
var style: CursorStyle {
3548
didSet {
3649
updateCursorStyle ()
@@ -40,35 +53,31 @@ class CaretView: NSView {
4053
func updateCursorStyle () {
4154
switch style {
4255
case .blinkUnderline, .blinkBlock, .blinkBar:
56+
updateAnimation(to: true)
57+
case .steadyBar, .steadyBlock, .steadyUnderline:
58+
updateAnimation(to: false)
59+
}
60+
updateView ()
61+
}
62+
63+
func updateAnimation (to: Bool) {
64+
layer?.removeAllAnimations()
65+
self.layer?.opacity = 1
66+
if to {
4367
let anim = CABasicAnimation.init(keyPath: #keyPath (CALayer.opacity))
4468
anim.duration = 0.7
4569
anim.autoreverses = true
4670
anim.repeatCount = Float.infinity
4771
anim.fromValue = NSNumber (floatLiteral: 1)
48-
anim.toValue = NSNumber (floatLiteral: 0.3)
49-
anim.timingFunction = CAMediaTimingFunction (name: .easeInEaseOut)
50-
sub.add(anim, forKey: #keyPath (CALayer.opacity))
51-
case .steadyBar, .steadyBlock, .steadyUnderline:
52-
sub.removeAllAnimations()
53-
sub.opacity = 1
54-
}
55-
56-
guard let layer = self.layer else {
57-
return
58-
}
59-
switch style {
60-
case .steadyBlock, .blinkBlock:
61-
sub.frame = CGRect (x: 0, y: 0, width: layer.bounds.width, height: layer.bounds.height)
62-
case .steadyUnderline, .blinkUnderline:
63-
sub.frame = CGRect (x: 0, y: 0, width: layer.bounds.width, height: 2)
64-
case .steadyBar, .blinkBar:
65-
sub.frame = CGRect (x: 0, y: 0, width: 2, height: layer.bounds.height)
72+
anim.toValue = NSNumber (floatLiteral: 0)
73+
anim.timingFunction = CAMediaTimingFunction (name: .easeIn)
74+
layer?.add(anim, forKey: #keyPath (CALayer.opacity))
6675
}
67-
6876
}
6977

7078
func disableAnimations () {
71-
sub.removeAllAnimations()
79+
layer?.removeAllAnimations()
80+
layer?.opacity = 1
7281
}
7382

7483
public var defaultCaretColor = NSColor.selectedControlColor
@@ -78,20 +87,29 @@ class CaretView: NSView {
7887
updateView()
7988
}
8089
}
81-
90+
91+
public var defaultCaretTextColor: NSColor? = nil
92+
public var caretTextColor: NSColor? = nil {
93+
didSet {
94+
updateView()
95+
}
96+
}
97+
8298
public var focused: Bool = false {
8399
didSet {
84100
updateView()
85101
}
86102
}
87103

88104
func updateView() {
89-
let isFirst = focused
90-
guard let layer = layer else { return }
91-
sub.frame = CGRect (origin: CGPoint.zero, size: layer.frame.size)
92-
sub.borderWidth = isFirst ? 0 : 1
93-
sub.borderColor = caretColor.cgColor
94-
sub.backgroundColor = isFirst ? caretColor.cgColor : NSColor.clear.cgColor
105+
setNeedsDisplay(bounds)
106+
}
107+
108+
func draw(_ layer: CALayer, in context: CGContext) {
109+
drawCursor (in: context, hasFocus: terminal?.hasFocus ?? true)
110+
}
111+
112+
override func draw(_ dirtyRect: NSRect) {
95113
}
96114

97115
override func hitTest(_ point: NSPoint) -> NSView? {

Sources/SwiftTerm/Mac/MacTerminalView.swift

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations,
152152
}
153153
setupScroller()
154154
setupOptions()
155+
setupFocusNotification()
155156
}
156157

157158
func startDisplayUpdates ()
@@ -164,6 +165,27 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations,
164165
// Not used on Mac
165166
}
166167

168+
var becomeMainObserver, resignMainObserver: NSObjectProtocol?
169+
170+
deinit {
171+
if let becomeMainObserver {
172+
NotificationCenter.default.removeObserver (becomeMainObserver)
173+
}
174+
if let resignMainObserver {
175+
NotificationCenter.default.removeObserver (resignMainObserver)
176+
}
177+
}
178+
179+
func setupFocusNotification() {
180+
becomeMainObserver = NotificationCenter.default.addObserver(forName: .init("NSWindowDidBecomeMainNotification"), object: nil, queue: nil) { [unowned self] notification in
181+
self.caretView.updateCursorStyle()
182+
}
183+
resignMainObserver = NotificationCenter.default.addObserver(forName: .init("NSWindowDidResignMainNotification"), object: nil, queue: nil) { [unowned self] notification in
184+
self.caretView.disableAnimations()
185+
self.caretView.updateView()
186+
}
187+
}
188+
167189
func setupOptions ()
168190
{
169191
setupOptions (width: getEffectiveWidth (size: bounds.size), height: bounds.height)
@@ -212,7 +234,14 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations,
212234
get { caretView.caretColor }
213235
set { caretView.caretColor = newValue }
214236
}
215-
237+
238+
/// Controls the color for the text in the caret when using a block cursor, if not set
239+
/// the cursor will render with the foreground color
240+
public var caretTextColor: NSColor? {
241+
get { caretView.caretTextColor }
242+
set { caretView.caretTextColor = newValue }
243+
}
244+
216245
var _selectedTextBackgroundColor = NSColor.selectedTextBackgroundColor
217246
/// The color used to render the selection
218247
public var selectedTextBackgroundColor: NSColor {
@@ -393,7 +422,10 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations,
393422

394423
private var _hasFocus = false
395424
open var hasFocus : Bool {
396-
get { _hasFocus }
425+
get {
426+
//print ("hasFocus: \(_hasFocus) window=\(window?.isKeyWindow)")
427+
return _hasFocus && (window?.isKeyWindow ?? true)
428+
}
397429
set {
398430
_hasFocus = newValue
399431
caretView.focused = newValue

0 commit comments

Comments
 (0)