Skip to content

Instantly share code, notes, and snippets.

@dabbott
Last active May 8, 2023 02:44
Show Gist options
  • Save dabbott/a6d61d79e666b1cd40bbd7d1c1138823 to your computer and use it in GitHub Desktop.
Save dabbott/a6d61d79e666b1cd40bbd7d1c1138823 to your computer and use it in GitHub Desktop.
//
// LNATextField.swift
// LonaStudio
//
// Created by Devin Abbott on 11/1/18.
// Copyright © 2018 Devin Abbott. All rights reserved.
//
import AppKit
import Foundation
// This NSTextField subclass draws strings with a .baselineOffset attribute correctly.
//
// OSX 10.14 fixes a layout issue when using the .baselineOffset attribute, but we use
// this subclass in order to support 10.12 and 10.13. This isn't a general-purpose replacement
// for NSTextField; it should only be used for non-editable labels. Whenever we drop support
// for pre-10.14 we can remove this and use NSTextField directly instead.
public class LNATextField: NSTextField {
public init() {
super.init(frame: .zero)
self.cell = LNATextFieldCell()
}
public convenience init(labelWithAttributedString attributedString: NSAttributedString) {
self.init()
self.isBordered = false
self.drawsBackground = false
self.isBezeled = false
self.bezelStyle = .squareBezel
self.isEnabled = true
self.isEditable = false
self.isSelectable = false
self.attributedStringValue = attributedString
}
public convenience init(labelWithString string: String) {
self.init(labelWithAttributedString: NSAttributedString(string: string))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Determine the baseline offset from the attributed string value. We store it as a member variable,
// then we remove it from the attributed string.
override public var attributedStringValue: NSAttributedString {
get { return super.attributedStringValue }
set {
baselineOffset = newValue.baselineOffset
let string = NSMutableAttributedString(attributedString: newValue)
string.removeAttribute(.baselineOffset, range: NSRange(location: 0, length: string.length))
super.attributedStringValue = string
}
}
var baselineOffset: CGFloat?
}
private class LNATextFieldCell: NSTextFieldCell {
override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
if let textView = controlView as? LNATextField,
let baselineOffset = textView.baselineOffset,
let lineHeight = attributedStringValue.lineHeight {
var rect = cellFrame.insetBy(dx: 2, dy: 0)
rect.origin.y -= baselineOffset
let truncatesLastVisibleLine = textView.maximumNumberOfLines > 0 &&
cellFrame.height / lineHeight >= CGFloat(textView.maximumNumberOfLines)
let options: NSString.DrawingOptions = truncatesLastVisibleLine
? [.usesLineFragmentOrigin, .truncatesLastVisibleLine]
: [.usesLineFragmentOrigin]
attributedStringValue.draw(with: rect, options: options)
} else {
super.drawInterior(withFrame: cellFrame, in: controlView)
}
}
}
private extension NSAttributedString {
var lineHeight: CGFloat? {
guard
length > 0,
let paragraphStyle = attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle
else { return nil }
let lineHeight = paragraphStyle.minimumLineHeight
if lineHeight <= 0 { return nil }
return lineHeight
}
var baselineOffset: CGFloat? {
guard
length > 0,
let lineHeight = lineHeight,
let font = attribute(.font, at: 0, effectiveRange: nil) as? NSFont
else { return nil }
return (lineHeight - font.ascender + font.descender) / 2
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment