1use std::collections::{HashMap, VecDeque};
30use std::sync::atomic::{AtomicU8, Ordering};
31use std::sync::mpsc::{self, Receiver, Sender};
32use std::sync::Arc;
33use std::thread;
34use std::time::Duration;
35
36use anyhow::Result;
37use wasmtime::{Caller, Linker};
38
39use crate::capabilities::{read_guest_bytes, read_guest_string, write_guest_bytes, HostState};
40
41const MAX_QUEUED_EVENTS: usize = 4096;
44
45const ONLINE_UNKNOWN: u8 = 0;
47const ONLINE_YES: u8 = 1;
48const ONLINE_NO: u8 = 2;
49
50#[derive(Clone, Debug)]
52pub struct PendingEvent {
53 pub event_type: String,
54 pub data: Vec<u8>,
55}
56
57#[derive(Clone, Default, Debug)]
59struct CurrentEvent {
60 event_type: String,
61 data: Vec<u8>,
62}
63
64pub struct EventState {
67 listeners: HashMap<String, Vec<(u32, u32)>>,
70 listener_owner: HashMap<u32, String>,
72 next_listener_id: u32,
73 queue: VecDeque<PendingEvent>,
74 current: CurrentEvent,
75
76 last_canvas_size: Option<(u32, u32)>,
78 last_focused: Option<bool>,
79 last_mouse_down: bool,
80 last_mouse_pos: (f32, f32),
81
82 online_state: Arc<AtomicU8>,
85 last_online: Option<bool>,
86 online_thread_started: bool,
87 online_shutdown: Option<Sender<()>>,
88
89 gamepad_rx: Option<Receiver<PendingEvent>>,
92 gamepad_thread_started: bool,
93 gamepad_shutdown: Option<Sender<()>>,
94}
95
96impl Default for EventState {
97 fn default() -> Self {
98 Self {
99 listeners: HashMap::new(),
100 listener_owner: HashMap::new(),
101 next_listener_id: 1,
102 queue: VecDeque::new(),
103 current: CurrentEvent::default(),
104 last_canvas_size: None,
105 last_focused: None,
106 last_mouse_down: false,
107 last_mouse_pos: (0.0, 0.0),
108 online_state: Arc::new(AtomicU8::new(ONLINE_UNKNOWN)),
109 last_online: None,
110 online_thread_started: false,
111 online_shutdown: None,
112 gamepad_rx: None,
113 gamepad_thread_started: false,
114 gamepad_shutdown: None,
115 }
116 }
117}
118
119impl EventState {
120 fn alloc_listener_id(&mut self) -> u32 {
121 let id = self.next_listener_id;
122 self.next_listener_id = self.next_listener_id.wrapping_add(1).max(1);
123 id
124 }
125
126 fn add_listener(&mut self, event_type: String, callback_id: u32) -> u32 {
127 if (event_type == "online" || event_type == "offline") && !self.online_thread_started {
129 self.start_online_checker();
130 }
131 if event_type.starts_with("gamepad_") && !self.gamepad_thread_started {
132 self.start_gamepad_poll();
133 }
134 let listener_id = self.alloc_listener_id();
135 self.listeners
136 .entry(event_type.clone())
137 .or_default()
138 .push((listener_id, callback_id));
139 self.listener_owner.insert(listener_id, event_type);
140 listener_id
141 }
142
143 fn remove_listener(&mut self, listener_id: u32) -> bool {
144 let Some(event_type) = self.listener_owner.remove(&listener_id) else {
145 return false;
146 };
147 if let Some(vec) = self.listeners.get_mut(&event_type) {
148 vec.retain(|(lid, _)| *lid != listener_id);
149 if vec.is_empty() {
150 self.listeners.remove(&event_type);
151 }
152 }
153 true
154 }
155
156 fn enqueue(&mut self, event: PendingEvent) {
157 if self.listeners.contains_key(&event.event_type) {
158 if self.queue.len() >= MAX_QUEUED_EVENTS {
159 self.queue.pop_front();
160 }
161 self.queue.push_back(event);
162 }
163 }
164
165 fn start_online_checker(&mut self) {
166 self.online_thread_started = true;
167 let state = self.online_state.clone();
168 let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>();
169 self.online_shutdown = Some(shutdown_tx);
170 thread::Builder::new()
171 .name("oxide-online-checker".into())
172 .spawn(move || {
173 let client = match reqwest::blocking::Client::builder()
174 .timeout(Duration::from_secs(5))
175 .build()
176 {
177 Ok(c) => c,
178 Err(_) => return,
179 };
180 loop {
181 let ok = client
182 .head("https://www.cloudflare.com/cdn-cgi/trace")
183 .send()
184 .map(|r| r.status().is_success())
185 .unwrap_or(false);
186 state.store(if ok { ONLINE_YES } else { ONLINE_NO }, Ordering::Relaxed);
187 match shutdown_rx.recv_timeout(Duration::from_secs(30)) {
189 Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => return,
190 Err(mpsc::RecvTimeoutError::Timeout) => {}
191 }
192 }
193 })
194 .ok();
195 }
196
197 fn start_gamepad_poll(&mut self) {
198 self.gamepad_thread_started = true;
199 let (tx, rx) = mpsc::channel::<PendingEvent>();
200 let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>();
201 self.gamepad_rx = Some(rx);
202 self.gamepad_shutdown = Some(shutdown_tx);
203 thread::Builder::new()
204 .name("oxide-gamepad-poll".into())
205 .spawn(move || gamepad_poll_loop(tx, shutdown_rx))
206 .ok();
207 }
208}
209
210pub fn drain_pending_events(
217 events: &Arc<std::sync::Mutex<EventState>>,
218 canvas_size: (u32, u32),
219 focused: bool,
220 mouse_down: bool,
221 mouse_pos: (f32, f32),
222) -> Vec<(u32, String, Vec<u8>)> {
223 let mut g = events.lock().unwrap();
224
225 match g.last_canvas_size {
227 Some(prev) if prev == canvas_size => {}
228 _ => {
229 if g.last_canvas_size.is_some() {
230 let mut data = Vec::with_capacity(8);
231 data.extend_from_slice(&canvas_size.0.to_le_bytes());
232 data.extend_from_slice(&canvas_size.1.to_le_bytes());
233 g.enqueue(PendingEvent {
234 event_type: "resize".into(),
235 data,
236 });
237 }
238 g.last_canvas_size = Some(canvas_size);
239 }
240 }
241
242 match g.last_focused {
244 Some(prev) if prev == focused => {}
245 _ => {
246 if g.last_focused.is_some() {
247 let (focus_evt, vis_payload) = if focused {
248 ("focus", b"visible".to_vec())
249 } else {
250 ("blur", b"hidden".to_vec())
251 };
252 g.enqueue(PendingEvent {
253 event_type: focus_evt.into(),
254 data: Vec::new(),
255 });
256 g.enqueue(PendingEvent {
257 event_type: "visibility_change".into(),
258 data: vis_payload,
259 });
260 }
261 g.last_focused = Some(focused);
262 }
263 }
264
265 let prev_down = g.last_mouse_down;
267 let prev_pos = g.last_mouse_pos;
268 if mouse_down && !prev_down {
269 g.enqueue(PendingEvent {
270 event_type: "touch_start".into(),
271 data: encode_xy(mouse_pos),
272 });
273 } else if mouse_down && prev_down && mouse_pos != prev_pos {
274 g.enqueue(PendingEvent {
275 event_type: "touch_move".into(),
276 data: encode_xy(mouse_pos),
277 });
278 } else if !mouse_down && prev_down {
279 g.enqueue(PendingEvent {
280 event_type: "touch_end".into(),
281 data: encode_xy(prev_pos),
282 });
283 }
284 g.last_mouse_down = mouse_down;
285 g.last_mouse_pos = mouse_pos;
286
287 let online_raw = g.online_state.load(Ordering::Relaxed);
289 if online_raw != ONLINE_UNKNOWN {
290 let now = online_raw == ONLINE_YES;
291 match g.last_online {
292 Some(prev) if prev == now => {}
293 _ => {
294 if g.last_online.is_some() {
295 g.enqueue(PendingEvent {
296 event_type: if now { "online" } else { "offline" }.into(),
297 data: Vec::new(),
298 });
299 }
300 g.last_online = Some(now);
301 }
302 }
303 }
304
305 if let Some(rx) = g.gamepad_rx.as_ref() {
307 let mut drained = Vec::new();
308 while let Ok(ev) = rx.try_recv() {
309 drained.push(ev);
310 }
311 for ev in drained {
312 g.enqueue(ev);
313 }
314 }
315
316 let mut out = Vec::new();
318 while let Some(ev) = g.queue.pop_front() {
319 if let Some(vec) = g.listeners.get(&ev.event_type) {
320 for &(_, cb) in vec {
321 out.push((cb, ev.event_type.clone(), ev.data.clone()));
322 }
323 }
324 }
325 out
326}
327
328pub fn enqueue_drop_files(
333 events: &Arc<std::sync::Mutex<EventState>>,
334 paths: &[std::path::PathBuf],
335) {
336 let mut g = events.lock().unwrap();
337 let mut json = String::from("[");
338 for (i, p) in paths.iter().enumerate() {
339 if i > 0 {
340 json.push(',');
341 }
342 json.push('"');
343 for c in p.to_string_lossy().chars() {
344 match c {
345 '"' => json.push_str("\\\""),
346 '\\' => json.push_str("\\\\"),
347 '\n' => json.push_str("\\n"),
348 '\r' => json.push_str("\\r"),
349 '\t' => json.push_str("\\t"),
350 c if (c as u32) < 0x20 => json.push_str(&format!("\\u{:04x}", c as u32)),
351 c => json.push(c),
352 }
353 }
354 json.push('"');
355 }
356 json.push(']');
357 g.enqueue(PendingEvent {
358 event_type: "drop_files".into(),
359 data: json.into_bytes(),
360 });
361}
362
363pub fn set_current_event(
367 events: &Arc<std::sync::Mutex<EventState>>,
368 event_type: String,
369 data: Vec<u8>,
370) {
371 let mut g = events.lock().unwrap();
372 g.current.event_type = event_type;
373 g.current.data = data;
374}
375
376fn encode_xy((x, y): (f32, f32)) -> Vec<u8> {
377 let mut data = Vec::with_capacity(8);
378 data.extend_from_slice(&x.to_le_bytes());
379 data.extend_from_slice(&y.to_le_bytes());
380 data
381}
382
383fn gamepad_poll_loop(tx: Sender<PendingEvent>, shutdown: Receiver<()>) {
386 use gilrs::{EventType, Gilrs};
387 let mut gilrs = match Gilrs::new() {
388 Ok(g) => g,
389 Err(_) => return,
390 };
391 loop {
392 while let Some(gilrs::Event { id, event, .. }) = gilrs.next_event() {
393 let id_u: u32 = Into::<usize>::into(id) as u32;
394 let pending = match event {
395 EventType::Connected => {
396 let name = gilrs.gamepad(id).name().to_string();
397 Some(PendingEvent {
398 event_type: "gamepad_connected".into(),
399 data: name.into_bytes(),
400 })
401 }
402 EventType::ButtonPressed(btn, _) => Some(PendingEvent {
403 event_type: "gamepad_button".into(),
404 data: encode_button(id_u, btn as u32, true),
405 }),
406 EventType::ButtonReleased(btn, _) => Some(PendingEvent {
407 event_type: "gamepad_button".into(),
408 data: encode_button(id_u, btn as u32, false),
409 }),
410 EventType::AxisChanged(axis, value, _) => Some(PendingEvent {
411 event_type: "gamepad_axis".into(),
412 data: encode_axis(id_u, axis as u32, value),
413 }),
414 _ => None,
415 };
416 if let Some(ev) = pending {
417 if tx.send(ev).is_err() {
418 return;
419 }
420 }
421 }
422 match shutdown.recv_timeout(Duration::from_millis(16)) {
424 Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => return,
425 Err(mpsc::RecvTimeoutError::Timeout) => {}
426 }
427 }
428}
429
430fn encode_button(id: u32, code: u32, pressed: bool) -> Vec<u8> {
431 let mut data = Vec::with_capacity(12);
432 data.extend_from_slice(&id.to_le_bytes());
433 data.extend_from_slice(&code.to_le_bytes());
434 data.extend_from_slice(&(pressed as u32).to_le_bytes());
435 data
436}
437
438fn encode_axis(id: u32, code: u32, value: f32) -> Vec<u8> {
439 let mut data = Vec::with_capacity(12);
440 data.extend_from_slice(&id.to_le_bytes());
441 data.extend_from_slice(&code.to_le_bytes());
442 data.extend_from_slice(&value.to_le_bytes());
443 data
444}
445
446pub fn register_event_functions(linker: &mut Linker<HostState>) -> Result<()> {
451 linker.func_wrap(
452 "oxide",
453 "api_on_event",
454 |caller: Caller<'_, HostState>, type_ptr: u32, type_len: u32, callback_id: u32| -> u32 {
455 let mem = match caller.data().memory {
456 Some(m) => m,
457 None => return 0,
458 };
459 let event_type = match read_guest_string(&mem, &caller, type_ptr, type_len) {
460 Ok(s) if !s.is_empty() => s,
461 _ => return 0,
462 };
463 let mut g = caller.data().events.lock().unwrap();
464 g.add_listener(event_type, callback_id)
465 },
466 )?;
467
468 linker.func_wrap(
469 "oxide",
470 "api_off_event",
471 |caller: Caller<'_, HostState>, listener_id: u32| -> u32 {
472 let mut g = caller.data().events.lock().unwrap();
473 u32::from(g.remove_listener(listener_id))
474 },
475 )?;
476
477 linker.func_wrap(
478 "oxide",
479 "api_emit_event",
480 |caller: Caller<'_, HostState>,
481 type_ptr: u32,
482 type_len: u32,
483 data_ptr: u32,
484 data_len: u32| {
485 let mem = match caller.data().memory {
486 Some(m) => m,
487 None => return,
488 };
489 let event_type = match read_guest_string(&mem, &caller, type_ptr, type_len) {
490 Ok(s) if !s.is_empty() => s,
491 _ => return,
492 };
493 let data = if data_len == 0 {
494 Vec::new()
495 } else {
496 read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default()
497 };
498 let mut g = caller.data().events.lock().unwrap();
499 g.enqueue(PendingEvent { event_type, data });
500 },
501 )?;
502
503 linker.func_wrap(
504 "oxide",
505 "api_event_type_len",
506 |caller: Caller<'_, HostState>| -> u32 {
507 let g = caller.data().events.lock().unwrap();
508 g.current.event_type.len() as u32
509 },
510 )?;
511
512 linker.func_wrap(
513 "oxide",
514 "api_event_type_read",
515 |mut caller: Caller<'_, HostState>, out_ptr: u32, out_cap: u32| -> u32 {
516 let mem = match caller.data().memory {
517 Some(m) => m,
518 None => return 0,
519 };
520 let bytes = caller
522 .data()
523 .events
524 .lock()
525 .unwrap()
526 .current
527 .event_type
528 .clone()
529 .into_bytes();
530 let len = bytes.len().min(out_cap as usize);
531 if write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..len]).is_err() {
532 return 0;
533 }
534 len as u32
535 },
536 )?;
537
538 linker.func_wrap(
539 "oxide",
540 "api_event_data_len",
541 |caller: Caller<'_, HostState>| -> u32 {
542 let g = caller.data().events.lock().unwrap();
543 g.current.data.len() as u32
544 },
545 )?;
546
547 linker.func_wrap(
548 "oxide",
549 "api_event_data_read",
550 |mut caller: Caller<'_, HostState>, out_ptr: u32, out_cap: u32| -> u32 {
551 let mem = match caller.data().memory {
552 Some(m) => m,
553 None => return 0,
554 };
555 let bytes = caller.data().events.lock().unwrap().current.data.clone();
557 let len = bytes.len().min(out_cap as usize);
558 if write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..len]).is_err() {
559 return 0;
560 }
561 len as u32
562 },
563 )?;
564
565 Ok(())
566}