ark-api 0.17.0-pre.15

Ark API
Documentation
//! # 🍏 Applet API
//!
//! This API provides Applet-specific functionality such as accessing Key,
//! Mouse and Text inputs, Window state, and more.
//!
//! ## Minimal example
//!
//! The minimal boilerplate that is required to create an applet module.
//!
//! ```rust,no_run
//! struct Module {}
//!
//! ark::require_applet_api!();
//!
//! impl ark::applet::Applet for Module {
//!     fn new() -> Self {
//!         Module {}
//!     }
//!     fn update(&mut self) {}
//! }
//!
//! ark_module::impl_applet!(Module);
//!
//! ```
//! For a more complete example see [example-applet](https://github.com/EmbarkStudios/ark-modules/blob/main/examples/example-applet/src/lib.rs)
//!
//! ## Bite-sized examples
//!
//! ### Handling key input events
//!
//! ```rust,no_run
//! for input in applet().input_events() {
//!     if let EventEnum::Key(KeyInput::KeyPress(VirtualKeyCode::Space)) = input {
//!         // Space has been pressed
//!     }
//! }
//! ```
//!
//! ### Handling mouse button events
//!
//! ```rust,no_run
//! for event in applet().input_events() {
//!     if let EventEnum::Mouse(MouseInput::ButtonPress{ button, .. }) = event {
//!         if let MouseButton::Primary = button {
//!             // Left mouse button has been pressed
//!         }
//!     }
//! }
//! ```

use crate::{Vec2, Vec3};

mod ffi {
    pub use crate::ffi::{applet_v0 as v0, applet_v0::*};
    pub use crate::ffi::{applet_v1 as v1, applet_v1::*};
    pub use crate::ffi::{applet_v2 as v2, applet_v2::*};
    pub use crate::ffi::{applet_v3 as v3, applet_v3::*};
    pub use crate::ffi::{applet_v4 as v4, applet_v4::*};
    pub use crate::ffi::{applet_v5 as v5, applet_v5::*};
}

/// Identifiers for players, with 0 meaning local player.
pub use ffi::PlayerIdRepr as PlayerId;
pub use ffi::{
    Axis, CursorMode, CursorShape, EventType, GamepadButton, KeyEventType, MouseButton,
    MouseEventType, SessionId, TaggedEvent, TouchEventType, VirtualKeyCode, WindowState,
    LOCAL_SESSION,
};

pub mod input;

use std::mem::size_of;

#[doc(hidden)]
pub use crate::ffi::applet_v5::API as FFI_API;

/// Represents a touch input event.
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "with_speedy", derive(speedy::Writable, speedy::Readable))]
pub enum TouchInput {
    /// Every time a user touches the screen a new [`TouchInput::Started`] event is generated
    Started {
        /// Unique identifier of a finger
        finger_id: u32,
        /// Coordinate in logical pixels
        pos: Vec2,
        /// World space origin of the ray (intersect this to find what you clicked on).
        ray_origin: Vec3,
        /// World space direction of the ray.
        ray_dir: Vec3,
    },
    /// When the finger is lifted, an [`TouchInput::Ended`] event is generated
    Ended {
        /// Unique identifier of a finger
        finger_id: u32,
        /// Coordinate in logical pixels
        pos: Vec2,
        /// World space origin of the ray (intersect this to find what you clicked on).
        ray_origin: Vec3,
        /// World space direction of the ray.
        ray_dir: Vec3,
    },
    /// After a [`TouchInput::Started`] event has been emitted, there may be zero or more [`TouchInput::Move`]
    /// events when the finger is moved.
    Move {
        /// Unique identifier of a finger
        finger_id: u32,
        /// Coordinate in logical pixels
        pos: Vec2,
        /// World space origin of the ray (intersect this to find what you clicked on).
        ray_origin: Vec3,
        /// World space direction of the ray.
        ray_dir: Vec3,
    },
    /// After a [`TouchInput::Started`] event has been emitted, there may be zero or more [`TouchInput::RelativeMove`]
    /// events when the finger is moved.
    RelativeMove {
        /// Unique identifier of a finger
        finger_id: u32,
        /// Movement delta in logical pixels (approximate).
        delta_logical_pixels: Vec2,
        /// Movement delta in radians.
        /// This is affected by the ark "mouse sensitivity" setting as well as "invert y axis".
        delta_angle: Vec2,
    },
}

