73e2fd1c
cursor: extract cursorInstance helper with DECSCUSR shape dispatch
a73x 2026-04-19 06:57
Pure helper returning a renderer.Instance for block/underline/bar. Replaces the inline instance build in main.zig so shape geometry is unit-testable without a Vulkan device. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
diff --git a/build.zig b/build.zig index 09778c1..a6ed382 100644 --- a/build.zig +++ b/build.zig @@ -328,7 +328,9 @@ pub fn build(b: *std.Build) void { }); vk_sync_mod.addImport("vulkan", vulkan_module); renderer_mod.addImport("vk_sync", vk_sync_mod); renderer_mod.addImport("font", font_mod); renderer_test_mod.addImport("vk_sync", vk_sync_mod); renderer_test_mod.addImport("font", font_mod); exe_mod.addImport("vk_sync", vk_sync_mod); main_test_mod.addImport("vk_sync", vk_sync_mod); diff --git a/src/main.zig b/src/main.zig index ab6bbfd..49ca018 100644 --- a/src/main.zig +++ b/src/main.zig @@ -554,26 +554,23 @@ fn runTerminal(alloc: std.mem.Allocator) !void { var cursor_instances_buf: [1]renderer.Instance = undefined; var cursor_instances: []const renderer.Instance = &.{}; if (term.render_state.cursor.viewport) |cursor| { const cursor_uv = atlas.cursorUV(); cursor_instances_buf[0] = .{ .cell_pos = .{ @floatFromInt(cursor.x), @floatFromInt(cursor.y), }, .glyph_size = .{ @floatFromInt(cell_w), @floatFromInt(cell_h), }, .glyph_bearing = .{ 0, 0 }, .uv_rect = .{ cursor_uv.u0, cursor_uv.v0, cursor_uv.u1, cursor_uv.v1, }, .fg = .{ 1.0, 1.0, 1.0, 0.5 }, .bg = .{ 0, 0, 0, 0 }, const shape: renderer.CursorShape = switch (term.render_state.cursor.visual_style) { .block, .block_hollow => .block, .underline => .underline, .bar => .bar, }; var inst = renderer.cursorInstance( shape, cell_w, cell_h, @as(u32, @intCast(geom.buffer_scale)), atlas.cursorUV(), ); inst.cell_pos = .{ @floatFromInt(cursor.x), @floatFromInt(cursor.y), }; cursor_instances_buf[0] = inst; cursor_instances = cursor_instances_buf[0..1]; } const previous_total_instance_count = render_cache.total_instance_count; diff --git a/src/renderer.zig b/src/renderer.zig index 912fb33..1958b25 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -2,6 +2,8 @@ const std = @import("std"); const vk = @import("vulkan"); const vk_sync = @import("vk_sync"); const GlyphUV = @import("font").GlyphUV; const dl = @cImport({ @cInclude("dlfcn.h"); }); @@ -2192,3 +2194,79 @@ test "uploadInstanceRange contract reports invalid ranges explicitly" { const action = planUploadInstanceRangeAction(std.math.maxInt(u32), std.math.maxInt(u32), 1); try std.testing.expectEqual(InstanceRangeUploadAction.invalid_range, action); } // --- cursor instance (pure, unit-tested) --- pub const CursorShape = enum { block, underline, bar }; pub fn cursorInstance( shape: CursorShape, cell_w: u32, cell_h: u32, buffer_scale: u32, uv: GlyphUV, ) Instance { const cell_w_f: f32 = @floatFromInt(cell_w); const cell_h_f: f32 = @floatFromInt(cell_h); const line_w_f: f32 = @floatFromInt(2 * buffer_scale); const size_xy: [2]f32 = switch (shape) { .block => .{ cell_w_f, cell_h_f }, .underline => .{ cell_w_f, line_w_f }, .bar => .{ line_w_f, cell_h_f }, }; const bearing_xy: [2]f32 = switch (shape) { .block, .bar => .{ 0, 0 }, .underline => .{ 0, cell_h_f - line_w_f }, }; return .{ .cell_pos = .{ 0, 0 }, // caller overwrites with actual grid position .glyph_size = size_xy, .glyph_bearing = bearing_xy, .uv_rect = .{ uv.u0, uv.v0, uv.u1, uv.v1 }, .fg = .{ 1.0, 1.0, 1.0, 0.5 }, .bg = .{ 0, 0, 0, 0 }, }; } test "cursorInstance: block fills the whole cell at scale 1" { const uv: GlyphUV = .{ .u0 = 0, .v0 = 0, .u1 = 0.01, .v1 = 0.01, .width = 1, .height = 1, .bearing_x = 0, .bearing_y = 0, .advance_x = 1 }; const inst = cursorInstance(.block, 10, 20, 1, uv); try std.testing.expectEqualSlices(f32, &[_]f32{ 10.0, 20.0 }, &inst.glyph_size); try std.testing.expectEqualSlices(f32, &[_]f32{ 0.0, 0.0 }, &inst.glyph_bearing); } test "cursorInstance: underline is a 2px bar at the cell bottom (scale 1)" { const uv: GlyphUV = .{ .u0 = 0, .v0 = 0, .u1 = 0.01, .v1 = 0.01, .width = 1, .height = 1, .bearing_x = 0, .bearing_y = 0, .advance_x = 1 }; const inst = cursorInstance(.underline, 10, 20, 1, uv); try std.testing.expectEqualSlices(f32, &[_]f32{ 10.0, 2.0 }, &inst.glyph_size); try std.testing.expectEqualSlices(f32, &[_]f32{ 0.0, 18.0 }, &inst.glyph_bearing); } test "cursorInstance: bar is a 2px column at cell left (scale 1)" { const uv: GlyphUV = .{ .u0 = 0, .v0 = 0, .u1 = 0.01, .v1 = 0.01, .width = 1, .height = 1, .bearing_x = 0, .bearing_y = 0, .advance_x = 1 }; const inst = cursorInstance(.bar, 10, 20, 1, uv); try std.testing.expectEqualSlices(f32, &[_]f32{ 2.0, 20.0 }, &inst.glyph_size); try std.testing.expectEqualSlices(f32, &[_]f32{ 0.0, 0.0 }, &inst.glyph_bearing); } test "cursorInstance: underline line width scales with buffer_scale" { const uv: GlyphUV = .{ .u0 = 0, .v0 = 0, .u1 = 0.01, .v1 = 0.01, .width = 1, .height = 1, .bearing_x = 0, .bearing_y = 0, .advance_x = 1 }; const inst = cursorInstance(.underline, 20, 40, 2, uv); try std.testing.expectEqualSlices(f32, &[_]f32{ 20.0, 4.0 }, &inst.glyph_size); try std.testing.expectEqualSlices(f32, &[_]f32{ 0.0, 36.0 }, &inst.glyph_bearing); } test "cursorInstance: bar line width scales with buffer_scale" { const uv: GlyphUV = .{ .u0 = 0, .v0 = 0, .u1 = 0.01, .v1 = 0.01, .width = 1, .height = 1, .bearing_x = 0, .bearing_y = 0, .advance_x = 1 }; const inst = cursorInstance(.bar, 20, 40, 2, uv); try std.testing.expectEqualSlices(f32, &[_]f32{ 4.0, 40.0 }, &inst.glyph_size); try std.testing.expectEqualSlices(f32, &[_]f32{ 0.0, 0.0 }, &inst.glyph_bearing); } test "cursorInstance: uv_rect comes from the provided UV" { const uv: GlyphUV = .{ .u0 = 0.1, .v0 = 0.2, .u1 = 0.3, .v1 = 0.4, .width = 1, .height = 1, .bearing_x = 0, .bearing_y = 0, .advance_x = 1 }; const inst = cursorInstance(.block, 10, 20, 1, uv); try std.testing.expectEqualSlices(f32, &[_]f32{ 0.1, 0.2, 0.3, 0.4 }, &inst.uv_rect); }