From cf7cfd96a62b26321de970496670005cf411c94c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 15 Sep 2026 17:36:32 -0300 Subject: Center GUI pane indicators using cached font ink bounds --- src/macos/Sources/PardesView.swift | 67 +++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 4 deletions(-) (limited to 'src/macos') diff --git a/src/macos/Sources/PardesView.swift b/src/macos/Sources/PardesView.swift index e2234264..f64678ee 100644 --- a/src/macos/Sources/PardesView.swift +++ b/src/macos/Sources/PardesView.swift @@ -562,6 +562,11 @@ final class PardesView: NSView { // body and tagline variants whenever the body face changes. private var bodyGlyphCache: [UInt32: CGGlyph] = [:] private var taglineGlyphCache: [UInt32: CGGlyph] = [:] + private struct GripGlyph { + let line: CTLine + let ink: CGRect + } + private var gripGlyphCache: [String: GripGlyph] = [:] // Scratch for one batched run of glyphs. Held rather than made per row so a // full redraw does not allocate 24 times. private var runGlyphs: [CGGlyph] = [] @@ -710,6 +715,7 @@ final class PardesView: NSView { // it would draw a different character, not a missing one. bodyGlyphCache.removeAll(keepingCapacity: true) taglineGlyphCache.removeAll(keepingCapacity: true) + gripGlyphCache.removeAll(keepingCapacity: true) metrics = next taglines = TaglineMetrics(body: next, percent: taglines.percent, scale: scale) metricsScale = scale @@ -728,6 +734,7 @@ final class PardesView: NSView { guard percent != taglines.percent else { return false } taglines = TaglineMetrics(body: metrics, percent: percent, scale: metricsScale) taglineGlyphCache.removeAll(keepingCapacity: true) + gripGlyphCache.removeAll(keepingCapacity: true) pardes_row_metrics(UInt16(max(1,(cellWidth*metricsScale).rounded())),UInt16(max(1,(cellHeight*metricsScale).rounded())), UInt16(max(1,(taglines.width*metricsScale).rounded())),UInt16(max(1,(taglines.height*metricsScale).rounded()))) needsDisplay = true @@ -1085,11 +1092,25 @@ final class PardesView: NSView { ctx.translateBy(x: 0, y: bounds.height) ctx.scaleBy(x: 1, y: -1) let height = bounds.height + var paneGripCells = Set() + if frozenImages == nil { + for index in 0..= 0 && x < cols && y >= 0 && y < rows { paneGripCells.insert(y * cols + x) } + } + } else { + for layer in presentedTagLayers where layer.values[1] == 2 { + let x = layer.values[4] - Int(pardes_tag_text_inset()), y = layer.values[5] + if x >= 0 && x < cols && y >= 0 && y < rows { paneGripCells.insert(y * cols + x) } + } + } for row in 0.. GripGlyph? { + let key = "\(face.rawValue):\(text)" + if let cached = gripGlyphCache[key] { return cached } + let attributed = NSAttributedString(string: text, attributes: [fontAttribute: taglines.fonts[face.rawValue], + NSAttributedString.Key(kCTForegroundColorFromContextAttributeName as String): true]) + let line = CTLineCreateWithAttributedString(attributed as CFAttributedString) + let ink = CTLineGetBoundsWithOptions(line, [.useGlyphPathBounds]) + guard !ink.isNull, !ink.isInfinite, !ink.isEmpty else { return nil } + let measured = GripGlyph(line: line, ink: ink) + gripGlyphCache[key] = measured + return measured + } + private func drawTagGrip(_ ctx: CGContext, values: [Int], canonical: UnsafePointer, cols: Int, rows: Int) { let width = Int(pardes_grip_columns()) let x = values[4] - Int(pardes_tag_text_inset()), y = values[5] - if values[1] != 2 || x < 0 || x >= cols || y >= rows || taglines.height < 6 || taglines.width < 3 { return } + if values[1] != 2 || x < 0 || x >= cols || y < 0 || y >= rows { return } var cell = canonical[y * cols + x] let text = withUnsafeBytes(of: &cell.text) { String(decoding: $0.prefix(Int(cell.len)), as: UTF8.self) } - if text != " " { return } + if text.isEmpty { return } let style = resolve(cell, block:false, ground:themeBG ?? pardesDefaultBG, clearGround:false) let centerX = (CGFloat(x) + CGFloat(width) / 2) * cellWidth let centerY = CGFloat(y) * cellHeight + taglines.top(row:y, canvasHeight:bounds.height) + taglines.height / 2 + if text != " " { + let face = Face(bold: cell.attrs & UInt16(PARDES_ATTR_BOLD) != 0, + italic: cell.attrs & UInt16(PARDES_ATTR_ITALIC) != 0) + guard style.visible, let measured = gripGlyph(text, face: face) else { return } + let originX = ((centerX - measured.ink.midX) * metricsScale).rounded() / metricsScale + let baseline = ((bounds.height - centerY - measured.ink.midY) * metricsScale).rounded() / metricsScale + ctx.saveGState() + ctx.clip(to: CGRect(x: CGFloat(x) * cellWidth, + y: CGFloat(y) * cellHeight + taglines.top(row:y, canvasHeight:bounds.height), + width: CGFloat(width) * cellWidth, height: taglines.height)) + ctx.translateBy(x: 0, y: bounds.height) + ctx.scaleBy(x: 1, y: -1) + ctx.textMatrix = .identity + ctx.textPosition = CGPoint(x: originX, y: baseline) + setFill(ctx, style.fg, style.alpha) + CTLineDraw(measured.line, ctx) + ctx.restoreGState() + return + } + if taglines.height < 6 || taglines.width < 3 { return } let halfWidth = max(1, min(3, cellWidth - 2)) fill(ctx, CGRect(x:floor(centerX-halfWidth), y:floor(centerY-2), width:2*halfWidth, height:1), style.fg, 1) fill(ctx, CGRect(x:floor(centerX-halfWidth), y:floor(centerY+1), width:2*halfWidth, height:1), style.fg, 1) @@ -1482,7 +1536,8 @@ final class PardesView: NSView { /// rather than recomputed per cell: it is one answer per row, and the /// glyph has to sit in the band the background pass painted. taglineTop: CGFloat, - blockCol: Int + blockCol: Int, + paneGripCells: Set ) { var runFace = Face.regular var runTagline = false @@ -1544,6 +1599,10 @@ final class PardesView: NSView { let face = Face(bold: cell.attrs & UInt16(PARDES_ATTR_BOLD) != 0, italic: cell.attrs & UInt16(PARDES_ATTR_ITALIC) != 0) + if paneGripCells.contains(base + col) && gripGlyph(text, face: face) != nil { + // The layer pass centers this symbol by its actual ink bounds. + continue + } let units = text.utf16 let known = units.count == 1 ? glyph(face, units.first!, tagline: tagline) : 0 if known != 0 { -- cgit v1.3