/// Represents a mouse input event.
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "with_speedy", derive(speedy::Writable, speedy::Readable))]
pub enum MouseInput {
    /// Mouse move
    Move {
        /// Coordinate in logical pixels
        pos: Vec2,
        /// World space origin of the ray (intersect this to find what you clicked on).
        ray_origin: Vec3,
        /// World space direction of the ray.
        ray_dir: Vec3,
    },
    /// Mouse button press
    ButtonPress {
        /// Which pointer button was pressed
        button: MouseButton,
        /// Coordinate in logical pixels
        pos: Vec2,
        /// World space origin of the click ray (intersect this to find what you clicked on).
        ray_origin: Vec3,
        /// World space direction of the click ray.
        ray_dir: Vec3,
    },
    /// Mouse button release
    ButtonRelease {
        /// Which pointer button was released
        button: MouseButton,
        /// Coordinate in logical pixels
        pos: Vec2,
        /// World space origin of the click ray (intersect this to find what you clicked on).
        ray_origin: Vec3,
        /// World space direction of the click ray.
        ray_dir: Vec3,
    },
    /// Relative cursor movement (higher precision, works at screen border).
    ///
    /// This event is disabled and won't be emitted when the window is not focused or when
    /// the Ark developer UI is visible. Press tab to show and hide the developer UI.
    RelativeMove {
        /// Raw (unaccelerated) mouse movement in unspecified units.
        /// Different devices may use different units, but in practice it should be close to logical pixels.
        delta_logical_pixels: Vec2,
        /// Movement delta in radians.
        /// This is affected by the ark "mouse sensitivity" setting as well as "invert y axis".
        delta_angle: Vec2,
    },
    /// Mouse wheel/trackpad scroll movement.
    Scroll {
        /// Movement vector
        delta: Vec2,
    },
}

/// Represents a key press or a text input event. Text is handled separately due to unicode,
/// multi-key accents etc which you don't want to simulate manually.
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "with_speedy", derive(speedy::Writable, speedy::Readable))]
pub enum KeyInput {
    /// A key was pressed.
    KeyPress(VirtualKeyCode),
    /// A key was released.
    KeyRelease(VirtualKeyCode),
    /// Got text input (one unicode character).
    Text(char),
}

/// A general axis change event, used for gamepad inputs like sticks and throttle.
#[derive(Clone, Copy, Debug)]
pub struct AxisInput {
    /// The axis that moved
    pub axis: Axis,
    /// Value. Range depends on the value of axis (generally, sticks and dpad are -1 to 1, most other things 0 to 1)
    pub value: f32,
}

/// Represents a key press or a text input event. Text is handled separately due to unicode,
/// multi-key accents etc which you don't want to simulate manually.
#[derive(Clone, Copy, Debug)]
pub struct GamepadButtonInput {
    /// Which gamepad button was pressed or released
    pub button: GamepadButton,
    /// True = pressed, false = released
    pub is_pressed: bool,
}

/// Represents a raw MIDI message.
#[derive(Clone, Copy, Debug)]
pub struct RawMidiInput {
    /// MIDI timestamp
    pub timestamp: u64,
    /// Device ID
    pub device_id: u32,
    /// Length of the valid MIDI bytes in `data`
    pub len: u32,
    /// Raw MIDI data, needs to be parsed
    pub data: [u8; 8],
}

/// An input event, such as a key press
#[derive(Copy, Clone)]
pub enum EventEnum {
    /// A key press
    Key(KeyInput),
    /// Mouse movement or button press
    Mouse(MouseInput),
    /// Touch start, move, end
    Touch(TouchInput),
    /// Axis events - analog inputs like gamepad sticks
    Axis(AxisInput),
    /// A gamepad button press
    GamepadButton(GamepadButtonInput),
    /// A raw MIDI event
    RawMidi(RawMidiInput),
}

