diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-16 13:20:29 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 7cbd44dd1cfe97126e7fc996214e458525d2980c (patch) | |
| tree | eeb02eabaf39231dece68647129040e546aa80a2 /src/macos/Sources | |
| parent | 3042f33df2b16b61c08d6535fba268103728dd1e (diff) | |
| download | pardes-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.swift | 2 | ||||
| -rw-r--r-- | src/macos/Sources/PardesView.swift | 128 |
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) { |
