1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
|
//! Steam Deck controls -> editor input, as a pure state machine: gui.zig
//! feeds SDL gamepad events in and applies the returned actions (cursor
//! 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.
const std = @import("std");
// SDL_GamepadButton / SDL_GamepadAxis / touchpad indices (SDL3), verified
// against real deck recordings (test/deck/*.zon) and the pinned SDL's
// hidapi deck driver (touchpad 0 = left pad, 1 = right pad).
pub const BTN_A = 0; // SOUTH
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;
pub const AXIS_L2 = 4; // LEFT_TRIGGER, analog 0..32767
pub const AXIS_R2 = 5; // RIGHT_TRIGGER
pub const PAD_LEFT = 0;
pub const PAD_RIGHT = 1;
/// feel knobs: a full-pad finger sweep covers this many screen pixels
/// (the deck's screen is 1280x800) / this many wheel ticks
pub const POINTER_SCALE: f32 = 600.0;
pub const SCROLL_SCALE: f32 = 24.0;
/// the pinned SDL 3.4.4 deck driver skips upstream's pad-Y negation, so
/// touch y arrives 0 = pad bottom; flip this when the sdl dep is bumped
pub const PAD_Y_SIGN: f32 = -1.0;
// analog triggers click at 30% and release at 20%: the gap keeps a value
// hovering near one threshold from chattering press/release pairs
pub const TRIG_PRESS: i16 = 9830;
pub const TRIG_RELEASE: i16 = 6553;
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 },
};
pub const Click = enum { select, execute, look }; // mouse buttons 1/2/3
pub const Keytap = enum { n, cap_n, enter, tab };
pub const Action = union(enum) {
move: struct { dx: f32, dy: f32 }, // pointer delta in screen pixels
click: struct { which: Click, down: bool },
wheel: i32, // whole ticks, positive = wheel_down
key: Keytap,
rumble,
};
pub const Deck = struct {
rpad_on: bool = false,
rpad_x: f32 = 0,
rpad_y: f32 = 0,
lpad_on: bool = false,
lpad_y: f32 = 0,
scroll: f32 = 0, // fractional wheel ticks carried between motions
l2_held: bool = false,
r2_held: bool = false,
look_held: bool = false,
pub fn feed(d: *Deck, in: Input, out: *[2]Action) usize {
var n: usize = 0;
switch (in) {
.button => |b| switch (b.idx) {
BTN_A => if (b.down) {
out[0] = .{ .key = .enter };
n = 1;
},
BTN_Y => if (b.down) {
out[0] = .{ .key = .tab };
n = 1;
},
BTN_DPAD_UP => if (b.down) {
out[0] = .{ .key = .cap_n };
n = 1;
},
BTN_DPAD_DOWN => if (b.down) {
out[0] = .{ .key = .n };
n = 1;
},
BTN_L1 => {
d.look_held = b.down;
out[0] = .{ .click = .{ .which = .look, .down = b.down } };
n = 1;
if (b.down) {
out[1] = .rumble;
n = 2;
}
},
else => {},
},
.axis => |a| {
if (a.idx != AXIS_L2 and a.idx != AXIS_R2) return 0;
const held = if (a.idx == AXIS_L2) &d.l2_held else &d.r2_held;
const which: Click = if (a.idx == AXIS_L2) .execute else .select;
if (!held.* and a.value >= TRIG_PRESS) {
held.* = true;
out[0] = .{ .click = .{ .which = which, .down = true } };
n = 1;
if (which == .execute) {
out[1] = .rumble;
n = 2;
}
} else if (held.* and a.value <= TRIG_RELEASE) {
held.* = false;
out[0] = .{ .click = .{ .which = which, .down = false } };
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;
},
.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;
},
.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;
}
},
.up => d.lpad_on = false,
},
else => {},
},
}
return n;
}
};
|