impl EventEnum {
    fn from_tagged(event: TaggedEvent) -> Option<Self> {
        match event.ty {
            ffi::EventType2::Key => Some(Self::Key(convert_key_event(&event.key()?))),
            ffi::EventType2::Mouse => Some(Self::Mouse(convert_mouse_event(&event.mouse()?)?)),
            ffi::EventType2::Touch => Some(Self::Touch(convert_touch_event(&event.touch()?)?)),
            ffi::EventType2::Axis => Some(Self::Axis(convert_axis_event(&event.axis()?))),
            ffi::EventType2::GamepadButton => Some(Self::GamepadButton(
                convert_gamepad_button_event(&event.gamepad_button()?),
            )),
            ffi::EventType2::RawMidi => {
                Some(Self::RawMidi(convert_raw_midi_event(&event.raw_midi()?)))
            }
            _ => None,
        }
    }
}

/// Commands for a virtual keyboard
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
pub enum VirtualKeyboardCommand {
    /// Show the virtual keyboard
    Show,
    /// Hide the virtual keyboard
    Hide,
}

/// The applet api gives you access to applet specific functionality, like accessing host events.
#[derive(Copy, Clone)]
pub struct Applet {}

impl Applet {
    /// Retrieve all input events for the current (local) player.
    pub fn input_events(self) -> Vec<EventEnum> {
        self.input_events_player(0)
    }

    /// Retrieve all input events for the given player.
    pub fn input_events_player(self, player_id: PlayerId) -> Vec<EventEnum> {
        let num = ffi::events_count(player_id) as usize;
        let mut events = vec![unsafe { std::mem::zeroed() }; num];
        ffi::events_get(player_id, &mut events);
        events
            .into_iter()
            .filter_map(EventEnum::from_tagged)
            .collect()
    }

    /// Retrieve the `WindowState` data for the current (local) player for the current frame.
    ///
    /// Returns `None` in headless.
    pub fn window_state(self) -> Option<WindowState> {
        self.window_state_player(0)
    }

    /// Retrieve the `WindowState` data for the player for the current frame.
    ///
    /// Returns `None` in headless.
    pub fn window_state_player(self, player_id: PlayerId) -> Option<WindowState> {
        let window_states = get_events_by_type::<WindowState>(EventType::Window, player_id);

        if window_states.is_empty() {
            None
        } else {
            Some(window_states[0])
        }
    }

    /// Retrieve the `PlayerId` of all connected players
    pub fn connected_players(self) -> Vec<PlayerId> {
        get_events_by_type::<PlayerId>(EventType::Players, 0) // dummy "0"
    }

    /// The time between the last and the current frame (game time, not `real_time_since_start` time).
    pub fn game_delta_time(self) -> f32 {
        ffi::delta_time()
    }

    /// Returns the game time passed since the module started, in seconds.
    pub fn game_time(self) -> f64 {
        ffi::game_time()
    }

    /// Returns the real time in seconds that has passed since the module was started.
    pub fn real_time_since_start(self) -> f64 {
        ffi::real_time_since_start()
    }

    /// Set if the mouse cursor should be visible, hidden or grabbed.
    pub fn set_cursor_mode(self, mode: CursorMode) {
        ffi::set_cursor_mode(mode);
    }

    /// Set the shape and appearance of the mouse cursor.
    pub fn set_cursor_shape(self, shape: CursorShape) {
        ffi::set_cursor_shape(shape);
    }

    /// Retrieves a fixed but random value (different each execution of the applet).
    pub fn random_seed_value(self) -> u128 {
        *ffi::random_seed_value()
    }

    /// Sets the contents of the system clipboard to the supplied string.
    ///
    /// NOTE: This doesn't really have much in the way of security implications, but it can be
    /// annoying for the user to get their clipboard contents overridden at any time. Use with care.
    pub fn set_clipboard_string(self, string: &str) {
        ffi::set_clipboard_string(string);
    }

