use egui::{Event, Key, Vec2};
use egui_rotate::Rotation;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LauncherAction {
PrevCard,
NextCard,
PrevRow,
NextRow,
Launch,
Cancel,
ToggleFavorite,
AdjustRating(i8),
CycleSort,
}
impl LauncherAction {
pub fn from_vpx_action(name: &str) -> Option<Self> {
match name {
"LeftFlipper" => Some(Self::PrevCard),
"RightFlipper" => Some(Self::NextCard),
"LeftMagna" => Some(Self::PrevRow),
"RightMagna" => Some(Self::NextRow),
"Start" => Some(Self::Launch),
"LaunchBall" => Some(Self::ToggleFavorite),
"ExtraBall" => Some(Self::CycleSort),
"LeftStagedFlipper" => Some(Self::AdjustRating(-5)),
"RightStagedFlipper" => Some(Self::AdjustRating(5)),
"ExitGame" => Some(Self::Cancel),
_ => None,
}
}
pub fn is_directional(self) -> bool {
matches!(
self,
Self::PrevCard | Self::NextCard | Self::PrevRow | Self::NextRow
)
}
}
pub fn is_hotkey_text(text: &str) -> bool {
matches!(text, "+" | "=" | "-")
}
fn unrotate_vec(rotation: Rotation, v: Vec2) -> Vec2 {
match rotation {
Rotation::None => v,
Rotation::CW90 => Vec2::new(v.y, -v.x),
Rotation::CW180 => Vec2::new(-v.x, -v.y),
Rotation::CW270 => Vec2::new(-v.y, v.x),
}
}
pub fn collect_actions(
ui: &egui::Ui,
rotation: Rotation,
search_empty: bool,
) -> Vec<LauncherAction> {
let cabinet_mode = !rotation.is_none();
let text_focused = ui.ctx().text_edit_focused();
let hotkeys = search_empty && !text_focused;
let button_focused = !text_focused && ui.ctx().memory(|m| m.focused().is_some());
ui.input(|i| {
i.events
.iter()
.filter_map(|e| match e {
Event::Key {
key, pressed: true, ..
} => match key {
Key::ArrowLeft => Some(LauncherAction::PrevCard),
Key::ArrowRight => Some(LauncherAction::NextCard),
Key::ArrowUp => Some(LauncherAction::PrevRow),
Key::ArrowDown => Some(LauncherAction::NextRow),
Key::ShiftLeft if !text_focused => Some(LauncherAction::PrevCard),
Key::ShiftRight if !text_focused => Some(LauncherAction::NextCard),
Key::ControlLeft if !text_focused => Some(LauncherAction::PrevRow),
Key::ControlRight if !text_focused => Some(LauncherAction::NextRow),
Key::Enter if !button_focused => Some(LauncherAction::Launch),
Key::Escape => Some(LauncherAction::Cancel),
Key::Plus | Key::Equals if hotkeys => Some(LauncherAction::AdjustRating(5)),
Key::Minus if hotkeys => Some(LauncherAction::AdjustRating(-5)),
_ => None,
},
Event::MouseWheel { delta, .. } if cabinet_mode => {
let phys = unrotate_vec(rotation, *delta);
if phys.y > 0.0 {
Some(LauncherAction::PrevRow)
} else if phys.y < 0.0 {
Some(LauncherAction::NextRow)
} else {
None
}
}
_ => None,
})
.collect()
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_vpx_action_known_mappings() {
assert_eq!(
LauncherAction::from_vpx_action("LeftFlipper"),
Some(LauncherAction::PrevCard)
);
assert_eq!(
LauncherAction::from_vpx_action("RightMagna"),
Some(LauncherAction::NextRow)
);
assert_eq!(
LauncherAction::from_vpx_action("Start"),
Some(LauncherAction::Launch)
);
assert_eq!(
LauncherAction::from_vpx_action("LaunchBall"),
Some(LauncherAction::ToggleFavorite)
);
assert_eq!(
LauncherAction::from_vpx_action("ExitGame"),
Some(LauncherAction::Cancel)
);
assert_eq!(LauncherAction::from_vpx_action("Tilt"), None);
}
#[test]
fn unrotate_inverts_egui_rotate_transform() {
for &(x, y) in &[(1.0, 0.0), (0.0, 1.0), (1.0, -1.0), (-2.5, 3.5)] {
let physical = Vec2::new(x, y);
for rot in [
Rotation::None,
Rotation::CW90,
Rotation::CW180,
Rotation::CW270,
] {
let logical = rot.transform_vec(physical);
let recovered = unrotate_vec(rot, logical);
let ok = (recovered.x - physical.x).abs() < 1e-4
&& (recovered.y - physical.y).abs() < 1e-4;
assert!(ok, "rotation {:?} did not round-trip", rot);
}
}
}
#[test]
fn directional_actions() {
assert!(LauncherAction::PrevCard.is_directional());
assert!(LauncherAction::NextCard.is_directional());
assert!(LauncherAction::PrevRow.is_directional());
assert!(LauncherAction::NextRow.is_directional());
assert!(!LauncherAction::Launch.is_directional());
assert!(!LauncherAction::Cancel.is_directional());
}
}