diff options
| -rw-r--r-- | build.zig | 9 | ||||
| -rw-r--r-- | main.zig | 26 | ||||
| -rw-r--r-- | replay.zig | 20 | ||||
| -rw-r--r-- | shaders/overlay.frag.es.glsl | 11 | ||||
| -rw-r--r-- | shaders/overlay.frag.glsl | 9 | ||||
| -rw-r--r-- | shaders/overlay.vert.es.glsl | 13 | ||||
| -rw-r--r-- | shaders/overlay.vert.glsl | 11 | ||||
| -rw-r--r-- | ui_sdl.zig | 467 |
8 files changed, 555 insertions, 11 deletions
@@ -250,6 +250,8 @@ pub fn build(b: *std.Build) void { if (is_emscripten) { ui_mod.addAnonymousImport("ui.vert.es.glsl", .{ .root_source_file = b.path("shaders/ui.vert.es.glsl") }); ui_mod.addAnonymousImport("ui.frag.es.glsl", .{ .root_source_file = b.path("shaders/ui.frag.es.glsl") }); + ui_mod.addAnonymousImport("overlay.vert.es.glsl", .{ .root_source_file = b.path("shaders/overlay.vert.es.glsl") }); + ui_mod.addAnonymousImport("overlay.frag.es.glsl", .{ .root_source_file = b.path("shaders/overlay.frag.es.glsl") }); } else { // GLSL -> SPIR-V at build time, embedded into ui_sdl.zig via @embedFile. ui_mod.addAnonymousImport("ui.vert.spv", .{ @@ -258,6 +260,12 @@ pub fn build(b: *std.Build) void { ui_mod.addAnonymousImport("ui.frag.spv", .{ .root_source_file = compileGlsl(b, "shaders/ui.frag.glsl", "fragment", "ui.frag.spv"), }); + ui_mod.addAnonymousImport("overlay.vert.spv", .{ + .root_source_file = compileGlsl(b, "shaders/overlay.vert.glsl", "vertex", "overlay.vert.spv"), + }); + ui_mod.addAnonymousImport("overlay.frag.spv", .{ + .root_source_file = compileGlsl(b, "shaders/overlay.frag.glsl", "fragment", "overlay.frag.spv"), + }); } } @@ -431,6 +439,7 @@ pub fn build(b: *std.Build) void { \\ image-rendering: crisp-edges; image-rendering: pixelated; \\ -ms-interpolation-mode: nearest-neighbor; \\ transform: translateZ(0); backface-visibility: hidden; } + \\ touch-action: none; } \\ </style> \\</head> \\<body> @@ -150,6 +150,7 @@ pub const Command = struct { winsize: vaxis.Winsize, mouse: vaxis.Mouse, paste: []const u8, // OSC 52 system-clipboard read reply (the loop decodes it) + pinch: f32, } = .nop; }; @@ -176,6 +177,7 @@ const Cli = struct { tty_toggle: u21 = default_tty_toggle, help: bool = false, perf_startup: bool = false, + debug_overlay: bool = false, }; const help_text = @@ -185,8 +187,9 @@ const help_text = \\ --tty start as one shell pane already in tty mode \\ --tty-toggle <key> use Ctrl-<key> to enter/leave tty mode \\ --tty-toggle=<key> same as above; accepts b, Ctrl-b, or ^b + \\ --debug-overlay start with debug overlay visible \\ -h, --help show this help and exit - \\ + \\\; ; fn printHelp(io: std.Io) !void { @@ -225,6 +228,8 @@ fn parseCli(args: []const [:0]const u8) !Cli { i += 1; if (i >= args.len) return error.BadArgs; cli.tty_toggle = parseCtrlKey(args[i]) orelse return error.BadArgs; + } else if (std.mem.eql(u8, a, "--debug-overlay")) { + cli.debug_overlay = true; } else { return error.BadArgs; } @@ -305,6 +310,7 @@ var colors_on: bool = true; // The stats overlay, toggled by the "Debug" builtin. Global (like colors_on) so the // central command dispatcher can flip it; the render reads it. var show_debug: bool = false; +var ov_pinch_scale: f32 = 1.0; // Builtins. Executing the name (middle-click / Tab) runs the action through the ONE // dispatcher (onSelect + actOnSelection), no matter where the name is shown — a pane @@ -2804,6 +2810,7 @@ pub fn main(init: std.process.Init) !void { const tty_only = cli.tty_only; const tty_toggle = cli.tty_toggle; const perf_startup = cli.perf_startup; + if (cli.debug_overlay) show_debug = true; var screen_w: u16 = 80; var screen_h: u16 = 24; @@ -2830,7 +2837,7 @@ pub fn main(init: std.process.Init) !void { var frame_arena: std.heap.ArenaAllocator = .init(gpa); defer frame_arena.deinit(); defer freeYank(gpa); - var ov_buf: [128]u8 = undefined; // overlay text; must outlive each vx.render() + var ov_buf: [192]u8 = undefined; // overlay text; must outlive each vx.render() // system_clipboard_allocator lets the loop's reader thread decode OSC 52 paste // replies (delivered as .paste events). gpa is already used cross-thread by the @@ -3383,6 +3390,9 @@ pub fn main(init: std.process.Init) !void { normalPaste(gpa, t); } }, + .pinch => |scale| { + ov_pinch_scale = scale; + }, .quit => break, else => {}, } @@ -3567,12 +3577,12 @@ pub fn main(init: std.process.Init) !void { sb_total = sb.total; } const s = std.fmt.bufPrint( - &ov_buf, - "pardes resizes={d}\nterms {d} cols {d}\nactive #{d} {d}x{d}\nscroll {d}/{d}", - .{ resize_count, nlive, layout.ncol, active, at.cols, at.rows, sb_off, sb_total }, - ) catch ""; - const ow: u16 = 26; - const oh: u16 = 6; + &ov_buf, + "pardes resizes={d}\nterms {d} cols {d}\nactive #{d} {d}x{d}\nscroll {d}/{d}\npinch {d:6.4}", + .{ resize_count, nlive, layout.ncol, active, at.cols, at.rows, sb_off, sb_total, ov_pinch_scale }, + ) catch ""; + const ow: u16 = 30; + const oh: u16 = 7; const ox: u16 = if (screen_w > ow) screen_w - ow else 0; const ov = win.child(.{ .x_off = ox, .y_off = TOPBAR_H, .width = ow, .height = oh, .border = .{ .where = .all } }); ov.fill(.{ .char = .{ .grapheme = " " }, .style = .{ .bg = .{ .rgb = gray.bar } } }); @@ -82,6 +82,7 @@ pub const Command = struct { winsize: ui.Winsize, mouse: ui.Mouse, paste: []const u8, + pinch: f32, } = .nop; }; @@ -293,6 +294,7 @@ fn render( drag: Drag, hover_col: u16, hover_row: u16, + ov_pinch_scale: f32, ) !void { var arena: std.heap.ArenaAllocator = .init(gpa); defer arena.deinit(); @@ -317,6 +319,20 @@ fn render( pane_ui.drawInteractionPreview(win, layout, rects, col_x, col_w, screen_h, drag, hover_col, hover_row, T().scroll_thumb); + // debug overlay with pinch scale + if (ov_pinch_scale != 1.0) { + var ov_buf: [64]u8 = undefined; + const s = std.fmt.bufPrint(&ov_buf, "pinch {d:6.4}", .{ov_pinch_scale}) catch ""; + if (s.len > 0) { + const ow: u16 = 16; + const oh: u16 = 2; + const ox: u16 = if (screen_w > ow) screen_w - ow else 0; + const ov = win.child(.{ .x_off = ox, .y_off = TOPBAR_H, .width = ow, .height = oh, .border = .{ .where = .all } }); + ov.fill(.{ .char = .{ .grapheme = " " }, .style = .{ .bg = rgbColor(T().scroll_track) } }); + _ = ov.printSegment(.{ .text = s, .style = .{ .fg = rgbColor(accent) } }, .{}); + } + } + try vx.render(writer); } @@ -424,6 +440,7 @@ const App = struct { drag: Drag = .none, hover_col: u16 = 0, hover_row: u16 = 0, + ov_pinch_scale: f32 = 1.0, fn init( self: *App, @@ -643,6 +660,7 @@ const App = struct { if (key.matches('q', .{}) or key.matches(ui.Key.escape, .{}) or key.matches('c', .{ .ctrl = true })) return false; }, .paste => |text| self.gpa.free(text), + .pinch => |scale| self.ov_pinch_scale = scale, .quit, .close => return false, else => {}, } @@ -659,7 +677,7 @@ const App = struct { p.rows = rows; clampScroll(p); } - try render(&self.vx, self.gpa, self.tty.writer(), self.state, self.panes, &self.rects, &self.layout, &self.col_x, &self.col_w, self.screen_w, self.screen_h, self.active, self.drag, self.hover_col, self.hover_row); + try render(&self.vx, self.gpa, self.tty.writer(), self.state, self.panes, &self.rects, &self.layout, &self.col_x, &self.col_w, self.screen_w, self.screen_h, self.active, self.drag, self.hover_col, self.hover_row, self.ov_pinch_scale); return true; } }; diff --git a/shaders/overlay.frag.es.glsl b/shaders/overlay.frag.es.glsl new file mode 100644 index 00000000..37032c9a --- /dev/null +++ b/shaders/overlay.frag.es.glsl @@ -0,0 +1,11 @@ +#version 300 es + +precision mediump float; + +in vec4 v_color; + +out vec4 o_col; + +void main() { + o_col = v_color; +} diff --git a/shaders/overlay.frag.glsl b/shaders/overlay.frag.glsl new file mode 100644 index 00000000..86c0188b --- /dev/null +++ b/shaders/overlay.frag.glsl @@ -0,0 +1,9 @@ +#version 450 + +layout(location = 0) in vec4 v_color; + +layout(location = 0) out vec4 o_col; + +void main() { + o_col = v_color; +} diff --git a/shaders/overlay.vert.es.glsl b/shaders/overlay.vert.es.glsl new file mode 100644 index 00000000..edea124d --- /dev/null +++ b/shaders/overlay.vert.es.glsl @@ -0,0 +1,13 @@ +#version 300 es + +precision mediump float; + +layout(location = 0) in vec2 a_pos; +layout(location = 1) in vec4 a_color; + +out vec4 v_color; + +void main() { + v_color = a_color; + gl_Position = vec4(a_pos, 0.0, 1.0); +} diff --git a/shaders/overlay.vert.glsl b/shaders/overlay.vert.glsl new file mode 100644 index 00000000..5f6be593 --- /dev/null +++ b/shaders/overlay.vert.glsl @@ -0,0 +1,11 @@ +#version 450 + +layout(location = 0) in vec2 a_pos; +layout(location = 1) in vec4 a_color; + +layout(location = 0) out vec4 v_color; + +void main() { + v_color = a_color; + gl_Position = vec4(a_pos, 0.0, 1.0); +} @@ -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, } } |
