diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-05 19:42:46 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | 7988d9bc6e31210ff13994f18d5622dd57ba9341 (patch) | |
| tree | 62138868db52638cbefb18086dc226810fad761a /ui_sdl.zig | |
| parent | e36e946f9a56c4ab1d10cef3aace20fe7d274dfa (diff) | |
| download | pardes-7988d9bc6e31210ff13994f18d5622dd57ba9341.tar.gz pardes-7988d9bc6e31210ff13994f18d5622dd57ba9341.zip | |
sdl: ui_sdl rework (380+ lines) + replay changes
Diffstat (limited to 'ui_sdl.zig')
| -rw-r--r-- | ui_sdl.zig | 380 |
1 files changed, 350 insertions, 30 deletions
@@ -139,6 +139,9 @@ gl_u_atlas: c_int = -1, gl_instances: []CellInstance = &.{}, overlay_vertices: []OverlayVertex = &.{}, touches: [max_touch_points]TouchPoint = [_]TouchPoint{.{}} ** max_touch_points, +touch_scroll: TouchScrollState = .{}, +touch_middle_clicks: u32 = 0, +touch_middle_flash_frames: u8 = 0, // font + cell metrics font: ?*c.UIFont, @@ -192,7 +195,10 @@ 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); +pub const touch_middle_flash_max_frames: u8 = 14; +pub const touch_middle_flash_vertices: usize = 1400; +pub const touch_scroll_tick: f32 = 0.02; +pub const max_overlay_vertices: usize = touch_middle_flash_vertices + 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. @@ -245,6 +251,25 @@ const TouchPoint = struct { } }; +const TouchNormPoint = struct { + x: f32 = 0, + y: f32 = 0, +}; + +const TouchScrollState = struct { + active: bool = false, + ids: [2]u64 = .{ 0, 0 }, + accum_y: f32 = 0, + scrolled: bool = false, + last_center: TouchNormPoint = .{}, +}; + +const TouchScrollResult = struct { + center: TouchNormPoint, + ticks: i32, + delta_y: f32, +}; + const OverlayColor = struct { r: f32, g: f32, @@ -425,8 +450,9 @@ pub fn init( // ---- test modes ------------------------------------------------------- // PARDES_TEST=1 keeps the real SDL/GPU/window renderer active, but drives - // input from stdin (raw terminal escape sequences parsed by vaxis.Parser) - // and writes rendered-frame captures into PARDES_TEST_CAPTURE_DIR. + // input from stdin (raw terminal escape sequences parsed by vaxis.Parser, + // plus private synthetic SDL event OSCs) and writes rendered-frame captures + // into PARDES_TEST_CAPTURE_DIR. // // PARDES_TEST_GRID=1 is retained as a debug escape hatch: no SDL/GPU/window, // stdin→events, retained cell grid→stdout. The real SDL e2e suite does not @@ -1151,7 +1177,7 @@ fn clampF32(value: f32, lo: f32, hi: f32) f32 { return value; } -fn findTouchSlot(self: *SdlBackend, id: u64) ?usize { +fn findTouchSlot(self: *const 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; @@ -1167,6 +1193,128 @@ fn inactiveTouchSlot(self: *SdlBackend) ?usize { return null; } +fn sortedTouchPair(a: u64, b: u64) [2]u64 { + return if (a <= b) .{ a, b } else .{ b, a }; +} + +fn sameTouchPair(a: [2]u64, b: [2]u64) bool { + return a[0] == b[0] and a[1] == b[1]; +} + +fn touchPairHas(ids: [2]u64, id: u64) bool { + return ids[0] == id or ids[1] == id; +} + +fn activeTouchPair(self: *const SdlBackend) ?[2]u64 { + var ids: [2]u64 = .{ 0, 0 }; + var count: usize = 0; + for (self.touches) |tp| { + if (!tp.active) continue; + if (count >= 2) return null; + ids[count] = tp.id; + count += 1; + } + return if (count == 2) sortedTouchPair(ids[0], ids[1]) else null; +} + +fn touchPairCenter(self: *const SdlBackend, ids: [2]u64) ?TouchNormPoint { + var sum_x: f32 = 0; + var sum_y: f32 = 0; + var count: usize = 0; + for (ids) |id| { + const idx = self.findTouchSlot(id) orelse return null; + sum_x += self.touches[idx].x; + sum_y += self.touches[idx].y; + count += 1; + } + if (count != 2) return null; + return .{ .x = sum_x * 0.5, .y = sum_y * 0.5 }; +} + +fn touchPairCenterWithEvent(self: *const SdlBackend, ids: [2]u64, tfinger: c.SDL_TouchFingerEvent) ?TouchNormPoint { + const event_id: u64 = @intCast(tfinger.fingerID); + var sum_x: f32 = 0; + var sum_y: f32 = 0; + var count: usize = 0; + for (ids) |id| { + if (id == event_id) { + sum_x += tfinger.x; + sum_y += tfinger.y; + } else { + const idx = self.findTouchSlot(id) orelse return null; + sum_x += self.touches[idx].x; + sum_y += self.touches[idx].y; + } + count += 1; + } + if (count != 2) return null; + return .{ .x = sum_x * 0.5, .y = sum_y * 0.5 }; +} + +fn resetTouchScroll(self: *SdlBackend) void { + self.touch_scroll = .{}; +} + +fn beginTouchScrollPair(self: *SdlBackend, ids: [2]u64) void { + self.touch_scroll = .{ + .active = true, + .ids = ids, + .last_center = self.touchPairCenter(ids) orelse .{}, + }; +} + +fn syncTouchScrollPair(self: *SdlBackend) void { + const ids = self.activeTouchPair() orelse { + self.resetTouchScroll(); + return; + }; + if (!self.touch_scroll.active or !sameTouchPair(self.touch_scroll.ids, ids)) { + self.beginTouchScrollPair(ids); + } +} + +fn finishTouchScrollPair(self: *SdlBackend, tfinger: c.SDL_TouchFingerEvent) ?TouchNormPoint { + const id: u64 = @intCast(tfinger.fingerID); + if (!self.touch_scroll.active or !touchPairHas(self.touch_scroll.ids, id)) return null; + + const tap = tfinger.type == c.SDL_EVENT_FINGER_UP and !self.touch_scroll.scrolled; + const center = if (tap) + self.touchPairCenterWithEvent(self.touch_scroll.ids, tfinger) orelse self.touch_scroll.last_center + else + null; + self.resetTouchScroll(); + return center; +} + +fn noteTouchScrollMotion(self: *SdlBackend, tfinger: c.SDL_TouchFingerEvent) ?TouchScrollResult { + const id: u64 = @intCast(tfinger.fingerID); + if (!self.touch_scroll.active or !touchPairHas(self.touch_scroll.ids, id)) return null; + + const prev_center = self.touch_scroll.last_center; + const center = self.touchPairCenter(self.touch_scroll.ids) orelse self.touch_scroll.last_center; + self.touch_scroll.last_center = center; + const delta_y = center.y - prev_center.y; + self.touch_scroll.accum_y += delta_y; + + var ticks: i32 = 0; + while (self.touch_scroll.accum_y >= touch_scroll_tick) { + self.touch_scroll.accum_y -= touch_scroll_tick; + ticks += 1; + } + while (self.touch_scroll.accum_y <= -touch_scroll_tick) { + self.touch_scroll.accum_y += touch_scroll_tick; + ticks -= 1; + } + if (ticks == 0) return null; + + self.touch_scroll.scrolled = true; + return .{ + .center = center, + .ticks = ticks, + .delta_y = @as(f32, @floatFromInt(ticks)) * touch_scroll_tick, + }; +} + fn updateTouch(self: *SdlBackend, tfinger: c.SDL_TouchFingerEvent) void { const id: u64 = @intCast(tfinger.fingerID); switch (tfinger.type) { @@ -1189,6 +1337,27 @@ fn updateTouch(self: *SdlBackend, tfinger: c.SDL_TouchFingerEvent) void { } } +fn noteTouchMiddleClick(self: *SdlBackend, is_press: bool) void { + if (is_press) self.touch_middle_clicks +%= 1; + self.touch_middle_flash_frames = touch_middle_flash_max_frames; +} + +fn fingerMousePoint(self: *const SdlBackend, x_norm: f32, y_norm: f32) struct { x: f32, y: f32 } { + var ww: c_int = 0; + var wh: c_int = 0; + if (is_emscripten) { + _ = c.SDL_GetWindowSize(self.sdl_window, &ww, &wh); + } else { + _ = c.SDL_GetWindowSizeInPixels(self.sdl_window, &ww, &wh); + } + const w: f32 = @floatFromInt(if (ww > 0) ww else 1); + const h: f32 = @floatFromInt(if (wh > 0) wh else 1); + return .{ + .x = clampF32(x_norm, 0.0, 1.0) * w, + .y = clampF32(y_norm, 0.0, 1.0) * h, + }; +} + fn touchColor(id: u64) OverlayColor { const palette = [_]OverlayColor{ .{ .r = 0.10, .g = 0.72, .b = 1.00, .a = 0.88 }, @@ -1216,6 +1385,25 @@ fn buildTouchOverlay(self: *SdlBackend, sw: u32, sh: u32) u32 { .win_h = win_h, }; + if (self.touch_middle_flash_frames != 0) { + const flash = @as(f32, @floatFromInt(self.touch_middle_flash_frames)) / @as(f32, @floatFromInt(touch_middle_flash_max_frames)); + const label = touchMiddleClickLabel(self.touch_middle_clicks); + const glyph_scale = 3.0 * ui_scale; + const char_step = 4.0 * glyph_scale; + const pad = 5.0 * ui_scale; + const stripe_w = 4.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; + const x = 8.0 * ui_scale; + const y = 8.0 * ui_scale; + + builder.addRect(0.0, 0.0, win_w, win_h, .{ .r = 1.0, .g = 0.78, .b = 0.18, .a = 0.055 * flash }); + builder.addRect(x, y, x + label_w, y + label_h, .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 0.54 + 0.22 * flash }); + builder.addRect(x + pad, y + pad, x + pad + stripe_w, y + label_h - pad, .{ .r = 1.0, .g = 0.78, .b = 0.18, .a = 0.96 }); + builder.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.96 }); + self.touch_middle_flash_frames -= 1; + } + for (self.touches) |tp| { if (!tp.active) continue; const cx = clampF32(tp.x, 0.0, 1.0) * win_w; @@ -1427,6 +1615,21 @@ fn touchIdLabel(id: u64) [9]u8 { return out; } +fn touchMiddleClickLabel(count: u32) [13]u8 { + var out: [13]u8 = undefined; + out[0] = 'M'; + out[1] = 'I'; + out[2] = 'D'; + out[3] = ' '; + out[4] = '#'; + var i: usize = 0; + while (i < 8) : (i += 1) { + const shift: u5 = @intCast((7 - i) * 4); + out[i + 5] = hexDigit((count >> shift) & 0xf); + } + return out; +} + fn hexDigit(n: u32) u8 { return if (n < 10) @intCast('0' + n) else @intCast('A' + (n - 10)); } @@ -1450,6 +1653,8 @@ fn miniGlyph(ch: u8) [5]u8 { 'D' => .{ 0b110, 0b101, 0b101, 0b101, 0b110 }, 'E' => .{ 0b111, 0b100, 0b111, 0b100, 0b111 }, 'F' => .{ 0b111, 0b100, 0b111, 0b100, 0b100 }, + 'I' => .{ 0b111, 0b010, 0b010, 0b010, 0b111 }, + 'M' => .{ 0b101, 0b111, 0b111, 0b101, 0b101 }, else => .{ 0, 0, 0, 0, 0 }, }; } @@ -2242,6 +2447,7 @@ fn SdlLoop(comptime EventType: type) type { var sev = std.mem.zeroes(c.SDL_Event); while (c.SDL_PollEvent(&sev)) { if (try self.dispatch(&sev)) |ev| return ev; + if (try self.queue.tryPop()) |ev| return ev; } if (is_emscripten) { self.postWinsize(); @@ -2306,6 +2512,16 @@ fn SdlLoop(comptime EventType: type) type { } }) catch {}; } + fn mouseEvent(self: *Self, cell: MouseCell, button: vaxis.Mouse.Button, t: vaxis.Mouse.Type) EventType { + return EventType{ .mouse = .{ + .col = cell.col, + .row = cell.row, + .button = button, + .mods = sdlMouseMods(self.live_mods), + .type = t, + } }; + } + fn dispatch(self: *Self, sev: *const c.SDL_Event) !?EventType { switch (sev.type) { c.SDL_EVENT_QUIT, c.SDL_EVENT_WINDOW_CLOSE_REQUESTED => return .quit, @@ -2344,13 +2560,7 @@ fn SdlLoop(comptime EventType: type) type { const t: vaxis.Mouse.Type = if (b.down) .press else .release; const button = mouseButton(b.button) orelse return null; const cell = self.vx.mouseCell(b.x, b.y); - return EventType{ .mouse = .{ - .col = cell.col, - .row = cell.row, - .button = button, - .mods = sdlMouseMods(self.live_mods), - .type = t, - } }; + return self.mouseEvent(cell, button, t); }, c.SDL_EVENT_MOUSE_MOTION => { const m = sev.motion; @@ -2360,30 +2570,59 @@ fn SdlLoop(comptime EventType: type) type { const t: vaxis.Mouse.Type = if (held != null) .drag else .motion; const button = held orelse .none; const cell = self.vx.mouseCell(m.x, m.y); - return EventType{ .mouse = .{ - .col = cell.col, - .row = cell.row, - .button = button, - .mods = sdlMouseMods(self.live_mods), - .type = t, - } }; + return self.mouseEvent(cell, button, t); }, c.SDL_EVENT_MOUSE_WHEEL => { const w = sev.wheel; const dy = w.y; const button: vaxis.Mouse.Button = if (dy > 0) .wheel_up else if (dy < 0) .wheel_down else return null; const cell = self.vx.mouseCell(w.mouse_x, w.mouse_y); - return EventType{ .mouse = .{ - .col = cell.col, - .row = cell.row, - .button = button, - .mods = sdlMouseMods(self.live_mods), - .type = .press, - } }; + return self.mouseEvent(cell, button, .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; + const f = sev.tfinger; + switch (f.type) { + c.SDL_EVENT_FINGER_DOWN => { + self.vx.updateTouch(f); + self.vx.syncTouchScrollPair(); + return .tick; + }, + c.SDL_EVENT_FINGER_MOTION => { + const had_pair = self.vx.touch_scroll.active; + self.vx.updateTouch(f); + self.vx.syncTouchScrollPair(); + if (had_pair) { + if (self.vx.noteTouchScrollMotion(f)) |scroll| { + const point = self.vx.fingerMousePoint(scroll.center.x, scroll.center.y); + const cell = self.vx.mouseCell(point.x, point.y); + const button: vaxis.Mouse.Button = if (scroll.ticks > 0) .wheel_up else .wheel_down; + var remaining: u32 = @intCast(if (scroll.ticks > 0) scroll.ticks else -scroll.ticks); + while (remaining > 0) : (remaining -= 1) { + try self.postEvent(self.mouseEvent(cell, button, .press)); + } + try self.postEvent(.{ .touch_scroll_delta = scroll.delta_y }); + return null; + } + } + return .tick; + }, + c.SDL_EVENT_FINGER_UP, c.SDL_EVENT_FINGER_CANCELED => { + const tap_center = self.vx.finishTouchScrollPair(f); + self.vx.updateTouch(f); + self.vx.syncTouchScrollPair(); + if (tap_center) |center| { + self.vx.noteTouchMiddleClick(true); + const point = self.vx.fingerMousePoint(center.x, center.y); + const cell = self.vx.mouseCell(point.x, point.y); + try self.postEvent(self.mouseEvent(cell, .middle, .press)); + self.vx.noteTouchMiddleClick(false); + try self.postEvent(self.mouseEvent(cell, .middle, .release)); + return null; + } + return .tick; + }, + else => return .tick, + } }, c.SDL_EVENT_PINCH_BEGIN, c.SDL_EVENT_PINCH_UPDATE, c.SDL_EVENT_PINCH_END => { return .{ .pinch = sev.pinch.scale }; @@ -2443,9 +2682,8 @@ fn SdlLoop(comptime EventType: type) type { // ---- test-mode stdin reader ---- // Parses terminal escape sequences from fd 0 (the pty slave) into the - // app's Event union and posts them to the queue. Identical to vaxis's - // ttyRun but reading from stdin instead of /dev/tty, and checking a - // SIGWINCH flag for resizes. + // app's Event union and posts them to the queue. This mirrors vaxis's + // ttyRun while also recognizing private PARDES_TEST SDL event OSCs. fn stdinRun(self: *Self) void { self.stdinRunTyped() catch {}; } @@ -2475,6 +2713,23 @@ fn SdlLoop(comptime EventType: type) type { const len: usize = read_start + n; var seq_start: usize = 0; while (seq_start < len) { + switch (try self.parseTestSdlEvent(buf[seq_start..len])) { + .no_match => {}, + .need_more => { + const initial = seq_start; + while (seq_start < len) : (seq_start += 1) { + buf[seq_start - initial] = buf[seq_start]; + } + read_start = len - initial; + break; + }, + .consumed => |consumed| { + read_start = 0; + seq_start += consumed; + continue; + }, + } + const result = parser.parse(buf[seq_start..len], null) catch break; if (result.n == 0) { // partial sequence — shift to front of buffer @@ -2505,6 +2760,71 @@ fn SdlLoop(comptime EventType: type) type { } } } + + const TestSdlParse = union(enum) { + no_match, + need_more, + consumed: usize, + }; + + // Private OSC used only by PARDES_TEST: + // ESC ] 777;finger;SDL_EVENT_FINGER_DOWN;<id>;<x>;<y>;<pressure> BEL + // x/y/pressure use SDL's normalized 0..1 touch coordinates. + fn parseTestSdlEvent(self: *Self, bytes: []const u8) !TestSdlParse { + const prefix = "\x1b]777;finger;"; + if (!std.mem.startsWith(u8, bytes, prefix)) { + if (bytes.len >= 2 and bytes.len < prefix.len and std.mem.eql(u8, bytes, prefix[0..bytes.len])) { + return .need_more; + } + return .no_match; + } + + const body = bytes[prefix.len..]; + const end = std.mem.indexOfScalar(u8, body, '\x07') orelse return .need_more; + try self.postTestFingerEvent(body[0..end]); + return .{ .consumed = prefix.len + end + 1 }; + } + + fn postTestFingerEvent(self: *Self, payload: []const u8) !void { + var parts = std.mem.splitScalar(u8, payload, ';'); + const type_name = parts.next() orelse return error.BadTestSdlEvent; + const finger_id_s = parts.next() orelse return error.BadTestSdlEvent; + const x_s = parts.next() orelse return error.BadTestSdlEvent; + const y_s = parts.next() orelse return error.BadTestSdlEvent; + const pressure_s = parts.next() orelse return error.BadTestSdlEvent; + if (parts.next() != null) return error.BadTestSdlEvent; + + const event_type = testFingerEventType(type_name) orelse return error.BadTestSdlEvent; + const finger_id = try std.fmt.parseInt(u64, finger_id_s, 10); + const x = try std.fmt.parseFloat(f32, x_s); + const y = try std.fmt.parseFloat(f32, y_s); + const pressure = try std.fmt.parseFloat(f32, pressure_s); + + var sev = std.mem.zeroes(c.SDL_Event); + sev.type = event_type; + sev.tfinger = .{ + .type = event_type, + .reserved = 0, + .timestamp = 0, + .touchID = 1, + .fingerID = finger_id, + .x = x, + .y = y, + .dx = 0, + .dy = 0, + .pressure = pressure, + .windowID = c.SDL_GetWindowID(self.vx.sdl_window), + }; + if (try self.dispatch(&sev)) |event| try self.postEvent(event); + } + + fn testFingerEventType(name: []const u8) ?c.SDL_EventType { + if (std.mem.eql(u8, name, "SDL_EVENT_FINGER_DOWN") or std.mem.eql(u8, name, "down")) return c.SDL_EVENT_FINGER_DOWN; + if (std.mem.eql(u8, name, "SDL_EVENT_FINGER_MOTION") or std.mem.eql(u8, name, "motion")) return c.SDL_EVENT_FINGER_MOTION; + if (std.mem.eql(u8, name, "SDL_EVENT_FINGER_UP") or std.mem.eql(u8, name, "up")) return c.SDL_EVENT_FINGER_UP; + if (std.mem.eql(u8, name, "SDL_EVENT_FINGER_CANCELED") or std.mem.eql(u8, name, "cancel")) return c.SDL_EVENT_FINGER_CANCELED; + return null; + } }; } |
