diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/deck.zig | 131 | ||||
| -rw-r--r-- | src/gui.zig | 48 |
2 files changed, 126 insertions, 53 deletions
diff --git a/src/deck.zig b/src/deck.zig index 365c420c..1859bae8 100644 --- a/src/deck.zig +++ b/src/deck.zig @@ -3,10 +3,13 @@ //! moves, synthetic clicks, wheel ticks, keys, rumble), so the whole //! mapping replays off-device from deckcap recordings (test/deck/). //! -//! Layout: right pad moves the pointer (relative, laptop-touchpad-style), -//! left pad scrolls, R2 = select (button 1), L2 = execute (button 2), -//! L1 = look (button 3), dpad down/up = n/N, A = Enter, Y = Tab; execute -//! and look presses also request a brief rumble ack. +//! Layout: right pad moves the pointer (relative, laptop-touchpad-style) and +//! a firm press on it = select (button 1); left pad scrolls and a firm press +//! = look (button 3); R2 = select, L2 = execute (button 2), L1 = look, dpad +//! down/up = n/N, A = Enter, Y = Tab, the back paddles (L4/R4/L5/R5) toggle +//! the terminal (Ctrl-b); execute, look and pad-clicks request a brief rumble +//! ack. (The GUI mirrors this on the sticks: right stick = pointer, left +//! stick = scroll.) const std = @import("std"); @@ -18,6 +21,11 @@ pub const BTN_Y = 3; // NORTH pub const BTN_L1 = 9; // LEFT_SHOULDER pub const BTN_DPAD_UP = 11; pub const BTN_DPAD_DOWN = 12; +// rear grip paddles (from a deckcap deckmap capture): the two-digit indices +pub const BTN_R4 = 16; +pub const BTN_L4 = 17; +pub const BTN_R5 = 18; +pub const BTN_L5 = 19; pub const AXIS_L2 = 4; // LEFT_TRIGGER, analog 0..32767 pub const AXIS_R2 = 5; // RIGHT_TRIGGER pub const PAD_LEFT = 0; @@ -34,15 +42,20 @@ pub const PAD_Y_SIGN: f32 = -1.0; // hovering near one threshold from chattering press/release pairs pub const TRIG_PRESS: i16 = 9830; pub const TRIG_RELEASE: i16 = 6553; +// a firm pad press (pressure 0..1) counts as a click, with the same release +// hysteresis. Real light-touch drags read ~0.03 in captures, so this is well +// clear of them; tune on-device against a real click capture. +pub const PAD_CLICK_PRESS: f32 = 0.10; +pub const PAD_CLICK_RELEASE: f32 = 0.06; pub const Input = union(enum) { button: struct { idx: u8, down: bool }, axis: struct { idx: u8, value: i16 }, - touch: struct { pad: u8, phase: enum { down, motion, up }, x: f32, y: f32 }, + touch: struct { pad: u8, phase: enum { down, motion, up }, x: f32, y: f32, pressure: f32 }, }; pub const Click = enum { select, execute, look }; // mouse buttons 1/2/3 -pub const Keytap = enum { n, cap_n, enter, tab }; +pub const Keytap = enum { n, cap_n, enter, tab, tty_toggle }; pub const Action = union(enum) { move: struct { dx: f32, dy: f32 }, // pointer delta in screen pixels @@ -62,8 +75,10 @@ pub const Deck = struct { l2_held: bool = false, r2_held: bool = false, look_held: bool = false, + rpad_click: bool = false, // firm-press click state, per pad (hysteresis) + lpad_click: bool = false, - pub fn feed(d: *Deck, in: Input, out: *[2]Action) usize { + pub fn feed(d: *Deck, in: Input, out: *[3]Action) usize { var n: usize = 0; switch (in) { .button => |b| switch (b.idx) { @@ -83,6 +98,11 @@ pub const Deck = struct { out[0] = .{ .key = .n }; n = 1; }, + // any back paddle toggles the terminal (Ctrl-b), applied in gui.zig + BTN_L4, BTN_R4, BTN_L5, BTN_R5 => if (b.down) { + out[0] = .{ .key = .tty_toggle }; + n = 1; + }, BTN_L1 => { d.look_held = b.down; out[0] = .{ .click = .{ .which = .look, .down = b.down } }; @@ -112,44 +132,71 @@ pub const Deck = struct { n = 1; } }, - .touch => |t| switch (t.pad) { - PAD_RIGHT => switch (t.phase) { - // a fresh touch only re-anchors — deltas start from the - // landing point, so the pointer never jumps to it - .down => { - d.rpad_on = true; - d.rpad_x = t.x; - d.rpad_y = t.y; + .touch => |t| { + switch (t.pad) { + PAD_RIGHT => switch (t.phase) { + // a fresh touch only re-anchors — deltas start from the + // landing point, so the pointer never jumps to it + .down => { + d.rpad_on = true; + d.rpad_x = t.x; + d.rpad_y = t.y; + }, + .motion => if (d.rpad_on) { + out[n] = .{ .move = .{ + .dx = (t.x - d.rpad_x) * POINTER_SCALE, + .dy = (t.y - d.rpad_y) * PAD_Y_SIGN * POINTER_SCALE, + } }; + n += 1; + d.rpad_x = t.x; + d.rpad_y = t.y; + }, + .up => d.rpad_on = false, }, - .motion => if (d.rpad_on) { - out[0] = .{ .move = .{ - .dx = (t.x - d.rpad_x) * POINTER_SCALE, - .dy = (t.y - d.rpad_y) * PAD_Y_SIGN * POINTER_SCALE, - } }; - n = 1; - d.rpad_x = t.x; - d.rpad_y = t.y; + PAD_LEFT => switch (t.phase) { + .down => { + d.lpad_on = true; + d.lpad_y = t.y; + }, + .motion => if (d.lpad_on) { + d.scroll += (t.y - d.lpad_y) * PAD_Y_SIGN * SCROLL_SCALE; + d.lpad_y = t.y; + const ticks: i32 = @intFromFloat(d.scroll); + if (ticks != 0) { + d.scroll -= @floatFromInt(ticks); + out[n] = .{ .wheel = ticks }; + n += 1; + } + }, + .up => d.lpad_on = false, }, - .up => d.rpad_on = false, - }, - PAD_LEFT => switch (t.phase) { - .down => { - d.lpad_on = true; - d.lpad_y = t.y; - }, - .motion => if (d.lpad_on) { - d.scroll += (t.y - d.lpad_y) * PAD_Y_SIGN * SCROLL_SCALE; - d.lpad_y = t.y; - const ticks: i32 = @intFromFloat(d.scroll); - if (ticks != 0) { - d.scroll -= @floatFromInt(ticks); - out[0] = .{ .wheel = ticks }; - n = 1; + else => {}, + } + // a firm press on either pad is a click: right pad -> select + // (left button), left pad -> look (right button). The press + // rumbles as click feedback, mirroring the stock driver (SDL + // exposes no per-pad haptic, so it's a short whole-pad buzz). + if (t.pad == PAD_RIGHT or t.pad == PAD_LEFT) { + const which: Click = if (t.pad == PAD_RIGHT) .select else .look; + const held = if (t.pad == PAD_RIGHT) &d.rpad_click else &d.lpad_click; + if (t.phase == .up) { + if (held.*) { + held.* = false; + out[n] = .{ .click = .{ .which = which, .down = false } }; + n += 1; } - }, - .up => d.lpad_on = false, - }, - else => {}, + } else if (!held.* and t.pressure >= PAD_CLICK_PRESS) { + held.* = true; + out[n] = .{ .click = .{ .which = which, .down = true } }; + n += 1; + out[n] = .rumble; + n += 1; + } else if (held.* and t.pressure <= PAD_CLICK_RELEASE) { + held.* = false; + out[n] = .{ .click = .{ .which = which, .down = false } }; + n += 1; + } + } }, } return n; diff --git a/src/gui.zig b/src/gui.zig index aba4f7a2..a53dfa24 100644 --- a/src/gui.zig +++ b/src/gui.zig @@ -448,7 +448,8 @@ const Gui = struct { gamepad: ?*c.SDL_Gamepad = null, pad_x: f32 = 0, pad_y: f32 = 0, - pad_scroll: f32 = 0, + pad_scroll: f32 = 0, // fractional vertical wheel ticks (left stick) + pad_scroll_h: f32 = 0, // fractional horizontal wheel ticks (left stick) deck: deck.Deck = .{}, // web: WebGL2 handles (plain GL ints) + the CPU-side instance staging @@ -506,6 +507,16 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { return error.SdlInit; } + // The trackpads only exist through SDL's built-in HIDAPI Steam Deck + // driver (it registers the two touchpads and disables "lizard mode"). + // It's default-on on Linux; pin it so intent is explicit. NOTE: this is + // NOT enough in Game Mode — Steam Input there hands the app a + // touchpad-less virtual gamepad instead of the real Neptune controller, + // so the pads go dead no matter what the app does. The only fix is to + // set "Disable Steam Input" on pardes (Steam -> Properties -> Controller), + // after which the real controller enumerates and the touchpad events flow. + _ = c.SDL_SetHint("SDL_JOYSTICK_HIDAPI", "1"); // SDL_HINT_JOYSTICK_HIDAPI + _ = c.SDL_SetHint("SDL_JOYSTICK_HIDAPI_STEAMDECK", "1"); // ..._STEAMDECK if (!c.SDL_Init(c.SDL_INIT_VIDEO | c.SDL_INIT_GAMEPAD)) { log.err("SDL_Init: {s}", .{c.SDL_GetError()}); return error.SdlInit; @@ -1423,9 +1434,10 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void { }, .x = sev.gtouchpad.x, .y = sev.gtouchpad.y, + .pressure = sev.gtouchpad.pressure, } }, }; - var acts: [2]deck.Action = undefined; + var acts: [3]deck.Action = undefined; for (acts[0..g.deck.feed(in, &acts)]) |act| switch (act) { .move => |mv| { var pw: c_int = 0; @@ -1442,7 +1454,8 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void { const mc = mouseCell(g, core, g.pad_x, g.pad_y); if (mc.col == old.col and mc.row == old.row) continue; // moving with a click held drags, so selections stretch - const held: ?pardes.Mouse.Button = if (g.deck.r2_held) + // (a firm right-pad press drags-selects like a laptop pad) + const held: ?pardes.Mouse.Button = if (g.deck.r2_held or g.deck.rpad_click) .left else if (g.deck.l2_held) .middle @@ -1488,6 +1501,8 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void { .cap_n => .{ .cp = 'N', .text = "N" }, .enter => .{ .cp = pardes.Key.enter }, .tab => .{ .cp = pardes.Key.tab }, + // back paddle: Ctrl-<configured toggle key> flips tty mode + .tty_toggle => .{ .cp = core.opts.tty_toggle, .ctrl = true }, } }), // a brief gentle ack for execute/look, not a buzz .rumble => if (g.gamepad) |pad| { @@ -1574,9 +1589,10 @@ fn mouseCell(g: *const Gui, core: *const pardes.Pardes, x: f32, y: f32) struct { }; } -/// steamdeck support: poll the sticks each frame. Left stick moves a virtual -/// cursor at ~cell granularity (emits button-less motion so hover works), -/// right stick Y accumulates into wheel ticks at the cursor position. +/// steamdeck support: poll the sticks each frame, mirroring the trackpads — +/// RIGHT stick moves the virtual cursor at ~cell granularity (emits +/// button-less motion so hover works), LEFT stick accumulates into wheel +/// ticks (both axes: vertical + horizontal) at the cursor position. fn pollGamepad(g: *Gui, core: *pardes.Pardes) void { const pad = g.gamepad orelse return; var pw: c_int = 0; @@ -1585,10 +1601,12 @@ fn pollGamepad(g: *Gui, core: *pardes.Pardes) void { const win_w: f32 = @floatFromInt(@max(pw, 1)); const win_h: f32 = @floatFromInt(@max(ph, 1)); const deadzone: f32 = 8000; - const ax: f32 = @floatFromInt(c.SDL_GetGamepadAxis(pad, c.SDL_GAMEPAD_AXIS_LEFTX)); - const ay: f32 = @floatFromInt(c.SDL_GetGamepadAxis(pad, c.SDL_GAMEPAD_AXIS_LEFTY)); + // right stick = pointer + const ax: f32 = @floatFromInt(c.SDL_GetGamepadAxis(pad, c.SDL_GAMEPAD_AXIS_RIGHTX)); + const ay: f32 = @floatFromInt(c.SDL_GetGamepadAxis(pad, c.SDL_GAMEPAD_AXIS_RIGHTY)); const old = mouseCell(g, core, g.pad_x, g.pad_y); - // full tilt ≈ 0.4 cell-heights per frame: a gentle, aimable glide + // full tilt ≈ 0.4 cell-heights per frame: a gentle, aimable glide (the + // mouse-move sensitivity knob — raise for a faster pointer) const speed: f32 = @as(f32, @floatFromInt(g.cell_h)) * 0.4; if (@abs(ax) > deadzone) g.pad_x = std.math.clamp(g.pad_x + ax / 32767.0 * speed, 0, win_w - 1); if (@abs(ay) > deadzone) g.pad_y = std.math.clamp(g.pad_y + ay / 32767.0 * speed, 0, win_h - 1); @@ -1598,13 +1616,21 @@ fn pollGamepad(g: *Gui, core: *pardes.Pardes) void { const mc = mouseCell(g, core, g.pad_x, g.pad_y); if (mc.col != old.col or mc.row != old.row) core.update(.{ .mouse = .{ .button = .none, .kind = .motion, .col = mc.col, .row = mc.row } }); - const ry: f32 = @floatFromInt(c.SDL_GetGamepadAxis(pad, c.SDL_GAMEPAD_AXIS_RIGHTY)); - if (@abs(ry) > deadzone) g.pad_scroll += ry / 32767.0 * 0.15; + // left stick = scroll (both axes) + const lx: f32 = @floatFromInt(c.SDL_GetGamepadAxis(pad, c.SDL_GAMEPAD_AXIS_LEFTX)); + const ly: f32 = @floatFromInt(c.SDL_GetGamepadAxis(pad, c.SDL_GAMEPAD_AXIS_LEFTY)); + if (@abs(ly) > deadzone) g.pad_scroll += ly / 32767.0 * 0.15; + if (@abs(lx) > deadzone) g.pad_scroll_h += lx / 32767.0 * 0.15; while (@abs(g.pad_scroll) >= 1.0) { const button: pardes.Mouse.Button = if (g.pad_scroll < 0) .wheel_up else .wheel_down; g.pad_scroll += if (g.pad_scroll < 0) 1.0 else -1.0; core.update(.{ .mouse = .{ .button = button, .kind = .press, .col = mc.col, .row = mc.row } }); } + while (@abs(g.pad_scroll_h) >= 1.0) { + const button: pardes.Mouse.Button = if (g.pad_scroll_h < 0) .wheel_left else .wheel_right; + g.pad_scroll_h += if (g.pad_scroll_h < 0) 1.0 else -1.0; + core.update(.{ .mouse = .{ .button = button, .kind = .press, .col = mc.col, .row = mc.row } }); + } } // ===================================================================== |