    /// Try to set the contents of the system clipboard of the given player to the supplied string.
    ///
    /// If there is no such player, this does nothing.
    ///
    /// NOTE: This doesn't really have much in the way of security implications, but it can be
    /// annoying for the user to get their clipboard contents overridden at any time. Use with care.
    pub fn set_player_clipboard_string(self, player_id: PlayerId, string: &str) {
        ffi::set_player_clipboard_string(player_id, string);
    }

    /// Gets the contents of the system clipboard, if any.
    ///
    /// NOTE: The existence of this has serious security implications that we need to work through
    /// later. We might need to add limitations like only allowing reading the clipboard after
    /// a certain keyboard shortcut, or Paste message, or something similar. Maybe a special permission
    /// flag for clipboard access. The clipboard could contain sensitive information so it's not good
    /// to allow just any app to easily sniff it.
    ///
    /// Tracked in <https://github.com/EmbarkStudios/ark/issues/4491>
    pub fn clipboard_string(self) -> Option<String> {
        let len = ffi::get_clipboard_string();
        if len == 0 {
            return None;
        };
        let mut buffer = vec![0; len as usize];
        ffi::retrieve_clipboard_string(&mut buffer);
        Some(String::from_utf8(buffer).unwrap())
    }

    /// Gets the launch argument string, if one is available
    ///
    /// This can be used to implement module-specific deep-linking to launch a module into a
    /// specific mode or action described by a URL parameter
    ///
    /// # Example
    ///
    /// If an applet is launched with the URL `https://ark.link/module/cube-simple?cid=zUnGgApwEF8PP4NqxYjHcS8NdxQXzLuc3L5F9DDgGqcc5&arg=THIS%20is%20TEST`
    /// this would make this function return `THIS is TEST`.
    ///
    /// NOTE: Only a single URL parameter named `arg` is allowed. Any parameters will be ignored.
    pub fn launch_argument(self) -> String {
        ffi::v5::get_launch_argument_string()
    }

    /// Launch a module applet from a URL
    ///
    /// This will panic if the URL is not a correctly formatted Ark module URL, example valid URLs:
    ///
    /// - `https://ark.link/module/cube-simple?cid=zUnGgApwEF8PP4NqxYjHcS8NdxQXzLuc3L5F9DDgGqcc5&arg=THIS%20is%20TEST`
    /// - `ark://module/cube-simple`
    ///
    /// Ark will start to load the applet specified in the URL, and if it fails it will prompt the user that.
    /// In the meantime the current module will continue to run. There is currently no way for the module itself to know
    /// if the other applet module was successfully launched.
    ///
    /// Ark will load the latests published module by the given name if no `cid` is specified.
    ///
    /// ### Example
    /// Load a module with some argument:
    ///
    /// ``` ignore
    /// /// Start `module` with the `applet().launch_argument() == arg`
    /// fn launch_module_with_arg(module: &str, arg: &str) {
    ///    applet().launch_applet_with_url(format!("ark://module/{}?arg={}", module, percent_encode(arg)));
    /// }
    /// ```
    pub fn launch_applet_with_url(self, url: &str) {
        ffi::launch_applet_with_url(url);
    }

    /// Requests that the applet is terminated gracefully.
    ///
    /// The module will not quit immediately but will continue running its current update loop.
    ///
    /// The host is free to ignore this request, delay it, or ask the user for confirmation.
    pub fn request_quit(self) {
        ffi::request_quit();
    }

    /// Broadcasts a message to the entire world, in multiplayer sessions, to be displayed for the
    /// specified duration (in milliseconds). This doesn't show up in single player sessions.
    pub fn broadcast_message(msg: &str, duration: f32) {
        ffi::broadcast_message(msg, duration);
    }

    /// Request the virtual keyboard to be shown or hidden for the specified player
    ///
    /// Key events generated by the virtual keyboard can be retrieved through [`Applet::input_events_player`]
    ///
    /// Calling this for players on non touch devices will be ignored
    pub fn show_virtual_keyboard(self, player_id: PlayerId, command: VirtualKeyboardCommand) {
        ffi::show_virtual_keyboard(
            player_id,
            u32::from(command == VirtualKeyboardCommand::Show),
        );
    }

