Skip to content

Commit

Permalink
Merge pull request #289 from migueldeicaza/cursorRenderContentsOnLayer
Browse files Browse the repository at this point in the history
The caret will now render the character underneath it.
  • Loading branch information
migueldeicaza authored Apr 17, 2023
2 parents 5991b97 + 64e3b8d commit 0d31c2e
Show file tree
Hide file tree
Showing 9 changed files with 275 additions and 64 deletions.
58 changes: 54 additions & 4 deletions Sources/SwiftTerm/Apple/AppleTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ extension TerminalView {

// Install carret view
if caretView == nil {
caretView = CaretView(frame: CGRect(origin: .zero, size: CGSize(width: cellDimension.width, height: cellDimension.height)), cursorStyle: terminal.options.cursorStyle)
caretView = CaretView(frame: CGRect(origin: .zero, size: CGSize(width: cellDimension.width, height: cellDimension.height)), cursorStyle: terminal.options.cursorStyle, terminal: self)
addSubview(caretView)
} else {
updateCaretView ()
Expand Down Expand Up @@ -238,14 +238,62 @@ extension TerminalView {
colorsChanged()
}

public func setCursorColor(source: Terminal, color: Color?) {
/// Sets the color for the cursor block, and the text when it is under that cursor in block mode
public func setCursorColor(source: Terminal, color: Color?, textColor: Color?) {
if let setColor = color {
caretColor = TTColor.make (color: setColor)
} else {
caretColor = caretView.defaultCaretColor
}
if let setColor = textColor {
caretTextColor = TTColor.make (color: setColor)
} else {
caretTextColor = caretView.defaultCaretTextColor
}
}

func getAttributedValue (_ attribute: Attribute, usingFg: TTColor, andBg: TTColor) -> [NSAttributedString.Key:Any]?
{
guard let terminal else {
return nil
}
let flags = attribute.style
var bg = andBg
var fg = usingFg

if flags.contains (.inverse) {
swap (&bg, &fg)
}

var tf: TTFont
let isBold = flags.contains(.bold)
if isBold {
if flags.contains (.italic) {
tf = fontSet.boldItalic
} else {
tf = fontSet.bold
}
} else if flags.contains (.italic) {
tf = fontSet.italic
} else {
tf = fontSet.normal
}

var nsattr: [NSAttributedString.Key:Any] = [
.font: tf,
.foregroundColor: fg,
.backgroundColor: bg
]
if flags.contains (.underline) {
nsattr [.underlineColor] = fg
nsattr [.underlineStyle] = NSUnderlineStyle.single.rawValue
}
if flags.contains (.crossedOut) {
nsattr [.strikethroughColor] = fg
nsattr [.strikethroughStyle] = NSUnderlineStyle.single.rawValue
}
return nsattr
}

//
// Given a vt100 attribute, return the NSAttributedString attributes used to render it
Expand Down Expand Up @@ -488,7 +536,8 @@ extension TerminalView {
{
let lineDescent = CTFontGetDescent(fontSet.normal)
let lineLeading = CTFontGetLeading(fontSet.normal)

let yOffset = ceil(lineDescent+lineLeading)

func calcLineOffset (forRow: Int) -> CGFloat {
cellDimension.height * CGFloat (forRow-bufferOffset+1) + offset
}
Expand Down Expand Up @@ -582,7 +631,7 @@ extension TerminalView {
}

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

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

// Does not use a default argument and merge, because it is called back
Expand Down
65 changes: 65 additions & 0 deletions Sources/SwiftTerm/Apple/CaretView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// File.swift
//
//
// Created by Miguel de Icaza on 4/16/23.
//

import Foundation
import CoreText

extension CaretView {
func drawCursor (in context: CGContext, hasFocus: Bool) {
guard let ctline else {
return
}
guard let terminal else {
return
}
context.setFillColor(TTColor.clear.cgColor)
context.fill ([bounds])

if !hasFocus {
context.setStrokeColor(bgColor)
context.setLineWidth(2)
context.stroke(bounds)
return
}
context.setFillColor(bgColor)
let region: CGRect
switch style {
case .blinkBar, .steadyBar:
region = CGRect (x: 0, y: 0, width: bounds.width, height: 2)
case .blinkBlock, .steadyBlock:
region = bounds
case .blinkUnderline, .steadyUnderline:
region = CGRect (x: 0, y: 0, width: bounds.width, height: 2)
}
context.fill([region])

let lineDescent = CTFontGetDescent(terminal.fontSet.normal)
let lineLeading = CTFontGetLeading(terminal.fontSet.normal)
let yOffset = ceil(lineDescent+lineLeading)

guard style == .steadyBlock || style == .blinkBlock else {
return
}
let caretFG = caretTextColor ?? terminal.nativeForegroundColor
context.setFillColor(TTColor.black.cgColor)
for run in CTLineGetGlyphRuns(ctline) as? [CTRun] ?? [] {
let runGlyphsCount = CTRunGetGlyphCount(run)
let runAttributes = CTRunGetAttributes(run) as? [NSAttributedString.Key: Any] ?? [:]
let runFont = runAttributes[.font] as! TTFont

let runGlyphs = [CGGlyph](unsafeUninitializedCapacity: runGlyphsCount) { (bufferPointer, count) in
CTRunGetGlyphs(run, CFRange(), bufferPointer.baseAddress!)
count = runGlyphsCount
}

var positions = runGlyphs.enumerated().map { (i: Int, glyph: CGGlyph) -> CGPoint in
CGPoint(x: 0, y: yOffset)
}
CTFontDrawGlyphs(runFont, runGlyphs, &positions, positions.count, context)
}
}
}
82 changes: 50 additions & 32 deletions Sources/SwiftTerm/Mac/MacCaretView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// MacCaretView.swift
//
// Implements the caret in the Mac caret view
// TODO: looks like I can kill sub now. unless it can be used to draw a border when out of focus
//
// Created by Miguel de Icaza on 3/20/20.
//
Expand All @@ -11,18 +12,21 @@ import Foundation
import AppKit
import CoreText
import CoreGraphics
import CoreText

// The CaretView is used to show the cursor
class CaretView: NSView {
var sub: CALayer
class CaretView: NSView, CALayerDelegate {
weak var terminal: TerminalView?
var ctline: CTLine?
var bgColor: CGColor

public init (frame: CGRect, cursorStyle: CursorStyle)
public init (frame: CGRect, cursorStyle: CursorStyle, terminal: TerminalView)
{
self.terminal = terminal
style = cursorStyle
sub = CALayer ()
bgColor = caretColor.cgColor
super.init(frame: frame)
wantsLayer = true
layer?.addSublayer(sub)

updateView()
}
Expand All @@ -31,6 +35,15 @@ class CaretView: NSView {
fatalError("init(coder:) has not been implemented")
}

func setText (ch: CharData) {
let res = NSAttributedString (
string: String (ch.getCharacter()),
attributes: terminal?.getAttributedValue(ch.attribute, usingFg: caretColor, andBg: caretTextColor ?? terminal?.nativeForegroundColor ?? NSColor.black))
ctline = CTLineCreateWithAttributedString(res)

setNeedsDisplay(bounds)
}

var style: CursorStyle {
didSet {
updateCursorStyle ()
Expand All @@ -40,35 +53,31 @@ class CaretView: NSView {
func updateCursorStyle () {
switch style {
case .blinkUnderline, .blinkBlock, .blinkBar:
updateAnimation(to: true)
case .steadyBar, .steadyBlock, .steadyUnderline:
updateAnimation(to: false)
}
updateView ()
}

func updateAnimation (to: Bool) {
layer?.removeAllAnimations()
self.layer?.opacity = 1
if to {
let anim = CABasicAnimation.init(keyPath: #keyPath (CALayer.opacity))
anim.duration = 0.7
anim.autoreverses = true
anim.repeatCount = Float.infinity
anim.fromValue = NSNumber (floatLiteral: 1)
anim.toValue = NSNumber (floatLiteral: 0.3)
anim.timingFunction = CAMediaTimingFunction (name: .easeInEaseOut)
sub.add(anim, forKey: #keyPath (CALayer.opacity))
case .steadyBar, .steadyBlock, .steadyUnderline:
sub.removeAllAnimations()
sub.opacity = 1
}

guard let layer = self.layer else {
return
}
switch style {
case .steadyBlock, .blinkBlock:
sub.frame = CGRect (x: 0, y: 0, width: layer.bounds.width, height: layer.bounds.height)
case .steadyUnderline, .blinkUnderline:
sub.frame = CGRect (x: 0, y: 0, width: layer.bounds.width, height: 2)
case .steadyBar, .blinkBar:
sub.frame = CGRect (x: 0, y: 0, width: 2, height: layer.bounds.height)
anim.toValue = NSNumber (floatLiteral: 0)
anim.timingFunction = CAMediaTimingFunction (name: .easeIn)
layer?.add(anim, forKey: #keyPath (CALayer.opacity))
}

}

func disableAnimations () {
sub.removeAllAnimations()
layer?.removeAllAnimations()
layer?.opacity = 1
}

public var defaultCaretColor = NSColor.selectedControlColor
Expand All @@ -78,20 +87,29 @@ class CaretView: NSView {
updateView()
}
}


public var defaultCaretTextColor: NSColor? = nil
public var caretTextColor: NSColor? = nil {
didSet {
updateView()
}
}

public var focused: Bool = false {
didSet {
updateView()
}
}

func updateView() {
let isFirst = focused
guard let layer = layer else { return }
sub.frame = CGRect (origin: CGPoint.zero, size: layer.frame.size)
sub.borderWidth = isFirst ? 0 : 1
sub.borderColor = caretColor.cgColor
sub.backgroundColor = isFirst ? caretColor.cgColor : NSColor.clear.cgColor
setNeedsDisplay(bounds)
}

func draw(_ layer: CALayer, in context: CGContext) {
drawCursor (in: context, hasFocus: terminal?.hasFocus ?? true)
}

override func draw(_ dirtyRect: NSRect) {
}

override func hitTest(_ point: NSPoint) -> NSView? {
Expand Down
36 changes: 34 additions & 2 deletions Sources/SwiftTerm/Mac/MacTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations,
}
setupScroller()
setupOptions()
setupFocusNotification()
}

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

var becomeMainObserver, resignMainObserver: NSObjectProtocol?

deinit {
if let becomeMainObserver {
NotificationCenter.default.removeObserver (becomeMainObserver)
}
if let resignMainObserver {
NotificationCenter.default.removeObserver (resignMainObserver)
}
}

func setupFocusNotification() {
becomeMainObserver = NotificationCenter.default.addObserver(forName: .init("NSWindowDidBecomeMainNotification"), object: nil, queue: nil) { [unowned self] notification in
self.caretView.updateCursorStyle()
}
resignMainObserver = NotificationCenter.default.addObserver(forName: .init("NSWindowDidResignMainNotification"), object: nil, queue: nil) { [unowned self] notification in
self.caretView.disableAnimations()
self.caretView.updateView()
}
}

func setupOptions ()
{
setupOptions (width: getEffectiveWidth (size: bounds.size), height: bounds.height)
Expand Down Expand Up @@ -212,7 +234,14 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations,
get { caretView.caretColor }
set { caretView.caretColor = newValue }
}


/// Controls the color for the text in the caret when using a block cursor, if not set
/// the cursor will render with the foreground color
public var caretTextColor: NSColor? {
get { caretView.caretTextColor }
set { caretView.caretTextColor = newValue }
}

var _selectedTextBackgroundColor = NSColor.selectedTextBackgroundColor
/// The color used to render the selection
public var selectedTextBackgroundColor: NSColor {
Expand Down Expand Up @@ -393,7 +422,10 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations,

private var _hasFocus = false
open var hasFocus : Bool {
get { _hasFocus }
get {
//print ("hasFocus: \(_hasFocus) window=\(window?.isKeyWindow)")
return _hasFocus && (window?.isKeyWindow ?? true)
}
set {
_hasFocus = newValue
caretView.focused = newValue
Expand Down
Loading

0 comments on commit 0d31c2e

Please sign in to comment.