xinput_mapper/
utils.rs

1//! Extra, pure utility helpers for post-processing sticks, buttons, and masks.
2
3/* ============================== Stick Utilities ============================== */
4
5/// Apply a centered deadzone on an i16 axis.
6/// `deadzone` in [0..=32767]. Values within [-deadzone..deadzone] -> 0.
7/// Outside, linearly re-scale to full range to preserve reach.
8pub fn apply_deadzone_i16(v: i16, deadzone: i16) -> i16 {
9    let dz = deadzone.clamp(0, 32767);
10    let vi = v as i32;
11    let sign = vi.signum();
12    let mag = vi.abs();
13    if mag <= (dz as i32) {
14        0
15    } else {
16        let rem = 32767 - (dz as i32);
17        let adj = ((mag - (dz as i32)) * 32767) / rem.max(1);
18        (adj * sign).clamp(-32768, 32767) as i16
19    }
20}
21
22/// Optionally apply anti-deadzone (minimum magnitude).
23/// `anti` in [0..=32767].
24pub fn apply_anti_deadzone_i16(v: i16, anti: i16) -> i16 {
25    let anti = anti.clamp(0, 32767) as i32;
26    let vi = v as i32;
27    if vi == 0 || anti == 0 {
28        return v;
29    }
30    let sign = vi.signum();
31    let mag = vi.abs().clamp(0, 32767);
32    let boosted = mag.max(anti);
33    (boosted * sign).clamp(-32768, 32767) as i16
34}
35
36/// Convert square stick into circular region while preserving direction.
37pub fn square_to_circle(lx: i16, ly: i16) -> (i16, i16) {
38    let nx = (lx as f32) / 32767.0;
39    let ny = (ly as f32) / 32767.0;
40    let sx = (nx * (1.0 - 0.5 * ny * ny).sqrt()).clamp(-1.0, 1.0);
41    let sy = (ny * (1.0 - 0.5 * nx * nx).sqrt()).clamp(-1.0, 1.0);
42    ((sx * 32767.0).round() as i16, (sy * 32767.0).round() as i16)
43}
44
45/// Radial deadzone for a stick. `deadzone` in [0..=32767].
46pub fn apply_radial_deadzone(lx: i16, ly: i16, deadzone: i16) -> (i16, i16) {
47    let dz = deadzone.clamp(0, 32767) as f32;
48    let x = lx as f32;
49    let y = ly as f32;
50    let r = (x * x + y * y).sqrt();
51    if r <= dz {
52        return (0, 0);
53    }
54    let r2 = ((r - dz) * 32767.0) / (32767.0 - dz).max(1.0);
55    if r == 0.0 {
56        return (0, 0);
57    }
58    let scale = r2 / r;
59    let nx = (x * scale).clamp(-32767.0, 32767.0);
60    let ny = (y * scale).clamp(-32767.0, 32767.0);
61    (nx.round() as i16, ny.round() as i16)
62}
63
64/// Compose helpers for a typical stick pipeline.
65pub fn postprocess_stick(
66    lx: i16,
67    ly: i16,
68    use_circle: bool,
69    radial_deadzone_v: i16,
70    anti_deadzone_v: i16
71) -> (i16, i16) {
72    let (mut x, mut y) = if use_circle { square_to_circle(lx, ly) } else { (lx, ly) };
73    (x, y) = apply_radial_deadzone(x, y, radial_deadzone_v);
74    x = apply_anti_deadzone_i16(x, anti_deadzone_v);
75    y = apply_anti_deadzone_i16(y, anti_deadzone_v);
76    (x, y)
77}
78
79/// Trigger thresholding: turn trigger value [0..255] into boolean press.
80pub fn trigger_pressed(v: u8, thresh: u8) -> bool {
81    v >= thresh
82}
83
84/* ============================== Button Utilities ============================== */
85
86/// Compute edge masks between previous and current buttons.
87pub fn button_edges(prev: u16, curr: u16) -> (u16, u16) {
88    let changed = prev ^ curr;
89    let pressed = changed & curr; // bits that went 0->1
90    let released = changed & !curr; // bits that went 1->0
91    (pressed, released)
92}
93
94/// Debounce mask: require a stable `hold_count` frames before accepting changes.
95pub fn debounce_bit(prev_out: bool, curr_in: bool, counter: u8, hold_count: u8) -> (bool, u8) {
96    if curr_in == prev_out {
97        (prev_out, 0)
98    } else {
99        let c = counter.saturating_add(1);
100        if c >= hold_count {
101            (curr_in, 0)
102        } else {
103            (prev_out, c)
104        }
105    }
106}
107
108/// Auto-repeat helper: when a bit is held, fires at (first_delay_ms then repeat_rate_ms).
109pub fn autorepeat_fire(
110    is_down: bool,
111    elapsed_ms_since_change: u32,
112    first_delay_ms: u32,
113    repeat_rate_ms: u32
114) -> bool {
115    if !is_down {
116        return false;
117    }
118    if elapsed_ms_since_change == 0 {
119        return true;
120    }
121    if elapsed_ms_since_change < first_delay_ms {
122        return false;
123    }
124    let after = elapsed_ms_since_change - first_delay_ms;
125    repeat_rate_ms != 0 && after % repeat_rate_ms == 0
126}
127
128/// Build a new mask by applying a remap table (source->dest). Missing sources are ignored.
129/// `remap`: list of (src_mask_bit, dst_mask_bit).
130pub fn remap_buttons(mask: u16, remap: &[(u16, u16)]) -> u16 {
131    let mut out = 0u16;
132    for &(src, dst) in remap {
133        if (mask & src) != 0 {
134            out |= dst;
135        }
136    }
137    out
138}
139
140/// Merge a list of masks (e.g., macro/extra layers) into one via OR.
141pub fn merge_masks(masks: &[u16]) -> u16 {
142    masks.iter().fold(0u16, |acc, &m| acc | m)
143}
144
145/// Set/clear/toggle helpers
146pub fn set_bit(mask: u16, bit: u16, on: bool) -> u16 {
147    if on { mask | bit } else { mask & !bit }
148}
149pub fn toggle_bit(mask: u16, bit: u16) -> u16 {
150    mask ^ bit
151}