summaryrefslogtreecommitdiff
path: root/src/gui/gui.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui/gui.zig')
-rw-r--r--src/gui/gui.zig152
1 files changed, 121 insertions, 31 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index bfd02ff9..91380e46 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -89,28 +89,45 @@ const crt_frag_spv = @embedFile("crt.frag.spv");
/// inside every cell the core left at its default background. The FALLBACK
/// for it, that is: the ground itself is the active theme's own background,
/// read off the core once a frame (see `ground`), and this is what a theme
-/// with none of its own gets.
+/// with none of its own gets in an OPAQUE window.
///
/// A theme declares no background (the curated `dark`, every vendored
/// `*_transparent`) to mean "wear whatever the terminal is wearing", and a
-/// window has nothing to wear. The AppKit shell answers that by going
-/// see-through over an NSVisualEffectView — pardes_theme_bg, docs/macos.md —
-/// and this shell cannot follow it there: SDL's GPU API refuses to claim a
-/// SDL_WINDOW_TRANSPARENT window at all ("The GPU API doesn't support
-/// transparent windows", SDL_gpu.c), because D3D12 has no transparent
-/// swapchain and the API says no everywhere rather than only where it must.
-/// So a themeless window keeps the terminal-native dark it always wore.
+/// window has nothing to wear unless it is see-through. The AppKit shell
+/// answers that by going transparent over an NSVisualEffectView
+/// (pardes_theme_bg, docs/macos.md); this shell answers it with
+/// `config.gui_transparent`, which asks SDL for a transparent window and lets
+/// the compositor be the backdrop. That knob is off by default and costs a
+/// readback per painted frame when it is on — the whole reason is written
+/// where it is declared. Without it a themeless window keeps the
+/// terminal-native dark it always wore.
const bg_default = [3]u8{ 18, 18, 18 };
const fg_default = [3]u8{ 204, 204, 204 };
+/// The ground under this frame, and whether it is a colour at all.
+///
+/// `clear` is the themeless case in a transparent window: nothing is painted
+/// there, the desktop is. `rgb` still carries the fallback colour because a
+/// reverse-video cell puts the ground in its FOREGROUND, where it is a real
+/// colour that paints — the same rule PardesView.styleFor states on macOS.
+const Ground = struct {
+ rgb: [3]u8,
+ clear: bool,
+
+ fn opaqueRgb(rgb: [3]u8) Ground {
+ return .{ .rgb = rgb, .clear = false };
+ }
+};
+
/// The ground under this frame. The theme's OWN background and not the
/// animated chrome colour: taglines fade between themes over a handful of
/// frames, document backgrounds switch the instant the theme does, and this is
/// one of those. Asked per frame, so a `Theme` command takes hold without a
/// relaunch — and asked at all because a hand-agreed constant was a black line
/// along the two edges of every light-themed window.
-fn ground(theme_bg: ?[3]u8) [3]u8 {
- return theme_bg orelse bg_default;
+fn ground(theme_bg: ?[3]u8, transparent: bool) Ground {
+ if (theme_bg) |rgb| return .opaqueRgb(rgb);
+ return .{ .rgb = bg_default, .clear = transparent };
}
// the plan9 arrow cursor, bytes verbatim from 9front /sys/src/9/port/
@@ -544,7 +561,14 @@ const ImageInstance = extern struct {
};
const initial_image_capacity: u32 = pardes.MAX_PANES;
+/// `CellInstance.effect` is an effect id in its low bits and flags in its top
+/// two. Both shaders that read the field mask the id off with `0x3fffffff`;
+/// widening this pair means widening that mask with it.
const old_layer_bit: u32 = 0x8000_0000;
+/// This cell's background IS the see-through ground: emit the glyph and let
+/// the compositor keep the rest. Only ever set when `Ground.clear` holds, so
+/// an opaque window never reaches the branch.
+const clear_bg_bit: u32 = 0x4000_0000;
/// Image placement retained across pane destruction. Deliberately does not
/// contain ImagePlace.rgba: the producer owns those bytes, while the renderer
@@ -1370,7 +1394,15 @@ const Gui = struct {
// not support presenting"). The GPU still renders, so render offscreen
// exactly like capture does and blit the readback through SDL_Renderer,
// which goes out over wl_shm.
+ //
+ // `config.gui_transparent` takes the same path deliberately rather than by
+ // failure: SDL's GPU API refuses to claim a transparent window at all, and
+ // SDL_Renderer is the presenter that does honour one.
soft_present: bool = false,
+ /// The window was created with SDL_WINDOW_TRANSPARENT, so a themeless
+ /// ground is nothing at all instead of `bg_default`. Implies
+ /// `soft_present`; read per frame by `ground`.
+ transparent: bool = false,
soft_renderer: ?*c.SDL_Renderer = null,
soft_texture: ?*c.SDL_Texture = null,
soft_tex_w: u32 = 0,
@@ -1737,6 +1769,12 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
}
var win_flags: c.SDL_WindowFlags = c.SDL_WINDOW_RESIZABLE;
if (!test_mode) win_flags |= c.SDL_WINDOW_HIGH_PIXEL_DENSITY;
+ // A see-through buffer, so a theme with no background of its own shows the
+ // compositor's backdrop instead of `bg_default`. Asked for at CREATION
+ // because that is the only time it can be: X11 picks the 32-bit visual
+ // here, and the Wayland backend decides here whether to keep an opaque
+ // region on the surface.
+ if (config.gui_transparent) win_flags |= c.SDL_WINDOW_TRANSPARENT;
const window = c.SDL_CreateWindow("pardes", 1120, 720, win_flags) orelse {
log.err("SDL_CreateWindow: {s}", .{c.SDL_GetError()});
return error.SdlInit;
@@ -1758,14 +1796,24 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
// the device and present the readback through SDL_Renderer instead.
// PARDES_SOFT_PRESENT=1 takes that path on a compositor that could present,
// which is how the path is exercised without a remote display.
+ //
+ // A transparent window does not even attempt the claim. It is not a
+ // compositor's shortcoming and there is nothing to retry: SDL_gpu.c fails
+ // SDL_ClaimWindowForGPUDevice for SDL_WINDOW_TRANSPARENT unconditionally,
+ // because D3D12 has no transparent swapchain and the API says no
+ // everywhere rather than only where it must. SDL_Renderer's own vulkan and
+ // opengl backends do honour one, and that is the presenter this path
+ // already had.
var soft_present = false;
var soft_renderer: ?*c.SDL_Renderer = null;
const force_soft = if (env.get("PARDES_SOFT_PRESENT")) |raw|
!(std.mem.eql(u8, raw, "0") or raw.len == 0)
else
false;
- if (force_soft or !c.SDL_ClaimWindowForGPUDevice(device, window)) {
- if (force_soft)
+ if (config.gui_transparent or force_soft or !c.SDL_ClaimWindowForGPUDevice(device, window)) {
+ if (config.gui_transparent)
+ log.info("transparent window: the GPU API will not claim one, presenting through SDL_Renderer", .{})
+ else if (force_soft)
log.info("PARDES_SOFT_PRESENT set: presenting in software", .{})
else
log.warn("ClaimWindowForGPUDevice: {s}; presenting in software", .{c.SDL_GetError()});
@@ -1912,6 +1960,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
.capture = test_mode,
.capture_dir = capture_dir orelse "",
.soft_present = soft_present,
+ .transparent = config.gui_transparent,
.soft_renderer = soft_renderer,
};
setGuiFontName(&g, "Adwaita Mono");
@@ -4325,7 +4374,7 @@ fn stepScroll(g: *Gui, core: *pardes.Pardes, gpa: std.mem.Allocator) void {
/// to the body, which is the whole of the clipping — the shell draws the grid
/// in one flat pass, so without it the overhanging rows would land on the
/// pane's own tag and on whatever is below it.
-fn emitScrollRows(g: *Gui, instances: [*]CellInstance, base: u32, surface: *pardes.Surface, layout: CellLayout, win_w: f32, win_h: f32, page: [3]u8) u32 {
+fn emitScrollRows(g: *Gui, instances: [*]CellInstance, base: u32, surface: *pardes.Surface, layout: CellLayout, win_w: f32, win_h: f32, page: Ground) u32 {
const scroll_pane = g.scroll_pane orelse return 0;
if (g.scroll_lag == 0) return 0;
for (surface.panelTracks()) |track|
@@ -4959,17 +5008,22 @@ fn renderFrame(
const cells: u32 = @as(u32, surface.cols) * surface.rows;
- const page = ground(theme_bg);
+ const page = ground(theme_bg, g.transparent);
const tagline_base = pardes.Cell{
.style = .{ .bg = .{ .rgb = tagline_rgb }, .font_role = .tagline },
.default = false,
};
var color_target = std.mem.zeroes(c.SDL_GPUColorTargetInfo);
color_target.texture = scene;
- color_target.clear_color = .{
- .r = @as(f32, @floatFromInt(page[0])) / 255.0,
- .g = @as(f32, @floatFromInt(page[1])) / 255.0,
- .b = @as(f32, @floatFromInt(page[2])) / 255.0,
+ // Premultiplied, because that is what both a wl_surface and an X11 ARGB
+ // visual are composited as, and the glyph pass writes premultiplied for
+ // the same reason. A see-through ground is therefore all four channels
+ // zero and not `page.rgb` at alpha zero — the leftover colour would tint
+ // every glyph edge that blends against it.
+ color_target.clear_color = if (page.clear) .{ .r = 0, .g = 0, .b = 0, .a = 0 } else .{
+ .r = @as(f32, @floatFromInt(page.rgb[0])) / 255.0,
+ .g = @as(f32, @floatFromInt(page.rgb[1])) / 255.0,
+ .b = @as(f32, @floatFromInt(page.rgb[2])) / 255.0,
.a = 1.0,
};
color_target.load_op = c.SDL_GPU_LOADOP_CLEAR;
@@ -5259,6 +5313,9 @@ const ResolvedCell = struct {
fg: [3]u8,
bg: [3]u8,
role: pardes.FontRole,
+ /// `bg` is the see-through ground rather than a colour: paint the glyph
+ /// and leave the rest of the cell to the compositor.
+ clear_bg: bool = false,
};
fn topbarPaneBorderPixels(cell_h: u32, tagline_h: u32) u32 {
@@ -5310,10 +5367,18 @@ test "tagline bands face the topbar rule and Tagbottom faces the window edge" {
try std.testing.expectEqual(@as(u32, 4), taglineBandOffset(9, canvas_h, cell_h, tagline_h));
}
-fn resolveCell(g: *Gui, cell: *const pardes.Cell, role: pardes.FontRole, is_cursor: bool, page: [3]u8) ResolvedCell {
+fn resolveCell(g: *Gui, cell: *const pardes.Cell, role: pardes.FontRole, is_cursor: bool, page: Ground) ResolvedCell {
var fg = fg_default;
- var bg = page;
+ var bg = page.rgb;
+ // Only an UNREVERSED default background is the ground; every branch below
+ // that names a real colour clears this, and the reverse at the end clears
+ // it because a reverse puts the TEXT colour there.
+ var clear_bg = page.clear;
var reverse = is_cursor;
+ // An invisible cell over a clear ground has nothing left to draw: `fg = bg`
+ // hides a glyph by painting it in the background, and a background that is
+ // not painted at all would let the ink through as a coloured silhouette.
+ var blank = false;
if (!cell.default) {
const st = cell.style;
fg = switch (st.fg) {
@@ -5321,25 +5386,38 @@ fn resolveCell(g: *Gui, cell: *const pardes.Cell, role: pardes.FontRole, is_curs
.index => |i| palColor(i),
.rgb => |rgb| rgb,
};
- bg = switch (st.bg) {
- .default => page,
- .index => |i| palColor(i),
- .rgb => |rgb| rgb,
- };
+ switch (st.bg) {
+ .default => {},
+ .index => |i| {
+ bg = palColor(i);
+ clear_bg = false;
+ },
+ .rgb => |rgb| {
+ bg = rgb;
+ clear_bg = false;
+ },
+ }
if (st.reverse) reverse = !reverse;
- if (st.invisible) fg = bg;
+ if (st.invisible) {
+ fg = bg;
+ blank = clear_bg;
+ }
if (st.dim) for (&fg) |*ch| {
ch.* = @intCast(@as(u16, ch.*) * 6 / 10);
};
}
- if (reverse) std.mem.swap([3]u8, &fg, &bg);
+ if (reverse) {
+ std.mem.swap([3]u8, &fg, &bg);
+ clear_bg = false;
+ }
- const cp = cellCodepoint(cell);
+ const cp = if (blank) ' ' else cellCodepoint(cell);
return .{
.slot = if (cp == ' ') g.space_slot else ensureGlyph(g, cp, role),
.fg = fg,
.bg = bg,
.role = role,
+ .clear_bg = clear_bg,
};
}
@@ -5401,7 +5479,7 @@ fn emitSurfaceCell(
tagline_base: *const pardes.Cell,
old_layer: bool,
is_cursor: bool,
- page: [3]u8,
+ page: Ground,
) void {
const role = drawnFontRole(core, cell, row);
if (role == .tagline) {
@@ -5437,8 +5515,9 @@ fn emitInstance(
/// change retains the correct band height on both sides of the diff.
old_layer: bool,
is_cursor: bool,
- /// the ground this frame: what a default background resolves to
- page: [3]u8,
+ /// the ground this frame: what a default background resolves to, and
+ /// whether that is a colour at all
+ page: Ground,
) void {
const resolved = resolveCell(g, cell, role, is_cursor, page);
@@ -5499,6 +5578,7 @@ fn emitInstance(
if (track) |active| panelCellCoord(active, col, row) else 0,
);
if (old_layer) instances[idx].effect |= old_layer_bit;
+ if (resolved.clear_bg) instances[idx].effect |= clear_bg_bit;
}
// EFFECT_CODE_CELL_INSTANCE_END
@@ -5946,6 +6026,11 @@ fn ensureSoftTexture(g: *Gui, width: u32, height: u32) !*c.SDL_Texture {
@intCast(height),
) orelse return error.GpuCreate;
_ = c.SDL_SetTextureScaleMode(next, c.SDL_SCALEMODE_NEAREST);
+ // The readback IS the frame, alpha included and already premultiplied.
+ // SDL's default for an alpha format is BLENDMODE_BLEND, which would blend
+ // it a second time against the cleared window and darken every glyph edge
+ // over a see-through ground.
+ _ = c.SDL_SetTextureBlendMode(next, c.SDL_BLENDMODE_NONE);
g.soft_texture = next;
g.soft_tex_w = width;
g.soft_tex_h = height;
@@ -5989,6 +6074,11 @@ fn softPresentFrame(g: *Gui, cmd: *c.SDL_GPUCommandBuffer, target: *c.SDL_GPUTex
const texture = try ensureSoftTexture(g, sw, sh);
const renderer = g.soft_renderer orelse return error.GpuCreate;
if (!c.SDL_UpdateTexture(texture, null, mapped, @intCast(sw * bpp))) return error.GpuMap;
+ // Clear to nothing rather than to opaque black: on a transparent window
+ // this is the pixel the compositor keeps wherever the frame does not
+ // cover, and BLENDMODE_NONE below writes the frame over it verbatim.
+ _ = c.SDL_SetRenderDrawBlendMode(renderer, c.SDL_BLENDMODE_NONE);
+ _ = c.SDL_SetRenderDrawColor(renderer, 0, 0, 0, if (g.transparent) 0 else 255);
_ = c.SDL_RenderClear(renderer);
_ = c.SDL_RenderTexture(renderer, texture, null, null);
_ = c.SDL_RenderPresent(renderer);