diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-04 00:57:06 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | e36e946f9a56c4ab1d10cef3aace20fe7d274dfa (patch) | |
| tree | baccd0be04b9b901bb74b4f3647bdb531c8dd450 /ui_sdl.zig | |
| parent | f3f0e27372d8cf111269dde674d4581cf4cb0c41 (diff) | |
| download | pardes-e36e946f9a56c4ab1d10cef3aace20fe7d274dfa.tar.gz pardes-e36e946f9a56c4ab1d10cef3aace20fe7d274dfa.zip | |
touch debug overlay: multi-touch circles with trails, finger event handling, 4x larger, fade trails, SDL GPU + WebGL overlay shaders, touch-action:none in HTML, --debug-overlay flag, pinch event dispatch for all SDL backends
Diffstat (limited to 'ui_sdl.zig')
| -rw-r--r-- | ui_sdl.zig | 467 |
1 files changed, 465 insertions, 2 deletions
@@ -75,8 +75,12 @@ pub fn adoptAltScreen(vx: *Vaxis) void { const font_ttf = @embedFile("assets/AdwaitaMono-Regular.ttf"); const vert_spv_bytes = if (is_emscripten) "" else @embedFile("ui.vert.spv"); const frag_spv_bytes = if (is_emscripten) "" else @embedFile("ui.frag.spv"); +const overlay_vert_spv_bytes = if (is_emscripten) "" else @embedFile("overlay.vert.spv"); +const overlay_frag_spv_bytes = if (is_emscripten) "" else @embedFile("overlay.frag.spv"); const vert_glsl_es = if (is_emscripten) @embedFile("ui.vert.es.glsl") else ""; const frag_glsl_es = if (is_emscripten) @embedFile("ui.frag.es.glsl") else ""; +const overlay_vert_glsl_es = if (is_emscripten) @embedFile("overlay.vert.es.glsl") else ""; +const overlay_frag_glsl_es = if (is_emscripten) @embedFile("overlay.frag.es.glsl") else ""; const log = std.log.scoped(.ui_sdl); const web_render_scale: f32 = ui_sdl_config.web_render_scale; @@ -107,12 +111,15 @@ swapchain_format: c.SDL_GPUTextureFormat, present_mode: c.SDL_GPUPresentMode, pipeline: *c.SDL_GPUGraphicsPipeline, +overlay_pipeline: *c.SDL_GPUGraphicsPipeline, atlas_tex: *c.SDL_GPUTexture, atlas_sampler: *c.SDL_GPUSampler, atlas_xfer: *c.SDL_GPUTransferBuffer, vbuf: *c.SDL_GPUBuffer, vxfer: *c.SDL_GPUTransferBuffer, +overlay_vbuf: *c.SDL_GPUBuffer, +overlay_vxfer: *c.SDL_GPUTransferBuffer, vbuf_cells: u32 = 0, // capacity (cols*rows) the vbuf/vxfer were sized for cell_keys: []CellKey = &.{}, dirty_ranges: []DirtyRange = &.{}, @@ -124,10 +131,14 @@ last_render_h: u32 = 0, // GLuint/GLint values, kept as plain C integers so native builds need no GL API. gl_context: ?c.SDL_GLContext = null, gl_program: c_uint = 0, +gl_overlay_program: c_uint = 0, gl_atlas_tex: c_uint = 0, gl_vbo: c_uint = 0, +gl_overlay_vbo: c_uint = 0, gl_u_atlas: c_int = -1, gl_instances: []CellInstance = &.{}, +overlay_vertices: []OverlayVertex = &.{}, +touches: [max_touch_points]TouchPoint = [_]TouchPoint{.{}} ** max_touch_points, // font + cell metrics font: ?*c.UIFont, @@ -178,6 +189,10 @@ pub const atlas_h: u32 = if (is_emscripten) 2048 else 1024; pub const Slot = struct { u: u32, v: u32 }; pub const CellKey = packed struct { cp: u32, fg: u32, bg: u32 }; pub const DirtyRange = struct { start: u32, count: u32 }; +pub const max_touch_points: usize = 16; +pub const max_touch_trail_points: usize = 8; +pub const overlay_circle_vertices: usize = 16 * 3; +pub const max_overlay_vertices: usize = max_touch_points * (1100 + max_touch_trail_points * overlay_circle_vertices); // One instance per cell. The vertex shader expands each instance into a 2-triangle // quad using gl_VertexIndex, so dirty uploads move 56 B/cell instead of 240 B/cell. @@ -198,6 +213,45 @@ const CellInstance = extern struct { bb: f32, }; +const OverlayVertex = extern struct { + x: f32, + y: f32, + r: f32, + g: f32, + b: f32, + a: f32, +}; + +const TouchSample = struct { + x: f32 = 0, + y: f32 = 0, + pressure: f32 = 1, +}; + +const TouchPoint = struct { + active: bool = false, + id: u64 = 0, + x: f32 = 0, + y: f32 = 0, + pressure: f32 = 1, + trail: [max_touch_trail_points]TouchSample = [_]TouchSample{.{}} ** max_touch_trail_points, + trail_len: usize = 0, + trail_next: usize = 0, + + fn pushTrail(self: *TouchPoint, x: f32, y: f32, pressure: f32) void { + self.trail[self.trail_next] = .{ .x = x, .y = y, .pressure = pressure }; + self.trail_next = (self.trail_next + 1) % max_touch_trail_points; + if (self.trail_len < max_touch_trail_points) self.trail_len += 1; + } +}; + +const OverlayColor = struct { + r: f32, + g: f32, + b: f32, + a: f32, +}; + const CellLayout = struct { w: f32, h: f32, @@ -497,9 +551,17 @@ pub fn init( // ---- pipeline ---- const pipeline = try makePipeline(device, swapchain_format); + const overlay_pipeline = try makeOverlayPipeline(device, swapchain_format); + + const overlay_buffer_size: u32 = @intCast(max_overlay_vertices * @sizeOf(OverlayVertex)); + var ovb_info = c.SDL_GPUBufferCreateInfo{ .usage = c.SDL_GPU_BUFFERUSAGE_VERTEX, .size = overlay_buffer_size, .props = 0 }; + const overlay_vbuf = c.SDL_CreateGPUBuffer(device, &ovb_info) orelse return error.GpuCreate; + var ovx_info = c.SDL_GPUTransferBufferCreateInfo{ .usage = c.SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = overlay_buffer_size, .props = 0 }; + const overlay_vxfer = c.SDL_CreateGPUTransferBuffer(device, &ovx_info) orelse return error.GpuCreate; const atlas_stage = try alloc.alloc(u8, atlas_w * atlas_h); @memset(atlas_stage, 0); + const overlay_vertices = try alloc.alloc(OverlayVertex, max_overlay_vertices); var self: SdlBackend = .{ .io = io, @@ -509,11 +571,15 @@ pub fn init( .swapchain_format = swapchain_format, .present_mode = present_mode, .pipeline = pipeline, + .overlay_pipeline = overlay_pipeline, .atlas_tex = atlas_tex, .atlas_sampler = atlas_sampler, .atlas_xfer = atlas_xfer, .vbuf = undefined, .vxfer = undefined, + .overlay_vbuf = overlay_vbuf, + .overlay_vxfer = overlay_vxfer, + .overlay_vertices = overlay_vertices, .font = font, .scale = scale, .cell_w = @intCast(@max(cw, 1)), @@ -581,6 +647,7 @@ fn initGl( const ascent: c_int = @intFromFloat(@ceil(@as(f32, @floatFromInt(@max(asc, 1))) * web_render_scale)); const program = try makeGlProgram(); + const overlay_program = try makeOverlayGlProgram(); var atlas_tex: c_uint = 0; c.glGenTextures(1, &atlas_tex); c.glBindTexture(c.GL_TEXTURE_2D, atlas_tex); @@ -594,9 +661,12 @@ fn initGl( var vbo: c_uint = 0; c.glGenBuffers(1, &vbo); + var overlay_vbo: c_uint = 0; + c.glGenBuffers(1, &overlay_vbo); const atlas_stage = try alloc.alloc(u8, atlas_w * atlas_h); @memset(atlas_stage, 0); + const overlay_vertices = try alloc.alloc(OverlayVertex, max_overlay_vertices); var self: SdlBackend = .{ .io = io, @@ -606,16 +676,22 @@ fn initGl( .swapchain_format = undefined, .present_mode = c.SDL_GPU_PRESENTMODE_VSYNC, .pipeline = undefined, + .overlay_pipeline = undefined, .atlas_tex = undefined, .atlas_sampler = undefined, .atlas_xfer = undefined, .vbuf = undefined, .vxfer = undefined, + .overlay_vbuf = undefined, + .overlay_vxfer = undefined, .gl_context = gl_context, .gl_program = program, + .gl_overlay_program = overlay_program, .gl_atlas_tex = atlas_tex, .gl_vbo = vbo, + .gl_overlay_vbo = overlay_vbo, .gl_u_atlas = c.glGetUniformLocation(program, "u_atlas"), + .overlay_vertices = overlay_vertices, .font = font, .scale = scale, .cell_w = cell_w, @@ -650,11 +726,14 @@ fn initTest(io: std.Io, alloc: std.mem.Allocator) !SdlBackend { .swapchain_format = undefined, .present_mode = c.SDL_GPU_PRESENTMODE_VSYNC, .pipeline = undefined, + .overlay_pipeline = undefined, .atlas_tex = undefined, .atlas_sampler = undefined, .atlas_xfer = undefined, .vbuf = undefined, .vxfer = undefined, + .overlay_vbuf = undefined, + .overlay_vxfer = undefined, .font = null, .scale = 1, .cell_w = 10, @@ -676,10 +755,13 @@ pub fn deinit(self: *SdlBackend, alloc: ?std.mem.Allocator, writer: *std.Io.Writ return; } if (is_emscripten) { + if (self.gl_overlay_vbo != 0) c.glDeleteBuffers(1, &self.gl_overlay_vbo); if (self.gl_vbo != 0) c.glDeleteBuffers(1, &self.gl_vbo); if (self.gl_atlas_tex != 0) c.glDeleteTextures(1, &self.gl_atlas_tex); + if (self.gl_overlay_program != 0) c.glDeleteProgram(self.gl_overlay_program); if (self.gl_program != 0) c.glDeleteProgram(self.gl_program); self.alloc.free(self.gl_instances); + self.alloc.free(self.overlay_vertices); if (self.font) |f| c.ui_font_free(f); self.screen.deinit(self.alloc); self.glyphs.deinit(); @@ -692,13 +774,17 @@ pub fn deinit(self: *SdlBackend, alloc: ?std.mem.Allocator, writer: *std.Io.Writ if (self.capture_tex) |t| c.SDL_ReleaseGPUTexture(self.device, t); if (self.capture_xfer) |x| c.SDL_ReleaseGPUTransferBuffer(self.device, x); if (self.trace_fd >= 0 and self.trace_fd != 2) closeFdBestEffort(self.trace_fd); + c.SDL_ReleaseGPUBuffer(self.device, self.overlay_vbuf); + c.SDL_ReleaseGPUTransferBuffer(self.device, self.overlay_vxfer); c.SDL_ReleaseGPUBuffer(self.device, self.vbuf); c.SDL_ReleaseGPUTransferBuffer(self.device, self.vxfer); + self.alloc.free(self.overlay_vertices); self.alloc.free(self.cell_keys); self.alloc.free(self.dirty_ranges); c.SDL_ReleaseGPUTransferBuffer(self.device, self.atlas_xfer); c.SDL_ReleaseGPUSampler(self.device, self.atlas_sampler); c.SDL_ReleaseGPUTexture(self.device, self.atlas_tex); + c.SDL_ReleaseGPUGraphicsPipeline(self.device, self.overlay_pipeline); c.SDL_ReleaseGPUGraphicsPipeline(self.device, self.pipeline); if (self.font) |f| c.ui_font_free(f); self.screen.deinit(self.alloc); @@ -787,6 +873,8 @@ pub fn render(self: *SdlBackend, writer: *std.Io.Writer) !void { const t_acquired = nowNs(); const win_w: f32 = @floatFromInt(sw); const win_h: f32 = @floatFromInt(sh); + const overlay_vertex_count = self.buildTouchOverlay(sw, sh); + if (overlay_vertex_count != 0) self.uploadOverlayGpu(cmd, overlay_vertex_count); const cols = self.screen.width; const rows = self.screen.height; @@ -873,6 +961,7 @@ pub fn render(self: *SdlBackend, writer: *std.Io.Writer) !void { const vbinding = c.SDL_GPUBufferBinding{ .buffer = self.vbuf, .offset = 0 }; c.SDL_BindGPUVertexBuffers(rp, 0, &vbinding, 1); c.SDL_DrawGPUPrimitives(rp, 6, cells, 0, 0); + self.drawOverlayGpu(rp, overlay_vertex_count); c.SDL_EndGPURenderPass(rp); t_drawn = nowNs(); } else { @@ -888,6 +977,7 @@ pub fn render(self: *SdlBackend, writer: *std.Io.Writer) !void { color_target.load_op = c.SDL_GPU_LOADOP_CLEAR; color_target.store_op = c.SDL_GPU_STOREOP_STORE; const rp = c.SDL_BeginGPURenderPass(cmd, &color_target, 1, null); + self.drawOverlayGpu(rp, overlay_vertex_count); c.SDL_EndGPURenderPass(rp); t_drawn = nowNs(); } @@ -926,7 +1016,9 @@ fn renderGl(self: *SdlBackend) !void { const cols = self.screen.width; const rows = self.screen.height; const cells: u32 = @as(u32, cols) * @as(u32, rows); + const overlay_vertex_count = self.buildTouchOverlay(sw, sh); if (cells == 0) { + self.drawOverlayGl(overlay_vertex_count); _ = c.SDL_GL_SwapWindow(self.sdl_window); return; } @@ -967,6 +1059,7 @@ fn renderGl(self: *SdlBackend) !void { c.glVertexAttribDivisor(3, 1); c.glDrawArraysInstanced(c.GL_TRIANGLES, 0, 6, @intCast(cells)); + self.drawOverlayGl(overlay_vertex_count); _ = c.SDL_GL_SwapWindow(self.sdl_window); } @@ -1052,6 +1145,315 @@ fn mouseCell(self: *const SdlBackend, x: f32, y: f32) MouseCell { }; } +fn clampF32(value: f32, lo: f32, hi: f32) f32 { + if (value < lo) return lo; + if (value > hi) return hi; + return value; +} + +fn findTouchSlot(self: *SdlBackend, id: u64) ?usize { + var i: usize = 0; + while (i < self.touches.len) : (i += 1) { + if (self.touches[i].active and self.touches[i].id == id) return i; + } + return null; +} + +fn inactiveTouchSlot(self: *SdlBackend) ?usize { + var i: usize = 0; + while (i < self.touches.len) : (i += 1) { + if (!self.touches[i].active) return i; + } + return null; +} + +fn updateTouch(self: *SdlBackend, tfinger: c.SDL_TouchFingerEvent) void { + const id: u64 = @intCast(tfinger.fingerID); + switch (tfinger.type) { + c.SDL_EVENT_FINGER_UP, c.SDL_EVENT_FINGER_CANCELED => { + if (self.findTouchSlot(id)) |idx| self.touches[idx].active = false; + }, + else => { + const n: u64 = self.touches.len; + const idx = self.findTouchSlot(id) orelse inactiveTouchSlot(self) orelse @as(usize, @intCast(id % n)); + if (!self.touches[idx].active or self.touches[idx].id != id) { + self.touches[idx] = .{ .active = true, .id = id }; + } else { + self.touches[idx].active = true; + } + self.touches[idx].x = tfinger.x; + self.touches[idx].y = tfinger.y; + self.touches[idx].pressure = tfinger.pressure; + self.touches[idx].pushTrail(tfinger.x, tfinger.y, tfinger.pressure); + }, + } +} + +fn touchColor(id: u64) OverlayColor { + const palette = [_]OverlayColor{ + .{ .r = 0.10, .g = 0.72, .b = 1.00, .a = 0.88 }, + .{ .r = 1.00, .g = 0.32, .b = 0.50, .a = 0.88 }, + .{ .r = 0.35, .g = 0.90, .b = 0.38, .a = 0.88 }, + .{ .r = 1.00, .g = 0.78, .b = 0.18, .a = 0.88 }, + .{ .r = 0.78, .g = 0.52, .b = 1.00, .a = 0.88 }, + .{ .r = 0.10, .g = 0.88, .b = 0.75, .a = 0.88 }, + .{ .r = 1.00, .g = 0.48, .b = 0.18, .a = 0.88 }, + .{ .r = 0.78, .g = 0.90, .b = 1.00, .a = 0.88 }, + }; + const n: u64 = palette.len; + return palette[@intCast(id % n)]; +} + +fn buildTouchOverlay(self: *SdlBackend, sw: u32, sh: u32) u32 { + if (self.overlay_vertices.len == 0 or sw == 0 or sh == 0) return 0; + + const win_w: f32 = @floatFromInt(sw); + const win_h: f32 = @floatFromInt(sh); + const ui_scale: f32 = if (is_emscripten) web_render_scale else 1.0; + var builder = OverlayBuilder{ + .vertices = self.overlay_vertices, + .win_w = win_w, + .win_h = win_h, + }; + + for (self.touches) |tp| { + if (!tp.active) continue; + const cx = clampF32(tp.x, 0.0, 1.0) * win_w; + const cy = clampF32(tp.y, 0.0, 1.0) * win_h; + const radius = (12.0 + clampF32(tp.pressure, 0.0, 1.0) * 4.0) * 4.0 * ui_scale; + const color = touchColor(tp.id); + + if (tp.trail_len > 1) { + const first = if (tp.trail_len == max_touch_trail_points) tp.trail_next else 0; + const trail_count = tp.trail_len - 1; + var trail_i: usize = 0; + while (trail_i < trail_count) : (trail_i += 1) { + const sample = tp.trail[(first + trail_i) % max_touch_trail_points]; + const fade = @as(f32, @floatFromInt(trail_i + 1)) / @as(f32, @floatFromInt(tp.trail_len)); + var trail_color = color; + trail_color.a *= 0.42 * fade; + const tx = clampF32(sample.x, 0.0, 1.0) * win_w; + const ty = clampF32(sample.y, 0.0, 1.0) * win_h; + const trail_radius = (12.0 + clampF32(sample.pressure, 0.0, 1.0) * 4.0) * 2.8 * ui_scale; + builder.addCircle(tx, ty, trail_radius, trail_color); + } + } + + builder.addCircle(cx, cy, radius + 4.0 * ui_scale, .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 0.62 }); + builder.addCircle(cx, cy, radius, color); + builder.addCircle(cx, cy, 2.4 * ui_scale, .{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 0.92 }); + builder.addIdTag(tp.id, cx, cy, ui_scale, color); + } + + return @intCast(builder.len); +} + +fn uploadOverlayGpu(self: *SdlBackend, cmd: *c.SDL_GPUCommandBuffer, vertex_count: u32) void { + if (vertex_count == 0 or is_emscripten) return; + const byte_len: u32 = vertex_count * @sizeOf(OverlayVertex); + const ptr = c.SDL_MapGPUTransferBuffer(self.device, self.overlay_vxfer, false) orelse return; + const dst: [*]u8 = @ptrCast(ptr); + const src: [*]const u8 = @ptrCast(self.overlay_vertices.ptr); + @memcpy(dst[0..byte_len], src[0..byte_len]); + c.SDL_UnmapGPUTransferBuffer(self.device, self.overlay_vxfer); + + const copy = c.SDL_BeginGPUCopyPass(cmd); + const src_loc = c.SDL_GPUTransferBufferLocation{ .transfer_buffer = self.overlay_vxfer, .offset = 0 }; + const dst_region = c.SDL_GPUBufferRegion{ .buffer = self.overlay_vbuf, .offset = 0, .size = byte_len }; + c.SDL_UploadToGPUBuffer(copy, &src_loc, &dst_region, false); + c.SDL_EndGPUCopyPass(copy); +} + +fn drawOverlayGpu(self: *SdlBackend, rp: ?*c.SDL_GPURenderPass, vertex_count: u32) void { + if (vertex_count == 0 or is_emscripten) return; + const pass = rp orelse return; + c.SDL_BindGPUGraphicsPipeline(pass, self.overlay_pipeline); + const binding = c.SDL_GPUBufferBinding{ .buffer = self.overlay_vbuf, .offset = 0 }; + c.SDL_BindGPUVertexBuffers(pass, 0, &binding, 1); + c.SDL_DrawGPUPrimitives(pass, vertex_count, 1, 0, 0); +} + +fn drawOverlayGl(self: *SdlBackend, vertex_count: u32) void { + if (!is_emscripten or vertex_count == 0) return; + c.glUseProgram(self.gl_overlay_program); + c.glBindBuffer(c.GL_ARRAY_BUFFER, self.gl_overlay_vbo); + c.glBufferData(c.GL_ARRAY_BUFFER, @intCast(vertex_count * @sizeOf(OverlayVertex)), self.overlay_vertices.ptr, c.GL_STREAM_DRAW); + + const stride: c_int = @intCast(@sizeOf(OverlayVertex)); + c.glEnableVertexAttribArray(0); + c.glVertexAttribPointer(0, 2, c.GL_FLOAT, c.GL_FALSE, stride, @ptrFromInt(@offsetOf(OverlayVertex, "x"))); + c.glVertexAttribDivisor(0, 0); + c.glEnableVertexAttribArray(1); + c.glVertexAttribPointer(1, 4, c.GL_FLOAT, c.GL_FALSE, stride, @ptrFromInt(@offsetOf(OverlayVertex, "r"))); + c.glVertexAttribDivisor(1, 0); + c.glDisableVertexAttribArray(2); + c.glDisableVertexAttribArray(3); + + c.glEnable(c.GL_BLEND); + c.glBlendFuncSeparate(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA, c.GL_ONE, c.GL_ONE_MINUS_SRC_ALPHA); + c.glDrawArrays(c.GL_TRIANGLES, 0, @intCast(vertex_count)); + c.glDisable(c.GL_BLEND); +} + +const OverlayBuilder = struct { + vertices: []OverlayVertex, + len: usize = 0, + win_w: f32, + win_h: f32, + + fn addVertex(self: *OverlayBuilder, px: f32, py: f32, color: OverlayColor) void { + if (self.len >= self.vertices.len) return; + self.vertices[self.len] = .{ + .x = (px / self.win_w) * 2.0 - 1.0, + .y = 1.0 - (py / self.win_h) * 2.0, + .r = color.r, + .g = color.g, + .b = color.b, + .a = color.a, + }; + self.len += 1; + } + + fn addTri(self: *OverlayBuilder, x0: f32, y0: f32, x1: f32, y1: f32, x2: f32, y2: f32, color: OverlayColor) void { + if (self.len + 3 > self.vertices.len) return; + self.addVertex(x0, y0, color); + self.addVertex(x1, y1, color); + self.addVertex(x2, y2, color); + } + + fn addRect(self: *OverlayBuilder, x0_in: f32, y0_in: f32, x1_in: f32, y1_in: f32, color: OverlayColor) void { + if (self.len + 6 > self.vertices.len) return; + const x0 = clampF32(@min(x0_in, x1_in), 0.0, self.win_w); + const x1 = clampF32(@max(x0_in, x1_in), 0.0, self.win_w); + const y0 = clampF32(@min(y0_in, y1_in), 0.0, self.win_h); + const y1 = clampF32(@max(y0_in, y1_in), 0.0, self.win_h); + if (x1 <= x0 or y1 <= y0) return; + self.addTri(x0, y0, x1, y0, x1, y1, color); + self.addTri(x0, y0, x1, y1, x0, y1, color); + } + + fn addCircle(self: *OverlayBuilder, cx: f32, cy: f32, radius: f32, color: OverlayColor) void { + var i: usize = 0; + while (i + 1 < circle_points.len) : (i += 1) { + const p0 = circle_points[i]; + const p1 = circle_points[i + 1]; + self.addTri( + cx, + cy, + cx + p0[0] * radius, + cy + p0[1] * radius, + cx + p1[0] * radius, + cy + p1[1] * radius, + color, + ); + } + } + + fn addIdTag(self: *OverlayBuilder, id: u64, cx: f32, cy: f32, ui_scale: f32, color: OverlayColor) void { + const label = touchIdLabel(id); + const glyph_scale = 2.0 * ui_scale; + const char_step = 4.0 * glyph_scale; + const pad = 4.0 * ui_scale; + const stripe_w = 3.0 * ui_scale; + const label_w = pad * 2.0 + stripe_w + 3.0 * ui_scale + @as(f32, @floatFromInt(label.len)) * char_step - glyph_scale; + const label_h = pad * 2.0 + 5.0 * glyph_scale; + + var x = cx + 20.0 * ui_scale; + if (x + label_w > self.win_w - 2.0 * ui_scale) x = cx - label_w - 20.0 * ui_scale; + x = clampF32(x, 2.0 * ui_scale, @max(2.0 * ui_scale, self.win_w - label_w - 2.0 * ui_scale)); + + var y = cy - label_h - 10.0 * ui_scale; + if (y < 2.0 * ui_scale) y = cy + 20.0 * ui_scale; + y = clampF32(y, 2.0 * ui_scale, @max(2.0 * ui_scale, self.win_h - label_h - 2.0 * ui_scale)); + + self.addRect(x, y, x + label_w, y + label_h, .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 0.72 }); + self.addRect(x + pad, y + pad, x + pad + stripe_w, y + label_h - pad, color); + self.addMiniText(label[0..], x + pad + stripe_w + 3.0 * ui_scale, y + pad, glyph_scale, .{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 0.95 }); + } + + fn addMiniText(self: *OverlayBuilder, text: []const u8, x: f32, y: f32, scale: f32, color: OverlayColor) void { + var cx = x; + for (text) |ch| { + self.addMiniGlyph(ch, cx, y, scale, color); + cx += 4.0 * scale; + } + } + + fn addMiniGlyph(self: *OverlayBuilder, ch: u8, x: f32, y: f32, scale: f32, color: OverlayColor) void { + const rows = miniGlyph(ch); + const masks = [_]u8{ 0b100, 0b010, 0b001 }; + var row: usize = 0; + while (row < rows.len) : (row += 1) { + var col: usize = 0; + while (col < masks.len) : (col += 1) { + if ((rows[row] & masks[col]) == 0) continue; + const x0 = x + @as(f32, @floatFromInt(col)) * scale; + const y0 = y + @as(f32, @floatFromInt(row)) * scale; + self.addRect(x0, y0, x0 + scale, y0 + scale, color); + } + } + } +}; + +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 }, + .{ -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 }, + .{ 1.0000000, 0.0000000 }, +}; + +fn touchIdLabel(id: u64) [9]u8 { + var out: [9]u8 = undefined; + out[0] = '#'; + const low: u32 = @truncate(id); + var i: usize = 0; + while (i < 8) : (i += 1) { + const shift: u5 = @intCast((7 - i) * 4); + out[i + 1] = hexDigit((low >> shift) & 0xf); + } + return out; +} + +fn hexDigit(n: u32) u8 { + return if (n < 10) @intCast('0' + n) else @intCast('A' + (n - 10)); +} + +fn miniGlyph(ch: u8) [5]u8 { + return switch (ch) { + '#' => .{ 0b101, 0b111, 0b101, 0b111, 0b101 }, + '0' => .{ 0b111, 0b101, 0b101, 0b101, 0b111 }, + '1' => .{ 0b010, 0b110, 0b010, 0b010, 0b111 }, + '2' => .{ 0b111, 0b001, 0b111, 0b100, 0b111 }, + '3' => .{ 0b111, 0b001, 0b111, 0b001, 0b111 }, + '4' => .{ 0b101, 0b101, 0b111, 0b001, 0b001 }, + '5' => .{ 0b111, 0b100, 0b111, 0b001, 0b111 }, + '6' => .{ 0b111, 0b100, 0b111, 0b101, 0b111 }, + '7' => .{ 0b111, 0b001, 0b010, 0b010, 0b010 }, + '8' => .{ 0b111, 0b101, 0b111, 0b101, 0b111 }, + '9' => .{ 0b111, 0b101, 0b111, 0b001, 0b111 }, + 'A' => .{ 0b111, 0b101, 0b111, 0b101, 0b101 }, + 'B' => .{ 0b110, 0b101, 0b110, 0b101, 0b110 }, + 'C' => .{ 0b111, 0b100, 0b100, 0b100, 0b111 }, + 'D' => .{ 0b110, 0b101, 0b101, 0b101, 0b110 }, + 'E' => .{ 0b111, 0b100, 0b111, 0b100, 0b111 }, + 'F' => .{ 0b111, 0b100, 0b111, 0b100, 0b100 }, + else => .{ 0, 0, 0, 0, 0 }, + }; +} + // PARDES_TEST_GRID debug mode: dump the retained cell grid as text to stdout // (fd 1). One frame per render, delimited by ---FRAME---/---ENDFRAME--- markers. fn dumpGrid(self: *SdlBackend) !void { @@ -1595,6 +1997,52 @@ fn makePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat) return c.SDL_CreateGPUGraphicsPipeline(device, &info) orelse error.GpuCreate; } +fn makeOverlayPipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat) !*c.SDL_GPUGraphicsPipeline { + const vs = try makeShader(device, overlay_vert_spv_bytes, c.SDL_GPU_SHADERSTAGE_VERTEX, 0); + defer c.SDL_ReleaseGPUShader(device, vs); + const fs = try makeShader(device, overlay_frag_spv_bytes, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 0); + defer c.SDL_ReleaseGPUShader(device, fs); + + var vbuf_desc = c.SDL_GPUVertexBufferDescription{ + .slot = 0, + .pitch = @sizeOf(OverlayVertex), + .input_rate = c.SDL_GPU_VERTEXINPUTRATE_VERTEX, + .instance_step_rate = 0, + }; + var attrs = [_]c.SDL_GPUVertexAttribute{ + .{ .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; + var col_desc = c.SDL_GPUColorTargetDescription{ .format = color_format, .blend_state = blend }; + + var info = std.mem.zeroes(c.SDL_GPUGraphicsPipelineCreateInfo); + info.vertex_shader = vs; + info.fragment_shader = fs; + info.primitive_type = c.SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; + info.vertex_input_state = .{ + .vertex_buffer_descriptions = &vbuf_desc, + .num_vertex_buffers = 1, + .vertex_attributes = &attrs, + .num_vertex_attributes = attrs.len, + }; + info.rasterizer_state.fill_mode = c.SDL_GPU_FILLMODE_FILL; + info.rasterizer_state.cull_mode = c.SDL_GPU_CULLMODE_NONE; + info.multisample_state.sample_count = c.SDL_GPU_SAMPLECOUNT_1; + info.target_info = .{ + .color_target_descriptions = &col_desc, + .num_color_targets = 1, + }; + return c.SDL_CreateGPUGraphicsPipeline(device, &info) orelse error.GpuCreate; +} + fn makeShader( device: *c.SDL_GPUDevice, code: []const u8, @@ -1612,9 +2060,17 @@ fn makeShader( } fn makeGlProgram() !c_uint { - const vs = try makeGlShader(c.GL_VERTEX_SHADER, vert_glsl_es); + return makeGlProgramFrom(vert_glsl_es, frag_glsl_es); +} + +fn makeOverlayGlProgram() !c_uint { + return makeGlProgramFrom(overlay_vert_glsl_es, overlay_frag_glsl_es); +} + +fn makeGlProgramFrom(vertex_source: []const u8, fragment_source: []const u8) !c_uint { + const vs = try makeGlShader(c.GL_VERTEX_SHADER, vertex_source); defer c.glDeleteShader(vs); - const fs = try makeGlShader(c.GL_FRAGMENT_SHADER, frag_glsl_es); + const fs = try makeGlShader(c.GL_FRAGMENT_SHADER, fragment_source); defer c.glDeleteShader(fs); const program = c.glCreateProgram(); @@ -1925,6 +2381,13 @@ fn SdlLoop(comptime EventType: type) type { .type = .press, } }; }, + c.SDL_EVENT_FINGER_DOWN, c.SDL_EVENT_FINGER_MOTION, c.SDL_EVENT_FINGER_UP, c.SDL_EVENT_FINGER_CANCELED => { + self.vx.updateTouch(sev.tfinger); + return .tick; + }, + c.SDL_EVENT_PINCH_BEGIN, c.SDL_EVENT_PINCH_UPDATE, c.SDL_EVENT_PINCH_END => { + return .{ .pinch = sev.pinch.scale }; + }, else => return null, } } |
