Skip to main content

input_lab/
input-lab.rs

1//! Every control the engine can bind, as a row of its own: a pad button
2//! and its keyboard alternative; a pad axis and a mouse or wheel lane; a
3//! stick and the pointer. Each row shows its live bindings, what it reads
4//! this frame, a capture rebind button and a reset. The frame draws no
5//! scene, only the panel.
6
7use core::fmt::Display;
8
9use mirage_engine::prelude::*;
10
11/// The gap a reading of `1` keeps from the frame, shared by `axis_bar` and
12/// `axis2_dot` so a span and a dot read at one distance from it.
13const READING_MARGIN: f32 = 4.0;
14
15/// The value column's width, shared by `axis_bar` and `axis2_dot` so a
16/// span's center and a square's center sit at one position down the
17/// column.
18const VALUE_COLUMN_WIDTH: f32 = 70.0;
19
20/// Every button the engine can bind: one per pad button, one per mouse
21/// button.
22#[derive(InputButtonAction, Clone, Copy, PartialEq)]
23enum ButtonAction {
24    South,
25    East,
26    West,
27    North,
28    LeftBumper,
29    RightBumper,
30    LeftTrigger,
31    RightTrigger,
32    Select,
33    Start,
34    Guide,
35    LeftStickPress,
36    RightStickPress,
37    DPadUp,
38    DPadDown,
39    DPadLeft,
40    DPadRight,
41    MouseLeft,
42    MouseRight,
43    MouseMiddle,
44}
45
46impl InputButtonAction for ButtonAction {
47    fn bindings(&self) -> Vec<ButtonBinding> {
48        match self {
49            Self::South => vec![Pad::South.into(), Key::Space.into()],
50            Self::East => vec![Pad::East.into(), Key::Escape.into()],
51            Self::West => vec![Pad::West.into(), Key::X.into()],
52            Self::North => vec![Pad::North.into(), Key::N.into()],
53            Self::LeftBumper => vec![Pad::LeftBumper.into(), Key::Q.into()],
54            Self::RightBumper => vec![Pad::RightBumper.into(), Key::E.into()],
55            Self::LeftTrigger => vec![Pad::LeftTrigger.into(), Key::BracketLeft.into()],
56            Self::RightTrigger => vec![Pad::RightTrigger.into(), Key::BracketRight.into()],
57            Self::Select => vec![Pad::Select.into(), Key::Tab.into()],
58            Self::Start => vec![Pad::Start.into(), Key::Enter.into()],
59            Self::Guide => vec![Pad::Guide.into(), Key::Backspace.into()],
60            Self::LeftStickPress => vec![Pad::LeftStick.into(), Key::Digit1.into()],
61            Self::RightStickPress => vec![Pad::RightStick.into(), Key::Digit2.into()],
62            Self::DPadUp => vec![Pad::DPadUp.into(), Key::T.into()],
63            Self::DPadDown => vec![Pad::DPadDown.into(), Key::G.into()],
64            Self::DPadLeft => vec![Pad::DPadLeft.into(), Key::F.into()],
65            Self::DPadRight => vec![Pad::DPadRight.into(), Key::H.into()],
66            Self::MouseLeft => vec![MouseButton::Left.into()],
67            Self::MouseRight => vec![MouseButton::Right.into()],
68            Self::MouseMiddle => vec![MouseButton::Middle.into()],
69        }
70    }
71}
72
73/// Every number the engine can bind: one per pad axis lane, one per
74/// pointer lane, one per wheel lane.
75#[derive(InputAxisAction, Clone, Copy, PartialEq)]
76enum AxisAction {
77    LeftX,
78    LeftY,
79    RightX,
80    RightY,
81    LeftTrigger,
82    RightTrigger,
83    PointerSideways,
84    PointerUp,
85    WheelUp,
86    WheelSideways,
87}
88
89impl InputAxisAction for AxisAction {
90    fn bindings(&self) -> Vec<AxisBinding> {
91        match self {
92            Self::LeftX => vec![
93                AxisBinding::pad(PadAxis::LeftX),
94                ButtonAxis {
95                    negative: Key::A,
96                    positive: Key::D,
97                }
98                .into(),
99            ],
100            Self::LeftY => vec![
101                AxisBinding::pad(PadAxis::LeftY),
102                ButtonAxis {
103                    negative: Key::S,
104                    positive: Key::W,
105                }
106                .into(),
107            ],
108            Self::RightX => vec![
109                AxisBinding::pad(PadAxis::RightX),
110                ButtonAxis {
111                    negative: Key::J,
112                    positive: Key::L,
113                }
114                .into(),
115            ],
116            Self::RightY => vec![
117                AxisBinding::pad(PadAxis::RightY),
118                ButtonAxis {
119                    negative: Key::K,
120                    positive: Key::I,
121                }
122                .into(),
123            ],
124            Self::LeftTrigger => vec![
125                AxisBinding::pad(PadAxis::LeftTrigger),
126                ButtonAxis {
127                    negative: Key::Minus,
128                    positive: Key::BracketLeft,
129                }
130                .into(),
131            ],
132            Self::RightTrigger => vec![
133                AxisBinding::pad(PadAxis::RightTrigger),
134                ButtonAxis {
135                    negative: Key::Equal,
136                    positive: Key::BracketRight,
137                }
138                .into(),
139            ],
140            Self::PointerSideways => vec![AxisBinding::pointer_delta(PointerDelta::Sideways)],
141            Self::PointerUp => vec![AxisBinding::pointer_delta(PointerDelta::Up)],
142            Self::WheelUp => vec![AxisBinding::wheel(WheelDelta::Up)],
143            Self::WheelSideways => vec![AxisBinding::wheel(WheelDelta::Sideways)],
144        }
145    }
146}
147
148/// Every vector the engine can bind: one per stick, one for the pointer,
149/// one for the wheel.
150#[derive(InputAxis2Action, Clone, Copy, PartialEq)]
151enum Axis2Action {
152    LeftStick,
153    RightStick,
154    Pointer,
155    Wheel,
156}
157
158impl InputAxis2Action for Axis2Action {
159    fn bindings(&self) -> Vec<Axis2Binding> {
160        match self {
161            Self::LeftStick => vec![
162                Axis2Binding::stick(Stick::Left),
163                ButtonAxis2 {
164                    left: Key::A,
165                    right: Key::D,
166                    down: Key::S,
167                    up: Key::W,
168                }
169                .into(),
170            ],
171            Self::RightStick => vec![
172                Axis2Binding::stick(Stick::Right),
173                ButtonAxis2 {
174                    left: Key::J,
175                    right: Key::L,
176                    down: Key::K,
177                    up: Key::I,
178                }
179                .into(),
180            ],
181            Self::Pointer => vec![Axis2Binding::pointer()],
182            Self::Wheel => vec![Axis2Binding::wheel()],
183        }
184    }
185}
186
187struct Controls;
188
189impl InputActions for Controls {
190    type Button = ButtonAction;
191    type Axis = AxisAction;
192    type Axis2 = Axis2Action;
193}
194
195/// The row a rebind or a reset applies to.
196#[derive(Clone, Copy, PartialEq)]
197enum Control {
198    Button(ButtonAction),
199    Axis(AxisAction),
200    Axis2(Axis2Action),
201}
202
203/// A gamepad panel built on the action system: every device control the
204/// engine can bind is a row, so pressing a pad or a keyboard control
205/// highlights the row it drives.
206struct InputLab {
207    /// The row waiting for a capture to rebind it, if any.
208    listening: Option<Control>,
209    /// The last button a capture query returned during the run, if any.
210    last_button: Option<ButtonBinding>,
211    /// The last axis binding a capture query returned during the run, if any.
212    last_axis: Option<AxisBinding>,
213    /// The last stick binding a capture query returned during the run, if any.
214    last_axis2: Option<Axis2Binding>,
215}
216
217impl InputLab {
218    fn init(_ctx: &mut InitContext<'_, Self>) -> Result<Self, Error> {
219        Ok(Self {
220            listening: None,
221            last_button: None,
222            last_axis: None,
223            last_axis2: None,
224        })
225    }
226}
227
228impl Game for InputLab {
229    type Meshes = NoMeshes;
230    type Sounds = NoSounds;
231    type InputActions = Controls;
232    type SurfaceStyles = NoSurfaceStyles;
233    type Skyboxes = NoSkyboxes;
234    type PostEffects = NoPostEffects;
235
236    fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
237
238    fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
239        // `ctx.ui` cannot borrow `ctx`, so every reading and capture query
240        // is taken first, and `rebind` is applied once the closure returns.
241        let buttons: Vec<_> = ButtonAction::all()
242            .into_iter()
243            .map(|action| {
244                (
245                    action,
246                    bindings_text(ctx.bindings(action)),
247                    ctx.down(action),
248                    ctx.pressed(action),
249                    ctx.released(action),
250                    ctx.clicks(action),
251                )
252            })
253            .collect();
254        let axes: Vec<_> = AxisAction::all()
255            .into_iter()
256            .map(|action| {
257                (
258                    action,
259                    bindings_text(ctx.bindings(action)),
260                    ctx.axis(action),
261                )
262            })
263            .collect();
264        let axes2: Vec<_> = Axis2Action::all()
265            .into_iter()
266            .map(|action| {
267                (
268                    action,
269                    bindings_text(ctx.bindings(action)),
270                    ctx.axis2(action),
271                )
272            })
273            .collect();
274
275        let capturing = !ctx.ui_wants_keyboard();
276        let actuated_button = capturing.then(|| ctx.actuated_button()).flatten();
277        let actuated_axis = capturing.then(|| ctx.actuated_axis()).flatten();
278        let actuated_axis2 = capturing.then(|| ctx.actuated_axis2()).flatten();
279        if actuated_button.is_some() {
280            self.last_button = actuated_button;
281        }
282        if actuated_axis.is_some() {
283            self.last_axis = actuated_axis;
284        }
285        if actuated_axis2.is_some() {
286            self.last_axis2 = actuated_axis2;
287        }
288        let pointer = ctx.pointer();
289        let mut edits = RowEdits {
290            listening: self.listening,
291            start_listening: None,
292            cancel: false,
293            reset: None,
294        };
295
296        ctx.ui(|ui| {
297            egui::CentralPanel::default().show(ui, |ui| {
298                ui.spacing_mut().item_spacing = egui::vec2(6.0, 2.0);
299                ui.style_mut().override_text_style = Some(egui::TextStyle::Small);
300                ui.label("rebinds persist across runs");
301                ui.label(format!(
302                    "last captured: button {}, pad axis {}, pad stick {}",
303                    text_of(self.last_button),
304                    text_of(self.last_axis),
305                    text_of(self.last_axis2),
306                ));
307                ui.label(format!("pointer {:.0}, {:.0}", pointer.x, pointer.y));
308                ui.separator();
309
310                ui.horizontal(|ui| {
311                    ui.vertical(|ui| {
312                        ui.heading("buttons");
313                        egui::Grid::new("buttons-grid")
314                            .num_columns(5)
315                            .spacing([6.0, 2.0])
316                            .show(ui, |ui| {
317                                for (action, bindings, down, pressed, released, clicks) in &buttons
318                                {
319                                    let control = Control::Button(*action);
320                                    ui.label(action.name());
321                                    ui.label(bindings);
322                                    ui.horizontal(|ui| {
323                                        mark(ui, "down", *down);
324                                        mark(ui, "pressed", *pressed);
325                                        mark(ui, "released", *released);
326                                        ui.label(format!("clicks {clicks}"));
327                                    });
328                                    rebind_cell(ui, control, &mut edits);
329                                    reset_cell(ui, control, &mut edits);
330                                    ui.end_row();
331                                }
332                            });
333                    });
334
335                    ui.separator();
336
337                    ui.vertical(|ui| {
338                        egui::Grid::new("axes-grid")
339                            .num_columns(5)
340                            .spacing([6.0, 2.0])
341                            .show(ui, |ui| {
342                                ui.heading("axes");
343                                ui.end_row();
344                                for (action, bindings, value) in &axes {
345                                    let control = Control::Axis(*action);
346                                    ui.label(action.name());
347                                    ui.label(bindings);
348                                    axis_bar(ui, *value);
349                                    rebind_cell(ui, control, &mut edits);
350                                    reset_cell(ui, control, &mut edits);
351                                    ui.end_row();
352                                }
353
354                                ui.heading("vectors");
355                                ui.end_row();
356                                for (action, bindings, value) in &axes2 {
357                                    let control = Control::Axis2(*action);
358                                    ui.label(action.name());
359                                    ui.label(bindings);
360                                    axis2_dot(ui, *value);
361                                    rebind_cell(ui, control, &mut edits);
362                                    reset_cell(ui, control, &mut edits);
363                                    ui.end_row();
364                                }
365                            });
366                    });
367                });
368            });
369        });
370
371        if edits.cancel {
372            self.listening = None;
373        }
374        if let Some(control) = edits.start_listening {
375            self.listening = Some(control);
376        }
377        if let Some(control) = edits.reset {
378            match control {
379                Control::Button(action) => ctx.rebind(action, action.bindings()),
380                Control::Axis(action) => ctx.rebind(action, action.bindings()),
381                Control::Axis2(action) => ctx.rebind(action, action.bindings()),
382            }
383        }
384        match (
385            self.listening,
386            actuated_button,
387            actuated_axis,
388            actuated_axis2,
389        ) {
390            (Some(Control::Button(action)), Some(binding), _, _) => {
391                ctx.rebind(action, vec![binding]);
392                self.listening = None;
393            }
394            (Some(Control::Axis(action)), _, Some(binding), _) => {
395                ctx.rebind(action, vec![binding]);
396                self.listening = None;
397            }
398            (Some(Control::Axis2(action)), _, _, Some(binding)) => {
399                ctx.rebind(action, vec![binding]);
400                self.listening = None;
401            }
402            _ => {}
403        }
404    }
405}
406
407/// The rebind row's result across one frame: the row waiting to start
408/// listening, the cancel of the one already listening, and the row a
409/// reset applies to.
410struct RowEdits {
411    listening: Option<Control>,
412    start_listening: Option<Control>,
413    cancel: bool,
414    reset: Option<Control>,
415}
416
417/// A capture rebind button, or a cancel while listening.
418fn rebind_cell(ui: &mut egui::Ui, control: Control, edits: &mut RowEdits) {
419    if edits.listening == Some(control) {
420        ui.horizontal(|ui| {
421            ui.label("listening");
422            if ui.button("cancel").clicked() {
423                edits.cancel = true;
424            }
425        });
426    } else if ui.button("rebind").clicked() {
427        edits.start_listening = Some(control);
428    }
429}
430
431/// A reset to `control`'s declared default bindings.
432fn reset_cell(ui: &mut egui::Ui, control: Control, edits: &mut RowEdits) {
433    if ui.button("reset").clicked() {
434        edits.reset = Some(control);
435    }
436}
437
438/// `label`, highlighted when `active` and dim otherwise, one of a button
439/// row's three marks.
440fn mark(ui: &mut egui::Ui, label: &str, active: bool) {
441    ui.colored_label(highlight_color(active), label);
442}
443
444/// Green while `active`, grey at rest, for a mark, an axis value and a
445/// stick value.
446fn highlight_color(active: bool) -> egui::Color32 {
447    match active {
448        true => egui::Color32::from_rgb(90, 200, 120),
449        false => egui::Color32::from_gray(90),
450    }
451}
452
453/// A span across `-1..=1` with the number beside it; a value past the
454/// range holds the fill at its edge and the number still reads it.
455fn axis_bar(ui: &mut egui::Ui, value: f32) {
456    let size = egui::vec2(VALUE_COLUMN_WIDTH, 12.0);
457    let (rect, _response) = ui.allocate_exact_size(size, egui::Sense::hover());
458    let painter = ui.painter();
459    painter.rect_filled(rect, 2.0, egui::Color32::from_gray(35));
460
461    let mid = rect.left() + rect.width() * 0.5;
462    let reach = rect.width() * 0.5 - READING_MARGIN;
463    let end = mid + value.clamp(-1.0, 1.0) * reach;
464    let fill = egui::Rect::from_min_max(
465        egui::pos2(mid.min(end), rect.top()),
466        egui::pos2(mid.max(end), rect.bottom()),
467    );
468    painter.rect_filled(fill, 2.0, highlight_color(value != 0.0));
469    painter.vline(
470        mid,
471        egui::Rangef::new(rect.top() + 3.0, rect.bottom() - 3.0),
472        egui::Stroke::new(1.0, egui::Color32::from_gray(160)),
473    );
474    painter.rect_stroke(
475        rect,
476        2.0,
477        egui::Stroke::new(1.0, egui::Color32::from_gray(120)),
478        egui::StrokeKind::Inside,
479    );
480
481    ui.label(format!("{value:.2}"));
482}
483
484/// A dot in a unit square with the vector beside it; a length past `1`
485/// holds the dot at the edge and the numbers still read it.
486fn axis2_dot(ui: &mut egui::Ui, value: Vec2) {
487    let square_side = 36.0;
488    let radius = 4.0;
489    let (column, _response) = ui.allocate_exact_size(
490        egui::vec2(VALUE_COLUMN_WIDTH, square_side),
491        egui::Sense::hover(),
492    );
493    let rect = egui::Rect::from_center_size(column.center(), egui::Vec2::splat(square_side));
494    let painter = ui.painter();
495    painter.rect_filled(rect, 2.0, egui::Color32::from_gray(35));
496
497    let shown = value.clamp_length_max(1.0);
498    let reach = rect.width() * 0.5 - READING_MARGIN - radius;
499    let point = rect.center() + egui::vec2(shown.x, -shown.y) * reach;
500    painter.circle_filled(point, radius, highlight_color(value != Vec2::ZERO));
501    painter.rect_stroke(
502        rect,
503        2.0,
504        egui::Stroke::new(1.0, egui::Color32::from_gray(120)),
505        egui::StrokeKind::Inside,
506    );
507
508    ui.label(format!("{:.2}, {:.2}", value.x, value.y));
509}
510
511/// The controls-menu text for a live binding list: each alternative,
512/// separated, in the order the player can use them.
513fn bindings_text<B: Display>(bindings: Vec<B>) -> String {
514    bindings
515        .iter()
516        .map(ToString::to_string)
517        .collect::<Vec<_>>()
518        .join(", ")
519}
520
521/// `binding`'s controls-menu text, or `none` where a frame captured
522/// nothing.
523fn text_of<B: Display>(binding: Option<B>) -> String {
524    match binding {
525        Some(binding) => binding.to_string(),
526        None => "none".to_string(),
527    }
528}
529
530fn main() {
531    run(
532        Config::new("Mirage: every control as an action"),
533        InputLab::init,
534    );
535}