mirage-engine 0.2.0

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! What the caller drives a session's controls with, and what the UI reads
//! of each one.

use crate::input::{Devices, PointerDelta, Switch, WheelDelta};
use crate::math::Vec2;
#[cfg(feature = "ui")]
use crate::platform::WHEEL_RATE;

/// One move the caller made: what [`Session::press`](super::Session::press),
/// [`Session::release`](super::Session::release),
/// [`Session::set_pointer`](super::Session::set_pointer),
/// [`Session::pointer_delta`](super::Session::pointer_delta),
/// [`Session::wheel_delta`](super::Session::wheel_delta) and
/// [`Session::type_text`](super::Session::type_text) each become, and the
/// one value the game's controls and the UI read between them.
pub(crate) enum Driven {
    /// A key or a mouse button, pressed where true and released where
    /// false.
    Switched(Switch, bool),
    /// The pointer placed, in physical pixels from the target's top left.
    Pointed(Vec2),
    /// One lane of the pointer moved, in physical pixels.
    Moved(PointerDelta, f32),
    /// One lane of the wheel turned, in notches.
    Turned(WheelDelta, f32),
    /// Text typed into the UI, which reads it alone: text entry belongs to
    /// the UI layer, and an action reads controls, never text.
    #[cfg(feature = "ui")]
    Typed(String),
}

impl Driven {
    /// Moves the devices the game's actions read, exactly as a window's
    /// own event moves them.
    pub(crate) fn drive(&self, devices: &mut Devices) {
        match *self {
            Self::Switched(control, down) => devices.press(control, down),
            Self::Pointed(at) => devices.point_at(at),
            Self::Moved(lane, pixels) => devices.move_pointer(lane.moving(pixels)),
            Self::Turned(lane, notches) => devices.turn_wheel(lane.turning(notches)),
            #[cfg(feature = "ui")]
            Self::Typed(_) => {}
        }
    }

    /// This as the UI reads it, over a layer of `pixels_per_point`
    /// physical pixels per point and under the keys `devices` hold down:
    /// `None` where the UI reads nothing of it.
    ///
    /// Call it after [`Driven::drive`], the order a window folds its own
    /// event in before the UI reads it: a move that both places the pointer
    /// and presses it, as a touch does, is then read where it placed it.
    ///
    /// A turn of the wheel reaches the UI as the lines a window reports for
    /// those notches, so a session scrolls a panel as a desktop does.
    ///
    /// A press of `C` or `X` under the command key copies and cuts, as it
    /// does behind a window. The two a single desktop reads that way — its
    /// `Shift` with `Delete`, its `Control` with `Insert` — are left as the
    /// keys they are, so a session reads the same on every machine.
    #[cfg(feature = "ui")]
    pub(crate) fn ui_event(self, devices: &Devices, pixels_per_point: f32) -> Option<egui::Event> {
        let modifiers = devices.modifiers();
        let point =
            |pixels: Vec2| egui::pos2(pixels.x / pixels_per_point, pixels.y / pixels_per_point);

        match self {
            Self::Switched(Switch::Key(key), pressed) => {
                let key = key.ui_key()?;
                match (pressed && modifiers.command, key) {
                    (true, egui::Key::C) => Some(egui::Event::Copy),
                    (true, egui::Key::X) => Some(egui::Event::Cut),
                    _ => Some(egui::Event::Key {
                        key,
                        physical_key: Some(key),
                        pressed,
                        repeat: false,
                        modifiers,
                    }),
                }
            }
            Self::Switched(Switch::Mouse(button), pressed) => Some(egui::Event::PointerButton {
                pos: point(devices.pointing_at()?),
                button: button.ui_button(),
                pressed,
                modifiers,
            }),
            Self::Pointed(at) => Some(egui::Event::PointerMoved(point(at))),
            Self::Turned(lane, notches) => {
                let lines = WHEEL_RATE.lines(lane.turning(notches));
                Some(egui::Event::MouseWheel {
                    unit: egui::MouseWheelUnit::Line,
                    delta: egui::vec2(lines.x, lines.y),
                    phase: egui::TouchPhase::Move,
                    modifiers,
                })
            }
            Self::Moved(..) => None,
            Self::Typed(text) => Some(egui::Event::Text(text)),
        }
    }
}

