diff options
Diffstat (limited to 'src/macos/Sources')
| -rw-r--r-- | src/macos/Sources/AppDelegate.swift | 18 | ||||
| -rw-r--r-- | src/macos/Sources/PardesView.swift | 82 |
2 files changed, 77 insertions, 23 deletions
diff --git a/src/macos/Sources/AppDelegate.swift b/src/macos/Sources/AppDelegate.swift index 96f4a184..107c40fd 100644 --- a/src/macos/Sources/AppDelegate.swift +++ b/src/macos/Sources/AppDelegate.swift @@ -462,17 +462,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate { private var dressed = false private func applyTheme() { - let changed = view.adoptThemeBG(pardes_theme_bg()) - guard changed || !dressed else { return } + // Both reads happen every pump, and neither may be short-circuited by + // the other: `changed` has to be the OR of two adoptions, not the first + // one that answered true, or a Theme and a WindowOpacity in the same + // tick would leave one of them unworn. + let themeChanged = view.adoptThemeBG(pardes_theme_bg()) + let opacityChanged = view.adoptWindowOpacity(pardes_window_opacity()) + guard themeChanged || opacityChanged || !dressed else { return } dressed = true + // A ground short of solid needs the same non-opaque window a + // background-less theme does — but not the blur behind it. The backdrop + // answers "this theme brings no background"; WindowOpacity says how + // much of the one it brings to paint, and what shows through the rest + // is the desktop, which is what asking for a translucent window means. if let rgb = view.themeBG { backdrop.isHidden = true - window.isOpaque = true + window.isOpaque = view.backgroundAlpha >= 1 window.backgroundColor = NSColor( srgbRed: CGFloat((rgb >> 16) & 0xFF) / 255, green: CGFloat((rgb >> 8) & 0xFF) / 255, blue: CGFloat(rgb & 0xFF) / 255, - alpha: 1) + alpha: view.backgroundAlpha) // ...and follow the theme into light mode, so the titlebar, the // traffic lights and the resize cursor stop belonging to a // different application than the grid under them. Luminance off 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() |
