summaryrefslogtreecommitdiff
path: root/src/gui/deck.zig
blob: 1859bae80fcb8943a55bcc60208c0284f9f5339d (plain) (blame)
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
//! 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) 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");

// 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;
// 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;
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;
// 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, pressure: f32 },
};

pub const Click = enum { select, execute, look }; // mouse buttons 1/2/3
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
    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,
    rpad_click: bool = false, // firm-press click state, per pad (hysteresis)
    lpad_click: bool = false,

    pub fn feed(d: *Deck, in: Input, out: *[3]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;
                },
                // 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 } };
                    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[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,
                    },
                    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,
                    },
                    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;
                        }
                    } 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;
    }
};