xinput_mapper/
utils.rs

1//! Extra, pure utility helpers for post-processing sticks, buttons, and masks.
2//! - Backward-compatible with previous helpers
3//! - Adds outer-deadzone, response curve, hysteresis trigger, jitter-tolerant autorepeat
4
5/* ============================== Stick Utilities ============================== */
6
7/// Apply a centered deadzone on an i16 axis.
8/// `deadzone` in [0..=32767]. Values within [-deadzone..deadzone] -> 0.
9/// Outside, linearly re-scale to full range to preserve reach.
10pub fn apply_deadzone_i16(v: i16, deadzone: i16) -> i16 {
11    let dz = deadzone.clamp(0, 32767);
12    let vi = v as i32;
13    let sign = vi.signum();
14    let mag = vi.unsigned_abs() as i32;
15    if mag <= (dz as i32) {
16        0
17    } else {
18        let rem = 32767 - (dz as i32);
19        let adj = ((mag - (dz as i32)) * 32767) / rem.max(1);
20        (adj * sign).clamp(-32768, 32767) as i16
21    }
22}
23
24/// Apply an *outer* deadzone (a.k.a. saturation zone) to compress near extremes.
25/// `outer` in [0..=32767]. If outer>0, map [0..(32767-outer)] -> [0..32767].
26pub fn apply_outer_deadzone_i16(v: i16, outer: i16) -> i16 {
27    let outer = outer.clamp(0, 32767) as i32;
28    if outer == 0 {
29        return v;
30    }
31    let vi = v as i32;
32    let sign = vi.signum();
33    let mag = vi.unsigned_abs() as i32;
34    let lim = 32767 - outer;
35    let rem = lim.max(1);
36    let scaled = (mag.min(lim) * 32767) / rem;
37    (scaled * sign).clamp(-32768, 32767) as i16
38}
39
40/// Apply a symmetric gamma-like response curve on an i16 axis.
41/// `gamma` > 0 (typical range 0.5..3.0). gamma>1: softer center, stronger end; gamma<1: more sensitive center.
42pub fn apply_response_curve_i16(v: i16, gamma: f32) -> i16 {
43    if gamma <= 0.0 {
44        return v;
45    }
46    let n = (v as f32) / 32767.0; // [-1,1]
47    let sign = if n >= 0.0 { 1.0 } else { -1.0 };
48    let mag = n.abs().min(1.0);
49    let curved = mag.powf(gamma) * sign;
50    (curved * 32767.0).round().clamp(-32768.0, 32767.0) as i16
51}
52
53/// Convert square stick into circular region while preserving direction.
54pub fn square_to_circle(lx: i16, ly: i16) -> (i16, i16) {
55    let nx = (lx as f32) / 32767.0;
56    let ny = (ly as f32) / 32767.0;
57    let sx = (nx * (1.0 - 0.5 * ny * ny).sqrt()).clamp(-1.0, 1.0);
58    let sy = (ny * (1.0 - 0.5 * nx * nx).sqrt()).clamp(-1.0, 1.0);
59    ((sx * 32767.0).round() as i16, (sy * 32767.0).round() as i16)
60}
61
62/// Radial deadzone for a stick. `deadzone` in [0..=32767].
63pub fn apply_radial_deadzone(lx: i16, ly: i16, deadzone: i16) -> (i16, i16) {
64    let dz = deadzone.clamp(0, 32767) as f32;
65    let x = lx as f32;
66    let y = ly as f32;
67    let r = (x * x + y * y).sqrt();
68    if r <= dz {
69        return (0, 0);
70    }
71    let r2 = ((r - dz) * 32767.0) / (32767.0 - dz).max(1.0);
72    if r == 0.0 {
73        return (0, 0);
74    }
75    let scale = r2 / r;
76    let nx = (x * scale).clamp(-32767.0, 32767.0);
77    let ny = (y * scale).clamp(-32767.0, 32767.0);
78    (nx.round() as i16, ny.round() as i16)
79}
80
81/// Compose helpers for a typical stick pipeline (backward-compatible).
82pub fn postprocess_stick(
83    lx: i16,
84    ly: i16,
85    use_circle: bool,
86    radial_deadzone_v: i16,
87    anti_deadzone_v: i16
88) -> (i16, i16) {
89    let (mut x, mut y) = if use_circle { square_to_circle(lx, ly) } else { (lx, ly) };
90    (x, y) = apply_radial_deadzone(x, y, radial_deadzone_v);
91    x = apply_anti_deadzone_i16(x, anti_deadzone_v);
92    y = apply_anti_deadzone_i16(y, anti_deadzone_v);
93    (x, y)
94}
95
96/// Extended pipeline with outer-deadzone and response curve (new API).
97pub fn postprocess_stick_ex(
98    lx: i16,
99    ly: i16,
100    use_circle: bool,
101    radial_deadzone_v: i16,
102    anti_deadzone_v: i16,
103    outer_deadzone_v: i16,
104    response_gamma: f32
105) -> (i16, i16) {
106    let (mut x, mut y) = if use_circle { square_to_circle(lx, ly) } else { (lx, ly) };
107    (x, y) = apply_radial_deadzone(x, y, radial_deadzone_v);
108    x = apply_anti_deadzone_i16(x, anti_deadzone_v);
109    y = apply_anti_deadzone_i16(y, anti_deadzone_v);
110    x = apply_outer_deadzone_i16(x, outer_deadzone_v);
111    y = apply_outer_deadzone_i16(y, outer_deadzone_v);
112    if response_gamma != 1.0 {
113        x = apply_response_curve_i16(x, response_gamma);
114        y = apply_response_curve_i16(y, response_gamma);
115    }
116    (x, y)
117}
118
119/// Optionally apply anti-deadzone (minimum magnitude).
120/// `anti` in [0..=32767].
121pub fn apply_anti_deadzone_i16(v: i16, anti: i16) -> i16 {
122    let anti = anti.clamp(0, 32767) as i32;
123    let vi = v as i32;
124    if vi == 0 || anti == 0 {
125        return v;
126    }
127    let sign = vi.signum();
128    let mag = vi.unsigned_abs() as i32;
129    let boosted = mag.max(anti);
130    (boosted * sign).clamp(-32768, 32767) as i16
131}
132
133/// Trigger thresholding: turn trigger value [0..255] into boolean press.
134pub fn trigger_pressed(v: u8, thresh: u8) -> bool {
135    v >= thresh
136}
137
138/// Trigger with hysteresis: returns new state given last state and two thresholds.
139/// - When currently released: becomes pressed if v >= press_thresh.
140/// - When currently pressed: stays pressed until v < release_thresh.
141pub fn trigger_hysteresis(last_pressed: bool, v: u8, press_thresh: u8, release_thresh: u8) -> bool {
142    if last_pressed { v >= release_thresh } else { v >= press_thresh }
143}
144
145/* ============================== Button Utilities ============================== */
146
147/// Compute edge masks between previous and current buttons.
148pub fn button_edges(prev: u16, curr: u16) -> (u16, u16) {
149    let changed = prev ^ curr;
150    let pressed = changed & curr; // bits that went 0->1
151    let released = changed & !curr; // bits that went 1->0
152    (pressed, released)
153}
154
155/// Debounce mask: require a stable `hold_count` frames before accepting changes.
156/// - `prev_out`: previous debounced output bit
157/// - `curr_in`: current raw input bit
158/// - `counter`: internal frame counter (call-site keeps one per bit)
159/// Returns: (new_out_bit, new_counter)
160pub fn debounce_bit(prev_out: bool, curr_in: bool, counter: u8, hold_count: u8) -> (bool, u8) {
161    if curr_in == prev_out {
162        (prev_out, 0)
163    } else {
164        let c = counter.saturating_add(1);
165        if c >= hold_count {
166            (curr_in, 0)
167        } else {
168            (prev_out, c)
169        }
170    }
171}
172
173/// Autorepeat helper (legacy): when a bit is held, fires at
174/// (first_delay_ms then repeat_rate_ms). Uses modulo and can miss if jittery polls.
175pub fn autorepeat_fire(
176    is_down: bool,
177    elapsed_ms_since_change: u32,
178    first_delay_ms: u32,
179    repeat_rate_ms: u32
180) -> bool {
181    if !is_down {
182        return false;
183    }
184    if elapsed_ms_since_change == 0 {
185        return true;
186    }
187    if elapsed_ms_since_change < first_delay_ms {
188        return false;
189    }
190    let after = elapsed_ms_since_change - first_delay_ms;
191    repeat_rate_ms != 0 && after % repeat_rate_ms == 0
192}
193
194/// Autorepeat helper (jitter-tolerant): fire if the time since last fire
195/// crosses the next repeat boundary within a small window.
196/// - `is_down`: key/button held
197/// - `elapsed_down_ms`: time since became down
198/// - `elapsed_since_last_fire_ms`: time since the last generated fire (0 at initial down)
199/// - `first_delay_ms`: initial delay before repeating
200/// - `repeat_rate_ms`: cadence for subsequent fires
201/// - `window_ms`: allowed jitter window (e.g., 5~16ms)
202pub fn autorepeat_fire_window(
203    is_down: bool,
204    elapsed_down_ms: u32,
205    elapsed_since_last_fire_ms: u32,
206    first_delay_ms: u32,
207    repeat_rate_ms: u32,
208    window_ms: u32
209) -> bool {
210    if !is_down {
211        return false;
212    }
213    if elapsed_down_ms == 0 {
214        return true;
215    } // initial press
216    if elapsed_down_ms < first_delay_ms {
217        return false;
218    }
219    if repeat_rate_ms == 0 {
220        return false;
221    }
222
223    // Compute target interval since last fire.
224    // If we recently fired, we expect next in `repeat_rate_ms`.
225    // Otherwise, at the very first repeat, we accept when elapsed_down_ms - first_delay_ms hits near multiples.
226    let target = repeat_rate_ms;
227    let e = elapsed_since_last_fire_ms;
228    // Fire once when we cross the boundary within [0..window_ms]
229    e >= target && e.saturating_sub(target) <= window_ms
230}
231
232/// Build a new mask by applying a remap table (source->dest). Missing sources are ignored.
233/// `remap`: list of (src_mask_bit, dst_mask_bit).
234pub fn remap_buttons(mask: u16, remap: &[(u16, u16)]) -> u16 {
235    let mut out = 0u16;
236    for &(src, dst) in remap {
237        if (mask & src) != 0 {
238            out |= dst;
239        }
240    }
241    out
242}
243
244/// Merge a list of masks (e.g., macro/extra layers) into one via OR.
245pub fn merge_masks(masks: &[u16]) -> u16 {
246    masks.iter().fold(0u16, |acc, &m| acc | m)
247}
248
249/// Set/clear/toggle helpers
250pub fn set_bit(mask: u16, bit: u16, on: bool) -> u16 {
251    if on { mask | bit } else { mask & !bit }
252}
253pub fn toggle_bit(mask: u16, bit: u16) -> u16 {
254    mask ^ bit
255}
256
257/* ============================== Convenience =============================== */
258
259/// Map u8 [0..255] through an anti-deadzone (minimum) and optional linear outer-deadzone.
260/// `anti` and `outer` in [0..=255].
261pub fn apply_u8_zones(v: u8, anti: u8, outer: u8) -> u8 {
262    let mut out = v as i32;
263    if out > 0 && anti > 0 {
264        out = out.max(anti as i32);
265    }
266    if outer > 0 {
267        let lim = 255 - (outer as i32);
268        out = (out.min(lim) * 255) / lim.max(1);
269    }
270    out.clamp(0, 255) as u8
271}