summaryrefslogtreecommitdiff
path: root/src/macos/Sources
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-16 13:20:29 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit7cbd44dd1cfe97126e7fc996214e458525d2980c (patch)
treeeeb02eabaf39231dece68647129040e546aa80a2 /src/macos/Sources
parent3042f33df2b16b61c08d6535fba268103728dd1e (diff)
downloadpardes-7cbd44dd1cfe97126e7fc996214e458525d2980c.tar.gz
pardes-7cbd44dd1cfe97126e7fc996214e458525d2980c.zip
Align macOS rendering with Linux and establish parity regressions
Diffstat (limited to 'src/macos/Sources')
-rw-r--r--src/macos/Sources/AppDelegate.swift2
-rw-r--r--src/macos/Sources/PardesView.swift128
2 files changed, 81 insertions, 49 deletions
diff --git a/src/macos/Sources/AppDelegate.swift b/src/macos/Sources/AppDelegate.swift
index 107c40fd..4e91e407 100644
--- a/src/macos/Sources/AppDelegate.swift
+++ b/src/macos/Sources/AppDelegate.swift
@@ -478,7 +478,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate {
if let rgb = view.themeBG {
backdrop.isHidden = true
window.isOpaque = view.backgroundAlpha >= 1
- window.backgroundColor = NSColor(
+ window.backgroundColor = view.backgroundAlpha < 1 ? .clear : NSColor(
srgbRed: CGFloat((rgb >> 16) & 0xFF) / 255,
green: CGFloat((rgb >> 8) & 0xFF) / 255,
blue: CGFloat(rgb & 0xFF) / 255,
diff --git a/src/macos/Sources/PardesView.swift b/src/macos/Sources/PardesView.swift
index ba3677e7..7c537055 100644
--- a/src/macos/Sources/PardesView.swift
+++ b/src/macos/Sources/PardesView.swift
@@ -1006,7 +1006,7 @@ final class PardesView: NSView {
// up on themselves — a translucent ground would darken toward opaque
// over a few frames otherwise.
if clearGround || backgroundAlpha < 1 { ctx.clear(bounds) }
- if !clearGround { fill(ctx, bounds, ground, backgroundAlpha) }
+ if !clearGround { fillBackground(ctx, bounds, ground, backgroundAlpha) }
if frozenImages == nil { frameImages.removeAll(keepingCapacity: true) }
guard cols > 0, rows > 0, Int(count) == cols * rows,
let cells = suppliedCells ?? pardes_frame_cells()
@@ -1060,7 +1060,7 @@ final class PardesView: NSView {
if color != bgClear {
let bandY = tagline ? y + bandTop : y
let bandHeight = tagline ? taglines.height : cellHeight
- fill(ctx, CGRect(x: CGFloat(start) * cellWidth, y: bandY,
+ fillBackground(ctx, CGRect(x: CGFloat(start) * cellWidth, y: bandY,
width: CGFloat(col - start) * cellWidth, height: bandHeight),
color, block ? 1 : backgroundAlpha)
}
@@ -1071,29 +1071,6 @@ final class PardesView: NSView {
}
}
- // The rule joining the topbar band to the first pane-tag band. Both were
- // centred in their own rows until now, and centring two reduced-height
- // bands is exactly what leaves their unused half-bands touching with a
- // strip of window background showing between them — the case
- // config.gui_topbar_pane_border_px exists to close. Drawn only when that
- // first pane row really carries a band, so a session whose second row is
- // pane BODY gets no stray line across it.
- let ruleThickness = taglines.borderThickness
- let ruleColor = pardes_topbar_pane_border_rgb()
- if ruleThickness > 0, ruleColor != UInt32(PARDES_COLOR_DEFAULT),
- rows > Int(PARDES_TOPBAR_H) {
- let base = Int(PARDES_TOPBAR_H) * cols
- let joins = (0..<cols).contains { col in
- let flags = cells[base + col].flags
- return flags & UInt8(PARDES_CELL_DEFAULT) == 0
- && flags & UInt8(PARDES_CELL_TAGLINE) != 0
- }
- if joins {
- fill(ctx, CGRect(x: 0, y: taglines.borderTop, width: bounds.width,
- height: ruleThickness), ruleColor, backgroundAlpha)
- }
- }
-
// The core grid contains only complete cells, so any window height that
// is not a whole multiple of the cell leaves a strip below the last row.
// A Tagbottom band there is bottom-aligned against the window edge, and
@@ -1108,7 +1085,7 @@ final class PardesView: NSView {
let bg = resolve(cell, block: false, ground: ground,
clearGround: clearGround).bg
if bg != bgClear {
- fill(ctx, CGRect(x: 0, y: gridBottom, width: bounds.width,
+ fillBackground(ctx, CGRect(x: 0, y: gridBottom, width: bounds.width,
height: canvasHeight - gridBottom), bg, backgroundAlpha)
}
break
@@ -1211,6 +1188,7 @@ final class PardesView: NSView {
if let base = cells.baseAddress { drawBodyLayer(ctx, values:layer.values, cells:base, hideCursor:true) }
}
}
+ drawTagRules(ctx, layers: presentedTagLayers.map { $0.values })
}
}
@@ -1219,7 +1197,7 @@ final class PardesView: NSView {
private func snapshotTagLayers() -> [FrozenBodyLayer] {
var result: [FrozenBodyLayer] = []
for index in 0..<pardes_tag_layer_limit() {
- let values = (0...10).map { Int(pardes_tag_layer_value(index, UInt32($0))) }
+ let values = (0...12).map { Int(pardes_tag_layer_value(index, UInt32($0))) }
if values[0] == 0 { continue }
if let cells = pardes_tag_layer_cells(index) {
result.append(FrozenBodyLayer(values: values, cells: Array(UnsafeBufferPointer(start: cells, count: values[0]))))
@@ -1229,12 +1207,35 @@ final class PardesView: NSView {
}
private func drawTagLayers(_ ctx: CGContext, hideCursor: Bool, canonical: UnsafePointer<pardes_cell_s>, cols: Int, rows: Int) {
+ var layers: [[Int]] = []
for index in 0..<pardes_tag_layer_limit() {
- let values = (0...10).map { Int(pardes_tag_layer_value(index, UInt32($0))) }
+ let values = (0...12).map { Int(pardes_tag_layer_value(index, UInt32($0))) }
if values[0] == 0 { continue }
+ layers.append(values)
drawTagGrip(ctx, values:values, canonical:canonical, cols:cols, rows:rows)
if let cells = pardes_tag_layer_cells(index) { drawTagLayer(ctx, values: values, cells: cells, hideCursor: hideCursor) }
}
+ drawTagRules(ctx, layers: layers)
+ }
+
+ /// SDL's chrome overlay is drawn after its compact layers. Keep these
+ /// one-pixel rules above the full-row tag backgrounds as well.
+ private func drawTagRules(_ ctx: CGContext, layers: [[Int]]) {
+ let pixel = 1 / metricsScale
+ for values in layers where values[1] != 0 {
+ let kind = values[1]
+ let bottom = kind == 2 && values[11] != 0
+ let left = CGFloat(max(0, values[4] - Int(pardes_tag_text_inset()))) * cellWidth
+ let right = CGFloat(values[4] + values[6]) * cellWidth
+ let y = CGFloat(values[5] + (bottom ? 0 : 1)) * cellHeight - (bottom ? 0 : pixel)
+ fillBackground(ctx, CGRect(x: left, y: y, width: right-left, height: pixel),
+ UInt32(values[12]), backgroundAlpha)
+ }
+ if layers.contains(where: { $0[5] == Int(PARDES_TOPBAR_H) }), taglines.borderThickness > 0 {
+ fillBackground(ctx, CGRect(x: 0, y: taglines.borderTop, width: bounds.width,
+ height: taglines.borderThickness),
+ pardes_topbar_pane_border_rgb(), backgroundAlpha)
+ }
}
private func gripGlyph(_ text: String, face: Face) -> GripGlyph? {
@@ -1288,10 +1289,13 @@ final class PardesView: NSView {
private func drawTagLayer(_ ctx: CGContext, values: [Int], cells: UnsafePointer<pardes_cell_s>, hideCursor: Bool) {
let left = CGFloat(values[4]) * cellWidth
let y = CGFloat(values[5]) * cellHeight + taglines.top(row: values[5], canvasHeight: bounds.height)
- let viewport = CGRect(x: left, y: y, width: CGFloat(values[6]) * cellWidth, height: taglines.height)
+ // SDL gives the tag a full body-row background; only its glyphs
+ // and per-cell highlights use the compact band.
+ let viewport = CGRect(x: left, y: CGFloat(values[5]) * cellHeight,
+ width: CGFloat(values[6]) * cellWidth, height: cellHeight)
ctx.saveGState()
ctx.clip(to: viewport)
- fill(ctx, viewport, UInt32(values[10]), backgroundAlpha)
+ fillBackground(ctx, viewport, UInt32(values[10]), backgroundAlpha)
for pass in 0...1 {
for col in 0..<values[0] {
var cell = cells[col]
@@ -1300,7 +1304,7 @@ final class PardesView: NSView {
let style = resolve(cell, block: block, ground: UInt32(values[10]), clearGround: false)
let x = left + CGFloat(col) * taglines.width
if pass == 0 {
- fill(ctx, CGRect(x: x, y: y, width: taglines.width, height: taglines.height),
+ fillBackground(ctx, CGRect(x: x, y: y, width: taglines.width, height: taglines.height),
style.bg, block ? 1 : backgroundAlpha)
continue
}
@@ -1314,7 +1318,8 @@ final class PardesView: NSView {
drawRules(ctx, cell, style, x: x, width: taglines.width, baseline: baseline)
setFill(ctx, style.fg, style.alpha)
ctx.textPosition = CGPoint(x: x, y: baseline)
- let line = NSAttributedString(string: text, attributes: [fontAttribute: taglines.fonts[face.rawValue]])
+ let line = NSAttributedString(string: text, attributes: [fontAttribute: taglines.fonts[face.rawValue],
+ NSAttributedString.Key(kCTForegroundColorFromContextAttributeName as String): true])
CTLineDraw(CTLineCreateWithAttributedString(line as CFAttributedString), ctx)
ctx.restoreGState()
}
@@ -1362,25 +1367,25 @@ final class PardesView: NSView {
height: CGFloat(value(6))*cellHeight)
ctx.saveGState()
ctx.clip(to: viewport)
- fill(ctx, viewport, themeBG ?? pardesDefaultBG, backgroundAlpha)
+ fillBackground(ctx, viewport, themeBG ?? bgClear, backgroundAlpha)
for row in 0..<rows {
let compact = row < contexts
let advance = compact ? taglines.width : cellWidth
let height = compact ? taglines.height : cellHeight
let y = top + CGFloat(min(row, contexts))*taglines.height + CGFloat(max(0, row-contexts))*cellHeight
if y >= viewport.maxY { break }
- let rowStyle = resolve(cells[row*cols], block: false, ground: themeBG ?? pardesDefaultBG, clearGround: false)
- fill(ctx, CGRect(x: left, y: y, width: viewport.width, height: height), rowStyle.bg, backgroundAlpha)
+ let rowStyle = resolve(cells[row*cols], block: false, ground: themeBG ?? pardesDefaultBG, clearGround: themeBG == nil)
+ fillBackground(ctx, CGRect(x: left, y: y, width: viewport.width, height: height), rowStyle.bg, backgroundAlpha)
for pass in 0...1 {
for col in 0..<cols {
var cell = cells[row*cols+col]
let isCursor = !hideCursor && col == value(8) && row == value(9)
let block = isCursor && value(10) == 0
let style = resolve(cell, block: block,
- ground: themeBG ?? pardesDefaultBG, clearGround: false)
+ ground: themeBG ?? pardesDefaultBG, clearGround: themeBG == nil)
let x = left + CGFloat(col)*advance
if pass == 0 {
- fill(ctx, CGRect(x: x, y: y, width: advance, height: height),
+ fillBackground(ctx, CGRect(x: x, y: y, width: advance, height: height),
style.bg, block ? 1 : backgroundAlpha)
continue
}
@@ -1398,7 +1403,8 @@ final class PardesView: NSView {
drawRules(ctx, cell, style, x: x, width: advance, baseline: baseline)
setFill(ctx, style.fg, style.alpha)
ctx.textPosition = CGPoint(x: x, y: baseline)
- let line = NSAttributedString(string: text, attributes: [fontAttribute: font])
+ let line = NSAttributedString(string: text, attributes: [fontAttribute: font,
+ NSAttributedString.Key(kCTForegroundColorFromContextAttributeName as String): true])
CTLineDraw(CTLineCreateWithAttributedString(line as CFAttributedString), ctx)
ctx.restoreGState()
}
@@ -1415,7 +1421,7 @@ final class PardesView: NSView {
// The SDL shell paints this rule over the row's LAST pixel, not
// the next row's first one; keep the two shells on the same pixel.
let thickness = taglines.contextBorderThickness
- fill(ctx, CGRect(x: left, y: top + CGFloat(row + 1)*taglines.height - thickness,
+ fillBackground(ctx, CGRect(x: left, y: top + CGFloat(row + 1)*taglines.height - thickness,
width: viewport.width, height: thickness), border, backgroundAlpha)
}
}
@@ -1542,10 +1548,18 @@ final class PardesView: NSView {
ctx.translateBy(x: placed.destination.minX, y: placed.destination.maxY)
ctx.scaleBy(x: 1, y: -1)
ctx.interpolationQuality = .high
- ctx.draw(placed.image, in: CGRect(
- x: 0, y: 0,
- width: placed.destination.width,
- height: placed.destination.height))
+ let rect = CGRect(x: 0, y: 0,
+ width: placed.destination.width, height: placed.destination.height)
+ // Match image.frag.glsl/backgroundLayerBlend: source coverage removes
+ // the destination independently of WindowOpacity, then the source is
+ // added at that opacity. Ordinary source-over would compound alpha;
+ // copy would discard the background under transparent image pixels.
+ ctx.setAlpha(1)
+ ctx.setBlendMode(.destinationOut)
+ ctx.draw(placed.image, in: rect)
+ ctx.setAlpha(backgroundAlpha)
+ ctx.setBlendMode(.plusLighter)
+ ctx.draw(placed.image, in: rect)
ctx.restoreGState()
}
@@ -1680,7 +1694,8 @@ final class PardesView: NSView {
flush()
setFill(ctx, style.fg, style.alpha)
let font = tagline ? taglines.fonts[face.rawValue] : metrics.fonts[face.rawValue]
- let attributed = NSAttributedString(string: text, attributes: [fontAttribute: font])
+ let attributed = NSAttributedString(string: text, attributes: [fontAttribute: font,
+ NSAttributedString.Key(kCTForegroundColorFromContextAttributeName as String): true])
ctx.textPosition = CGPoint(x: x, y: cellBaseline)
CTLineDraw(CTLineCreateWithAttributedString(attributed as CFAttributedString), ctx)
// CTLineDraw leaves the text position at the END of what it drew,
@@ -1750,10 +1765,27 @@ final class PardesView: NSView {
}
private func setFill(_ ctx: CGContext, _ rgb: UInt32, _ alpha: CGFloat) {
- ctx.setFillColor(red: CGFloat((rgb >> 16) & 0xFF) / 255,
- green: CGFloat((rgb >> 8) & 0xFF) / 255,
- blue: CGFloat(rgb & 0xFF) / 255,
- alpha: alpha)
+ // Theme channels, like PDF raster bytes, are sRGB. Device RGB can
+ // brighten them when AppKit's backing bitmap uses another profile.
+ ctx.setFillColor(CGColor(srgbRed: CGFloat((rgb >> 16) & 0xFF) / 255,
+ green: CGFloat((rgb >> 8) & 0xFF) / 255,
+ blue: CGFloat(rgb & 0xFF) / 255,
+ alpha: alpha))
+ }
+
+ /// One surface pixel has one background opacity, even when a compact
+ /// layer covers a canonical cell. Source-over would turn 71% into 92%,
+ /// then 98%, as the ground, row and cell are painted over each other.
+ private func fillBackground(_ ctx: CGContext, _ rect: CGRect, _ rgb: UInt32, _ alpha: CGFloat) {
+ ctx.saveGState()
+ ctx.setShouldAntialias(false)
+ if rgb == bgClear {
+ ctx.clear(rect)
+ } else {
+ ctx.setBlendMode(.copy)
+ fill(ctx, rect, rgb, alpha)
+ }
+ ctx.restoreGState()
}
private func fill(_ ctx: CGContext, _ rect: CGRect, _ rgb: UInt32, _ alpha: CGFloat) {