use std::time::Duration;
use bevy::ecs::system::In;
use bevy::input::ButtonState;
use bevy::input::mouse::MouseButton;
use bevy::input::mouse::MouseButtonInput;
use bevy::input::mouse::MouseMotion;
use bevy::input::mouse::MouseScrollUnit;
use bevy::input::mouse::MouseWheel;
use bevy::math::Vec2;
use bevy::prelude::*;
use bevy::remote::BrpError;
use bevy::remote::BrpResult;
use bevy::remote::error_codes::INTERNAL_ERROR;
use bevy::remote::error_codes::INVALID_PARAMS;
use bevy::window::CursorMoved;
use bevy::window::PrimaryWindow;
use bevy::window::WindowEvent;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Map;
use serde_json::Value;
use crate::window_event::write_input_event;
const MAX_MOUSE_DURATION_MS: u32 = 60_000;
const DEFAULT_MOUSE_DURATION_MS: u32 = 100;
const DEFAULT_DOUBLE_CLICK_DELAY_MS: u32 = 250;
fn parse_request<T: serde::de::DeserializeOwned>(
params: Option<Value>,
allow_empty: bool,
) -> Result<T, BrpError> {
if allow_empty && params.is_none() {
return serde_json::from_value(Value::Object(Map::default())).map_err(|e| BrpError {
code: INVALID_PARAMS,
message: format!("Failed to parse parameters: {e}"),
data: None,
});
}
let params = params.ok_or_else(|| BrpError {
code: INVALID_PARAMS,
message: "Missing request parameters".to_string(),
data: None,
})?;
serde_json::from_value(params).map_err(|e| BrpError {
code: INVALID_PARAMS,
message: format!("Failed to parse parameters: {e}"),
data: None,
})
}
fn serialize_response<T: Serialize>(response: T, handler_name: &str) -> BrpResult {
serde_json::to_value(response).map_err(|e| {
warn!("Failed to serialize {handler_name} response: {e}");
BrpError {
code: INTERNAL_ERROR,
message: format!("Failed to serialize response: {e}"),
data: None,
}
})
}
fn resolve_window_entity(window: Option<Entity>) -> Entity { window.unwrap_or(Entity::PLACEHOLDER) }
fn send_timed_button_press(
world: &mut World,
button: MouseButton,
window: Entity,
duration_ms: u32,
) {
write_input_event(
world,
MouseButtonInput {
button,
state: ButtonState::Pressed,
window,
},
);
world.spawn(TimedButtonRelease {
button,
window: Some(window),
timer: Timer::new(Duration::from_millis(duration_ms.into()), TimerMode::Once),
});
}
fn send_motion_events(world: &mut World, window: Entity, position: Vec2, delta: Vec2) {
write_input_event(world, MouseMotion { delta });
write_input_event(
world,
CursorMoved {
window,
position,
delta: Some(delta),
},
);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DragState {
Pressed,
Dragging,
Released,
}
#[derive(Deserialize)]
pub struct MoveMouseRequest {
#[serde(default)]
pub delta: Option<Vec2>,
#[serde(default)]
pub position: Option<Vec2>,
#[serde(default)]
pub window: Option<u64>,
}
#[derive(Serialize)]
pub struct MoveMouseResponse {
pub new_position: Vec2,
pub delta: Vec2,
}
#[derive(Resource, Default)]
pub struct SimulatedCursorPosition {
pub positions: std::collections::HashMap<Entity, Vec2>,
pub last_window: Option<Entity>,
}
impl SimulatedCursorPosition {
pub fn get_position(&self, window: Entity) -> Vec2 {
self.positions.get(&window).copied().unwrap_or(Vec2::ZERO)
}
pub fn update_position(&mut self, window: Entity, new_pos: Vec2) -> Vec2 {
let old_pos = self.get_position(window);
self.positions.insert(window, new_pos);
new_pos - old_pos
}
}
#[derive(Component)]
pub struct TimedButtonRelease {
pub button: MouseButton,
pub window: Option<Entity>,
pub timer: Timer,
}
#[derive(Component)]
pub struct DragOperation {
pub button: MouseButton,
pub window: Option<Entity>,
pub start: Vec2,
pub end: Vec2,
pub total_frames: u32,
pub current_frame: u32,
pub state: DragState,
}
#[derive(Component)]
pub struct ScheduledClick {
pub button: MouseButton,
pub window: Option<Entity>,
pub delay_timer: Timer,
pub click_duration: u32,
}
#[derive(Deserialize)]
pub struct SendMouseButtonRequest {
pub button: MouseButton,
#[serde(default)]
pub duration_ms: Option<u32>,
#[serde(default)]
pub window: Option<u64>,
}
#[derive(Serialize)]
pub struct SendMouseButtonResponse {
pub button: MouseButton,
pub duration_ms: u32,
}
#[derive(Deserialize)]
pub struct ClickMouseRequest {
pub button: MouseButton,
#[serde(default)]
pub window: Option<u64>,
}
#[derive(Serialize)]
pub struct ClickMouseResponse {
pub button: MouseButton,
}
#[derive(Deserialize)]
pub struct DoubleClickMouseRequest {
pub button: MouseButton,
#[serde(default)]
pub delay_ms: Option<u32>,
#[serde(default)]
pub window: Option<u64>,
}
#[derive(Serialize)]
pub struct DoubleClickMouseResponse {
pub button: MouseButton,
pub delay_ms: u32,
}
#[derive(Deserialize)]
pub struct DragMouseRequest {
pub button: MouseButton,
pub start: Vec2,
pub end: Vec2,
pub frames: u32,
#[serde(default)]
pub window: Option<u64>,
}
#[derive(Serialize)]
pub struct DragMouseResponse {
pub button: MouseButton,
pub start: Vec2,
pub end: Vec2,
pub frames: u32,
}
#[derive(Deserialize)]
pub struct ScrollMouseRequest {
pub x: f32,
pub y: f32,
pub unit: MouseScrollUnit,
#[serde(default)]
pub window: Option<u64>,
}
#[derive(Serialize)]
pub struct ScrollMouseResponse {
pub x: f32,
pub y: f32,
pub unit: MouseScrollUnit,
}
#[derive(Deserialize)]
pub struct PinchGestureRequest {
pub delta: f32,
}
#[derive(Serialize)]
pub struct PinchGestureResponse {
pub delta: f32,
}
#[derive(Deserialize)]
pub struct RotationGestureRequest {
pub delta: f32,
}
#[derive(Serialize)]
pub struct RotationGestureResponse {
pub delta: f32,
}
#[derive(Deserialize)]
pub struct DoubleTapGestureRequest {
}
#[derive(Serialize)]
pub struct DoubleTapGestureResponse {
}
fn resolve_window(world: &mut World, window_id: Option<u64>) -> Result<Entity, BrpError> {
if let Some(id) = window_id {
let entity = Entity::from_bits(id);
if world.get_entity(entity).is_err() {
return Err(BrpError {
code: INVALID_PARAMS,
message: format!("Invalid window entity: {id}"),
data: None,
});
}
return Ok(entity);
}
if let Some(cursor_pos) = world.get_resource::<SimulatedCursorPosition>()
&& let Some(last_window) = cursor_pos.last_window
{
return Ok(last_window);
}
let entity = {
let mut query = world.query_filtered::<Entity, With<PrimaryWindow>>();
let mut iter = query.iter(world);
iter.next()
};
entity.ok_or_else(|| BrpError {
code: INVALID_PARAMS,
message: "No primary window found".to_string(),
data: None,
})
}
pub fn move_mouse_handler(In(params): In<Option<Value>>, world: &mut World) -> BrpResult {
let request: MoveMouseRequest = parse_request(params, false)?;
if request.delta.is_none() && request.position.is_none() {
return Err(BrpError {
code: INVALID_PARAMS,
message: "Must provide either 'delta' or 'position'".to_string(),
data: None,
});
}
if request.delta.is_some() && request.position.is_some() {
return Err(BrpError {
code: INVALID_PARAMS,
message: "Cannot provide both 'delta' and 'position'".to_string(),
data: None,
});
}
let window = resolve_window(world, request.window)?;
if !world.contains_resource::<SimulatedCursorPosition>() {
world.init_resource::<SimulatedCursorPosition>();
}
let mut cursor_res = world.resource_mut::<SimulatedCursorPosition>();
let current_pos = cursor_res.get_position(window);
let (new_position, delta) = request.delta.map_or_else(
|| {
#[allow(clippy::expect_used)]
let new_pos = request
.position
.expect("Position is required when delta is not provided");
let delta = new_pos - current_pos;
(new_pos, delta)
},
|delta| {
let new_pos = current_pos + delta;
(new_pos, delta)
},
);
cursor_res.positions.insert(window, new_position);
cursor_res.last_window = Some(window);
send_motion_events(world, window, new_position, delta);
serialize_response(
MoveMouseResponse {
new_position,
delta,
},
"move_mouse",
)
}
pub fn click_mouse_handler(In(params): In<Option<Value>>, world: &mut World) -> BrpResult {
let request: ClickMouseRequest = parse_request(params, false)?;
let window = resolve_window(world, request.window)?;
send_timed_button_press(world, request.button, window, DEFAULT_MOUSE_DURATION_MS);
serialize_response(
ClickMouseResponse {
button: request.button,
},
"click_mouse",
)
}
pub fn send_mouse_button_handler(In(params): In<Option<Value>>, world: &mut World) -> BrpResult {
let request: SendMouseButtonRequest = parse_request(params, false)?;
let duration_ms = request.duration_ms.unwrap_or(DEFAULT_MOUSE_DURATION_MS);
if duration_ms > MAX_MOUSE_DURATION_MS {
return Err(BrpError {
code: INVALID_PARAMS,
message: format!(
"Duration exceeds maximum: {duration_ms}ms > {MAX_MOUSE_DURATION_MS}ms"
),
data: None,
});
}
let window = resolve_window(world, request.window)?;
send_timed_button_press(world, request.button, window, duration_ms);
serialize_response(
SendMouseButtonResponse {
button: request.button,
duration_ms,
},
"send_mouse_button",
)
}
pub fn double_click_mouse_handler(In(params): In<Option<Value>>, world: &mut World) -> BrpResult {
let request: DoubleClickMouseRequest = parse_request(params, false)?;
let delay_ms = request.delay_ms.unwrap_or(DEFAULT_DOUBLE_CLICK_DELAY_MS);
let window = resolve_window(world, request.window)?;
write_input_event(
world,
MouseButtonInput {
button: request.button,
state: ButtonState::Pressed,
window,
},
);
write_input_event(
world,
MouseButtonInput {
button: request.button,
state: ButtonState::Released,
window,
},
);
world.spawn(ScheduledClick {
button: request.button,
window: Some(window),
delay_timer: Timer::new(Duration::from_millis(delay_ms.into()), TimerMode::Once),
click_duration: DEFAULT_MOUSE_DURATION_MS,
});
serialize_response(
DoubleClickMouseResponse {
button: request.button,
delay_ms,
},
"double_click_mouse",
)
}
pub fn scroll_mouse_handler(In(params): In<Option<Value>>, world: &mut World) -> BrpResult {
let request: ScrollMouseRequest = parse_request(params, false)?;
let window = resolve_window(world, request.window)?;
write_input_event(
world,
MouseWheel {
unit: request.unit,
x: request.x,
y: request.y,
window,
},
);
serialize_response(
ScrollMouseResponse {
x: request.x,
y: request.y,
unit: request.unit,
},
"scroll_mouse",
)
}
pub fn drag_mouse_handler(In(params): In<Option<Value>>, world: &mut World) -> BrpResult {
let request: DragMouseRequest = parse_request(params, false)?;
if request.frames == 0 {
return Err(BrpError {
code: INVALID_PARAMS,
message: "Frames must be greater than 0".to_string(),
data: None,
});
}
let window = resolve_window(world, request.window)?;
world.spawn(DragOperation {
button: request.button,
window: Some(window),
start: request.start,
end: request.end,
total_frames: request.frames,
current_frame: 0,
state: DragState::Pressed,
});
serialize_response(
DragMouseResponse {
button: request.button,
start: request.start,
end: request.end,
frames: request.frames,
},
"drag_mouse",
)
}
pub fn pinch_gesture_handler(In(params): In<Option<Value>>, world: &mut World) -> BrpResult {
let request: PinchGestureRequest = parse_request(params, false)?;
write_input_event(world, bevy::input::gestures::PinchGesture(request.delta));
serialize_response(
PinchGestureResponse {
delta: request.delta,
},
"pinch_gesture",
)
}
pub fn rotation_gesture_handler(In(params): In<Option<Value>>, world: &mut World) -> BrpResult {
let request: RotationGestureRequest = parse_request(params, false)?;
write_input_event(world, bevy::input::gestures::RotationGesture(request.delta));
serialize_response(
RotationGestureResponse {
delta: request.delta,
},
"rotation_gesture",
)
}
pub fn double_tap_gesture_handler(In(params): In<Option<Value>>, world: &mut World) -> BrpResult {
let _request: DoubleTapGestureRequest = parse_request(params, true)?;
write_input_event(world, bevy::input::gestures::DoubleTapGesture);
serialize_response(DoubleTapGestureResponse {}, "double_tap_gesture")
}
pub fn process_timed_button_releases(
mut commands: Commands,
time: Res<Time>,
mut query: Query<(Entity, &mut TimedButtonRelease)>,
mut button_events: MessageWriter<MouseButtonInput>,
mut window_events: MessageWriter<WindowEvent>,
) {
for (entity, mut release) in &mut query {
release.timer.tick(time.delta());
if release.timer.is_finished() {
let event = MouseButtonInput {
button: release.button,
state: ButtonState::Released,
window: resolve_window_entity(release.window),
};
window_events.write(WindowEvent::from(event));
button_events.write(event);
commands.entity(entity).despawn();
}
}
}
pub fn process_scheduled_clicks(
mut commands: Commands,
time: Res<Time>,
mut query: Query<(Entity, &mut ScheduledClick)>,
mut button_events: MessageWriter<MouseButtonInput>,
mut window_events: MessageWriter<WindowEvent>,
) {
for (entity, mut scheduled) in &mut query {
scheduled.delay_timer.tick(time.delta());
if scheduled.delay_timer.is_finished() {
let event = MouseButtonInput {
button: scheduled.button,
state: ButtonState::Pressed,
window: resolve_window_entity(scheduled.window),
};
window_events.write(WindowEvent::from(event));
button_events.write(event);
commands.spawn(TimedButtonRelease {
button: scheduled.button,
window: scheduled.window,
timer: Timer::new(
Duration::from_millis(scheduled.click_duration.into()),
TimerMode::Once,
),
});
commands.entity(entity).despawn();
}
}
}
pub fn process_drag_operations(
mut commands: Commands,
mut query: Query<(Entity, &mut DragOperation)>,
mut cursor_res: ResMut<SimulatedCursorPosition>,
mut motion_events: MessageWriter<MouseMotion>,
mut cursor_events: MessageWriter<CursorMoved>,
mut button_events: MessageWriter<MouseButtonInput>,
mut window_events: MessageWriter<WindowEvent>,
) {
for (entity, mut drag) in &mut query {
let window = resolve_window_entity(drag.window);
match drag.state {
DragState::Pressed => {
let btn_event = MouseButtonInput {
button: drag.button,
state: ButtonState::Pressed,
window,
};
window_events.write(WindowEvent::from(btn_event));
button_events.write(btn_event);
let delta = cursor_res.update_position(window, drag.start);
let motion = MouseMotion { delta };
window_events.write(WindowEvent::from(motion));
motion_events.write(motion);
let cursor = CursorMoved {
window,
position: drag.start,
delta: Some(delta),
};
window_events.write(WindowEvent::from(cursor.clone()));
cursor_events.write(cursor);
drag.state = DragState::Dragging;
},
DragState::Dragging => {
#[allow(clippy::cast_precision_loss)]
let t = drag.current_frame as f32 / drag.total_frames as f32;
let new_position = drag.start.lerp(drag.end, t);
let delta = cursor_res.update_position(window, new_position);
let motion = MouseMotion { delta };
window_events.write(WindowEvent::from(motion));
motion_events.write(motion);
let cursor = CursorMoved {
window,
position: new_position,
delta: Some(delta),
};
window_events.write(WindowEvent::from(cursor.clone()));
cursor_events.write(cursor);
drag.current_frame += 1;
if drag.current_frame > drag.total_frames {
drag.state = DragState::Released;
}
},
DragState::Released => {
let btn_event = MouseButtonInput {
button: drag.button,
state: ButtonState::Released,
window,
};
window_events.write(WindowEvent::from(btn_event));
button_events.write(btn_event);
commands.entity(entity).despawn();
},
}
}
}
pub fn sync_cursor_position(
mut cursor_res: ResMut<SimulatedCursorPosition>,
mut cursor_events: MessageReader<CursorMoved>,
) {
for event in cursor_events.read() {
cursor_res.positions.insert(event.window, event.position);
cursor_res.last_window = Some(event.window);
}
}