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.zig242
1 files changed, 209 insertions, 33 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 0b2bdd18..082c8159 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -433,7 +433,16 @@ const CellInstance = extern struct {
cell_coord: u32 = 0,
};
-const OverlayVertex = extern struct { x: f32, y: f32, r: f32, g: f32, b: f32, a: f32 };
+const OverlayVertex = extern struct {
+ x: f32,
+ y: f32,
+ r: f32,
+ g: f32,
+ b: f32,
+ a: f32,
+ // CPU draw grouping metadata; not a shader attribute.
+ ink: u32 = 0,
+};
const OverlayColor = struct { r: f32, g: f32, b: f32, a: f32 };
const ImageInstance = extern struct {
@@ -462,6 +471,7 @@ const ImageInstance = extern struct {
const initial_image_capacity: u32 = pardes.MAX_PANES;
const old_layer_bit: u32 = 0x8000_0000;
const clear_bg_bit: u32 = 0x4000_0000;
+const opaque_bg_bit: u32 = 0x2000_0000;
const SavedImagePlace = struct {
key: pardes.ImageCacheKey,
@@ -1799,6 +1809,8 @@ const Gui = struct {
soft_present: bool = false,
transparent: bool = false,
+ surface_opacity: bool = false,
+ applied_window_opacity: u8 = 100,
soft_renderer: ?*c.SDL_Renderer = null,
soft_texture: ?*c.SDL_Texture = null,
soft_tex_w: u32 = 0,
@@ -2091,11 +2103,16 @@ 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;
- if (config.gui_transparent) win_flags |= c.SDL_WINDOW_TRANSPARENT;
+ const native_wayland = !test_mode and std.mem.eql(u8, std.mem.span(c.SDL_GetCurrentVideoDriver()), "wayland");
+ // Background-only opacity needs per-pixel alpha; whole-window compositor
+ // opacity would dim the text as well. SDL requires this flag at creation.
+ const surface_opacity = config.gui_transparent or native_wayland;
+ if (surface_opacity) 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;
};
+ if (native_wayland) log.info("Wayland opacity: background-only transparent surface", .{});
if (c.SDL_CreateCursor(&p9_arrow_set, &p9_arrow_mask, 16, 16, 1, 1)) |cur| {
_ = c.SDL_SetCursor(cur);
} else log.err("SDL_CreateCursor: {s}", .{c.SDL_GetError()});
@@ -2109,8 +2126,8 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
!(std.mem.eql(u8, raw, "0") or raw.len == 0)
else
false;
- if (config.gui_transparent or force_soft or !c.SDL_ClaimWindowForGPUDevice(device, window)) {
- if (config.gui_transparent)
+ if (surface_opacity or force_soft or !c.SDL_ClaimWindowForGPUDevice(device, window)) {
+ if (surface_opacity)
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", .{})
@@ -2254,6 +2271,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
.capture_dir = capture_dir orelse "",
.soft_present = soft_present,
.transparent = config.gui_transparent,
+ .surface_opacity = surface_opacity,
.soft_renderer = soft_renderer,
};
setGuiFontName(&g, "Adwaita Mono");
@@ -3688,7 +3706,14 @@ fn pollFrame(ctx: ?*anyopaque) void {
s.reconcilePtys();
const core = s.core;
if (s.fs) |f| if (f.tick(core).pending) s.queue.push(.fs_ready);
- const g = s.gui orelse return;
+ const g = s.gui orelse {
+ if (core.settings.window_opacity_pending) {
+ var unchanged_opacity: u8 = 100;
+ finishWindowOpacity(core, &unchanged_opacity, if (core.settings.window_opacity == 100) null else "requires a native SDL window");
+ }
+ return;
+ };
+ syncWindowOpacity(g, core);
syncTaglineFont(g, core);
if (core.takeFontRequest()) |path| blk: {
const bytes = filesystem.readFile(s.gpa, path) catch {
@@ -3717,6 +3742,56 @@ fn pollFrame(ctx: ?*anyopaque) void {
stepScroll(g, core, s.gpa);
}
+fn finishWindowOpacity(core: *pardes.Pardes, applied: *u8, failure: ?[]const u8) void {
+ core.settings.window_opacity_pending = false;
+ if (failure) |reason| {
+ // Do not claim a value the compositor refused; an explicit retry of
+ // the same command will arm a new request.
+ core.settings.window_opacity = applied.*;
+ var buf: [256]u8 = undefined;
+ core.setMessage(core.active, std.fmt.bufPrint(&buf, "WindowOpacity: {s}", .{reason[0..@min(reason.len, 240)]}) catch "WindowOpacity: compositor rejected the request");
+ } else {
+ applied.* = core.settings.window_opacity;
+ }
+}
+
+fn syncWindowOpacity(g: *Gui, core: *pardes.Pardes) void {
+ if (!core.settings.window_opacity_pending) return;
+ // Offscreen captures also support per-pixel alpha. Never approximate this
+ // with SDL_SetWindowOpacity: that would make foreground text translucent.
+ finishWindowOpacity(core, &g.applied_window_opacity, if (g.surface_opacity or g.capture or core.settings.window_opacity == 100)
+ null
+ else
+ "background transparency requires a native Wayland or alpha-capable SDL window");
+}
+
+test "background opacity acknowledges success and rolls back unsupported surfaces" {
+ const core = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer core.deinit();
+ var applied: u8 = 100;
+ try std.testing.expect(core.executeBuiltinLine(0, "WindowOpacity 70"));
+ try std.testing.expect(core.settings.window_opacity_pending);
+ finishWindowOpacity(core, &applied, null);
+ try std.testing.expectEqual(@as(u8, 70), applied);
+ try std.testing.expect(!core.settings.window_opacity_pending);
+
+ try std.testing.expect(core.executeBuiltinLine(0, "WindowOpacity 40"));
+ finishWindowOpacity(core, &applied, "background transparency requires an alpha-capable SDL window");
+ try std.testing.expectEqual(@as(u8, 70), applied);
+ try std.testing.expectEqual(applied, core.settings.window_opacity);
+ try std.testing.expect(!core.settings.window_opacity_pending);
+ const pane = core.panes[0].?;
+ try std.testing.expectEqualStrings("WindowOpacity: background transparency requires an alpha-capable SDL window", pane.msg[0..pane.msg_len]);
+
+ try std.testing.expect(core.executeBuiltinLine(0, "WindowOpacity 40"));
+ try std.testing.expect(core.settings.window_opacity_pending);
+ finishWindowOpacity(core, &applied, null);
+ try std.testing.expectEqual(@as(u8, 40), applied);
+ try std.testing.expect(core.executeBuiltinLine(0, "WindowOpacity 100"));
+ finishWindowOpacity(core, &applied, null);
+ try std.testing.expectEqual(@as(u8, 100), applied);
+}
+
fn present(ctx: ?*anyopaque, surface: *const pardes.Surface) void {
const s = shellOf(ctx);
const core = s.core;
@@ -4746,6 +4821,7 @@ fn renderFrame(
const cells: u32 = @as(u32, surface.cols) * surface.rows;
const page = ground(theme_bg, g.transparent);
+ const background_opacity = backgroundOpacity(g.applied_window_opacity);
const tagline_base = pardes.Cell{
.style = .{ .bg = .{ .rgb = tagline_rgb }, .font_role = .tagline },
.default = false,
@@ -4753,10 +4829,10 @@ fn renderFrame(
var color_target = std.mem.zeroes(c.SDL_GPUColorTargetInfo);
color_target.texture = scene;
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,
+ .r = @as(f32, @floatFromInt(page.rgb[0])) / 255.0 * background_opacity,
+ .g = @as(f32, @floatFromInt(page.rgb[1])) / 255.0 * background_opacity,
+ .b = @as(f32, @floatFromInt(page.rgb[2])) / 255.0 * background_opacity,
+ .a = background_opacity,
};
color_target.load_op = c.SDL_GPU_LOADOP_CLEAR;
color_target.store_op = c.SDL_GPU_STOREOP_STORE;
@@ -4919,6 +4995,7 @@ fn renderFrame(
}
const rp = c.SDL_BeginGPURenderPass(cmd, &color_target, 1, null);
+ pushLayerOpacity(cmd, rp, background_opacity);
const whole = c.SDL_Rect{ .x = 0, .y = 0, .w = @intCast(sw), .h = @intCast(sh) };
for (paint_plan.batches[0..paint_plan.len], 0..) |batch, batch_index| {
const has_shifted = batch_index == 0 and shifted != 0;
@@ -4955,7 +5032,7 @@ fn renderFrame(
if (batch.image_count != 0)
drawNativeImagesGpu(g, rp, surface, &paint_plan, batch_index);
}
- drawOverlayGpu(g, rp, overlay_count);
+ drawOverlayGpu(g, cmd, rp, overlay_count);
c.SDL_EndGPURenderPass(rp);
var rendered_scene: crt.Frame = .{};
@@ -5086,7 +5163,9 @@ fn resolveCell(g: *Gui, cell: *const pardes.Cell, role: pardes.FontRole, is_curs
if (st.reverse) reverse = !reverse;
if (st.invisible) {
fg = bg;
- blank = clear_bg;
+ // With a translucent background, painting bg-colored glyphs at
+ // full opacity would expose text that the terminal concealed.
+ blank = true;
}
if (st.dim) for (&fg) |*ch| {
ch.* = @intCast(@as(u16, ch.*) * 6 / 10);
@@ -5112,6 +5191,18 @@ fn cellFontRole(cell: *const pardes.Cell) pardes.FontRole {
return if (cell.default) .body else cell.style.font_role;
}
+test "concealed terminal text emits no ink on opaque or transparent ground" {
+ var g: Gui = undefined;
+ g.space_slot = .{ .u = 0, .v = 0 };
+ var cell: pardes.Cell = .{ .default = false, .style = .{ .invisible = true, .ul = .single } };
+ cell.text[0] = 'X';
+ for ([_]Ground{ .opaqueRgb(bg_default), .{ .rgb = bg_default, .clear = true } }) |page| {
+ const resolved = resolveCell(&g, &cell, .body, false, page);
+ try std.testing.expectEqualDeep(g.space_slot, resolved.slot);
+ try std.testing.expectEqual(page.rgb, resolved.bg);
+ }
+}
+
fn drawnFontRole(core: ?*const pardes.Pardes, cell: *const pardes.Cell, row: u16) pardes.FontRole {
const role = cellFontRole(cell);
if (role != .tagline or core != null or row < pardes.TOPBAR_H) return role;
@@ -5288,6 +5379,22 @@ fn emitInstance(
);
if (old_layer) instances[idx].effect |= old_layer_bit;
if (resolved.clear_bg) instances[idx].effect |= clear_bg_bit;
+ // A block cursor is foreground UI ink, even though its color is carried
+ // in the reversed cell background. Keep it visible at WindowOpacity 0.
+ if (is_cursor) instances[idx].effect |= opaque_bg_bit;
+}
+
+test "block cursor alone overrides transparent cell background" {
+ var g: Gui = undefined;
+ g.cell_w = 10;
+ g.cell_h = 20;
+ g.space_slot = .{ .u = 0, .v = 0 };
+ const cell: pardes.Cell = .{};
+ var instances: [2]CellInstance = undefined;
+ for ([_]bool{ false, true }, 0..) |cursor, i| {
+ emitInstance(&g, &instances, @intCast(i), 0, 0, fixedCellLayout(&g), 800, 480, null, .body, &cell, false, cursor, Ground.opaqueRgb(bg_default));
+ try std.testing.expectEqual(cursor, instances[i].effect & opaque_bg_bit != 0);
+ }
}
fn cellCodepoint(cell: *const pardes.Cell) u32 {
@@ -5319,6 +5426,8 @@ test "insert cursor overlays without replacing the character beneath it" {
try std.testing.expectApproxEqAbs(@as(f32, -0.32), vertices[0].y, 0.0001);
try std.testing.expectApproxEqAbs(@as(f32, -0.48), vertices[2].y, 0.0001);
try std.testing.expectApproxEqAbs(@as(f32, 32.0 / 255.0), vertices[0].r, 0.0001);
+ for (vertices) |vertex| try std.testing.expectEqual(@as(u32, 1), vertex.ink);
+ try std.testing.expect(!builder.ink);
}
fn palColor(idx: u8) [3]u8 {
@@ -5512,7 +5621,7 @@ fn ensureVbuf(g: *Gui, cells: u32) !void {
fn makePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat) !*c.SDL_GPUGraphicsPipeline {
const vs = try makeShader(device, vert_spv, c.SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
defer c.SDL_ReleaseGPUShader(device, vs);
- const fs = try makeShader(device, frag_spv, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
+ const fs = try makeShader(device, frag_spv, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
defer c.SDL_ReleaseGPUShader(device, fs);
var vbuf_desc = c.SDL_GPUVertexBufferDescription{
@@ -5549,10 +5658,35 @@ fn makePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat)
return c.SDL_CreateGPUGraphicsPipeline(device, &info) orelse error.GpuCreate;
}
+fn backgroundLayerBlend() c.SDL_GPUColorTargetBlendState {
+ var blend = std.mem.zeroes(c.SDL_GPUColorTargetBlendState);
+ blend.src_color_blendfactor = c.SDL_GPU_BLENDFACTOR_SRC_ALPHA;
+ blend.dst_color_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
+ blend.color_blend_op = c.SDL_GPU_BLENDOP_ADD;
+ blend.src_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_CONSTANT_COLOR;
+ blend.dst_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
+ blend.alpha_blend_op = c.SDL_GPU_BLENDOP_ADD;
+ blend.enable_blend = true;
+ return blend;
+}
+
+test "overlapping background layers keep one uniform opacity" {
+ const blend = backgroundLayerBlend();
+ try std.testing.expectEqual(@as(c.SDL_GPUBlendFactor, c.SDL_GPU_BLENDFACTOR_CONSTANT_COLOR), blend.src_alpha_blendfactor);
+ try std.testing.expectEqual(@as(c.SDL_GPUBlendFactor, c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA), blend.dst_alpha_blendfactor);
+ for ([_]f32{ 0, 0.25, 0.7, 1 }) |opacity| {
+ var alpha = opacity;
+ for ([_]f32{ 1, 0.5, 0.88, 0, 1 }) |coverage| {
+ alpha = opacity * coverage + alpha * (1 - coverage);
+ try std.testing.expectApproxEqAbs(opacity, alpha, 0.00001);
+ }
+ }
+}
+
fn makeOverlayPipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat) !*c.SDL_GPUGraphicsPipeline {
const vs = try makeShader(device, overlay_vert_spv, c.SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
defer c.SDL_ReleaseGPUShader(device, vs);
- const fs = try makeShader(device, overlay_frag_spv, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 0, 0);
+ const fs = try makeShader(device, overlay_frag_spv, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 0, 1);
defer c.SDL_ReleaseGPUShader(device, fs);
var vbuf_desc = c.SDL_GPUVertexBufferDescription{
@@ -5565,14 +5699,7 @@ fn makeOverlayPipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureF
.{ .location = 0, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, .offset = @offsetOf(OverlayVertex, "x") },
.{ .location = 1, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, .offset = @offsetOf(OverlayVertex, "r") },
};
- var blend = std.mem.zeroes(c.SDL_GPUColorTargetBlendState);
- blend.src_color_blendfactor = c.SDL_GPU_BLENDFACTOR_SRC_ALPHA;
- blend.dst_color_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
- blend.color_blend_op = c.SDL_GPU_BLENDOP_ADD;
- blend.src_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE;
- blend.dst_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
- blend.alpha_blend_op = c.SDL_GPU_BLENDOP_ADD;
- blend.enable_blend = true;
+ const blend = backgroundLayerBlend();
var col_desc = c.SDL_GPUColorTargetDescription{ .format = color_format, .blend_state = blend };
var info = std.mem.zeroes(c.SDL_GPUGraphicsPipelineCreateInfo);
@@ -5595,7 +5722,7 @@ fn makeOverlayPipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureF
fn makeImagePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat) !*c.SDL_GPUGraphicsPipeline {
const vs = try makeShader(device, image_vert_spv, c.SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
defer c.SDL_ReleaseGPUShader(device, vs);
- const fs = try makeShader(device, image_frag_spv, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
+ const fs = try makeShader(device, image_frag_spv, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
defer c.SDL_ReleaseGPUShader(device, fs);
var vbuf_desc = c.SDL_GPUVertexBufferDescription{
@@ -5611,14 +5738,7 @@ fn makeImagePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFor
.{ .location = 3, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, .offset = @offsetOf(ImageInstance, "present_x0") },
.{ .location = 4, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_UINT4, .offset = @offsetOf(ImageInstance, "effect") },
};
- var blend = std.mem.zeroes(c.SDL_GPUColorTargetBlendState);
- blend.src_color_blendfactor = c.SDL_GPU_BLENDFACTOR_SRC_ALPHA;
- blend.dst_color_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
- blend.color_blend_op = c.SDL_GPU_BLENDOP_ADD;
- blend.src_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE;
- blend.dst_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
- blend.alpha_blend_op = c.SDL_GPU_BLENDOP_ADD;
- blend.enable_blend = true;
+ const blend = backgroundLayerBlend();
var col_desc = c.SDL_GPUColorTargetDescription{ .format = color_format, .blend_state = blend };
var info = std.mem.zeroes(c.SDL_GPUGraphicsPipelineCreateInfo);
info.vertex_shader = vs;
@@ -5788,13 +5908,29 @@ 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;
+ // The cell shader already produced premultiplied backgrounds with opaque
+ // foreground ink. Copy that alpha unchanged; never modulate the full frame.
_ = c.SDL_SetRenderDrawBlendMode(renderer, c.SDL_BLENDMODE_NONE);
- _ = c.SDL_SetRenderDrawColor(renderer, 0, 0, 0, if (g.transparent) 0 else 255);
+ _ = c.SDL_SetRenderDrawColor(renderer, 0, 0, 0, if (g.transparent or g.surface_opacity) 0 else 255);
_ = c.SDL_RenderClear(renderer);
_ = c.SDL_RenderTexture(renderer, texture, null, null);
_ = c.SDL_RenderPresent(renderer);
}
+fn backgroundOpacity(percent: u8) f32 {
+ std.debug.assert(percent <= 100);
+ return @as(f32, @floatFromInt(percent)) / 100;
+}
+
+test "background opacity retains exact endpoints and percentage precision" {
+ try std.testing.expectEqual(@as(f32, 0), backgroundOpacity(0));
+ try std.testing.expectEqual(@as(f32, 0.7), backgroundOpacity(70));
+ try std.testing.expectEqual(@as(f32, 1), backgroundOpacity(100));
+ for (0..100) |percent| {
+ try std.testing.expect(backgroundOpacity(@intCast(percent)) < backgroundOpacity(@intCast(percent + 1)));
+ }
+}
+
fn captureFrame(g: *Gui, gpa: std.mem.Allocator, cmd: *c.SDL_GPUCommandBuffer, target: *c.SDL_GPUTexture, sw: u32, sh: u32) !void {
const size = c.SDL_CalculateGPUTextureFormatSize(g.swapchain_format, sw, sh, 1);
if (size == 0) {
@@ -5878,6 +6014,9 @@ fn addCursorBar(
rgb: [3]u8,
) void {
if (!bar) return;
+ const previous_ink = builder.ink;
+ builder.ink = true;
+ defer builder.ink = previous_ink;
const x0 = layout.x_off + @as(f32, @floatFromInt(x)) * layout.w;
const height = std.math.clamp(visual_height, 1.0, layout.h);
const y0 = layout.y_off + @as(f32, @floatFromInt(y)) * layout.h +
@@ -6263,13 +6402,30 @@ fn uploadOverlayGpu(g: *Gui, cmd: *c.SDL_GPUCommandBuffer, vertex_count: u32) bo
return true;
}
-fn drawOverlayGpu(g: *Gui, rp: ?*c.SDL_GPURenderPass, vertex_count: u32) void {
+fn pushLayerOpacity(cmd: *c.SDL_GPUCommandBuffer, pass: ?*c.SDL_GPURenderPass, opacity: f32) void {
+ const uniforms = [4]f32{ opacity, 0, 0, 0 };
+ c.SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms, @sizeOf(@TypeOf(uniforms)));
+ // The source alpha is geometric/image coverage, not window opacity. Its
+ // alpha contribution is multiplied by this constant separately, so a
+ // rail repainted over a gutter still has alpha p, not p + p*(1-p).
+ c.SDL_SetGPUBlendConstants(pass, .{ .r = opacity, .g = opacity, .b = opacity, .a = opacity });
+}
+
+fn drawOverlayGpu(g: *Gui, cmd: *c.SDL_GPUCommandBuffer, rp: ?*c.SDL_GPURenderPass, vertex_count: u32) void {
if (vertex_count == 0) return;
const pass = rp orelse return;
c.SDL_BindGPUGraphicsPipeline(pass, g.overlay_pipeline);
const binding = c.SDL_GPUBufferBinding{ .buffer = g.overlay_vbuf, .offset = 0 };
c.SDL_BindGPUVertexBuffers(pass, 0, &binding, 1);
- c.SDL_DrawGPUPrimitives(pass, vertex_count, 1, 0, 0);
+ var first: u32 = 0;
+ while (first < vertex_count) {
+ const ink = g.overlay_vertices[first].ink;
+ var end = first + 1;
+ while (end < vertex_count and g.overlay_vertices[end].ink == ink) : (end += 1) {}
+ pushLayerOpacity(cmd, pass, if (ink != 0) 1 else backgroundOpacity(g.applied_window_opacity));
+ c.SDL_DrawGPUPrimitives(pass, end - first, 1, first, 0);
+ first = end;
+ }
}
fn touchColor(id: u64) OverlayColor {
@@ -6291,6 +6447,7 @@ const OverlayBuilder = struct {
len: usize = 0,
win_w: f32,
win_h: f32,
+ ink: bool = false,
fn addVertex(b: *OverlayBuilder, px: f32, py: f32, color: OverlayColor) void {
if (b.len >= b.vertices.len) return;
@@ -6301,6 +6458,7 @@ const OverlayBuilder = struct {
.g = color.g,
.b = color.b,
.a = color.a,
+ .ink = @intFromBool(b.ink),
};
b.len += 1;
}
@@ -6354,6 +6512,9 @@ const OverlayBuilder = struct {
}
fn addMiniText(b: *OverlayBuilder, text: []const u8, x: f32, y: f32, scale: f32, color: OverlayColor) void {
+ const previous_ink = b.ink;
+ b.ink = true;
+ defer b.ink = previous_ink;
var cx = x;
for (text) |ch| {
const rows = miniGlyph(ch);
@@ -6371,6 +6532,21 @@ const OverlayBuilder = struct {
}
};
+test "overlay labels preserve ink without exempting surrounding chrome" {
+ var vertices: [192]OverlayVertex = undefined;
+ var builder: OverlayBuilder = .{ .vertices = &vertices, .win_w = 100, .win_h = 100 };
+ const color: OverlayColor = .{ .r = 1, .g = 1, .b = 1, .a = 1 };
+ builder.addRect(0, 0, 10, 10, color);
+ const text_start = builder.len;
+ builder.addMiniText("1", 0, 0, 1, color);
+ const text_end = builder.len;
+ try std.testing.expect(text_end > text_start);
+ builder.addRect(20, 20, 30, 30, color);
+ for (vertices[0..builder.len], 0..) |vertex, i|
+ try std.testing.expectEqual(@as(u32, @intFromBool(i >= text_start and i < text_end)), vertex.ink);
+ try std.testing.expect(!builder.ink);
+}
+
const circle_points = [_][2]f32{
.{ 1.0000000, 0.0000000 }, .{ 0.9238795, 0.3826834 }, .{ 0.7071068, 0.7071068 }, .{ 0.3826834, 0.9238795 },
.{ 0.0000000, 1.0000000 }, .{ -0.3826834, 0.9238795 }, .{ -0.7071068, 0.7071068 }, .{ -0.9238795, 0.3826834 },