    /// Requests to detach a player to another instance of this applet that runs with the given
    /// arguments.
    pub fn detach_player(player: PlayerId, parameters: &str) -> DetachPlayerRequest {
        DetachPlayerRequest {
            handle: ffi::detach_player_to_applet(player, parameters),
        }
    }

    /// When a detach player requests causes a hot-reload, indicate that the hot-reload is done
    /// and that the applet is in a ready state for further use.
    ///
    /// This will panic if there was not hot-reload happening. As such it should only be called
    /// after the hot-reload entrypoint has been called *and* the applet is done performing the
    /// hot-reloading steps. It should be called only once per hot-reload request, and it will
    /// panic if it's called multiple times for the same hot-reload request.
    pub fn notify_finished_hot_reload(is_success: bool) {
        ffi::notify_finished_hot_reload(u32::from(is_success));
    }

    /// Returns a single session identifier that is the same for all the applet instances detached
    /// from the same applet instance root.
    pub fn get_session_id() -> SessionId {
        ffi::get_session_id()
    }
}

/// A pending request to detach a player (created with `Applet::detach_player`).
pub struct DetachPlayerRequest {
    handle: ffi::DetachPlayerHandle,
}

/// The result of a request to detach a player.
pub enum DetachPlayerResult {
    /// The request is still pending on the host. Retry to `poll` with the wrapped request.
    ///
    /// Note the request may be spawned a few times for retrying, in case of failure.
    Waiting(DetachPlayerRequest),

    /// The request is ready for consumption.
    Ready(ReadyDetachPlayerRequest),

    /// The request failed.
    Failed,
}

impl DetachPlayerRequest {
    /// Polls the request; returns true if it is ready for consumption, false otherwise.
    pub fn poll(self) -> DetachPlayerResult {
        match ffi::detach_player_is_ready(self.handle) {
            Ok(ready) => {
                if ready {
                    DetachPlayerResult::Ready(ReadyDetachPlayerRequest {
                        handle: self.handle,
                    })
                } else {
                    DetachPlayerResult::Waiting(self)
                }
            }

            Err(err) => {
                log::warn!("Error when polling a detach-player request: {err:?}");
                DetachPlayerResult::Failed
            }
        }
    }
}

/// Represents a `DetachPlayerRequest` that's ready for consumption.
pub struct ReadyDetachPlayerRequest {
    handle: ffi::DetachPlayerHandle,
}

impl ReadyDetachPlayerRequest {
    /// Confirms a detach player request that's ready.
    pub fn confirm(self) {
        ffi::detach_player_confirm(self.handle);
    }
}

fn get_events_by_type<T>(event_type: EventType, player: PlayerId) -> Vec<T> {
    let size = ffi::v4::event_size(event_type as u32, player) as usize;
    let len = size / size_of::<T>();

    assert_eq!(
        len * size_of::<T>(),
        size,
        "FFI and module-side type definitions of {:?} ({}) are out-of-sync",
        event_type,
        std::any::type_name::<T>()
    );

    let mut inputs: Vec<T> = Vec::with_capacity(len);

    let raw_slice: &mut [u8] =
        unsafe { std::slice::from_raw_parts_mut(inputs.as_mut_ptr().cast::<u8>(), size) };
    ffi::v4::retrieve_events(event_type as u32, player, raw_slice);

    unsafe {
        inputs.set_len(len);
    }

    inputs
}

// Convert from FFI type to friendly enum type.
fn convert_key_event(ffi_key: &ffi::KeyInput) -> KeyInput {
    match ffi_key.event_type {
        KeyEventType::KeyPress => KeyInput::KeyPress(ffi_key.code),
        KeyEventType::KeyRelease => KeyInput::KeyRelease(ffi_key.code),
        KeyEventType::Text => KeyInput::Text(ffi_key.text),
    }
}

