diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-16 12:00:52 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 556c5b50bbfd4a6aaae192377d7810f610fc7d9e (patch) | |
| tree | d2acef085c6e6589fbf94936702222d914abc86a /src/macos/Sources/PardesView.swift | |
| parent | 0b54c599fa5e26fb1754ca0aab53da5fa0e14af1 (diff) | |
| download | pardes-556c5b50bbfd4a6aaae192377d7810f610fc7d9e.tar.gz pardes-556c5b50bbfd4a6aaae192377d7810f610fc7d9e.zip | |
Let WindowOpacity through on macOS
The builtin was gated on the SDL platform, so the macOS shell never registered
it and the setting had nowhere to land. macOS already had transparency, but
only the binary kind a theme decides — a theme with no background of its own
drops the ground and the blur shows through. WindowOpacity is the graded,
theme-independent version, and the window had no way to hear about it.
pardes_window_opacity reports the percentage and acknowledges the request in
one read, beside pardes_theme_bg: an SDL surface can arrive without an alpha
channel and has to be able to refuse, while an AppKit window always composites
per pixel, so there is nothing here to refuse and no rollback to perform.
The view then paints the rule shaders/ui.frag.glsl states for the SDL shell:
backgrounds — ground, cell and band fills, and the chrome rules over them —
take the alpha, glyph ink never does, and the block cursor is exempt because
it is foreground chrome that happens to be carried in a cell background. That
exemption is why the cursor joins the colour in the background run key; it can
no longer share a fill with the cells beside it.
Two things the harness was missing fall out of testing it: it never adopted
the theme background or the opacity, so a Theme or WindowOpacity in a script
moved the core and never reached a pixel. draw-opacity records both ends of
the alpha range, which is what makes the two-sided contract assertable —
60% reads 153..255, and 0% reads 0..255 with the ink still standing.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Diffstat (limited to 'src/macos/Sources/PardesView.swift')
| -rw-r--r-- | src/macos/Sources/PardesView.swift | 82 |
1 files changed, 63 insertions, 19 deletions
diff --git a/src/macos/Sources/PardesView.swift b/src/macos/Sources/PardesView.swift index cd3ec980..5df1fd73 100644 --- a/src/macos/Sources/PardesView.swift +++ b/src/macos/Sources/PardesView.swift @@ -626,6 +626,32 @@ final class PardesView: NSView { return true } + /// What `WindowOpacity` asked for, as the alpha every BACKGROUND is painted + /// at: the ground, cell and band backgrounds, and the chrome rules over + /// them. Glyph ink is never scaled by it, and neither is the block cursor — + /// it is foreground chrome that merely happens to be carried in a cell's + /// background, and it has to stay visible at `WindowOpacity 0`. + /// + /// The rule is stated once for the SDL shell in shaders/ui.frag.glsl; this + /// is the same rule in CoreGraphics, where painting the ground at this + /// alpha and the glyph over it at its own coverage composites to what that + /// shader writes premultiplied in one pass. + /// + /// Orthogonal to `themeBG`: a theme with no background of its own drops the + /// ground whatever this says, and an opaque theme still honours it. + private(set) var backgroundAlpha: CGFloat = 1 + + /// Adopt the core's percentage. Returns whether anything moved, on the same + /// contract as adoptThemeBG: the host redresses the window only when it has. + @discardableResult + func adoptWindowOpacity(_ percent: UInt8) -> Bool { + let wanted = CGFloat(min(percent, 100)) / 100 + guard wanted != backgroundAlpha else { return false } + backgroundAlpha = wanted + needsDisplay = true + return true + } + // The button a left-stream click actually started with. mouseDown decides // it from the fingers on the trackpad, and mouseDragged/mouseUp must use // the same one: a press of right followed by a release of left leaves the @@ -792,10 +818,11 @@ final class PardesView: NSView { // Row 0 at the top, so the drawing arithmetic reads like the grid it is. override var isFlipped: Bool { true } - // Opaque only while the theme brings its own background. A transparent - // theme has none, and an opaque view over a visual-effect backdrop is a - // grey rectangle where the blur should be. - override var isOpaque: Bool { themeBG != nil } + // Opaque only while the theme brings its own background AND that background + // is painted solid. A transparent theme has none, and an opaque view over a + // visual-effect backdrop is a grey rectangle where the blur should be; + // a WindowOpacity below 100 has the same problem against the desktop. + override var isOpaque: Bool { themeBG != nil && backgroundAlpha >= 1 } override var acceptsFirstResponder: Bool { true } // A click that focuses the window should also land in the grid: this is a // text surface, and having to click twice after switching apps is the kind @@ -974,7 +1001,12 @@ final class PardesView: NSView { // otherwise pile up on themselves. let ground = themeBG ?? pardesDefaultBG let clearGround = themeBG == nil - if clearGround { ctx.clear(bounds) } else { fill(ctx, bounds, ground, 1) } + // AppKit does not blank a non-opaque view, so anything that leaves the + // ground short of solid has to clear first or last frame's pixels pile + // 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 frozenImages == nil { frameImages.removeAll(keepingCapacity: true) } guard cols > 0, rows > 0, Int(count) == cols * rows, let cells = suppliedCells ?? pardes_frame_cells() @@ -1004,30 +1036,38 @@ final class PardesView: NSView { // cell in the row, and it is a call across the ABI. let bandTop = taglines.top(row: row, canvasHeight: canvasHeight) var start = 0 - var color = resolve(cells[base], block: blockY == row && blockX == 0, + var block = blockY == row && blockX == 0 + var color = resolve(cells[base], block: block, ground: ground, clearGround: clearGround).bg var tagline = cells[base].flags & UInt8(PARDES_CELL_TAGLINE) != 0 for col in 1...cols { // A real color is 24 bits, so .max is a sentinel that cannot // compare equal and therefore always flushes the last run. + let nextBlock = col < cols && blockY == row && blockX == col let next: UInt32 = col == cols ? .max - : resolve(cells[base + col], block: blockY == row && blockX == col, + : resolve(cells[base + col], block: nextBlock, ground: ground, clearGround: clearGround).bg let nextTagline = col < cols && cells[base + col].flags & UInt8(PARDES_CELL_TAGLINE) != 0 - if next == color && nextTagline == tagline { continue } + // The cursor is in the run key as well as the colour: it is the + // one background that stays solid under WindowOpacity, so it + // cannot share a fill with the cells beside it even when the + // reverse happened to land on their colour. + if next == color && nextTagline == tagline && nextBlock == block { continue } // bgClear runs are the ground showing through, and the ground is // already clear — painting them would be painting the hole shut. if color != bgClear { let bandY = tagline ? y + bandTop : y let bandHeight = tagline ? taglines.height : cellHeight fill(ctx, CGRect(x: CGFloat(start) * cellWidth, y: bandY, - width: CGFloat(col - start) * cellWidth, height: bandHeight), color, 1) + width: CGFloat(col - start) * cellWidth, height: bandHeight), + color, block ? 1 : backgroundAlpha) } start = col color = next tagline = nextTagline + block = nextBlock } } @@ -1050,7 +1090,7 @@ final class PardesView: NSView { } if joins { fill(ctx, CGRect(x: 0, y: taglines.borderTop, width: bounds.width, - height: ruleThickness), ruleColor, 1) + height: ruleThickness), ruleColor, backgroundAlpha) } } @@ -1069,7 +1109,7 @@ final class PardesView: NSView { clearGround: clearGround).bg if bg != bgClear { fill(ctx, CGRect(x: 0, y: gridBottom, width: bounds.width, - height: canvasHeight - gridBottom), bg, 1) + height: canvasHeight - gridBottom), bg, backgroundAlpha) } break } @@ -1251,15 +1291,17 @@ final class PardesView: NSView { let viewport = CGRect(x: left, y: y, width: CGFloat(values[6]) * cellWidth, height: taglines.height) ctx.saveGState() ctx.clip(to: viewport) - fill(ctx, viewport, UInt32(values[10]), 1) + fill(ctx, viewport, UInt32(values[10]), backgroundAlpha) for pass in 0...1 { for col in 0..<values[0] { var cell = cells[col] let isCursor = !hideCursor && col == values[8] - let style = resolve(cell, block: isCursor && values[9] == 0, ground: UInt32(values[10]), clearGround: false) + let block = isCursor && values[9] == 0 + 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), style.bg, 1) + fill(ctx, CGRect(x: x, y: y, width: taglines.width, height: taglines.height), + style.bg, block ? 1 : backgroundAlpha) continue } let text = withUnsafeBytes(of: &cell.text) { String(decoding: $0.prefix(Int(cell.len)), as: UTF8.self) } @@ -1320,7 +1362,7 @@ final class PardesView: NSView { height: CGFloat(value(6))*cellHeight) ctx.saveGState() ctx.clip(to: viewport) - fill(ctx, viewport, themeBG ?? pardesDefaultBG, 1) + fill(ctx, viewport, themeBG ?? pardesDefaultBG, backgroundAlpha) for row in 0..<rows { let compact = row < contexts let advance = compact ? taglines.width : cellWidth @@ -1328,16 +1370,18 @@ final class PardesView: NSView { 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, 1) + fill(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 style = resolve(cell, block: isCursor && value(10) == 0, + let block = isCursor && value(10) == 0 + let style = resolve(cell, block: block, ground: themeBG ?? pardesDefaultBG, clearGround: false) let x = left + CGFloat(col)*advance if pass == 0 { - fill(ctx, CGRect(x: x, y: y, width: advance, height: height), style.bg, 1) + fill(ctx, CGRect(x: x, y: y, width: advance, height: height), + style.bg, block ? 1 : backgroundAlpha) continue } let text = withUnsafeBytes(of: &cell.text) { bytes in @@ -1372,7 +1416,7 @@ final class PardesView: NSView { // 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, - width: viewport.width, height: thickness), border, 1) + width: viewport.width, height: thickness), border, backgroundAlpha) } } ctx.restoreGState() |