#[cfg(all(test, feature = "ui"))]
mod tests {
    use super::*;
    use crate::input::{Key, MouseButton, Pads};

    /// Devices nothing has driven yet.
    fn controls() -> Devices {
        Devices::new(Pads::silent(), crate::platform::DOUBLE_CLICK_INTERVAL)
    }

    /// What the UI reads of `driven`, over a layer of one pixel per point,
    /// once the devices have taken it.
    fn ui_event(devices: &mut Devices, driven: Driven) -> Option<egui::Event> {
        driven.drive(devices);
        driven.ui_event(devices, 1.0)
    }

    #[test]
    fn a_key_under_the_command_key_copies_and_cuts_where_a_window_would() {
        let input = &mut controls();

        assert!(
            matches!(
                ui_event(input, Driven::Switched(Switch::Key(Key::C), true)),
                Some(egui::Event::Key { .. })
            ),
            "a key on its own is the key"
        );

        ui_event(input, Driven::Switched(Switch::Key(Key::LeftControl), true));

        assert_eq!(
            ui_event(input, Driven::Switched(Switch::Key(Key::C), true)),
            Some(egui::Event::Copy)
        );
        assert_eq!(
            ui_event(input, Driven::Switched(Switch::Key(Key::X), true)),
            Some(egui::Event::Cut)
        );
        assert!(
            matches!(
                ui_event(input, Driven::Switched(Switch::Key(Key::C), false)),
                Some(egui::Event::Key { pressed: false, .. })
            ),
            "and a release is the key either way"
        );
    }

    #[test]
    fn a_press_before_the_pointer_is_placed_reaches_the_ui_as_nothing() {
        let input = &mut controls();
        let press = || Driven::Switched(Switch::Mouse(MouseButton::Left), true);

        assert_eq!(ui_event(input, press()), None, "the pointer is nowhere yet");

        ui_event(input, Driven::Pointed(Vec2::new(12.0, 20.0)));

        assert_eq!(
            ui_event(input, press()),
            Some(egui::Event::PointerButton {
                pos: egui::pos2(12.0, 20.0),
                button: egui::PointerButton::Primary,
                pressed: true,
                modifiers: egui::Modifiers::NONE,
            }),
            "and it lands where the pointer was placed"
        );
    }

    #[test]
    fn a_pointer_that_moves_reaches_the_ui_as_nothing() {
        let input = &mut controls();

        assert_eq!(
            ui_event(input, Driven::Moved(PointerDelta::Sideways, 4.0)),
            None
        );
        assert_eq!(ui_event(input, Driven::Moved(PointerDelta::Up, 4.0)), None);
    }

    /// What the UI reads of a window's own report of `x` and `y` lines,
    /// which is what a desktop reports for that many notches.
    fn scrolled(x: f32, y: f32) -> Option<egui::Event> {
        Some(egui::Event::MouseWheel {
            unit: egui::MouseWheelUnit::Line,
            delta: egui::vec2(x, y),
            phase: egui::TouchPhase::Move,
            modifiers: egui::Modifiers::NONE,
        })
    }

    #[test]
    fn one_notch_reaches_the_ui_as_the_lines_a_desktop_window_reports_for_it() {
        let input = &mut controls();

        assert_eq!(
            ui_event(input, Driven::Turned(WheelDelta::Up, 1.0)),
            scrolled(0.0, 1.0),
            "a roll away scrolls the way a window's own roll away does"
        );
        assert_eq!(
            ui_event(input, Driven::Turned(WheelDelta::Sideways, 1.0)),
            scrolled(-1.0, 0.0),
            "and a tilt to the right the way a window's own tilt does"
        );
    }
}