// Convert from FFI type to friendly enum type.
fn convert_mouse_event(ffi_mouse: &ffi::v5::MouseInput) -> Option<MouseInput> {
    // Convert from FFI type to friendly enum type.
    #[allow(clippy::wildcard_enum_match_arm)]
    match ffi_mouse.event_type {
        MouseEventType::ButtonPress => Some(MouseInput::ButtonPress {
            button: ffi_mouse.button,
            pos: Vec2::new(ffi_mouse.x, ffi_mouse.y),
            ray_origin: ffi_mouse.ray_origin.into(),
            ray_dir: ffi_mouse.ray_dir.into(),
        }),
        MouseEventType::ButtonRelease => Some(MouseInput::ButtonRelease {
            button: ffi_mouse.button,
            pos: Vec2::new(ffi_mouse.x, ffi_mouse.y),
            ray_origin: ffi_mouse.ray_origin.into(),
            ray_dir: ffi_mouse.ray_dir.into(),
        }),
        MouseEventType::Move => Some(MouseInput::Move {
            pos: Vec2::new(ffi_mouse.x, ffi_mouse.y),
            ray_origin: ffi_mouse.ray_origin.into(),
            ray_dir: ffi_mouse.ray_dir.into(),
        }),
        MouseEventType::Scroll => Some(MouseInput::Scroll {
            delta: Vec2::new(ffi_mouse.x, ffi_mouse.y),
        }),
        MouseEventType::RelativeMove => Some(MouseInput::RelativeMove {
            delta_logical_pixels: Vec2::new(ffi_mouse.x, ffi_mouse.y),
            delta_angle: ffi_mouse.delta_angle.into(),
        }),
        _ => None,
    }
}

// Convert from FFI type to friendly enum type.
fn convert_touch_event(ffi_touch: &ffi::v5::TouchInput) -> Option<TouchInput> {
    match ffi_touch.event_type {
        TouchEventType::Started => Some(TouchInput::Started {
            finger_id: ffi_touch.id,
            pos: Vec2::new(ffi_touch.x, ffi_touch.y),
            ray_origin: ffi_touch.ray_origin.into(),
            ray_dir: ffi_touch.ray_dir.into(),
        }),
        TouchEventType::Ended => Some(TouchInput::Ended {
            finger_id: ffi_touch.id,
            pos: Vec2::new(ffi_touch.x, ffi_touch.y),
            ray_origin: ffi_touch.ray_origin.into(),
            ray_dir: ffi_touch.ray_dir.into(),
        }),
        TouchEventType::Move => Some(TouchInput::Move {
            finger_id: ffi_touch.id,
            pos: Vec2::new(ffi_touch.x, ffi_touch.y),
            ray_origin: ffi_touch.ray_origin.into(),
            ray_dir: ffi_touch.ray_dir.into(),
        }),
        TouchEventType::RelativeMove => Some(TouchInput::RelativeMove {
            finger_id: ffi_touch.id,
            delta_logical_pixels: Vec2::new(ffi_touch.x, ffi_touch.y),
            delta_angle: ffi_touch.delta_angle.into(),
        }),
        TouchEventType::Still => None,
    }
}

// Convert from FFI type to friendly enum type.
fn convert_axis_event(ffi_axis: &ffi::AxisInput) -> AxisInput {
    AxisInput {
        axis: ffi_axis.axis,
        value: ffi_axis.value,
    }
}

// Convert from FFI type to friendly enum type.
fn convert_gamepad_button_event(ffi_button: &ffi::GamepadButtonInput) -> GamepadButtonInput {
    GamepadButtonInput {
        button: ffi_button.button,
        is_pressed: ffi_button.is_pressed,
    }
}

// Convert from FFI type to friendly enum type.
fn convert_raw_midi_event(ffi_raw_midi: &ffi::RawMidiInput) -> RawMidiInput {
    RawMidiInput {
        timestamp: ffi_raw_midi.timestamp(),
        data: ffi_raw_midi.data,
        len: ffi_raw_midi.len,
        device_id: ffi_raw_midi.device_id,
    }
}