Skip to content

Instantly share code, notes, and snippets.

@danielpunkass
Last active April 17, 2024 13:17
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danielpunkass/548f4ecd2f5debb987d02a46a1d48450 to your computer and use it in GitHub Desktop.
Save danielpunkass/548f4ecd2f5debb987d02a46a1d48450 to your computer and use it in GitHub Desktop.
NSTextField subclasses that grow/shrink their width or height to fit text content
//
// RSDimensionHuggingTextField.swift
// RSUIKit
//
// Created by Daniel Jalkut on 6/13/18.
//
import Cocoa
// You probably want to use one of RSHeightHuggingTextField or RSWidthHuggingTextField, below
open class RSDimensionHuggingTextField: NSTextField {
public enum Dimension {
case vertical
case horizontal
}
var huggedDimension: Dimension
init(frame frameRect: NSRect, huggedDimension: Dimension) {
self.huggedDimension = huggedDimension
super.init(frame: frameRect)
}
// For subclasses to pass in the dimension setting
public init?(coder: NSCoder, huggedDimension: Dimension) {
self.huggedDimension = huggedDimension
super.init(coder: coder)
}
public required init?(coder: NSCoder) {
// We don't yet support dimension being coded, just default to vertical
self.huggedDimension = .vertical
super.init(coder: coder)
}
open override var intrinsicContentSize: NSSize {
get {
let defaultCellSize = super.intrinsicContentSize
guard let textCell = self.cell else {
return defaultCellSize
}
// Set up the bounds to induce unlimited sizing in the desired dimension
var cellSizeBounds = self.bounds
switch self.huggedDimension {
case .vertical: cellSizeBounds.size.height = CGFloat(Float.greatestFiniteMagnitude)
case .horizontal: cellSizeBounds.size.width = CGFloat(Float.greatestFiniteMagnitude)
}
// Do the actual sizing
var nativeCellSize = textCell.cellSize(forBounds: cellSizeBounds)
// Special case - work around a bug in which cellSize(forBounds:) returns a width
// greater than the supplied bounds. This only seems to occur when the string just
// barely doesn't fit. Coerce it into computing a taller value by temporary setting
// a slightly longer string... I'm limiting this to vertical hugging fields for now
// because that's the only scenario where I know it causes a problem. FB7448962.
if self.huggedDimension == .vertical && (nativeCellSize.width > cellSizeBounds.width) {
// Use a copy of the cell to avoid sending the view into a life updateConstraints/loopfkdsj
if
let measuringCell = textCell.copy() as? NSTextFieldCell,
let mutableString = measuringCell.attributedStringValue.mutableCopy() as? NSMutableAttributedString
{
mutableString.append(NSAttributedString(string: "X"))
measuringCell.attributedStringValue = mutableString
nativeCellSize = measuringCell.cellSize(forBounds: cellSizeBounds)
}
}
// Return an intrinsic size that imposes calculated (hugged) dimensional size
var intrinsicSize = NSSize(width: NSView.noIntrinsicMetric, height: NSView.noIntrinsicMetric)
switch self.huggedDimension {
case .vertical:
intrinsicSize.height = nativeCellSize.height
case .horizontal:
intrinsicSize.width = nativeCellSize.width
}
return intrinsicSize
}
}
open override func textDidChange(_ notification: Notification) {
super.textDidChange(notification)
self.invalidateIntrinsicContentSize()
// It seems important to set the string from the cell on ourself to
// get the change to be respected by the cell and to get the cellSize
// computation to update!
if let changedCell = self.cell {
self.stringValue = changedCell.stringValue
}
}
}
open class RSHeightHuggingTextField: RSDimensionHuggingTextField {
@objc init(frame frameRect: NSRect) {
super.init(frame: frameRect, huggedDimension: .vertical)
}
public required init?(coder: NSCoder) {
super.init(coder: coder, huggedDimension: .vertical)
}
public override init(frame frameRect: NSRect, huggedDimension: Dimension = .vertical) {
super.init(frame: frameRect, huggedDimension: huggedDimension)
}
}
open class RSWidthHuggingTextField: RSDimensionHuggingTextField {
@objc init(frame frameRect: NSRect) {
super.init(frame: frameRect, huggedDimension: .horizontal)
}
public required init?(coder: NSCoder) {
super.init(coder: coder, huggedDimension: .horizontal)
}
public override init(frame frameRect: NSRect, huggedDimension: Dimension = .horizontal) {
super.init(frame: frameRect, huggedDimension: huggedDimension)
}
}
@michaeljtsai
Copy link

michaeljtsai commented Feb 17, 2023

I found that setting the non-hugged dimension to .noIntrinsicMetric in line 56 looked too tight with an editable text field, so I instead used super.intrinsicContentSize to get the height when having the width grow to fix the content.

@danielpunkass
Copy link
Author

@michaeljtsai Thanks, I'll have to take a look and see if something similar would work well for my uses too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment