Skip to content

Commit 7cedf85

Browse files
Coiggahou2002claude
andcommitted
Center glyphs vertically when lineSpacing > 1
lineSpacing scales the cell height in computeFontDimensions(), but the baseline stayed pinned at ceil(descent + leading) from the cell bottom, so all the added height piled up above the text and lines looked bottom-heavy at larger values (1.4-1.6). Introduce a single shared TerminalView.baselineOffset that splits the extra height evenly above and below the glyph (the half-leading model iTerm2 and WezTerm use), and make every place that previously computed its own ceil(descent + leading) read it instead: - drawTerminalContents (CoreGraphics text path) - CaretView.drawCursor, so the glyph drawn inside the caret stays on the same baseline as the text under it - MetalTerminalRenderer buildDrawData and buildCursorDrawData (which no longer takes lineDescent/lineLeading parameters) - glyphSlotFit's vertical-centering math for scaled wide glyphs, whose dy is applied on top of the draw-time offset and therefore has to agree with it At lineSpacing == 1.0 the extra is zero and baselineOffset reduces to the classic ceil(descent + leading): offscreen renders of upstream and this branch are byte-identical. At 1.6 the caret glyph, CG text, and DECDHL double-height rows were verified against offscreen renders. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 58915b1 commit 7cedf85

3 files changed

Lines changed: 30 additions & 16 deletions

File tree

