diff options
Diffstat (limited to 'src/gui')
| -rw-r--r-- | src/gui/gui.zig | 242 |
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 }, |
