summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/builtins.zig2
-rw-r--r--src/macos.zig19
-rw-r--r--src/macos/Sources/AppDelegate.swift18
-rw-r--r--src/macos/Sources/PardesView.swift82
-rw-r--r--src/macos/pardes.h9
5 files changed, 106 insertions, 24 deletions
diff --git a/src/builtins.zig b/src/builtins.zig
index a21a20c4..1cd10a99 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -17,7 +17,7 @@ pub const capabilities: config.Runtime.Capabilities = .{
// The tty's font belongs to its emulator, and the P4 firmware's belongs to
// whatever terminal is on the other end of the serial line.
.tagline_font_size = pardes.platform != .tty and pardes.platform != .esp32p4,
- .window_opacity = pardes.platform == .gui,
+ .window_opacity = pardes.platform == .gui or pardes.platform == .macos,
};
pub const Ctx = struct {
diff --git a/src/macos.zig b/src/macos.zig
index 3003fd46..67a5ec5b 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -875,6 +875,24 @@ export fn pardes_theme_bg() u32 {
return @as(u32, bg[0]) << 16 | @as(u32, bg[1]) << 8 | bg[2];
}
+/// The percentage the shell must paint window backgrounds at, read once per
+/// pump beside pardes_theme_bg().
+///
+/// Reading it also ACKNOWLEDGES whatever `WindowOpacity` asked for. The SDL
+/// shell has to answer that request because an SDL surface can arrive without
+/// an alpha channel and the core must not claim a value the compositor
+/// refused; an AppKit window always composites per pixel, so there is nothing
+/// here to refuse and no rollback for the core to perform.
+///
+/// The theme's own decision stays separate: a theme with no background of its
+/// own (pardes_theme_bg == PARDES_COLOR_DEFAULT) drops the ground entirely,
+/// whatever percentage is set here.
+export fn pardes_window_opacity() u8 {
+ const st = &(state orelse return 100);
+ st.core.settings.window_opacity_pending = false;
+ return st.core.settings.window_opacity;
+}
+
fn taglineFontPercent(core: ?*const pardes.Pardes) u8 {
return if (core) |p| p.settings.font.tagline_percent else pardes.config.gui_tagline_font_percent;
}
@@ -2437,6 +2455,7 @@ test "pardes.h declares every export the way it is defined" {
try expectSameAbi(@TypeOf(c.pardes_active_path), @TypeOf(pardes_active_path));
try expectSameAbi(@TypeOf(c.pardes_active_dirty), @TypeOf(pardes_active_dirty));
try expectSameAbi(@TypeOf(c.pardes_theme_bg), @TypeOf(pardes_theme_bg));
+ try expectSameAbi(@TypeOf(c.pardes_window_opacity), @TypeOf(pardes_window_opacity));
}
test "pardes.h matches the Zig boundary" {
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()
diff --git a/src/macos/pardes.h b/src/macos/pardes.h
index 48f38e58..5f1ccf77 100644
--- a/src/macos/pardes.h
+++ b/src/macos/pardes.h
@@ -254,6 +254,15 @@ bool pardes_animating(void);
// wear, so the host should go transparent and show its own backdrop.
uint32_t pardes_theme_bg(void);
+// The percentage the host must paint window BACKGROUNDS at: the ground behind
+// the grid, cell and band backgrounds, and the chrome rules over them. Glyph
+// ink and the block cursor are never scaled by it — see shaders/ui.frag.glsl,
+// which states the same rule for the SDL shell.
+//
+// Read it once per pump beside pardes_theme_bg(): reading also acknowledges
+// whatever `WindowOpacity` asked for. 100 is opaque and is the default.
+uint8_t pardes_window_opacity(void);
+
// Pane taglines use this percentage of the current body font size. It is the
// embedded default before init and the live core setting afterwards; the host
// polls it and rebuilds only tagline glyph/metric state.