Sources/SwiftTerm/Apple/AppleTerminalView.swift

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ extension TerminalView {
123123

124124
/// Multiplier for vertical line spacing. 1.0 = default (ascent + descent + leading).
125125
/// Set to 1.1 for 110% vertical spacing (matches iTerm2's vertical spacing setting).
126+
/// The extra height is split evenly above and below each glyph (see
127+
/// ``baselineOffset``), so text stays vertically centered within its cell.
126128
/// Triggers a font reset and terminal resize when changed.
127129
@objc open var lineSpacing: CGFloat {
128130
get { _lineSpacing }
@@ -277,6 +279,25 @@ extension TerminalView {
277279
return CellDimension(width: max(1, snappedWidth), height: max(min(snappedHeight, 8192), 1))
278280
}
279281

282+
/// Distance from the bottom of a cell to the glyph baseline.
283+
///
284+
/// When `lineSpacing > 1` the cell is taller than the font's natural line
285+
/// (ascent + descent + leading); the extra height is split evenly above and
286+
/// below the glyph so text is vertically centered in the cell, matching how
287+
/// iTerm2 and WezTerm distribute their line spacing. At `lineSpacing == 1`
288+
/// this reduces to the classic `ceil(descent + leading)`.
289+
///
290+
/// Every renderer (CoreGraphics, Metal, and the caret views) must position
291+
/// baselines with this value rather than computing its own offset — local
292+
/// copies of this formula are how the renderers drift out of sync.
293+
var baselineOffset: CGFloat {
294+
let lineDescent = CTFontGetDescent (fontSet.normal)
295+
let lineLeading = CTFontGetLeading (fontSet.normal)
296+
let naturalHeight = ceil(CTFontGetAscent (fontSet.normal) + lineDescent + lineLeading)
297+
let extra = cellDimension == nil ? 0 : max(0, cellDimension.height - naturalHeight)
298+
return ceil(lineDescent + lineLeading + extra / 2)
299+
}
300+
280301
/// Computes how to center `glyph` within its `columnWidth`-cell slot (and
281302
/// scale it down if its ink overflows). Returns ``GlyphSlotFit/identity`` for
282303
/// ordinary single-cell glyphs, so Latin text in a monospace font is rendered
@@ -314,11 +335,12 @@ extension TerminalView {
314335

315336
// Preserve the natural Latin baseline unless the glyph was scaled, in
316337
// which case center its ink vertically so it doesn't sit too low/high.
338+
// `dy` is applied on top of `baselineOffset` at draw time, so the ink
339+
// center lands at cellHeight/2 only if both use the same baseline.
317340
var dy: CGFloat = 0
318341
if scale < 1, ink.height > 0 {
319-
let baselineFromBottom = ceil(CTFontGetDescent(fontSet.normal) + CTFontGetLeading(fontSet.normal))
320342
let inkCenterFromBaseline = (ink.origin.y + ink.height / 2) * scale
321-
dy = (cellHeight / 2 - baselineFromBottom) - inkCenterFromBaseline
343+
dy = (cellHeight / 2 - baselineOffset) - inkCenterFromBaseline
322344
}
323345

324346
return GlyphSlotFit(dx: dx, dy: dy, scale: scale)
@@ -1254,9 +1276,7 @@ extension TerminalView {
12541276
// TODO: this should not render any lines outside the dirtyRect
12551277
func drawTerminalContents (dirtyRect: TTRect, context: CGContext, bufferOffset: Int)
12561278
{
1257-
let lineDescent = CTFontGetDescent(fontSet.normal)
1258-
let lineLeading = CTFontGetLeading(fontSet.normal)
1259-
let yOffset = ceil(lineDescent+lineLeading)
1279+
let yOffset = baselineOffset
12601280
let displayBuffer = terminal.displayBuffer
12611281

12621282
func calcLineOffset (forRow: Int) -> CGFloat {

Sources/SwiftTerm/Apple/CaretView.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ extension CaretView {
3939
}
4040
context.fill([region])
4141

42-
let lineDescent = CTFontGetDescent(terminal.fontSet.normal)
43-
let lineLeading = CTFontGetLeading(terminal.fontSet.normal)
44-
let yOffset = ceil(lineDescent+lineLeading)
42+
// Must match the offset drawTerminalContents uses, or the glyph drawn
43+
// inside the caret lands on a different baseline than the text under it.
44+
let yOffset = terminal.baselineOffset
4545

4646
guard style == .steadyBlock || style == .blinkBlock else {
4747
return

Sources/SwiftTerm/Apple/Metal/MetalTerminalRenderer.swift

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -607,9 +607,7 @@ final class MetalTerminalRenderer: NSObject, MTKViewDelegate {
607607
let buffer = terminalView.terminal.displayBuffer
608608
let cellWidth = terminalView.cellDimension.width
609609
let cellHeight = terminalView.cellDimension.height
610-
let lineDescent = CTFontGetDescent(terminalView.fontSet.normal)
611-
let lineLeading = CTFontGetLeading(terminalView.fontSet.normal)
612-
let yOffset = ceil(lineDescent + lineLeading)
610+
let yOffset = terminalView.baselineOffset
613611
let viewWidthPx = terminalView.bounds.width * scale
614612

615613
let rowInfo = visibleRowRange(buffer: buffer, cellHeight: cellHeight, terminalView: terminalView)
@@ -787,8 +785,6 @@ final class MetalTerminalRenderer: NSObject, MTKViewDelegate {
787785
let cursorData = buildCursorDrawData(scale: scale,
788786
cellWidth: cellWidth,
789787
cellHeight: cellHeight,
790-
lineDescent: lineDescent,
791-
lineLeading: lineLeading,
792788
yDisp: visibleDisp,
793789
firstRow: firstRow,
794790
lastRow: lastRow)
@@ -2124,8 +2120,6 @@ final class MetalTerminalRenderer: NSObject, MTKViewDelegate {
21242120
private func buildCursorDrawData(scale: CGFloat,
21252121
cellWidth: CGFloat,
21262122
cellHeight: CGFloat,
2127-
lineDescent: CGFloat,
2128-
lineLeading: CGFloat,
21292123
yDisp: Int,
21302124
firstRow: Int,
21312125
lastRow: Int) -> (colorVertices: [ColorVertex],
@@ -2236,7 +2230,7 @@ final class MetalTerminalRenderer: NSObject, MTKViewDelegate {
22362230
guard let runs = CTLineGetGlyphRuns(ctline) as? [CTRun] else {
22372231
return (colorVertices, [], [])
22382232
}
2239-
let yOffset = ceil(lineDescent + lineLeading)
2233+
let yOffset = terminalView.baselineOffset
22402234
let textColorSIMD = colorToSIMD(caretTextColor)
22412235

22422236
for run in runs {

0 commit comments

Comments
 (0)