summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-09 10:41:49 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commitf948213755d6cc1920be1794149644108c6a3f30 (patch)
tree31c7f9d35841ffa33cef25fc985ed63ce554bc6c /src
parenta99f039bc2ce01302d6f6c5fe42e260950870008 (diff)
downloadpardes-f948213755d6cc1920be1794149644108c6a3f30.tar.gz
pardes-f948213755d6cc1920be1794149644108c6a3f30.zip
steam deck feeback 1: adjustments
Diffstat (limited to 'src')
-rw-r--r--src/deck.zig131
-rw-r--r--src/gui.zig48
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 } });
+ }
}
// =====================================================================