use core::fmt;
use std::collections::VecDeque;
use crcbl_core::input::{KeyCode, PointerButton};
use crate::binding_text::{stick_name, trigger_name};
use crate::context::View;
use crate::gamepad::{pad_trigger, radial_deadzone};
use crate::{ActionKind, ActionMap, ActionSlot, Binding, PadButton, Stick, Trigger};
pub const RESOLUTION_TRACE_CAP: usize = 16;
#[derive(Clone, Debug, PartialEq)]
pub enum TracedInput {
Key(KeyCode),
MouseButton(PointerButton),
Wheel,
Control(String),
ControlStick(String),
PadButton(PadButton),
PadStick(Stick),
PadTrigger(Trigger),
}
impl fmt::Display for TracedInput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Key(key) => f.write_str(key.as_str()),
Self::MouseButton(button) => Binding::MouseButton(*button).fmt(f),
Self::Wheel => Binding::MouseScroll.fmt(f),
Self::Control(id) => write!(f, "Virtual:{id}"),
Self::ControlStick(id) => write!(f, "Virtual:{id} (stick)"),
Self::PadButton(button) => Binding::PadButton(*button).fmt(f),
Self::PadStick(stick) => write!(f, "PadStick:{}", stick_name(*stick)),
Self::PadTrigger(trigger) => write!(f, "PadTrigger:{}", trigger_name(*trigger)),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TracedRead {
pub action: String,
pub binding: Binding,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Outcome {
Read {
context: String,
reads: Vec<TracedRead>,
},
Withheld {
context: String,
},
Blocked {
modal: String,
},
Unbound,
Claimed {
by: String,
},
}
impl fmt::Display for Outcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Read { context, reads } => {
write!(f, "{context}: ")?;
if reads.is_empty() {
return f.write_str("nothing read it");
}
for (index, read) in reads.iter().enumerate() {
if index > 0 {
f.write_str(", ")?;
}
write!(f, "{} ({})", read.action, read.binding)?;
}
Ok(())
}
Self::Withheld { context } => write!(f, "withheld by {context} until released"),
Self::Blocked { modal } => write!(f, "blocked by modal {modal}"),
Self::Unbound => f.write_str("unbound"),
Self::Claimed { by } => write!(f, "claimed by {by}"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TraceEntry {
pub input: TracedInput,
pub outcome: Outcome,
pub count: u32,
}
impl ActionMap {
pub fn set_tracing(&mut self, on: bool) {
if !on {
self.trace = None;
} else if self.trace.is_none() {
self.trace = Some(VecDeque::with_capacity(RESOLUTION_TRACE_CAP));
}
}
#[must_use]
pub const fn is_tracing(&self) -> bool {
self.trace.is_some()
}
pub fn trace(&self) -> impl DoubleEndedIterator<Item = &TraceEntry> {
self.trace.iter().flatten()
}
pub fn trace_claimed(&mut self, input: TracedInput, by: &str) {
if self.trace.is_some() {
self.push_trace(input, Outcome::Claimed { by: by.to_owned() });
}
}
pub(crate) fn traced(&mut self, input: impl FnOnce() -> TracedInput) {
if self.trace.is_none() {
return;
}
let input = input();
let outcome = self.explain(&input);
self.push_trace(input, outcome);
}
fn push_trace(&mut self, input: TracedInput, outcome: Outcome) {
let Some(trace) = &mut self.trace else {
return;
};
if let Some(newest) = trace.back_mut()
&& newest.input == input
&& newest.outcome == outcome
{
newest.count = newest.count.saturating_add(1);
return;
}
if trace.len() == RESOLUTION_TRACE_CAP {
trace.pop_front();
}
trace.push_back(TraceEntry {
input,
outcome,
count: 1,
});
}
fn explain(&self, input: &TracedInput) -> Outcome {
let Some(context) = self.routes.owner(input, self.held_pad_buttons) else {
return match self.blocking_modal(|binding| claims(binding, input)) {
Some(modal) => Outcome::Blocked {
modal: self.contexts[modal].clone(),
},
None => Outcome::Unbound,
};
};
let name = self.contexts[context].clone();
if self.suppressed.holds(input) {
return Outcome::Withheld { context: name };
}
let view = &self.view(context);
let reads = self
.slots
.iter()
.enumerate()
.filter(|&(idx, slot)| slot.context == context && self.is_live(idx))
.flat_map(|(_, slot)| {
slot.decl
.bindings
.iter()
.filter(move |binding| self.reads(view, slot, binding, input))
.map(|binding| TracedRead {
action: slot.decl.name.clone(),
binding: binding.clone(),
})
})
.collect();
Outcome::Read {
context: name,
reads,
}
}
fn view(&self, context: usize) -> View<'_> {
View {
context,
routes: &self.routes,
suppressed: &self.suppressed,
held_keys: &self.held_keys,
held_buttons: &self.held_buttons,
held_controls: &self.held_controls,
held_pad_buttons: self.held_pad_buttons,
}
}
fn reads(
&self,
view: &View<'_>,
slot: &ActionSlot,
binding: &Binding,
input: &TracedInput,
) -> bool {
use ActionKind::{Axis1, Axis2, Button};
let kind = slot.kind();
let absolute = kind == Axis1
&& self.pointer.is_some()
&& view.pointer()
&& slot
.decl
.bindings
.iter()
.any(|bound| matches!(bound, Binding::PointerPosition { .. }));
if absolute {
return false;
}
match (input, binding) {
(TracedInput::Key(key), Binding::Key(bound)) => {
kind != Axis2 && bound == key && view.key(*key)
}
(
TracedInput::Key(key),
Binding::Chord {
modifier,
key: bound,
},
) => kind != Axis2 && bound == key && view.chord(*modifier, *key),
(TracedInput::Key(key), Binding::KeyAxis { .. }) => {
kind != Axis2 && binding.owns_key(*key) && view.key(*key)
}
(TracedInput::Key(key), Binding::Wasd { .. }) => {
kind != Axis1 && binding.owns_key(*key) && view.key(*key)
}
(TracedInput::MouseButton(button), Binding::MouseButton(bound)) => {
kind == Button && bound == button && view.button(*button)
}
(
TracedInput::MouseButton(button),
Binding::ButtonChord {
modifier,
button: bound,
},
) => kind == Button && bound == button && view.button_chord(*modifier, *button),
(TracedInput::Wheel, Binding::MouseScroll) => kind == Axis1 && view.scroll(),
(TracedInput::Wheel, Binding::ScrollChord { held }) => {
kind == Axis1 && view.scroll_chord(*held)
}
(TracedInput::Control(id), Binding::Virtual(bound)) => {
kind == Button && bound == id && view.control(id)
}
(TracedInput::ControlStick(id), Binding::Virtual(bound)) => {
kind == Axis2
&& bound == id
&& view.stick(id)
&& self
.control_sticks
.get(id.as_str())
.is_some_and(|&deflection| deflection != (0.0, 0.0))
}
(TracedInput::PadButton(button), Binding::PadButton(bound)) => {
kind != Axis2 && bound == button && view.pad_button(*button)
}
(
TracedInput::PadButton(button),
Binding::PadChord {
modifier,
button: bound,
},
) => kind != Axis2 && bound == button && view.pad_chord(*modifier, *button),
(TracedInput::PadButton(button), Binding::PadDpad) => {
kind != Axis1 && PadButton::DPAD.contains(button) && view.pad_button(*button)
}
(
TracedInput::PadStick(stick),
Binding::PadStick {
stick: bound,
deadzone,
},
) => {
kind == Axis2
&& bound == stick
&& view.pad_stick(*stick)
&& self
.pads
.values()
.any(|pad| radial_deadzone(pad.stick(*stick), *deadzone) != (0.0, 0.0))
}
(
TracedInput::PadTrigger(trigger),
Binding::PadTrigger {
trigger: bound,
threshold,
},
) => {
kind != Axis2
&& bound == trigger
&& view.trigger(*trigger)
&& pad_trigger(&self.pads, *trigger) > *threshold
}
_ => false,
}
}
}
fn claims(binding: &Binding, input: &TracedInput) -> bool {
match input {
TracedInput::Key(key) => binding.owns_key(*key),
TracedInput::MouseButton(button) => binding.mouse_button() == Some(*button),
TracedInput::Wheel => binding.reads_wheel(),
TracedInput::Control(id) | TracedInput::ControlStick(id) => {
matches!(binding, Binding::Virtual(bound) if bound == id)
}
TracedInput::PadButton(button) => match binding {
Binding::PadButton(bound) | Binding::PadChord { button: bound, .. } => bound == button,
Binding::PadDpad => PadButton::DPAD.contains(button),
_ => false,
},
TracedInput::PadStick(stick) => {
matches!(binding, Binding::PadStick { stick: bound, .. } if bound == stick)
}
TracedInput::PadTrigger(trigger) => {
matches!(binding, Binding::PadTrigger { trigger: bound, .. } if bound == trigger)
}
}
}
#[cfg(test)]
mod tests;