1
2use glam::{Vec2, Vec4};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum InputDevice {
14 Keyboard,
15 Mouse,
16 GamepadXbox,
17 GamepadPlayStation,
18 GamepadSwitch,
19 GamepadGeneric,
20 Joystick,
21 Wheel,
22 HOTAS,
23 TouchScreen,
24 Pen,
25 Midi,
26 Custom,
27}
28
29impl InputDevice {
30 pub fn label(&self) -> &'static str {
31 match self {
32 Self::Keyboard => "Keyboard",
33 Self::Mouse => "Mouse",
34 Self::GamepadXbox => "Xbox Controller",
35 Self::GamepadPlayStation => "PlayStation Controller",
36 Self::GamepadSwitch => "Switch Pro Controller",
37 Self::GamepadGeneric => "Generic Gamepad",
38 Self::Joystick => "Joystick",
39 Self::Wheel => "Racing Wheel",
40 Self::HOTAS => "HOTAS",
41 Self::TouchScreen => "Touch Screen",
42 Self::Pen => "Pen/Stylus",
43 Self::Midi => "MIDI Controller",
44 Self::Custom => "Custom Device",
45 }
46 }
47 pub fn supports_rumble(&self) -> bool {
48 matches!(self, Self::GamepadXbox | Self::GamepadPlayStation | Self::GamepadSwitch | Self::GamepadGeneric | Self::Wheel)
49 }
50 pub fn has_analog_sticks(&self) -> bool {
51 matches!(self, Self::GamepadXbox | Self::GamepadPlayStation | Self::GamepadSwitch | Self::GamepadGeneric | Self::Joystick | Self::HOTAS)
52 }
53 pub fn has_gyro(&self) -> bool {
54 matches!(self, Self::GamepadPlayStation | Self::GamepadSwitch)
55 }
56 pub fn has_touchpad(&self) -> bool {
57 matches!(self, Self::GamepadPlayStation)
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
66pub enum KeyCode {
67 A, B, C, D, E, F, G, H, I, J, K, L, M,
68 N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
69 Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9,
70 F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12,
71 F13, F14, F15, F16, F17, F18, F19, F20,
72 Space, Return, Escape, Backspace, Tab, CapsLock,
73 LShift, RShift, LCtrl, RCtrl, LAlt, RAlt, LSuper, RSuper,
74 Up, Down, Left, Right,
75 Insert, Delete, Home, End, PageUp, PageDown,
76 Numpad0, Numpad1, Numpad2, Numpad3, Numpad4,
77 Numpad5, Numpad6, Numpad7, Numpad8, Numpad9,
78 NumpadAdd, NumpadSub, NumpadMul, NumpadDiv, NumpadEnter, NumpadDecimal,
79 NumLock, ScrollLock, PrintScreen, Pause, Menu,
80 BracketLeft, BracketRight, Semicolon, Quote, Comma, Period, Slash, Backslash,
81 Grave, Minus, Equals,
82}
83
84impl KeyCode {
85 pub fn label(&self) -> &'static str {
86 match self {
87 Self::A => "A", Self::B => "B", Self::C => "C", Self::D => "D",
88 Self::E => "E", Self::F => "F", Self::G => "G", Self::H => "H",
89 Self::I => "I", Self::J => "J", Self::K => "K", Self::L => "L",
90 Self::M => "M", Self::N => "N", Self::O => "O", Self::P => "P",
91 Self::Q => "Q", Self::R => "R", Self::S => "S", Self::T => "T",
92 Self::U => "U", Self::V => "V", Self::W => "W", Self::X => "X",
93 Self::Y => "Y", Self::Z => "Z",
94 Self::Num0 => "0", Self::Num1 => "1", Self::Num2 => "2",
95 Self::Num3 => "3", Self::Num4 => "4", Self::Num5 => "5",
96 Self::Num6 => "6", Self::Num7 => "7", Self::Num8 => "8", Self::Num9 => "9",
97 Self::F1 => "F1", Self::F2 => "F2", Self::F3 => "F3", Self::F4 => "F4",
98 Self::F5 => "F5", Self::F6 => "F6", Self::F7 => "F7", Self::F8 => "F8",
99 Self::F9 => "F9", Self::F10 => "F10", Self::F11 => "F11", Self::F12 => "F12",
100 Self::F13 => "F13", Self::F14 => "F14", Self::F15 => "F15", Self::F16 => "F16",
101 Self::F17 => "F17", Self::F18 => "F18", Self::F19 => "F19", Self::F20 => "F20",
102 Self::Space => "Space", Self::Return => "Enter", Self::Escape => "Escape",
103 Self::Backspace => "Backspace", Self::Tab => "Tab", Self::CapsLock => "Caps Lock",
104 Self::LShift => "L.Shift", Self::RShift => "R.Shift",
105 Self::LCtrl => "L.Ctrl", Self::RCtrl => "R.Ctrl",
106 Self::LAlt => "L.Alt", Self::RAlt => "R.Alt",
107 Self::LSuper => "L.Super", Self::RSuper => "R.Super",
108 Self::Up => "Up", Self::Down => "Down", Self::Left => "Left", Self::Right => "Right",
109 Self::Insert => "Insert", Self::Delete => "Delete",
110 Self::Home => "Home", Self::End => "End",
111 Self::PageUp => "Page Up", Self::PageDown => "Page Down",
112 Self::Numpad0 => "Num0", Self::Numpad1 => "Num1", Self::Numpad2 => "Num2",
113 Self::Numpad3 => "Num3", Self::Numpad4 => "Num4", Self::Numpad5 => "Num5",
114 Self::Numpad6 => "Num6", Self::Numpad7 => "Num7", Self::Numpad8 => "Num8",
115 Self::Numpad9 => "Num9",
116 Self::NumpadAdd => "Num+", Self::NumpadSub => "Num-",
117 Self::NumpadMul => "Num*", Self::NumpadDiv => "Num/",
118 Self::NumpadEnter => "NumEnter", Self::NumpadDecimal => "Num.",
119 Self::NumLock => "Num Lock", Self::ScrollLock => "Scroll Lock",
120 Self::PrintScreen => "Print Screen", Self::Pause => "Pause", Self::Menu => "Menu",
121 Self::BracketLeft => "[", Self::BracketRight => "]",
122 Self::Semicolon => ";", Self::Quote => "'",
123 Self::Comma => ",", Self::Period => ".", Self::Slash => "/",
124 Self::Backslash => "\\", Self::Grave => "`",
125 Self::Minus => "-", Self::Equals => "=",
126 }
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131pub enum MouseButton { Left, Right, Middle, X1, X2 }
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
133pub enum MouseAxis { X, Y, ScrollX, ScrollY }
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
136pub enum GamepadButton {
137 South, East, West, North,
138 LeftBumper, RightBumper,
139 LeftTrigger, RightTrigger,
140 LeftStickClick, RightStickClick,
141 DPadUp, DPadDown, DPadLeft, DPadRight,
142 Start, Select, Home, Touchpad,
143 LeftStickUp, LeftStickDown, LeftStickLeft, LeftStickRight,
144 RightStickUp, RightStickDown, RightStickLeft, RightStickRight,
145 L4, R4, L5, R5,
146}
147
148impl GamepadButton {
149 pub fn label_xbox(&self) -> &'static str {
150 match self {
151 Self::South => "A", Self::East => "B", Self::West => "X", Self::North => "Y",
152 Self::LeftBumper => "LB", Self::RightBumper => "RB",
153 Self::LeftTrigger => "LT", Self::RightTrigger => "RT",
154 Self::LeftStickClick => "LS", Self::RightStickClick => "RS",
155 Self::DPadUp => "D-Up", Self::DPadDown => "D-Down",
156 Self::DPadLeft => "D-Left", Self::DPadRight => "D-Right",
157 Self::Start => "Menu", Self::Select => "View", Self::Home => "Xbox",
158 Self::Touchpad => "Share",
159 Self::LeftStickUp => "LS Up", Self::LeftStickDown => "LS Down",
160 Self::LeftStickLeft => "LS Left", Self::LeftStickRight => "LS Right",
161 Self::RightStickUp => "RS Up", Self::RightStickDown => "RS Down",
162 Self::RightStickLeft => "RS Left", Self::RightStickRight => "RS Right",
163 Self::L4 => "P1", Self::R4 => "P2", Self::L5 => "P3", Self::R5 => "P4",
164 }
165 }
166 pub fn label_ps(&self) -> &'static str {
167 match self {
168 Self::South => "Cross", Self::East => "Circle", Self::West => "Square", Self::North => "Triangle",
169 Self::LeftBumper => "L1", Self::RightBumper => "R1",
170 Self::LeftTrigger => "L2", Self::RightTrigger => "R2",
171 Self::LeftStickClick => "L3", Self::RightStickClick => "R3",
172 Self::Start => "Options", Self::Select => "Create", Self::Home => "PS", Self::Touchpad => "Touchpad",
173 _ => self.label_xbox(),
174 }
175 }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
179pub enum GamepadAxis {
180 LeftStickX, LeftStickY,
181 RightStickX, RightStickY,
182 LeftTrigger, RightTrigger,
183 GyroX, GyroY, GyroZ,
184 AccelX, AccelY, AccelZ,
185}
186
187impl GamepadAxis {
188 pub fn label(&self) -> &'static str {
189 match self {
190 Self::LeftStickX => "Left Stick X", Self::LeftStickY => "Left Stick Y",
191 Self::RightStickX => "Right Stick X", Self::RightStickY => "Right Stick Y",
192 Self::LeftTrigger => "Left Trigger", Self::RightTrigger => "Right Trigger",
193 Self::GyroX => "Gyro X", Self::GyroY => "Gyro Y", Self::GyroZ => "Gyro Z",
194 Self::AccelX => "Accel X", Self::AccelY => "Accel Y", Self::AccelZ => "Accel Z",
195 }
196 }
197}
198
199#[derive(Debug, Clone, PartialEq)]
204pub enum InputBinding {
205 Key(KeyCode),
206 KeyCombo(Vec<KeyCode>),
207 MouseButton(MouseButton),
208 MouseAxis(MouseAxis, f32),
209 GamepadButton(GamepadButton),
210 GamepadAxis(GamepadAxis, f32),
211 GamepadAxisPositive(GamepadAxis),
212 GamepadAxisNegative(GamepadAxis),
213 Touch(u32),
214 Any(Vec<InputBinding>),
215 Sequence(Vec<InputBinding>),
216 Hold { binding: Box<InputBinding>, duration_secs: f32 },
217 DoubleTap { binding: Box<InputBinding>, max_interval_secs: f32 },
218 Chord(Box<InputBinding>, Box<InputBinding>),
219}
220
221impl InputBinding {
222 pub fn label(&self) -> String {
223 match self {
224 Self::Key(k) => k.label().to_string(),
225 Self::KeyCombo(keys) => keys.iter().map(|k| k.label()).collect::<Vec<_>>().join("+"),
226 Self::MouseButton(b) => format!("Mouse{:?}", b),
227 Self::MouseAxis(a, scale) => format!("Mouse{:?}*{:.1}", a, scale),
228 Self::GamepadButton(b) => b.label_xbox().to_string(),
229 Self::GamepadAxis(a, scale) => format!("{} *{:.1}", a.label(), scale),
230 Self::GamepadAxisPositive(a) => format!("{}+", a.label()),
231 Self::GamepadAxisNegative(a) => format!("{}-", a.label()),
232 Self::Touch(id) => format!("Touch{}", id),
233 Self::Any(bindings) => bindings.iter().map(|b| b.label()).collect::<Vec<_>>().join(" | "),
234 Self::Sequence(bindings) => bindings.iter().map(|b| b.label()).collect::<Vec<_>>().join(", "),
235 Self::Hold { binding, duration_secs } => format!("Hold({}, {:.1}s)", binding.label(), duration_secs),
236 Self::DoubleTap { binding, .. } => format!("2x{}", binding.label()),
237 Self::Chord(a, b) => format!("{}+{}", a.label(), b.label()),
238 }
239 }
240
241 pub fn is_analog(&self) -> bool {
242 matches!(self, Self::MouseAxis(..) | Self::GamepadAxis(..) | Self::GamepadAxisPositive(..) | Self::GamepadAxisNegative(..))
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq)]
251pub enum ActionValueType { Bool, Float, Vec2, Vec3 }
252
253impl ActionValueType {
254 pub fn label(&self) -> &'static str {
255 match self {
256 Self::Bool => "Digital",
257 Self::Float => "Axis 1D",
258 Self::Vec2 => "Axis 2D",
259 Self::Vec3 => "Axis 3D",
260 }
261 }
262}
263
264#[derive(Debug, Clone, Copy)]
265pub enum ActionValue {
266 Bool(bool),
267 Float(f32),
268 Vec2(Vec2),
269 Vec3(glam::Vec3),
270}
271
272impl ActionValue {
273 pub fn as_bool(&self) -> bool {
274 match self {
275 Self::Bool(v) => *v,
276 Self::Float(v) => *v != 0.0,
277 Self::Vec2(v) => v.length() > 0.0,
278 Self::Vec3(v) => v.length() > 0.0,
279 }
280 }
281 pub fn as_float(&self) -> f32 {
282 match self {
283 Self::Bool(v) => if *v { 1.0 } else { 0.0 },
284 Self::Float(v) => *v,
285 Self::Vec2(v) => v.length(),
286 Self::Vec3(v) => v.length(),
287 }
288 }
289}
290
291#[derive(Debug, Clone)]
296pub enum InputModifier {
297 DeadZone { lower: f32, upper: f32, kind: DeadZoneKind },
298 Scale(f32),
299 ScaleByDeltaTime,
300 Negate,
301 Clamp { min: f32, max: f32 },
302 Normalize,
303 Smooth { speed: f32 },
304 Accumulate,
305 RelativeInput,
306 ResponseCurve { kind: ResponseCurveKind, exponent: f32 },
307 SwizzleAxes(SwizzleOrder),
308 InvertAxis { x: bool, y: bool, z: bool },
309 SensitivityMultiplier { x: f32, y: f32 },
310 AimAssist { strength: f32, radius: f32 },
311 FovScale { base_fov: f32 },
312 CustomCode(String),
313}
314
315#[derive(Debug, Clone, Copy, PartialEq)]
316pub enum DeadZoneKind { Axial, Radial, Hysteresis }
317
318#[derive(Debug, Clone, Copy, PartialEq)]
319pub enum ResponseCurveKind { Linear, Exponential, Logarithmic, SineLike, Custom }
320
321#[derive(Debug, Clone, Copy, PartialEq)]
322pub enum SwizzleOrder { XYZ, XZY, YXZ, YZX, ZXY, ZYX }
323
324impl InputModifier {
325 pub fn label(&self) -> &'static str {
326 match self {
327 Self::DeadZone { .. } => "Dead Zone",
328 Self::Scale(_) => "Scale",
329 Self::ScaleByDeltaTime => "Scale by Delta Time",
330 Self::Negate => "Negate",
331 Self::Clamp { .. } => "Clamp",
332 Self::Normalize => "Normalize",
333 Self::Smooth { .. } => "Smooth",
334 Self::Accumulate => "Accumulate",
335 Self::RelativeInput => "Relative Input",
336 Self::ResponseCurve { .. } => "Response Curve",
337 Self::SwizzleAxes(_) => "Swizzle Axes",
338 Self::InvertAxis { .. } => "Invert Axis",
339 Self::SensitivityMultiplier { .. } => "Sensitivity",
340 Self::AimAssist { .. } => "Aim Assist",
341 Self::FovScale { .. } => "FOV Scale",
342 Self::CustomCode(_) => "Custom",
343 }
344 }
345
346 pub fn apply_float(&self, value: f32, dt: f32) -> f32 {
347 match self {
348 Self::DeadZone { lower, upper, kind: DeadZoneKind::Axial } => {
349 let abs = value.abs();
350 if abs < *lower { 0.0 }
351 else if abs > *upper { value.signum() }
352 else { value.signum() * (abs - lower) / (upper - lower) }
353 }
354 Self::Scale(s) => value * s,
355 Self::ScaleByDeltaTime => value * dt,
356 Self::Negate => -value,
357 Self::Clamp { min, max } => value.clamp(*min, *max),
358 Self::Normalize => if value.abs() > 0.001 { value.signum() } else { 0.0 },
359 Self::ResponseCurve { kind: ResponseCurveKind::Exponential, exponent, .. } => {
360 value.signum() * value.abs().powf(*exponent)
361 }
362 _ => value,
363 }
364 }
365}
366
367#[derive(Debug, Clone)]
372pub enum InputTrigger {
373 Down,
374 Pressed,
375 Released,
376 Hold { hold_time: f32, is_one_shot: bool },
377 HoldAndRelease { hold_time: f32 },
378 Tap { release_time_limit: f32 },
379 Pulse { interval: f32, trigger_on_start: bool, trigger_limit: u32 },
380 ChordAction { chord_action: String },
381 Combo { sequence: Vec<String>, max_time_between: f32 },
382}
383
384impl InputTrigger {
385 pub fn label(&self) -> &'static str {
386 match self {
387 Self::Down => "Down",
388 Self::Pressed => "Pressed",
389 Self::Released => "Released",
390 Self::Hold { .. } => "Hold",
391 Self::HoldAndRelease { .. } => "Hold & Release",
392 Self::Tap { .. } => "Tap",
393 Self::Pulse { .. } => "Pulse",
394 Self::ChordAction { .. } => "Chord Action",
395 Self::Combo { .. } => "Combo",
396 }
397 }
398}
399
400#[derive(Debug, Clone)]
405pub struct InputAction {
406 pub name: String,
407 pub value_type: ActionValueType,
408 pub bindings: Vec<ActionBinding>,
409 pub consume_input: bool,
410 pub consume_lower_priority: bool,
411 pub description: String,
412 pub category: String,
413 pub icon: String,
414}
415
416#[derive(Debug, Clone)]
417pub struct ActionBinding {
418 pub binding: InputBinding,
419 pub device: InputDevice,
420 pub modifiers: Vec<InputModifier>,
421 pub triggers: Vec<InputTrigger>,
422 pub platform_filter: Vec<Platform>,
423 pub override_context: Option<String>,
424}
425
426#[derive(Debug, Clone, Copy, PartialEq)]
427pub enum Platform { PC, XboxConsole, PlayStation, Switch, Mobile, VR, All }
428
429impl ActionBinding {
430 pub fn new(binding: InputBinding, device: InputDevice) -> Self {
431 Self {
432 binding,
433 device,
434 modifiers: Vec::new(),
435 triggers: vec![InputTrigger::Pressed],
436 platform_filter: vec![Platform::All],
437 override_context: None,
438 }
439 }
440 pub fn with_modifier(mut self, modifier: InputModifier) -> Self {
441 self.modifiers.push(modifier);
442 self
443 }
444 pub fn with_trigger(mut self, trigger: InputTrigger) -> Self {
445 self.triggers = vec![trigger];
446 self
447 }
448 pub fn analog_binding(binding: InputBinding, device: InputDevice) -> Self {
449 Self {
450 binding,
451 device,
452 modifiers: vec![
453 InputModifier::DeadZone { lower: 0.15, upper: 1.0, kind: DeadZoneKind::Radial },
454 InputModifier::ResponseCurve { kind: ResponseCurveKind::Exponential, exponent: 1.5 },
455 ],
456 triggers: vec![InputTrigger::Down],
457 platform_filter: vec![Platform::All],
458 override_context: None,
459 }
460 }
461}
462
463impl InputAction {
464 pub fn new(name: &str, value_type: ActionValueType) -> Self {
465 Self {
466 name: name.to_string(),
467 value_type,
468 bindings: Vec::new(),
469 consume_input: true,
470 consume_lower_priority: false,
471 description: String::new(),
472 category: "General".to_string(),
473 icon: String::new(),
474 }
475 }
476 pub fn with_binding(mut self, binding: ActionBinding) -> Self {
477 self.bindings.push(binding);
478 self
479 }
480 pub fn with_description(mut self, desc: &str) -> Self {
481 self.description = desc.to_string();
482 self
483 }
484 pub fn with_category(mut self, cat: &str) -> Self {
485 self.category = cat.to_string();
486 self
487 }
488}
489
490#[derive(Debug, Clone)]
495pub struct ActionMapContext {
496 pub name: String,
497 pub priority: i32,
498 pub block_lower_priority: bool,
499 pub actions: Vec<InputAction>,
500 pub enabled: bool,
501 pub description: String,
502}
503
504impl ActionMapContext {
505 pub fn new(name: &str, priority: i32) -> Self {
506 Self {
507 name: name.to_string(),
508 priority,
509 block_lower_priority: false,
510 actions: Vec::new(),
511 enabled: true,
512 description: String::new(),
513 }
514 }
515 pub fn with_action(mut self, action: InputAction) -> Self {
516 self.actions.push(action);
517 self
518 }
519 pub fn find_action(&self, name: &str) -> Option<&InputAction> {
520 self.actions.iter().find(|a| a.name == name)
521 }
522 pub fn find_action_mut(&mut self, name: &str) -> Option<&mut InputAction> {
523 self.actions.iter_mut().find(|a| a.name == name)
524 }
525 pub fn action_names(&self) -> Vec<&str> {
526 self.actions.iter().map(|a| a.name.as_str()).collect()
527 }
528}
529
530#[derive(Debug, Clone)]
535pub struct DeadZoneConfig {
536 pub inner_radius: f32,
537 pub outer_radius: f32,
538 pub axial_x: f32,
539 pub axial_y: f32,
540 pub kind: DeadZoneKind,
541 pub snap_to_axis: bool,
542 pub snap_threshold: f32,
543 pub anti_deadzone: f32,
544}
545
546impl Default for DeadZoneConfig {
547 fn default() -> Self {
548 Self {
549 inner_radius: 0.15,
550 outer_radius: 0.95,
551 axial_x: 0.15,
552 axial_y: 0.15,
553 kind: DeadZoneKind::Radial,
554 snap_to_axis: false,
555 snap_threshold: 0.1,
556 anti_deadzone: 0.0,
557 }
558 }
559}
560
561impl DeadZoneConfig {
562 pub fn apply_radial(&self, raw: Vec2) -> Vec2 {
563 let len = raw.length();
564 if len < self.inner_radius { return Vec2::ZERO; }
565 if len > self.outer_radius { return raw / len; }
566 let rescaled = (len - self.inner_radius) / (self.outer_radius - self.inner_radius);
567 raw / len * rescaled
568 }
569 pub fn apply_axial(&self, raw: Vec2) -> Vec2 {
570 let x = if raw.x.abs() < self.axial_x { 0.0 } else { (raw.x.abs() - self.axial_x) / (1.0 - self.axial_x) * raw.x.signum() };
571 let y = if raw.y.abs() < self.axial_y { 0.0 } else { (raw.y.abs() - self.axial_y) / (1.0 - self.axial_y) * raw.y.signum() };
572 Vec2::new(x, y)
573 }
574 pub fn apply(&self, raw: Vec2) -> Vec2 {
575 match self.kind {
576 DeadZoneKind::Radial => self.apply_radial(raw),
577 DeadZoneKind::Axial => self.apply_axial(raw),
578 DeadZoneKind::Hysteresis => self.apply_radial(raw),
579 }
580 }
581}
582
583#[derive(Debug, Clone)]
588pub struct SensitivityProfile {
589 pub name: String,
590 pub mouse_x: f32,
591 pub mouse_y: f32,
592 pub right_stick_x: f32,
593 pub right_stick_y: f32,
594 pub aim_mouse_x: f32,
595 pub aim_mouse_y: f32,
596 pub aim_stick_x: f32,
597 pub aim_stick_y: f32,
598 pub invert_y_mouse: bool,
599 pub invert_y_stick: bool,
600 pub invert_x_stick: bool,
601 pub ads_sensitivity_multiplier: f32,
602 pub acceleration: f32,
603 pub input_lag_compensation_ms: f32,
604 pub mouse_smoothing: f32,
605 pub raw_input: bool,
606}
607
608impl Default for SensitivityProfile {
609 fn default() -> Self {
610 Self {
611 name: "Default".to_string(),
612 mouse_x: 1.0, mouse_y: 1.0,
613 right_stick_x: 1.0, right_stick_y: 1.0,
614 aim_mouse_x: 0.6, aim_mouse_y: 0.6,
615 aim_stick_x: 0.6, aim_stick_y: 0.6,
616 invert_y_mouse: false,
617 invert_y_stick: false,
618 invert_x_stick: false,
619 ads_sensitivity_multiplier: 0.7,
620 acceleration: 0.0,
621 input_lag_compensation_ms: 0.0,
622 mouse_smoothing: 0.0,
623 raw_input: true,
624 }
625 }
626}
627
628impl SensitivityProfile {
629 pub fn esports() -> Self {
630 Self { name: "eSports".to_string(), mouse_x: 0.5, mouse_y: 0.5, raw_input: true, mouse_smoothing: 0.0, ..Default::default() }
631 }
632 pub fn casual() -> Self {
633 Self { name: "Casual".to_string(), mouse_x: 1.5, mouse_y: 1.5, mouse_smoothing: 0.3, ..Default::default() }
634 }
635 pub fn console() -> Self {
636 Self { name: "Console".to_string(), right_stick_x: 2.0, right_stick_y: 1.8, aim_stick_x: 1.2, aim_stick_y: 1.2, ..Default::default() }
637 }
638}
639
640#[derive(Debug, Clone, Copy, PartialEq)]
645pub enum HapticEffectKind {
646 Constant,
647 Sine,
648 Square,
649 Triangle,
650 SawUp,
651 SawDown,
652 Custom,
653 Rumble,
654 AdaptiveTriggerFeedback,
655 AdaptiveTriggerWeapon,
656 AdaptiveTriggerBow,
657}
658
659impl HapticEffectKind {
660 pub fn label(&self) -> &'static str {
661 match self {
662 Self::Constant => "Constant",
663 Self::Sine => "Sine",
664 Self::Square => "Square",
665 Self::Triangle => "Triangle",
666 Self::SawUp => "Saw Up",
667 Self::SawDown => "Saw Down",
668 Self::Custom => "Custom",
669 Self::Rumble => "Rumble",
670 Self::AdaptiveTriggerFeedback => "Adaptive: Feedback",
671 Self::AdaptiveTriggerWeapon => "Adaptive: Weapon",
672 Self::AdaptiveTriggerBow => "Adaptive: Bow",
673 }
674 }
675}
676
677#[derive(Debug, Clone)]
678pub struct HapticEffect {
679 pub name: String,
680 pub kind: HapticEffectKind,
681 pub duration_secs: f32,
682 pub low_freq_motor: f32,
683 pub high_freq_motor: f32,
684 pub left_trigger_intensity: f32,
685 pub right_trigger_intensity: f32,
686 pub start_delay_secs: f32,
687 pub fade_in_secs: f32,
688 pub fade_out_secs: f32,
689 pub frequency_hz: f32,
690 pub attack_intensity: f32,
691 pub sustain_intensity: f32,
692 pub release_intensity: f32,
693}
694
695impl HapticEffect {
696 pub fn new(name: &str, kind: HapticEffectKind) -> Self {
697 Self {
698 name: name.to_string(),
699 kind,
700 duration_secs: 0.3,
701 low_freq_motor: 0.5,
702 high_freq_motor: 0.5,
703 left_trigger_intensity: 0.0,
704 right_trigger_intensity: 0.0,
705 start_delay_secs: 0.0,
706 fade_in_secs: 0.0,
707 fade_out_secs: 0.1,
708 frequency_hz: 60.0,
709 attack_intensity: 1.0,
710 sustain_intensity: 0.7,
711 release_intensity: 0.0,
712 }
713 }
714
715 pub fn gun_shot() -> Self {
716 let mut e = Self::new("GunShot", HapticEffectKind::Rumble);
717 e.duration_secs = 0.15;
718 e.low_freq_motor = 0.9;
719 e.high_freq_motor = 0.6;
720 e.fade_out_secs = 0.08;
721 e
722 }
723
724 pub fn explosion() -> Self {
725 let mut e = Self::new("Explosion", HapticEffectKind::Rumble);
726 e.duration_secs = 0.6;
727 e.low_freq_motor = 1.0;
728 e.high_freq_motor = 0.4;
729 e.fade_in_secs = 0.05;
730 e.fade_out_secs = 0.3;
731 e
732 }
733
734 pub fn footstep() -> Self {
735 let mut e = Self::new("Footstep", HapticEffectKind::Sine);
736 e.duration_secs = 0.08;
737 e.low_freq_motor = 0.3;
738 e.high_freq_motor = 0.2;
739 e
740 }
741
742 pub fn ui_click() -> Self {
743 let mut e = Self::new("UIClick", HapticEffectKind::Constant);
744 e.duration_secs = 0.04;
745 e.low_freq_motor = 0.1;
746 e.high_freq_motor = 0.4;
747 e
748 }
749
750 pub fn intensity_at_time(&self, t: f32) -> f32 {
751 if t < 0.0 || t > self.duration_secs { return 0.0; }
752 let fade_in_end = self.fade_in_secs;
753 let fade_out_start = self.duration_secs - self.fade_out_secs;
754 let envelope = if t < fade_in_end {
755 t / fade_in_end.max(0.001)
756 } else if t > fade_out_start {
757 1.0 - (t - fade_out_start) / self.fade_out_secs.max(0.001)
758 } else {
759 1.0
760 };
761 let wave = match self.kind {
762 HapticEffectKind::Sine => (t * self.frequency_hz * std::f32::consts::TAU).sin() * 0.5 + 0.5,
763 HapticEffectKind::Square => if (t * self.frequency_hz).fract() < 0.5 { 1.0 } else { 0.0 },
764 HapticEffectKind::Triangle => {
765 let phase = (t * self.frequency_hz).fract();
766 if phase < 0.5 { phase * 2.0 } else { 2.0 - phase * 2.0 }
767 }
768 _ => 1.0,
769 };
770 envelope * wave * self.sustain_intensity
771 }
772}
773
774#[derive(Debug, Clone)]
779pub struct InputMacroStep {
780 pub action_name: String,
781 pub value: f32,
782 pub delay_before_ms: u32,
783 pub duration_ms: u32,
784 pub repeat: u32,
785}
786
787#[derive(Debug, Clone)]
788pub struct InputMacro {
789 pub name: String,
790 pub description: String,
791 pub steps: Vec<InputMacroStep>,
792 pub loop_count: u32,
793 pub allow_during_gameplay: bool,
794 pub trigger_binding: Option<InputBinding>,
795 pub recording: bool,
796 pub playback_speed: f32,
797}
798
799impl InputMacro {
800 pub fn new(name: &str) -> Self {
801 Self {
802 name: name.to_string(),
803 description: String::new(),
804 steps: Vec::new(),
805 loop_count: 1,
806 allow_during_gameplay: false,
807 trigger_binding: None,
808 recording: false,
809 playback_speed: 1.0,
810 }
811 }
812 pub fn total_duration_ms(&self) -> u32 {
813 self.steps.iter().map(|s| s.delay_before_ms + s.duration_ms * s.repeat.max(1)).sum()
814 }
815 pub fn add_step(&mut self, action: &str, value: f32, delay_ms: u32, dur_ms: u32) {
816 self.steps.push(InputMacroStep {
817 action_name: action.to_string(),
818 value,
819 delay_before_ms: delay_ms,
820 duration_ms: dur_ms,
821 repeat: 1,
822 });
823 }
824}
825
826#[derive(Debug, Clone)]
831pub struct ComboInput {
832 pub action: String,
833 pub min_value: f32,
834 pub max_hold_ms: Option<u32>,
835}
836
837#[derive(Debug, Clone)]
838pub struct ComboDefinition {
839 pub name: String,
840 pub sequence: Vec<ComboInput>,
841 pub max_gap_ms: u32,
842 pub output_action: String,
843 pub output_value: f32,
844 pub priority: i32,
845 pub enabled: bool,
846}
847
848impl ComboDefinition {
849 pub fn new(name: &str, output: &str) -> Self {
850 Self {
851 name: name.to_string(),
852 sequence: Vec::new(),
853 max_gap_ms: 300,
854 output_action: output.to_string(),
855 output_value: 1.0,
856 priority: 0,
857 enabled: true,
858 }
859 }
860 pub fn add_input(mut self, action: &str, min_value: f32) -> Self {
861 self.sequence.push(ComboInput { action: action.to_string(), min_value, max_hold_ms: None });
862 self
863 }
864 pub fn input_count(&self) -> usize { self.sequence.len() }
865}
866
867#[derive(Debug, Clone)]
872pub struct InputEvent {
873 pub timestamp_ms: f64,
874 pub device: InputDevice,
875 pub action_name: String,
876 pub value: f32,
877 pub value2: f32,
878 pub frame: u64,
879}
880
881#[derive(Debug, Clone, Default)]
882pub struct InputReplay {
883 pub events: Vec<InputEvent>,
884 pub duration_ms: f64,
885 pub recording: bool,
886 pub playing: bool,
887 pub playback_pos_ms: f64,
888 pub loop_replay: bool,
889 pub playback_speed: f32,
890}
891
892impl InputReplay {
893 pub fn start_recording(&mut self) { self.recording = true; self.events.clear(); }
894 pub fn stop_recording(&mut self) {
895 self.recording = false;
896 self.duration_ms = self.events.last().map(|e| e.timestamp_ms).unwrap_or(0.0);
897 }
898 pub fn start_playback(&mut self) { self.playing = true; self.playback_pos_ms = 0.0; }
899 pub fn stop_playback(&mut self) { self.playing = false; }
900 pub fn record_event(&mut self, event: InputEvent) {
901 if self.recording { self.events.push(event); }
902 }
903 pub fn advance(&mut self, dt_ms: f64) {
904 if !self.playing { return; }
905 self.playback_pos_ms += dt_ms * self.playback_speed as f64;
906 if self.playback_pos_ms >= self.duration_ms {
907 if self.loop_replay { self.playback_pos_ms -= self.duration_ms; }
908 else { self.playing = false; }
909 }
910 }
911 pub fn current_events(&self) -> Vec<&InputEvent> {
912 let t = self.playback_pos_ms;
913 self.events.iter().filter(|e| (e.timestamp_ms - t).abs() < 16.0).collect()
914 }
915 pub fn event_count(&self) -> usize { self.events.len() }
916}
917
918#[derive(Debug, Clone)]
923pub struct VirtualCursor {
924 pub position: Vec2,
925 pub velocity: Vec2,
926 pub acceleration: f32,
927 pub max_speed: f32,
928 pub friction: f32,
929 pub deadzone: f32,
930 pub screen_size: Vec2,
931 pub enabled: bool,
932 pub stick_axis: (GamepadAxis, GamepadAxis),
933}
934
935impl Default for VirtualCursor {
936 fn default() -> Self {
937 Self {
938 position: Vec2::new(960.0, 540.0),
939 velocity: Vec2::ZERO,
940 acceleration: 1200.0,
941 max_speed: 800.0,
942 friction: 8.0,
943 deadzone: 0.15,
944 screen_size: Vec2::new(1920.0, 1080.0),
945 enabled: false,
946 stick_axis: (GamepadAxis::RightStickX, GamepadAxis::RightStickY),
947 }
948 }
949}
950
951impl VirtualCursor {
952 pub fn update(&mut self, stick: Vec2, dt: f32) {
953 let len = stick.length();
954 if len < self.deadzone {
955 self.velocity *= 1.0 - self.friction * dt;
956 } else {
957 let dir = stick / len;
958 let t = (len - self.deadzone) / (1.0 - self.deadzone);
959 self.velocity += dir * self.acceleration * t * dt;
960 let speed = self.velocity.length();
961 if speed > self.max_speed {
962 self.velocity = self.velocity / speed * self.max_speed;
963 }
964 }
965 self.position += self.velocity * dt;
966 self.position.x = self.position.x.clamp(0.0, self.screen_size.x);
967 self.position.y = self.position.y.clamp(0.0, self.screen_size.y);
968 }
969}
970
971#[derive(Debug, Clone, Copy, PartialEq)]
976pub enum GestureKind {
977 Tap,
978 DoubleTap,
979 LongPress,
980 Swipe,
981 Pinch,
982 Rotate,
983 Pan,
984 Flick,
985 TwoFingerTap,
986 ThreeFingerTap,
987 EdgeSwipe,
988 ForceTap,
989}
990
991impl GestureKind {
992 pub fn label(&self) -> &'static str {
993 match self {
994 Self::Tap => "Tap",
995 Self::DoubleTap => "Double Tap",
996 Self::LongPress => "Long Press",
997 Self::Swipe => "Swipe",
998 Self::Pinch => "Pinch",
999 Self::Rotate => "Rotate",
1000 Self::Pan => "Pan",
1001 Self::Flick => "Flick",
1002 Self::TwoFingerTap => "Two-Finger Tap",
1003 Self::ThreeFingerTap => "Three-Finger Tap",
1004 Self::EdgeSwipe => "Edge Swipe",
1005 Self::ForceTap => "Force Tap",
1006 }
1007 }
1008 pub fn min_touch_points(&self) -> u32 {
1009 match self {
1010 Self::Pinch | Self::Rotate | Self::TwoFingerTap => 2,
1011 Self::ThreeFingerTap => 3,
1012 _ => 1,
1013 }
1014 }
1015}
1016
1017#[derive(Debug, Clone)]
1018pub struct GestureBinding {
1019 pub gesture: GestureKind,
1020 pub action: String,
1021 pub min_velocity: f32,
1022 pub max_duration_secs: f32,
1023 pub direction_filter: Option<Vec2>,
1024 pub direction_tolerance_deg: f32,
1025}
1026
1027#[derive(Debug, Clone)]
1032pub struct AccessibilitySettings {
1033 pub sticky_keys: bool,
1034 pub slow_keys: bool,
1035 pub slow_keys_delay_ms: u32,
1036 pub toggle_keys: bool,
1037 pub filter_keys: bool,
1038 pub filter_keys_repeat_delay_ms: u32,
1039 pub mouse_keys: bool,
1040 pub mouse_keys_speed: f32,
1041 pub high_contrast_cursor: bool,
1042 pub cursor_size_scale: f32,
1043 pub single_button_mode: bool,
1044 pub auto_sprint: bool,
1045 pub aim_assist_strength: f32,
1046 pub auto_aim: bool,
1047 pub hold_to_toggle_aim: bool,
1048 pub simplified_controls: bool,
1049 pub button_remapping_enabled: bool,
1050 pub hold_alternatives: HashMap<String, String>,
1051 pub color_blind_mode: ColorBlindMode,
1052 pub text_size_scale: f32,
1053}
1054
1055#[derive(Debug, Clone, Copy, PartialEq)]
1056pub enum ColorBlindMode { None, Protanopia, Deuteranopia, Tritanopia, Monochromacy }
1057
1058impl ColorBlindMode {
1059 pub fn label(&self) -> &'static str {
1060 match self {
1061 Self::None => "None",
1062 Self::Protanopia => "Protanopia (Red-Green)",
1063 Self::Deuteranopia => "Deuteranopia (Green-Red)",
1064 Self::Tritanopia => "Tritanopia (Blue-Yellow)",
1065 Self::Monochromacy => "Monochromacy",
1066 }
1067 }
1068}
1069
1070impl Default for AccessibilitySettings {
1071 fn default() -> Self {
1072 Self {
1073 sticky_keys: false,
1074 slow_keys: false,
1075 slow_keys_delay_ms: 500,
1076 toggle_keys: false,
1077 filter_keys: false,
1078 filter_keys_repeat_delay_ms: 200,
1079 mouse_keys: false,
1080 mouse_keys_speed: 1.0,
1081 high_contrast_cursor: false,
1082 cursor_size_scale: 1.0,
1083 single_button_mode: false,
1084 auto_sprint: false,
1085 aim_assist_strength: 0.0,
1086 auto_aim: false,
1087 hold_to_toggle_aim: false,
1088 simplified_controls: false,
1089 button_remapping_enabled: true,
1090 hold_alternatives: HashMap::new(),
1091 color_blind_mode: ColorBlindMode::None,
1092 text_size_scale: 1.0,
1093 }
1094 }
1095}
1096
1097#[derive(Debug, Clone)]
1102pub struct GamepadState {
1103 pub device_id: u32,
1104 pub device: InputDevice,
1105 pub connected: bool,
1106 pub left_stick: Vec2,
1107 pub right_stick: Vec2,
1108 pub left_trigger: f32,
1109 pub right_trigger: f32,
1110 pub buttons_pressed: std::collections::HashSet<u32>,
1111 pub gyro: glam::Vec3,
1112 pub accel: glam::Vec3,
1113 pub touchpad_pos: Vec2,
1114 pub touchpad_pressed: bool,
1115 pub battery_level: f32,
1116 pub is_wireless: bool,
1117}
1118
1119impl Default for GamepadState {
1120 fn default() -> Self {
1121 Self {
1122 device_id: 0,
1123 device: InputDevice::GamepadGeneric,
1124 connected: false,
1125 left_stick: Vec2::ZERO,
1126 right_stick: Vec2::ZERO,
1127 left_trigger: 0.0,
1128 right_trigger: 0.0,
1129 buttons_pressed: std::collections::HashSet::new(),
1130 gyro: glam::Vec3::ZERO,
1131 accel: glam::Vec3::ZERO,
1132 touchpad_pos: Vec2::ZERO,
1133 touchpad_pressed: false,
1134 battery_level: 1.0,
1135 is_wireless: false,
1136 }
1137 }
1138}
1139
1140impl GamepadState {
1141 pub fn synthetic_idle() -> Self {
1142 Self {
1143 device_id: 0,
1144 device: InputDevice::GamepadXbox,
1145 connected: true,
1146 battery_level: 0.85,
1147 ..Default::default()
1148 }
1149 }
1150}
1151
1152#[derive(Debug, Clone, Default)]
1153pub struct KeyboardState {
1154 pub keys_down: std::collections::HashSet<u32>,
1155 pub keys_just_pressed: std::collections::HashSet<u32>,
1156 pub keys_just_released: std::collections::HashSet<u32>,
1157 pub shift: bool,
1158 pub ctrl: bool,
1159 pub alt: bool,
1160 pub super_key: bool,
1161}
1162
1163#[derive(Debug, Clone, Default)]
1164pub struct MouseState {
1165 pub position: Vec2,
1166 pub delta: Vec2,
1167 pub scroll: Vec2,
1168 pub left: bool,
1169 pub right: bool,
1170 pub middle: bool,
1171 pub x1: bool,
1172 pub x2: bool,
1173 pub just_pressed: std::collections::HashSet<u32>,
1174 pub just_released: std::collections::HashSet<u32>,
1175 pub captured: bool,
1176 pub dpi: u32,
1177}
1178
1179#[derive(Debug, Clone)]
1184pub struct BindingConflict {
1185 pub context_a: String,
1186 pub action_a: String,
1187 pub context_b: String,
1188 pub action_b: String,
1189 pub conflicting_binding: String,
1190 pub severity: ConflictSeverity,
1191}
1192
1193#[derive(Debug, Clone, Copy, PartialEq)]
1194pub enum ConflictSeverity { Warning, Error }
1195
1196#[derive(Debug, Clone)]
1201pub struct InputAsset {
1202 pub name: String,
1203 pub contexts: Vec<ActionMapContext>,
1204 pub haptic_effects: Vec<HapticEffect>,
1205 pub macros: Vec<InputMacro>,
1206 pub combos: Vec<ComboDefinition>,
1207 pub sensitivity_profiles: Vec<SensitivityProfile>,
1208 pub active_sensitivity_profile: usize,
1209 pub deadzone_configs: HashMap<GamepadAxis, DeadZoneConfig>,
1210 pub accessibility: AccessibilitySettings,
1211 pub gesture_bindings: Vec<GestureBinding>,
1212 pub virtual_cursor: VirtualCursor,
1213 pub replay: InputReplay,
1214}
1215
1216impl InputAsset {
1217 pub fn default_fps() -> Self {
1218 let gameplay = ActionMapContext::new("Gameplay", 100)
1219 .with_action(
1220 InputAction::new("Move", ActionValueType::Vec2)
1221 .with_category("Movement")
1222 .with_description("WASD / Left Stick movement")
1223 .with_binding(ActionBinding::new(InputBinding::KeyCombo(vec![KeyCode::W]), InputDevice::Keyboard))
1224 .with_binding(ActionBinding::analog_binding(InputBinding::GamepadAxis(GamepadAxis::LeftStickY, 1.0), InputDevice::GamepadXbox))
1225 )
1226 .with_action(
1227 InputAction::new("Look", ActionValueType::Vec2)
1228 .with_category("Movement")
1229 .with_description("Mouse / Right Stick look")
1230 .with_binding(ActionBinding::new(InputBinding::MouseAxis(MouseAxis::X, 1.0), InputDevice::Mouse))
1231 .with_binding(ActionBinding::analog_binding(InputBinding::GamepadAxis(GamepadAxis::RightStickX, 1.0), InputDevice::GamepadXbox))
1232 )
1233 .with_action(
1234 InputAction::new("Jump", ActionValueType::Bool)
1235 .with_category("Movement")
1236 .with_description("Jump / A button")
1237 .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::Space), InputDevice::Keyboard))
1238 .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::South), InputDevice::GamepadXbox))
1239 )
1240 .with_action(
1241 InputAction::new("Sprint", ActionValueType::Bool)
1242 .with_category("Movement")
1243 .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::LShift), InputDevice::Keyboard)
1244 .with_trigger(InputTrigger::Down))
1245 .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::LeftStickClick), InputDevice::GamepadXbox))
1246 )
1247 .with_action(
1248 InputAction::new("Crouch", ActionValueType::Bool)
1249 .with_category("Movement")
1250 .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::LCtrl), InputDevice::Keyboard))
1251 .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::RightStickClick), InputDevice::GamepadXbox))
1252 )
1253 .with_action(
1254 InputAction::new("Fire", ActionValueType::Bool)
1255 .with_category("Combat")
1256 .with_binding(ActionBinding::new(InputBinding::MouseButton(MouseButton::Left), InputDevice::Mouse)
1257 .with_trigger(InputTrigger::Down))
1258 .with_binding(ActionBinding::new(InputBinding::GamepadAxis(GamepadAxis::RightTrigger, 1.0), InputDevice::GamepadXbox)
1259 .with_modifier(InputModifier::DeadZone { lower: 0.3, upper: 1.0, kind: DeadZoneKind::Axial }))
1260 )
1261 .with_action(
1262 InputAction::new("Aim", ActionValueType::Bool)
1263 .with_category("Combat")
1264 .with_binding(ActionBinding::new(InputBinding::MouseButton(MouseButton::Right), InputDevice::Mouse)
1265 .with_trigger(InputTrigger::Down))
1266 .with_binding(ActionBinding::new(InputBinding::GamepadAxis(GamepadAxis::LeftTrigger, 1.0), InputDevice::GamepadXbox)
1267 .with_modifier(InputModifier::DeadZone { lower: 0.3, upper: 1.0, kind: DeadZoneKind::Axial }))
1268 )
1269 .with_action(
1270 InputAction::new("Reload", ActionValueType::Bool)
1271 .with_category("Combat")
1272 .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::R), InputDevice::Keyboard))
1273 .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::West), InputDevice::GamepadXbox))
1274 )
1275 .with_action(
1276 InputAction::new("Interact", ActionValueType::Bool)
1277 .with_category("Interaction")
1278 .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::E), InputDevice::Keyboard))
1279 .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::North), InputDevice::GamepadXbox))
1280 )
1281 .with_action(
1282 InputAction::new("Inventory", ActionValueType::Bool)
1283 .with_category("UI")
1284 .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::I), InputDevice::Keyboard))
1285 .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::DPadUp), InputDevice::GamepadXbox))
1286 )
1287 .with_action(
1288 InputAction::new("Pause", ActionValueType::Bool)
1289 .with_category("System")
1290 .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::Escape), InputDevice::Keyboard))
1291 .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::Start), InputDevice::GamepadXbox))
1292 );
1293
1294 let ui_context = ActionMapContext::new("UI", 50)
1295 .with_action(
1296 InputAction::new("Navigate", ActionValueType::Vec2)
1297 .with_category("UI")
1298 .with_binding(ActionBinding::analog_binding(InputBinding::GamepadAxis(GamepadAxis::LeftStickY, 1.0), InputDevice::GamepadXbox))
1299 )
1300 .with_action(
1301 InputAction::new("Confirm", ActionValueType::Bool)
1302 .with_category("UI")
1303 .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::Return), InputDevice::Keyboard))
1304 .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::South), InputDevice::GamepadXbox))
1305 )
1306 .with_action(
1307 InputAction::new("Cancel", ActionValueType::Bool)
1308 .with_category("UI")
1309 .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::Escape), InputDevice::Keyboard))
1310 .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::East), InputDevice::GamepadXbox))
1311 )
1312 .with_action(
1313 InputAction::new("TabNext", ActionValueType::Bool)
1314 .with_category("UI")
1315 .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::Tab), InputDevice::Keyboard))
1316 .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::RightBumper), InputDevice::GamepadXbox))
1317 );
1318
1319 let mut deadzone_configs = HashMap::new();
1320 deadzone_configs.insert(GamepadAxis::LeftStickX, DeadZoneConfig::default());
1321 deadzone_configs.insert(GamepadAxis::LeftStickY, DeadZoneConfig::default());
1322 deadzone_configs.insert(GamepadAxis::RightStickX, DeadZoneConfig { inner_radius: 0.12, ..Default::default() });
1323 deadzone_configs.insert(GamepadAxis::RightStickY, DeadZoneConfig { inner_radius: 0.12, ..Default::default() });
1324
1325 let haptic_effects = vec![
1326 HapticEffect::gun_shot(),
1327 HapticEffect::explosion(),
1328 HapticEffect::footstep(),
1329 HapticEffect::ui_click(),
1330 ];
1331
1332 let sensitivity_profiles = vec![
1333 SensitivityProfile::default(),
1334 SensitivityProfile::esports(),
1335 SensitivityProfile::casual(),
1336 SensitivityProfile::console(),
1337 ];
1338
1339 let combos = vec![
1340 ComboDefinition::new("DashForward", "Dash")
1341 .add_input("Move_Forward", 0.8)
1342 .add_input("Move_Forward", 0.8),
1343 ComboDefinition::new("SpinAttack", "SpinAttack")
1344 .add_input("Fire", 0.5)
1345 .add_input("Fire", 0.5)
1346 .add_input("Fire", 0.5),
1347 ];
1348
1349 Self {
1350 name: "FPS Default".to_string(),
1351 contexts: vec![gameplay, ui_context],
1352 haptic_effects,
1353 macros: Vec::new(),
1354 combos,
1355 sensitivity_profiles,
1356 active_sensitivity_profile: 0,
1357 deadzone_configs,
1358 accessibility: AccessibilitySettings::default(),
1359 gesture_bindings: Vec::new(),
1360 virtual_cursor: VirtualCursor::default(),
1361 replay: InputReplay::default(),
1362 }
1363 }
1364
1365 pub fn detect_conflicts(&self) -> Vec<BindingConflict> {
1366 let mut conflicts = Vec::new();
1367 for (ci, ctx) in self.contexts.iter().enumerate() {
1368 for (ai, action) in ctx.actions.iter().enumerate() {
1369 for (bi, binding) in action.bindings.iter().enumerate() {
1370 for (ci2, ctx2) in self.contexts.iter().enumerate() {
1371 if ci2 < ci { continue; }
1372 for (ai2, action2) in ctx2.actions.iter().enumerate() {
1373 if ci2 == ci && ai2 <= ai { continue; }
1374 for binding2 in &action2.bindings {
1375 if binding.binding == binding2.binding {
1376 conflicts.push(BindingConflict {
1377 context_a: ctx.name.clone(),
1378 action_a: action.name.clone(),
1379 context_b: ctx2.name.clone(),
1380 action_b: action2.name.clone(),
1381 conflicting_binding: binding.binding.label(),
1382 severity: ConflictSeverity::Warning,
1383 });
1384 }
1385 }
1386 }
1387 }
1388 }
1389 }
1390 }
1391 conflicts
1392 }
1393
1394 pub fn all_actions(&self) -> Vec<(&str, &InputAction)> {
1395 self.contexts.iter()
1396 .flat_map(|c| c.actions.iter().map(move |a| (c.name.as_str(), a)))
1397 .collect()
1398 }
1399
1400 pub fn actions_by_category(&self) -> HashMap<&str, Vec<&InputAction>> {
1401 let mut map: HashMap<&str, Vec<&InputAction>> = HashMap::new();
1402 for ctx in &self.contexts {
1403 for action in &ctx.actions {
1404 map.entry(action.category.as_str()).or_default().push(action);
1405 }
1406 }
1407 map
1408 }
1409}
1410
1411#[derive(Debug, Clone, Copy, PartialEq)]
1416pub enum InputEditorPanel {
1417 ActionMaps,
1418 Bindings,
1419 Deadzone,
1420 Haptics,
1421 Sensitivity,
1422 Macros,
1423 Combos,
1424 Accessibility,
1425 Devices,
1426 Replay,
1427 Gestures,
1428 Conflicts,
1429}
1430
1431impl InputEditorPanel {
1432 pub fn label(&self) -> &'static str {
1433 match self {
1434 Self::ActionMaps => "Action Maps",
1435 Self::Bindings => "Bindings",
1436 Self::Deadzone => "Dead Zone",
1437 Self::Haptics => "Haptics",
1438 Self::Sensitivity => "Sensitivity",
1439 Self::Macros => "Macros",
1440 Self::Combos => "Combos",
1441 Self::Accessibility => "Accessibility",
1442 Self::Devices => "Devices",
1443 Self::Replay => "Replay",
1444 Self::Gestures => "Gestures",
1445 Self::Conflicts => "Conflicts",
1446 }
1447 }
1448}
1449
1450#[derive(Debug)]
1451pub struct InputEditor {
1452 pub asset: InputAsset,
1453 pub active_panel: InputEditorPanel,
1454 pub selected_context: Option<usize>,
1455 pub selected_action: Option<usize>,
1456 pub selected_binding: Option<usize>,
1457 pub selected_haptic: Option<usize>,
1458 pub selected_macro: Option<usize>,
1459 pub rebinding_mode: bool,
1460 pub rebind_action: Option<String>,
1461 pub rebind_binding_index: Option<usize>,
1462 pub gamepad_state: GamepadState,
1463 pub keyboard_state: KeyboardState,
1464 pub mouse_state: MouseState,
1465 pub live_input_values: HashMap<String, f32>,
1466 pub deadzone_test_input: Vec2,
1467 pub deadzone_test_output: Vec2,
1468 pub selected_deadzone_axis: GamepadAxis,
1469 pub haptic_preview_time: f32,
1470 pub haptic_playing: bool,
1471 pub search_query: String,
1472 pub show_all_platforms: bool,
1473 pub active_device_tab: InputDevice,
1474 pub conflicts: Vec<BindingConflict>,
1475 pub conflict_filter: Option<ConflictSeverity>,
1476 pub history: Vec<InputAsset>,
1477 pub history_pos: usize,
1478}
1479
1480impl Default for InputEditor {
1481 fn default() -> Self {
1482 let asset = InputAsset::default_fps();
1483 let conflicts = asset.detect_conflicts();
1484 Self {
1485 asset,
1486 active_panel: InputEditorPanel::ActionMaps,
1487 selected_context: Some(0),
1488 selected_action: None,
1489 selected_binding: None,
1490 selected_haptic: None,
1491 selected_macro: None,
1492 rebinding_mode: false,
1493 rebind_action: None,
1494 rebind_binding_index: None,
1495 gamepad_state: GamepadState::synthetic_idle(),
1496 keyboard_state: KeyboardState::default(),
1497 mouse_state: MouseState::default(),
1498 live_input_values: HashMap::new(),
1499 deadzone_test_input: Vec2::ZERO,
1500 deadzone_test_output: Vec2::ZERO,
1501 selected_deadzone_axis: GamepadAxis::LeftStickX,
1502 haptic_preview_time: 0.0,
1503 haptic_playing: false,
1504 search_query: String::new(),
1505 show_all_platforms: true,
1506 active_device_tab: InputDevice::Keyboard,
1507 conflicts,
1508 conflict_filter: None,
1509 history: Vec::new(),
1510 history_pos: 0,
1511 }
1512 }
1513}
1514
1515impl InputEditor {
1516 pub fn snapshot(&mut self) {
1517 self.history.truncate(self.history_pos);
1518 self.history.push(self.asset.clone());
1519 self.history_pos = self.history.len();
1520 }
1521
1522 pub fn undo(&mut self) {
1523 if self.history_pos > 1 {
1524 self.history_pos -= 1;
1525 self.asset = self.history[self.history_pos - 1].clone();
1526 self.refresh_conflicts();
1527 }
1528 }
1529
1530 pub fn redo(&mut self) {
1531 if self.history_pos < self.history.len() {
1532 self.asset = self.history[self.history_pos].clone();
1533 self.history_pos += 1;
1534 self.refresh_conflicts();
1535 }
1536 }
1537
1538 pub fn refresh_conflicts(&mut self) {
1539 self.conflicts = self.asset.detect_conflicts();
1540 }
1541
1542 pub fn begin_rebind(&mut self, action: &str, binding_index: usize) {
1543 self.rebinding_mode = true;
1544 self.rebind_action = Some(action.to_string());
1545 self.rebind_binding_index = Some(binding_index);
1546 }
1547
1548 pub fn cancel_rebind(&mut self) {
1549 self.rebinding_mode = false;
1550 self.rebind_action = None;
1551 self.rebind_binding_index = None;
1552 }
1553
1554 pub fn update_deadzone_test(&mut self, raw: Vec2) {
1555 self.deadzone_test_input = raw;
1556 if let Some(config) = self.asset.deadzone_configs.get(&self.selected_deadzone_axis) {
1557 self.deadzone_test_output = config.apply(raw);
1558 }
1559 }
1560
1561 pub fn update_haptic_preview(&mut self, dt: f32) {
1562 if self.haptic_playing {
1563 self.haptic_preview_time += dt;
1564 if let Some(idx) = self.selected_haptic {
1565 if let Some(effect) = self.asset.haptic_effects.get(idx) {
1566 if self.haptic_preview_time > effect.duration_secs {
1567 self.haptic_playing = false;
1568 self.haptic_preview_time = 0.0;
1569 }
1570 }
1571 }
1572 }
1573 }
1574
1575 pub fn current_haptic_intensity(&self) -> f32 {
1576 if !self.haptic_playing { return 0.0; }
1577 if let Some(idx) = self.selected_haptic {
1578 if let Some(effect) = self.asset.haptic_effects.get(idx) {
1579 return effect.intensity_at_time(self.haptic_preview_time);
1580 }
1581 }
1582 0.0
1583 }
1584
1585 pub fn filtered_actions(&self) -> Vec<(&str, &InputAction)> {
1586 let q = self.search_query.to_lowercase();
1587 self.asset.all_actions().into_iter().filter(|(_, a)| {
1588 q.is_empty() || a.name.to_lowercase().contains(&q) || a.category.to_lowercase().contains(&q)
1589 }).collect()
1590 }
1591
1592 pub fn active_context(&self) -> Option<&ActionMapContext> {
1593 self.selected_context.and_then(|i| self.asset.contexts.get(i))
1594 }
1595
1596 pub fn active_context_mut(&mut self) -> Option<&mut ActionMapContext> {
1597 self.selected_context.and_then(|i| self.asset.contexts.get_mut(i))
1598 }
1599
1600 pub fn generate_code_bindings(&self) -> String {
1601 let mut lines = Vec::new();
1602 lines.push("// Auto-generated input bindings".to_string());
1603 for ctx in &self.asset.contexts {
1604 lines.push(format!("// Context: {} (priority {})", ctx.name, ctx.priority));
1605 for action in &ctx.actions {
1606 lines.push(format!("// Action: {} ({:?})", action.name, action.value_type));
1607 for binding in &action.bindings {
1608 lines.push(format!("// Binding: {} [{}]", binding.binding.label(), binding.device.label()));
1609 }
1610 }
1611 }
1612 lines.join("\n")
1613 }
1614}