Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -105,29 +105,36 @@ enum LabelText {

private static func isWrapping(_ mode: NSLineBreakMode) -> Bool { mode == .byWordWrapping || mode == .byCharWrapping }

/// The text the shared stack laid out last (tests: whether a measure used TextKit).
static var lastLaidOutString: String { storage.string }
/// A text layout: what ``measure(_:font:shape:width:scale:layOut:)`` asks for a text that
/// needs TextKit (by default ``laidOutMetrics(_:font:shape:width:scale:)``).
typealias Layout = @MainActor (NSAttributedString, NSFont?, Shape, CGFloat?, CGFloat) -> Metrics

/// The layout of `text` (font set, no paragraph settings yet) with `width` points for the
/// text (nil: unbounded), at `scale` pixels per point.
static func measure(_ text: NSAttributedString, font: NSFont?, shape: Shape, width: CGFloat?, scale: CGFloat) -> Metrics {
/// text (nil: unbounded), at `scale` pixels per point. `layOut` lays out a text that needs it.
static func measure(_ text: NSAttributedString, font: NSFont?, shape: Shape, width: CGFloat?, scale: CGFloat,
layOut: Layout = LabelText.laidOutMetrics) -> Metrics {
// An empty label at an unbounded width is one empty line of its font: as tall as
// ``WholePointLines`` sets that line, its baseline at the rounded ascender, no width. It
// needs no layout (REOPEN-F7: a window's sidebar makes about 100 empty labels before its
// first frame, and laying each out cost 22 to 25 ms profiled).
if text.length == 0, width == nil {
let line = NSAttributedString(string: " ", attributes: [.font: font ?? .systemFont(ofSize: NSFont.systemFontSize)])
let line = emptyLine(font: font)
if let metrics = WholePointLines.metrics(of: line, in: NSRange(location: 0, length: line.length)) {
let scale = max(scale, 1)
let height = (metrics.height * scale).rounded(.up) / scale
return Metrics(size: NSSize(width: 0, height: height), firstBaseline: metrics.ascent,
lastBaselineFromBottom: height - metrics.ascent)
}
}
return laidOutMetrics(text, font: font, shape: shape, width: width, scale: scale)
return layOut(text, font, shape, width, scale)
}

/// ``measure(_:font:shape:width:scale:)`` laid out in the shared TextKit stack.
/// The one empty line an empty label is: a space in `font` (nil: the system font).
private static func emptyLine(font: NSFont?) -> NSAttributedString {
NSAttributedString(string: " ", attributes: [.font: font ?? .systemFont(ofSize: NSFont.systemFontSize)])
}

/// ``measure(_:font:shape:width:scale:layOut:)`` laid out in the shared TextKit stack.
static func laidOutMetrics(_ text: NSAttributedString, font: NSFont?, shape: Shape, width: CGFloat?, scale: CGFloat) -> Metrics {
// Alignment moves lines, not their breaks or heights: measure left-aligned, so an
// unbounded width keeps the lines at the left edge.
Expand All @@ -136,8 +143,7 @@ enum LabelText {
// A field measures its lines whole; only its drawing truncates the last visible one.
measuring.truncatesLastVisibleLine = false
// An empty label is one empty line of its font.
let shown = text.length > 0 ? text
: NSAttributedString(string: " ", attributes: [.font: font ?? .systemFont(ofSize: NSFont.systemFontSize)])
let shown = text.length > 0 ? text : emptyLine(font: font)
// A field measures the lines it shows: its first `lines` lines, laid out as if unlimited
// (a limited container sets the rest of the text on its last line).
let shownLines = measuring.wraps && measuring.lines > 0 ? measuring.lines : Int.max
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,46 +8,48 @@ import Testing
/// That was 22 to 25 ms of the profiled window build. An empty line's size is its font's line
/// height (``LabelText/WholePointLines``): it needs no layout.
@MainActor @Suite(.serialized) struct EmptyLabelMeasureTests {
private static let sentinel = "sentinel, laid out before the label"
private static let fonts: [NSFont?] = [.systemFont(ofSize: 13), .systemFont(ofSize: 11), .systemFont(ofSize: 10.5),
.monospacedSystemFont(ofSize: 12, weight: .regular), .boldSystemFont(ofSize: 17),
.systemFont(ofSize: 9.5), NSFont(name: "Times New Roman", size: 14), nil]

private static func layOutSentinel() {
_ = LabelText.measure(NSAttributedString(string: sentinel), font: .systemFont(ofSize: 13),
shape: ChromeLabel(frame: .zero).shape, width: nil, scale: 2)
#expect(LabelText.lastLaidOutString == sentinel)
}

@Test func anEmptyLabelLaysNoTextOut() {
Self.layOutSentinel()
let label = ChromeLabel(font: .systemFont(ofSize: 11))
_ = label.intrinsicContentSize
_ = label.firstBaselineOffsetFromTop
#expect(LabelText.lastLaidOutString == Self.sentinel, "an empty label laid its line out in TextKit")
#expect(label.frame.height > 0)
}

@Test func anEmptyLabelLaysOutAsTextKitWould() {
let fonts: [NSFont] = [.systemFont(ofSize: 13), .systemFont(ofSize: 11), .systemFont(ofSize: 10.5),
.monospacedSystemFont(ofSize: 12, weight: .regular), .boldSystemFont(ofSize: 17),
.systemFont(ofSize: 9.5)]
private static var shapes: [LabelText.Shape] {
var shapes = [ChromeLabel(frame: .zero).shape, ChromeLabel(font: .systemFont(ofSize: 13)).shape,
ChromeLabel(wrappingLabelWithString: "").shape]
var twoLines = shapes[2]
twoLines.lines = 2
twoLines.truncatesLastVisibleLine = true
shapes.append(twoLines)
return shapes
}

@Test func anEmptyLabelLaysNoTextOut() {
var layouts = 0
let counting: LabelText.Layout = { text, font, shape, width, scale in
layouts += 1
return LabelText.laidOutMetrics(text, font: font, shape: shape, width: width, scale: scale)
}
for font in Self.fonts {
for shape in Self.shapes {
_ = LabelText.measure(NSAttributedString(), font: font, shape: shape, width: nil, scale: 2, layOut: counting)
}
}
#expect(layouts == 0, "an empty label at an unbounded width laid its line out in TextKit")
// Text, and an empty text at a set width, still lay out.
_ = LabelText.measure(NSAttributedString(string: "Title"), font: nil, shape: Self.shapes[0], width: nil, scale: 2, layOut: counting)
_ = LabelText.measure(NSAttributedString(), font: nil, shape: Self.shapes[2], width: 120, scale: 2, layOut: counting)
#expect(layouts == 2)
}

@Test func anEmptyLabelLaysOutAsTextKitWould() {
let empty = NSAttributedString()
for font in fonts {
for shape in shapes {
for font in Self.fonts {
for shape in Self.shapes {
for scale: CGFloat in [1, 2, 3] {
let measured = LabelText.measure(empty, font: font, shape: shape, width: nil, scale: scale)
let laidOut = LabelText.laidOutMetrics(empty, font: font, shape: shape, width: nil, scale: scale)
#expect(measured == laidOut, "\(font.fontName) \(font.pointSize) pt, \(shape), scale \(scale)")
#expect(measured == laidOut, "\(font?.fontName ?? "no font") \(font?.pointSize ?? 0) pt, \(shape), scale \(scale)")
}
}
}
// No font: the system font, as TextKit lays it out.
let shape = shapes[0]
#expect(LabelText.measure(empty, font: nil, shape: shape, width: nil, scale: 2)
== LabelText.laidOutMetrics(empty, font: nil, shape: shape, width: nil, scale: 2))
}
}
Loading