//! # 🍏 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,
}
}