use core::fmt::Display;
use mirage_engine::prelude::*;
const READING_MARGIN: f32 = 4.0;
const VALUE_COLUMN_WIDTH: f32 = 70.0;
#[derive(InputButtonAction, Clone, Copy, PartialEq)]
enum ButtonAction {
South,
East,
West,
North,
LeftBumper,
RightBumper,
LeftTrigger,
RightTrigger,
Select,
Start,
Guide,
LeftStickPress,
RightStickPress,
DPadUp,
DPadDown,
DPadLeft,
DPadRight,
MouseLeft,
MouseRight,
MouseMiddle,
}
impl InputButtonAction for ButtonAction {
fn bindings(&self) -> Vec<ButtonBinding> {
match self {
Self::South => vec![Pad::South.into(), Key::Space.into()],
Self::East => vec![Pad::East.into(), Key::Escape.into()],
Self::West => vec![Pad::West.into(), Key::X.into()],
Self::North => vec![Pad::North.into(), Key::N.into()],
Self::LeftBumper => vec![Pad::LeftBumper.into(), Key::Q.into()],
Self::RightBumper => vec![Pad::RightBumper.into(), Key::E.into()],
Self::LeftTrigger => vec![Pad::LeftTrigger.into(), Key::BracketLeft.into()],
Self::RightTrigger => vec![Pad::RightTrigger.into(), Key::BracketRight.into()],
Self::Select => vec![Pad::Select.into(), Key::Tab.into()],
Self::Start => vec![Pad::Start.into(), Key::Enter.into()],
Self::Guide => vec![Pad::Guide.into(), Key::Backspace.into()],
Self::LeftStickPress => vec![Pad::LeftStick.into(), Key::Digit1.into()],
Self::RightStickPress => vec![Pad::RightStick.into(), Key::Digit2.into()],
Self::DPadUp => vec![Pad::DPadUp.into(), Key::T.into()],
Self::DPadDown => vec![Pad::DPadDown.into(), Key::G.into()],
Self::DPadLeft => vec![Pad::DPadLeft.into(), Key::F.into()],
Self::DPadRight => vec![Pad::DPadRight.into(), Key::H.into()],
Self::MouseLeft => vec![MouseButton::Left.into()],
Self::MouseRight => vec![MouseButton::Right.into()],
Self::MouseMiddle => vec![MouseButton::Middle.into()],
}
}
}
#[derive(InputAxisAction, Clone, Copy, PartialEq)]
enum AxisAction {
LeftX,
LeftY,
RightX,
RightY,
LeftTrigger,
RightTrigger,
PointerSideways,
PointerUp,
WheelUp,
WheelSideways,
}
impl InputAxisAction for AxisAction {
fn bindings(&self) -> Vec<AxisBinding> {
match self {
Self::LeftX => vec![
AxisBinding::pad(PadAxis::LeftX),
ButtonAxis {
negative: Key::A,
positive: Key::D,
}
.into(),
],
Self::LeftY => vec![
AxisBinding::pad(PadAxis::LeftY),
ButtonAxis {
negative: Key::S,
positive: Key::W,
}
.into(),
],
Self::RightX => vec![
AxisBinding::pad(PadAxis::RightX),
ButtonAxis {
negative: Key::J,
positive: Key::L,
}
.into(),
],
Self::RightY => vec![
AxisBinding::pad(PadAxis::RightY),
ButtonAxis {
negative: Key::K,
positive: Key::I,
}
.into(),
],
Self::LeftTrigger => vec![
AxisBinding::pad(PadAxis::LeftTrigger),
ButtonAxis {
negative: Key::Minus,
positive: Key::BracketLeft,
}
.into(),
],
Self::RightTrigger => vec![
AxisBinding::pad(PadAxis::RightTrigger),
ButtonAxis {
negative: Key::Equal,
positive: Key::BracketRight,
}
.into(),
],
Self::PointerSideways => vec![AxisBinding::pointer_delta(PointerDelta::Sideways)],
Self::PointerUp => vec![AxisBinding::pointer_delta(PointerDelta::Up)],
Self::WheelUp => vec![AxisBinding::wheel(WheelDelta::Up)],
Self::WheelSideways => vec![AxisBinding::wheel(WheelDelta::Sideways)],
}
}
}
#[derive(InputAxis2Action, Clone, Copy, PartialEq)]
enum Axis2Action {
LeftStick,
RightStick,
Pointer,
Wheel,
}
impl InputAxis2Action for Axis2Action {
fn bindings(&self) -> Vec<Axis2Binding> {
match self {
Self::LeftStick => vec![
Axis2Binding::stick(Stick::Left),
ButtonAxis2 {
left: Key::A,
right: Key::D,
down: Key::S,
up: Key::W,
}
.into(),
],
Self::RightStick => vec![
Axis2Binding::stick(Stick::Right),
ButtonAxis2 {
left: Key::J,
right: Key::L,
down: Key::K,
up: Key::I,
}
.into(),
],
Self::Pointer => vec![Axis2Binding::pointer()],
Self::Wheel => vec![Axis2Binding::wheel()],
}
}
}
struct Controls;
impl InputActions for Controls {
type Button = ButtonAction;
type Axis = AxisAction;
type Axis2 = Axis2Action;
}
#[derive(Clone, Copy, PartialEq)]
enum Control {
Button(ButtonAction),
Axis(AxisAction),
Axis2(Axis2Action),
}
struct InputLab {
listening: Option<Control>,
last_button: Option<ButtonBinding>,
last_axis: Option<AxisBinding>,
last_axis2: Option<Axis2Binding>,
}
impl InputLab {
fn init(_ctx: &mut InitContext<'_, Self>) -> Result<Self, Error> {
Ok(Self {
listening: None,
last_button: None,
last_axis: None,
last_axis2: None,
})
}
}
impl Game for InputLab {
type Meshes = ();
type Sounds = NoSounds;
type InputActions = Controls;
type SurfaceStyles = ();
type Skyboxes = NoSkyboxes;
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
let buttons: Vec<_> = ButtonAction::all()
.into_iter()
.map(|action| {
(
action,
bindings_text(ctx.bindings(action)),
ctx.down(action),
ctx.pressed(action),
ctx.released(action),
ctx.clicks(action),
)
})
.collect();
let axes: Vec<_> = AxisAction::all()
.into_iter()
.map(|action| {
(
action,
bindings_text(ctx.bindings(action)),
ctx.axis(action),
)
})
.collect();
let axes2: Vec<_> = Axis2Action::all()
.into_iter()
.map(|action| {
(
action,
bindings_text(ctx.bindings(action)),
ctx.axis2(action),
)
})
.collect();
let capturing = !ctx.ui_wants_keyboard();
let actuated_button = capturing.then(|| ctx.actuated_button()).flatten();
let actuated_axis = capturing.then(|| ctx.actuated_axis()).flatten();
let actuated_axis2 = capturing.then(|| ctx.actuated_axis2()).flatten();
if actuated_button.is_some() {
self.last_button = actuated_button;
}
if actuated_axis.is_some() {
self.last_axis = actuated_axis;
}
if actuated_axis2.is_some() {
self.last_axis2 = actuated_axis2;
}
let pointer = ctx.pointer();
let mut edits = RowEdits {
listening: self.listening,
start_listening: None,
cancel: false,
reset: None,
};
ctx.ui(|ui| {
egui::CentralPanel::default().show(ui, |ui| {
ui.spacing_mut().item_spacing = egui::vec2(6.0, 2.0);
ui.style_mut().override_text_style = Some(egui::TextStyle::Small);
ui.label("rebinds persist across runs");
ui.label(format!(
"last captured: button {}, pad axis {}, pad stick {}",
text_of(self.last_button),
text_of(self.last_axis),
text_of(self.last_axis2),
));
ui.label(format!("pointer {:.0}, {:.0}", pointer.x, pointer.y));
ui.separator();
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.heading("buttons");
egui::Grid::new("buttons-grid")
.num_columns(5)
.spacing([6.0, 2.0])
.show(ui, |ui| {
for (action, bindings, down, pressed, released, clicks) in &buttons
{
let control = Control::Button(*action);
ui.label(action.name());
ui.label(bindings);
ui.horizontal(|ui| {
mark(ui, "down", *down);
mark(ui, "pressed", *pressed);
mark(ui, "released", *released);
ui.label(format!("clicks {clicks}"));
});
rebind_cell(ui, control, &mut edits);
reset_cell(ui, control, &mut edits);
ui.end_row();
}
});
});
ui.separator();
ui.vertical(|ui| {
egui::Grid::new("axes-grid")
.num_columns(5)
.spacing([6.0, 2.0])
.show(ui, |ui| {
ui.heading("axes");
ui.end_row();
for (action, bindings, value) in &axes {
let control = Control::Axis(*action);
ui.label(action.name());
ui.label(bindings);
axis_bar(ui, *value);
rebind_cell(ui, control, &mut edits);
reset_cell(ui, control, &mut edits);
ui.end_row();
}
ui.heading("vectors");
ui.end_row();
for (action, bindings, value) in &axes2 {
let control = Control::Axis2(*action);
ui.label(action.name());
ui.label(bindings);
axis2_dot(ui, *value);
rebind_cell(ui, control, &mut edits);
reset_cell(ui, control, &mut edits);
ui.end_row();
}
});
});
});
});
});
if edits.cancel {
self.listening = None;
}
if let Some(control) = edits.start_listening {
self.listening = Some(control);
}
if let Some(control) = edits.reset {
match control {
Control::Button(action) => ctx.rebind(action, action.bindings()),
Control::Axis(action) => ctx.rebind(action, action.bindings()),
Control::Axis2(action) => ctx.rebind(action, action.bindings()),
}
}
match (
self.listening,
actuated_button,
actuated_axis,
actuated_axis2,
) {
(Some(Control::Button(action)), Some(binding), _, _) => {
ctx.rebind(action, vec![binding]);
self.listening = None;
}
(Some(Control::Axis(action)), _, Some(binding), _) => {
ctx.rebind(action, vec![binding]);
self.listening = None;
}
(Some(Control::Axis2(action)), _, _, Some(binding)) => {
ctx.rebind(action, vec![binding]);
self.listening = None;
}
_ => {}
}
}
}
struct RowEdits {
listening: Option<Control>,
start_listening: Option<Control>,
cancel: bool,
reset: Option<Control>,
}
fn rebind_cell(ui: &mut egui::Ui, control: Control, edits: &mut RowEdits) {
if edits.listening == Some(control) {
ui.horizontal(|ui| {
ui.label("listening");
if ui.button("cancel").clicked() {
edits.cancel = true;
}
});
} else if ui.button("rebind").clicked() {
edits.start_listening = Some(control);
}
}
fn reset_cell(ui: &mut egui::Ui, control: Control, edits: &mut RowEdits) {
if ui.button("reset").clicked() {
edits.reset = Some(control);
}
}
fn mark(ui: &mut egui::Ui, label: &str, active: bool) {
ui.colored_label(highlight_color(active), label);
}
fn highlight_color(active: bool) -> egui::Color32 {
match active {
true => egui::Color32::from_rgb(90, 200, 120),
false => egui::Color32::from_gray(90),
}
}
fn axis_bar(ui: &mut egui::Ui, value: f32) {
let size = egui::vec2(VALUE_COLUMN_WIDTH, 12.0);
let (rect, _response) = ui.allocate_exact_size(size, egui::Sense::hover());
let painter = ui.painter();
painter.rect_filled(rect, 2.0, egui::Color32::from_gray(35));
let mid = rect.left() + rect.width() * 0.5;
let reach = rect.width() * 0.5 - READING_MARGIN;
let end = mid + value.clamp(-1.0, 1.0) * reach;
let fill = egui::Rect::from_min_max(
egui::pos2(mid.min(end), rect.top()),
egui::pos2(mid.max(end), rect.bottom()),
);
painter.rect_filled(fill, 2.0, highlight_color(value != 0.0));
painter.vline(
mid,
egui::Rangef::new(rect.top() + 3.0, rect.bottom() - 3.0),
egui::Stroke::new(1.0, egui::Color32::from_gray(160)),
);
painter.rect_stroke(
rect,
2.0,
egui::Stroke::new(1.0, egui::Color32::from_gray(120)),
egui::StrokeKind::Inside,
);
ui.label(format!("{value:.2}"));
}
fn axis2_dot(ui: &mut egui::Ui, value: Vec2) {
let square_side = 36.0;
let radius = 4.0;
let (column, _response) = ui.allocate_exact_size(
egui::vec2(VALUE_COLUMN_WIDTH, square_side),
egui::Sense::hover(),
);
let rect = egui::Rect::from_center_size(column.center(), egui::Vec2::splat(square_side));
let painter = ui.painter();
painter.rect_filled(rect, 2.0, egui::Color32::from_gray(35));
let shown = value.clamp_length_max(1.0);
let reach = rect.width() * 0.5 - READING_MARGIN - radius;
let point = rect.center() + egui::vec2(shown.x, -shown.y) * reach;
painter.circle_filled(point, radius, highlight_color(value != Vec2::ZERO));
painter.rect_stroke(
rect,
2.0,
egui::Stroke::new(1.0, egui::Color32::from_gray(120)),
egui::StrokeKind::Inside,
);
ui.label(format!("{:.2}, {:.2}", value.x, value.y));
}
fn bindings_text<B: Display>(bindings: Vec<B>) -> String {
bindings
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
}
fn text_of<B: Display>(binding: Option<B>) -> String {
match binding {
Some(binding) => binding.to_string(),
None => "none".to_string(),
}
}
fn main() {
run(
Config::new("Mirage: every control as an action"),
InputLab::init,
);
}