summaryrefslogtreecommitdiff
path: root/src/gui.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui.zig')
-rw-r--r--src/gui.zig401
1 files changed, 356 insertions, 45 deletions
diff --git a/src/gui.zig b/src/gui.zig
index a53dfa24..f170e1a6 100644
--- a/src/gui.zig
+++ b/src/gui.zig
@@ -100,10 +100,13 @@ const Slot = struct { u: u32, v: u32 };
const max_touch_points: usize = 16;
const max_touch_trail_points: usize = 8;
const overlay_circle_vertices: usize = 16 * 3;
-const touch_middle_flash_max_frames: u8 = 14;
-const touch_middle_flash_vertices: usize = 1400;
+const touch_click_flash_max_frames: u8 = 14;
+const touch_click_flash_vertices: usize = 1400;
const touch_scroll_tick: f32 = 0.02;
-const max_overlay_vertices: usize = touch_middle_flash_vertices + max_touch_points * (1100 + max_touch_trail_points * overlay_circle_vertices);
+/// Web tap-vs-scroll heuristic, measured after normalized coordinates have
+/// been mapped into backing pixels (12px = 8 CSS px at web_render_scale).
+const touch_tap_slop_px: f32 = 12.0;
+const max_overlay_vertices: usize = touch_click_flash_vertices + max_touch_points * (1100 + max_touch_trail_points * overlay_circle_vertices);
// One instance per cell; the vertex shader expands it to a 2-triangle quad
// with gl_VertexIndex. Coords are NDC (y up), uv into the atlas, colors 0..1.
@@ -127,7 +130,7 @@ const CellInstance = extern struct {
const OverlayVertex = extern struct { x: f32, y: f32, r: f32, g: f32, b: f32, a: f32 };
const OverlayColor = struct { r: f32, g: f32, b: f32, a: f32 };
-// ---- touch: per-finger tracking + the two-finger scroll/tap machine ----
+// ---- touch: per-finger tracking + shared scroll/tap machines ----
const TouchSample = struct { x: f32 = 0, y: f32 = 0, pressure: f32 = 1 };
@@ -157,8 +160,22 @@ const Touch = struct {
scrolled: bool = false,
last_center: TouchNormPoint = .{},
} = .{},
- middle_clicks: u32 = 0,
- middle_flash_frames: u8 = 0,
+ // Web policy: one primary finger is either a body tap/scroll or a latched
+ // left-mouse gesture when it begins on layout chrome. A second simultaneous
+ // finger cancels a body tap rather than producing a click.
+ single: struct {
+ active: bool = false,
+ id: u64 = 0,
+ start: TouchNormPoint = .{},
+ last: TouchNormPoint = .{},
+ accum_y: f32 = 0,
+ scrolling: bool = false,
+ mouse_drag: bool = false,
+ multitouch: bool = false,
+ } = .{},
+ click_count: u32 = 0,
+ click_button: pardes.Mouse.Button = .middle,
+ click_flash_frames: u8 = 0,
fn findSlot(t: *const Touch, id: u64) ?usize {
for (&t.points, 0..) |*tp, i| if (tp.active and tp.id == id) return i;
@@ -187,6 +204,12 @@ const Touch = struct {
}
}
+ fn activeCount(t: *const Touch) usize {
+ var count: usize = 0;
+ for (&t.points) |*tp| count += @intFromBool(tp.active);
+ return count;
+ }
+
fn pairCenter(t: *const Touch, ids: [2]u64) ?TouchNormPoint {
var sum: TouchNormPoint = .{};
for (ids) |id| {
@@ -250,20 +273,174 @@ const Touch = struct {
const delta_y = center.y - t.scroll.last_center.y;
t.scroll.last_center = center;
t.scroll.accum_y += delta_y;
- var ticks: i32 = 0;
- while (t.scroll.accum_y >= touch_scroll_tick) : (ticks += 1) t.scroll.accum_y -= touch_scroll_tick;
- while (t.scroll.accum_y <= -touch_scroll_tick) : (ticks -= 1) t.scroll.accum_y += touch_scroll_tick;
+ const ticks = takeScrollTicks(&t.scroll.accum_y);
if (ticks == 0) return null;
t.scroll.scrolled = true;
return .{ .center = center, .ticks = ticks, .delta_y = @as(f32, @floatFromInt(ticks)) * touch_scroll_tick };
}
+
+ fn beginSingle(t: *Touch, f: Finger, mouse_drag: bool) void {
+ if (t.single.active) {
+ if (t.single.id != f.id) t.single.multitouch = true;
+ return;
+ }
+ if (t.activeCount() != 1) return;
+ const point = TouchNormPoint{ .x = f.x, .y = f.y };
+ t.single = .{ .active = true, .id = f.id, .start = point, .last = point, .mouse_drag = mouse_drag };
+ }
+
+ fn noteSingleMotion(t: *Touch, f: Finger, win_w: f32, win_h: f32) ?struct { center: TouchNormPoint, ticks: i32, delta_y: f32 } {
+ if (!t.single.active or t.single.id != f.id or t.single.multitouch) return null;
+ const center = TouchNormPoint{ .x = f.x, .y = f.y };
+ var delta_y = center.y - t.single.last.y;
+ t.single.last = center;
+ if (!t.single.scrolling) {
+ const dx = (center.x - t.single.start.x) * win_w;
+ const dy = (center.y - t.single.start.y) * win_h;
+ if (dx * dx + dy * dy < touch_tap_slop_px * touch_tap_slop_px) return null;
+ // Once the pointer leaves the tap slop it can never click. Include
+ // all travel since finger-down so the first scroll tick is prompt.
+ t.single.scrolling = true;
+ delta_y = center.y - t.single.start.y;
+ }
+ t.single.accum_y += delta_y;
+ const ticks = takeScrollTicks(&t.single.accum_y);
+ if (ticks == 0) return null;
+ return .{ .center = center, .ticks = ticks, .delta_y = @as(f32, @floatFromInt(ticks)) * touch_scroll_tick };
+ }
+
+ fn finishSingle(t: *Touch, f: Finger, win_w: f32, win_h: f32) ?TouchNormPoint {
+ if (!t.single.active or t.single.id != f.id) return null;
+ const point = TouchNormPoint{ .x = f.x, .y = f.y };
+ const dx = (point.x - t.single.start.x) * win_w;
+ const dy = (point.y - t.single.start.y) * win_h;
+ const left_slop = dx * dx + dy * dy >= touch_tap_slop_px * touch_tap_slop_px;
+ const tap = f.kind == .up and !left_slop and !t.single.scrolling and !t.single.mouse_drag and !t.single.multitouch;
+ t.single = .{};
+ return if (tap) point else null;
+ }
};
-/// Two-finger drag → synthesized wheel presses + a touch_scroll event; a
-/// two-finger tap → synthesized middle press+release. win/cell dims map
-/// normalized coords to cells (grid test mode passes cols/rows with cell=1).
+fn takeScrollTicks(accum: *f32) i32 {
+ var ticks: i32 = 0;
+ while (accum.* >= touch_scroll_tick) : (ticks += 1) accum.* -= touch_scroll_tick;
+ while (accum.* <= -touch_scroll_tick) : (ticks -= 1) accum.* += touch_scroll_tick;
+ return ticks;
+}
+
+/// The SDL boundary owns touch policy: a web body uses one-finger natural
+/// scrolling and tap-as-LOOK, while layout chrome latches a left-mouse drag;
+/// native keeps two-finger scroll/tap-as-execute. Both feed the same core mouse
+/// events and coordinate mapping.
fn handleFinger(t: *Touch, core: *pardes.Pardes, f: Finger, win_w: f32, win_h: f32, cell_w: f32, cell_h: f32) void {
std.debug.assert(cell_w > 0 and cell_h > 0);
+ if (is_emscripten)
+ return handleSingleFinger(t, core, f, win_w, win_h, cell_w, cell_h);
+ return handlePairFinger(t, core, f, win_w, win_h, cell_w, cell_h);
+}
+
+fn handleSingleFinger(t: *Touch, core: *pardes.Pardes, f: Finger, win_w: f32, win_h: f32, cell_w: f32, cell_h: f32) void {
+ switch (f.kind) {
+ .down => {
+ t.updatePoint(f);
+ const point = TouchNormPoint{ .x = f.x, .y = f.y };
+ const col = normCell(point.x, win_w, cell_w);
+ const row = normCell(point.y, win_h, cell_h);
+ const chrome = webChromeTarget(core, col, row);
+ t.beginSingle(f, chrome != null);
+ if (chrome) |target|
+ core.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = target.col, .row = target.row } });
+ },
+ .motion => {
+ t.updatePoint(f);
+ if (t.single.active and t.single.id == f.id and t.single.mouse_drag) {
+ core.update(.{ .mouse = .{
+ .button = .left,
+ .kind = .drag,
+ .col = normCell(f.x, win_w, cell_w),
+ .row = normCell(f.y, win_h, cell_h),
+ } });
+ return;
+ }
+ const res = t.noteSingleMotion(f, win_w, win_h) orelse return;
+ emitTouchScroll(core, res.center, res.ticks, res.delta_y, win_w, win_h, cell_w, cell_h);
+ },
+ .up, .cancel => {
+ const mouse_drag = t.single.active and t.single.id == f.id and t.single.mouse_drag;
+ const tap = t.finishSingle(f, win_w, win_h);
+ t.updatePoint(f);
+ if (mouse_drag) {
+ core.update(.{ .mouse = .{
+ .button = .left,
+ .kind = .release,
+ .col = normCell(f.x, win_w, cell_w),
+ .row = normCell(f.y, win_h, cell_h),
+ } });
+ } else if (tap) |point| emitTouchClick(t, core, point, .right, win_w, win_h, cell_w, cell_h);
+ },
+ }
+}
+
+const WebChromeTarget = struct { col: u16, row: u16 };
+
+/// Fat-finger hit test for web layout chrome. A gesture is classified once at
+/// finger-down, then never changes into scrolling. Handle coordinates are
+/// snapped onto the core's one-cell resize edge; body rows remain untouched.
+fn webChromeTarget(core: *const pardes.Pardes, col: u16, row: u16) ?WebChromeTarget {
+ if (row < pardes.TOPBAR_H) return .{ .col = col, .row = row };
+
+ // The move box wins over a horizontal handle when a pane is tag-only,
+ // matching the core's own mouse hit-test priority.
+ for (core.panes, 0..) |slot, id| {
+ if (slot == null) continue;
+ const rect = core.rects[id];
+ if (row == rect.y and col >= rect.x and col < rect.x + rect.w and col < rect.x + pardes.GUTTER)
+ return .{ .col = col, .row = rect.y };
+ }
+
+ for (0..core.ncol -| 1) |column| {
+ const handle = core.col_x[column] + core.col_w[column] -| 1;
+ if (col == handle) return .{ .col = handle, .row = row };
+ }
+ for (0..core.ncol) |column| {
+ if (col < core.col_x[column] or col >= core.col_x[column] + core.col_w[column]) continue;
+ for (0..core.col_n[column] -| 1) |index| {
+ const rect = core.rects[core.col_terms[column][index]];
+ const handle = rect.y + rect.h -| 1;
+ if (row == handle) return .{ .col = col, .row = handle };
+ }
+ }
+
+ // The remainder of each tag row is tag editing, not a scrollable body.
+ for (core.panes, 0..) |slot, id| {
+ if (slot == null) continue;
+ const rect = core.rects[id];
+ if (row == rect.y and col >= rect.x and col < rect.x + rect.w)
+ return .{ .col = col, .row = rect.y };
+ }
+
+ // One-cell tolerance around thin separators, after exact tag rows have had
+ // first refusal so a fat-finger tag edit never turns into a resize.
+ for (0..core.ncol -| 1) |column| {
+ const handle = core.col_x[column] + core.col_w[column] -| 1;
+ if (cellDistance(col, handle) == 1) return .{ .col = handle, .row = row };
+ }
+ for (0..core.ncol) |column| {
+ if (col < core.col_x[column] or col >= core.col_x[column] + core.col_w[column]) continue;
+ for (0..core.col_n[column] -| 1) |index| {
+ const rect = core.rects[core.col_terms[column][index]];
+ const handle = rect.y + rect.h -| 1;
+ if (cellDistance(row, handle) == 1) return .{ .col = col, .row = handle };
+ }
+ }
+ return null;
+}
+
+fn cellDistance(a: u16, b: u16) u16 {
+ return if (a > b) a - b else b - a;
+}
+
+fn handlePairFinger(t: *Touch, core: *pardes.Pardes, f: Finger, win_w: f32, win_h: f32, cell_w: f32, cell_h: f32) void {
switch (f.kind) {
.down => {
t.updatePoint(f);
@@ -275,30 +452,37 @@ fn handleFinger(t: *Touch, core: *pardes.Pardes, f: Finger, win_w: f32, win_h: f
t.syncPair();
if (!had_pair) return;
const res = t.noteMotion(f) orelse return;
- const col = normCell(res.center.x, win_w, cell_w);
- const row = normCell(res.center.y, win_h, cell_h);
- const button: pardes.Mouse.Button = if (res.ticks > 0) .wheel_up else .wheel_down;
- var remaining: u32 = @abs(res.ticks);
- while (remaining > 0) : (remaining -= 1)
- core.update(.{ .mouse = .{ .button = button, .kind = .press, .col = col, .row = row } });
- core.update(.{ .touch_scroll = res.delta_y });
+ emitTouchScroll(core, res.center, res.ticks, res.delta_y, win_w, win_h, cell_w, cell_h);
},
.up, .cancel => {
const tap_center = t.finishPair(f);
t.updatePoint(f);
t.syncPair();
- if (tap_center) |center| {
- t.middle_clicks +%= 1;
- t.middle_flash_frames = touch_middle_flash_max_frames;
- const col = normCell(center.x, win_w, cell_w);
- const row = normCell(center.y, win_h, cell_h);
- core.update(.{ .mouse = .{ .button = .middle, .kind = .press, .col = col, .row = row } });
- core.update(.{ .mouse = .{ .button = .middle, .kind = .release, .col = col, .row = row } });
- }
+ if (tap_center) |center| emitTouchClick(t, core, center, .middle, win_w, win_h, cell_w, cell_h);
},
}
}
+fn emitTouchScroll(core: *pardes.Pardes, center: TouchNormPoint, ticks: i32, delta_y: f32, win_w: f32, win_h: f32, cell_w: f32, cell_h: f32) void {
+ const col = normCell(center.x, win_w, cell_w);
+ const row = normCell(center.y, win_h, cell_h);
+ const button: pardes.Mouse.Button = if (ticks > 0) .wheel_up else .wheel_down;
+ var remaining: u32 = @abs(ticks);
+ while (remaining > 0) : (remaining -= 1)
+ core.update(.{ .mouse = .{ .button = button, .kind = .press, .col = col, .row = row } });
+ core.update(.{ .touch_scroll = delta_y });
+}
+
+fn emitTouchClick(t: *Touch, core: *pardes.Pardes, point: TouchNormPoint, button: pardes.Mouse.Button, win_w: f32, win_h: f32, cell_w: f32, cell_h: f32) void {
+ t.click_count +%= 1;
+ t.click_button = button;
+ t.click_flash_frames = touch_click_flash_max_frames;
+ const col = normCell(point.x, win_w, cell_w);
+ const row = normCell(point.y, win_h, cell_h);
+ core.update(.{ .mouse = .{ .button = button, .kind = .press, .col = col, .row = row } });
+ core.update(.{ .mouse = .{ .button = button, .kind = .release, .col = col, .row = row } });
+}
+
fn normCell(norm: f32, win: f32, cell: f32) u16 {
const px = std.math.clamp(norm, 0.0, 1.0) * win;
return @intFromFloat(@max(0, @floor(px / cell)));
@@ -768,7 +952,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
// 7. render
_ = frame_arena.reset(.retain_capacity);
const surface = try core.render(frame_arena.allocator());
- renderFrame(&g, gpa, surface, core.crt_on) catch |err| log.err("render: {t}", .{err});
+ renderFrame(&g, gpa, surface, core.crt_on, core.show_debug) catch |err| log.err("render: {t}", .{err});
}
}
@@ -788,11 +972,130 @@ const WebState = struct {
gpa: std.mem.Allocator,
frame_arena: std.heap.ArenaAllocator,
};
+var live_web_state: ?*WebState = null;
+
+// Browser snapshot tests drive real DOM touch input, then read the exact same
+// rendered Surface that WebGL consumed. These tiny exports keep the test
+// contract textual (the existing .snap convention) without adding test state
+// or browser concepts to the core.
+comptime {
+ if (is_emscripten) {
+ @export(&webTestCols, .{ .name = "pardes_web_test_cols" });
+ @export(&webTestRows, .{ .name = "pardes_web_test_rows" });
+ @export(&webTestCursorX, .{ .name = "pardes_web_test_cursor_x" });
+ @export(&webTestCursorY, .{ .name = "pardes_web_test_cursor_y" });
+ @export(&webTestCell, .{ .name = "pardes_web_test_cell" });
+ @export(&webTestCellFg, .{ .name = "pardes_web_test_cell_fg" });
+ @export(&webTestCellBg, .{ .name = "pardes_web_test_cell_bg" });
+ @export(&webTestCellAttrs, .{ .name = "pardes_web_test_cell_attrs" });
+ @export(&webTestCellX, .{ .name = "pardes_web_test_cell_x" });
+ @export(&webTestCellY, .{ .name = "pardes_web_test_cell_y" });
+ }
+}
+
+fn webTestCols() callconv(.c) c_uint {
+ const st = live_web_state orelse return 0;
+ return st.core.surface.cols;
+}
+
+fn webTestRows() callconv(.c) c_uint {
+ const st = live_web_state orelse return 0;
+ return st.core.surface.rows;
+}
+
+fn webTestCursorX() callconv(.c) c_int {
+ const st = live_web_state orelse return -1;
+ return if (st.core.surface.cursor) |cursor| cursor.x else -1;
+}
+
+fn webTestCursorY() callconv(.c) c_int {
+ const st = live_web_state orelse return -1;
+ return if (st.core.surface.cursor) |cursor| cursor.y else -1;
+}
+
+fn webTestCell(col: c_uint, row: c_uint) callconv(.c) c_uint {
+ const cell = webTestCellAt(col, row) orelse return ' ';
+ const glyph = cell.grapheme();
+ if (glyph.len == 0) return ' ';
+ return std.unicode.utf8Decode(glyph) catch 0xfffd;
+}
+
+// RGB occupies 0x000000-0xffffff; the high byte distinguishes the two other
+// Color tags. The browser .snap harness renders these into d/pN/#rrggbb runs.
+fn webTestCellFg(col: c_uint, row: c_uint) callconv(.c) c_uint {
+ const cell = webTestCellAt(col, row) orelse return 0x01000000;
+ return webTestColor(cell.style.fg);
+}
+
+fn webTestCellBg(col: c_uint, row: c_uint) callconv(.c) c_uint {
+ const cell = webTestCellAt(col, row) orelse return 0x01000000;
+ return webTestColor(cell.style.bg);
+}
+
+fn webTestColor(color: pardes.Color) c_uint {
+ return switch (color) {
+ .default => 0x01000000,
+ .index => |idx| 0x02000000 | @as(c_uint, idx),
+ .rgb => |rgb| (@as(c_uint, rgb[0]) << 16) | (@as(c_uint, rgb[1]) << 8) | rgb[2],
+ };
+}
+
+fn webTestCellAttrs(col: c_uint, row: c_uint) callconv(.c) c_uint {
+ const cell = webTestCellAt(col, row) orelse return 0;
+ const style = cell.style;
+ var attrs: c_uint = 0;
+ if (style.bold) attrs |= 1 << 0;
+ if (style.dim) attrs |= 1 << 1;
+ if (style.italic) attrs |= 1 << 2;
+ if (style.blink) attrs |= 1 << 3;
+ if (style.reverse) attrs |= 1 << 4;
+ if (style.invisible) attrs |= 1 << 5;
+ if (style.strikethrough) attrs |= 1 << 6;
+ attrs |= @as(c_uint, @intFromEnum(style.ul)) << 8;
+ return attrs;
+}
+
+fn webTestCellAt(col: c_uint, row: c_uint) ?*const pardes.Cell {
+ const st = live_web_state orelse return null;
+ const surface = &st.core.surface;
+ if (col >= surface.cols or row >= surface.rows) return null;
+ return surface.at(@intCast(col), @intCast(row));
+}
+
+fn webTestCellX(col: f32) callconv(.c) f32 {
+ return webTestCellCenter(col, true);
+}
+
+fn webTestCellY(row: f32) callconv(.c) f32 {
+ return webTestCellCenter(row, false);
+}
+
+fn webTestCellCenter(cell: f32, horizontal: bool) f32 {
+ const st = live_web_state orelse return 0;
+ var pixel_w: c_int = 0;
+ var pixel_h: c_int = 0;
+ _ = c.SDL_GetWindowSizeInPixels(st.g.window, &pixel_w, &pixel_h);
+ const pw: u32 = @intCast(@max(pixel_w, 1));
+ const ph: u32 = @intCast(@max(pixel_h, 1));
+ var css_w: f64 = 0;
+ var css_h: f64 = 0;
+ if (emscripten_get_element_css_size("#canvas", &css_w, &css_h) != EMSCRIPTEN_RESULT_SUCCESS) return 0;
+ const layout = cellLayout(&st.g, st.core.surface.cols, st.core.surface.rows, pw, ph);
+ if (horizontal) {
+ const backing = layout.x_off + (cell + 0.5) * layout.w;
+ return backing * @as(f32, @floatCast(css_w)) / @as(f32, @floatFromInt(pw));
+ }
+ const backing = layout.y_off + (cell + 0.5) * layout.h;
+ return backing * @as(f32, @floatCast(css_h)) / @as(f32, @floatFromInt(ph));
+}
fn runWeb(opts_in: pardes.Options) !void {
web_fba = .init(&web_heap);
const gpa = web_fba.allocator();
+ // Our finger machine deliberately maps touch to scroll/LOOK. Suppress
+ // SDL's default synthetic left mouse event for the same browser touch.
+ _ = c.SDL_SetHint(c.SDL_HINT_TOUCH_MOUSE_EVENTS, "0");
if (!c.SDL_Init(c.SDL_INIT_VIDEO)) { // no gamepad in the browser
log.err("SDL_Init: {s}", .{c.SDL_GetError()});
return error.SdlInit;
@@ -900,6 +1203,7 @@ fn runWeb(opts_in: pardes.Options) !void {
opts.cols = @intCast(@max(1, @divTrunc(@as(u32, @intCast(@max(pw, 1))), cell_w)));
opts.rows = @intCast(@max(1, @divTrunc(@as(u32, @intCast(@max(ph, 1))), cell_h)));
st.core = try pardes.Pardes.initFromDump(gpa, opts, embedded_dump);
+ live_web_state = st;
_ = c.SDL_StartTextInput(window);
std.os.emscripten.emscripten_set_main_loop_arg(webFrame, st, 0, 0);
@@ -927,7 +1231,7 @@ fn webFrame(arg: ?*anyopaque) callconv(.c) void {
// 4. render
_ = st.frame_arena.reset(.retain_capacity);
const surface = core.render(st.frame_arena.allocator()) catch return;
- renderFrameGl(g, st.gpa, surface) catch return;
+ renderFrameGl(g, st.gpa, surface, core.show_debug) catch return;
}
/// the canvas follows the page (CSS); the SDL window follows the canvas,
@@ -1382,7 +1686,7 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void {
}
if (is_emscripten) {
// remap normalized coords into the letterboxed grid so the
- // shared two-finger machine's cell math stays exact
+ // shared touch machine's cell math stays exact
const layout = cellLayout(g, core.screen_w, core.screen_h, @intCast(@max(pw, 1)), @intCast(@max(ph, 1)));
const grid_w = @max(1.0, @as(f32, @floatFromInt(core.screen_w)) * layout.w);
const grid_h = @max(1.0, @as(f32, @floatFromInt(core.screen_h)) * layout.h);
@@ -1772,7 +2076,7 @@ fn openLinkWeb(gpa: std.mem.Allocator, url: []const u8) void {
// render: Surface → instanced quads → SDL GPU
// =====================================================================
-fn renderFrame(g: *Gui, gpa: std.mem.Allocator, surface: *pardes.Surface, crt_on: bool) !void {
+fn renderFrame(g: *Gui, gpa: std.mem.Allocator, surface: *pardes.Surface, crt_on: bool, debug_on: bool) !void {
const cmd = c.SDL_AcquireGPUCommandBuffer(g.device) orelse return;
var sw: u32 = 0;
var sh: u32 = 0;
@@ -1806,7 +2110,7 @@ fn renderFrame(g: *Gui, gpa: std.mem.Allocator, surface: *pardes.Surface, crt_on
if (crt_on) try ensureSceneTexture(g, sw, sh);
const scene: *c.SDL_GPUTexture = if (crt_on) g.scene_tex.? else target;
- const overlay_count = buildTouchOverlay(g, sw, sh);
+ const overlay_count = buildTouchOverlay(g, sw, sh, debug_on);
if (overlay_count != 0) uploadOverlayGpu(g, cmd, overlay_count);
const cells: u32 = @as(u32, surface.cols) * surface.rows;
@@ -2135,7 +2439,7 @@ fn makeShader(device: *c.SDL_GPUDevice, code: []const u8, stage: c.SDL_GPUShader
// re-upload each frame; attribs re-specified per draw (no VAO to manage).
// =====================================================================
-fn renderFrameGl(g: *Gui, gpa: std.mem.Allocator, surface: *pardes.Surface) !void {
+fn renderFrameGl(g: *Gui, gpa: std.mem.Allocator, surface: *pardes.Surface, debug_on: bool) !void {
var pw: c_int = 0;
var ph: c_int = 0;
_ = c.SDL_GetWindowSizeInPixels(g.window, &pw, &ph);
@@ -2150,7 +2454,7 @@ fn renderFrameGl(g: *Gui, gpa: std.mem.Allocator, surface: *pardes.Surface) !voi
);
c.glClear(c.GL_COLOR_BUFFER_BIT);
- const overlay_count = buildTouchOverlay(g, sw, sh);
+ const overlay_count = buildTouchOverlay(g, sw, sh, debug_on);
const cells: u32 = @as(u32, surface.cols) * surface.rows;
if (cells == 0) {
drawOverlayGl(g, overlay_count);
@@ -2376,20 +2680,24 @@ fn writeCapturePpm(g: *Gui, gpa: std.mem.Allocator, pixels: []const u8, width: u
}
// =====================================================================
-// touch debug overlay: per-finger colored circles + trails + a middle-click
-// flash HUD, alpha-blended over the grid. Always active on touch input.
+// touch debug overlay: per-finger colored circles + trails + a click-action
+// flash HUD, alpha-blended over the grid only while the Debug builtin is on.
// =====================================================================
-fn buildTouchOverlay(g: *Gui, sw: u32, sh: u32) u32 {
+fn buildTouchOverlay(g: *Gui, sw: u32, sh: u32, debug_on: bool) u32 {
+ if (!debug_on) {
+ g.touch.click_flash_frames = 0;
+ return 0;
+ }
if (g.overlay_vertices.len == 0 or sw == 0 or sh == 0) return 0;
const win_w: f32 = @floatFromInt(sw);
const win_h: f32 = @floatFromInt(sh);
var builder = OverlayBuilder{ .vertices = g.overlay_vertices, .win_w = win_w, .win_h = win_h };
const t = &g.touch;
- if (t.middle_flash_frames != 0) {
- const flash = @as(f32, @floatFromInt(t.middle_flash_frames)) / @as(f32, @floatFromInt(touch_middle_flash_max_frames));
- const label = touchMiddleClickLabel(t.middle_clicks);
+ if (t.click_flash_frames != 0) {
+ const flash = @as(f32, @floatFromInt(t.click_flash_frames)) / @as(f32, @floatFromInt(touch_click_flash_max_frames));
+ const label = touchClickLabel(t.click_count, t.click_button);
const glyph_scale = 3.0;
const char_step = 4.0 * glyph_scale;
const pad = 5.0;
@@ -2400,7 +2708,7 @@ fn buildTouchOverlay(g: *Gui, sw: u32, sh: u32) u32 {
builder.addRect(8.0, 8.0, 8.0 + label_w, 8.0 + label_h, .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 0.54 + 0.22 * flash });
builder.addRect(8.0 + pad, 8.0 + pad, 8.0 + pad + stripe_w, 8.0 + label_h - pad, .{ .r = 1.0, .g = 0.78, .b = 0.18, .a = 0.96 });
builder.addMiniText(label[0..], 8.0 + pad + stripe_w + 3.0, 8.0 + pad, glyph_scale, .{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 0.96 });
- t.middle_flash_frames -= 1;
+ t.click_flash_frames -= 1;
}
for (&t.points) |*tp| {
@@ -2573,12 +2881,12 @@ fn touchIdLabel(id: u64) [9]u8 {
return out;
}
-fn touchMiddleClickLabel(count: u32) [13]u8 {
- var out: [13]u8 = undefined;
- @memcpy(out[0..5], "MID #");
+fn touchClickLabel(count: u32, button: pardes.Mouse.Button) [14]u8 {
+ var out: [14]u8 = undefined;
+ @memcpy(out[0..6], if (button == .right) "LOOK #" else "MID #");
for (0..8) |i| {
const shift: u5 = @intCast((7 - i) * 4);
- out[i + 5] = hexDigit((count >> shift) & 0xf);
+ out[i + 6] = hexDigit((count >> shift) & 0xf);
}
return out;
}
@@ -2607,7 +2915,10 @@ fn miniGlyph(ch: u8) [5]u8 {
'E' => .{ 0b111, 0b100, 0b111, 0b100, 0b111 },
'F' => .{ 0b111, 0b100, 0b111, 0b100, 0b100 },
'I' => .{ 0b111, 0b010, 0b010, 0b010, 0b111 },
+ 'K' => .{ 0b101, 0b101, 0b110, 0b101, 0b101 },
+ 'L' => .{ 0b100, 0b100, 0b100, 0b100, 0b111 },
'M' => .{ 0b101, 0b111, 0b111, 0b101, 0b101 },
+ 'O' => .{ 0b111, 0b101, 0b101, 0b101, 0b111 },
else => .{ 0, 0, 0, 0, 0 },
};
}