diff options
Diffstat (limited to 'src/gui.zig')
| -rw-r--r-- | src/gui.zig | 401 |
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 }, }; } |
