1use std::collections::HashSet;
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8use gilrs::{Button, Gilrs};
9use winit::application::ApplicationHandler;
10use winit::dpi::LogicalSize;
11use winit::event::{DeviceEvent, DeviceId, ElementState, MouseButton, MouseScrollDelta, WindowEvent};
12use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
13use winit::keyboard::{Key, KeyCode, NamedKey, PhysicalKey};
14use winit::window::{CursorGrabMode, Window, WindowId};
15
16use crate::audio::{AudioOut, AudioSink, AudioStream};
17use crate::angle::NativeWindow;
18use crate::gfx::Gfx;
19use crate::host::{self, Game, Gamepad, KEY_STATE_BYTES, Pointer, RawInput, Stop};
20use crate::keymap;
21use crate::present::Present;
22use crate::session::Session;
23use crate::splash;
24
25pub struct Options {
26 pub size: (u32, u32),
28 pub keymap: keymap::Keymap,
29 pub mute: bool,
30 pub present: Present,
32 pub screenshot: Option<(u64, String)>,
34 pub splash: bool,
36 pub copy_key: Option<KeyCode>,
38}
39
40struct Splash {
42 frame: u32,
43 next: Instant,
44 compiling: Option<std::thread::JoinHandle<Result<wasmtime::Module, Stop>>>,
45 module: Option<Result<wasmtime::Module, Stop>>,
46 skip: bool,
48 audio: Option<Box<dyn AudioOut>>,
50 gfx: Gfx,
51 gl: Option<crate::angle::Angle>,
52}
53
54fn native_window(window: &Window) -> Result<NativeWindow, String> {
56 use winit::raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle};
57 let w = window.window_handle().map_err(|e| e.to_string())?.as_raw();
58 let d = window.display_handle().map_err(|e| e.to_string())?.as_raw();
59 Ok(match (w, d) {
60 (RawWindowHandle::AppKit(h), _) => NativeWindow::AppKit(h.ns_view.as_ptr()),
61 (RawWindowHandle::Win32(h), _) => NativeWindow::Win32(h.hwnd.get() as *mut std::ffi::c_void),
62 (RawWindowHandle::Xlib(h), RawDisplayHandle::Xlib(d)) => {
63 NativeWindow::Xlib(d.display.map_or(std::ptr::null_mut(), |p| p.as_ptr()), h.window)
64 }
65 (RawWindowHandle::Wayland(_), RawDisplayHandle::Wayland(d)) => NativeWindow::Wayland(d.display.as_ptr()),
66 _ => return Err("gasm:gl: this kind of window isn't supported".into()),
67 })
68}
69
70pub fn run(session: Session, opts: Options) -> Result<i32, String> {
73 let mut builder = EventLoop::builder();
74 #[cfg(all(unix, not(target_os = "macos")))]
76 if session.uses_gl() {
77 use winit::platform::x11::EventLoopBuilderExtX11;
78 builder.with_x11();
79 }
80 let event_loop = builder.build().map_err(|e| e.to_string())?;
81 let default_title = host::static_title(&session.wasm).unwrap_or_else(|| {
83 std::path::Path::new(&session.name).file_stem().map_or(session.name.clone(), |s| s.to_string_lossy().into_owned())
84 });
85 let mut app = App {
86 title: default_title.clone(),
87 default_title,
88 fps: 0,
89 opts,
90 consent: session.consent.clone(),
91 asking: None,
92 swallowed: HashSet::new(),
93 session: Some(session),
94 window: None,
95 game: None,
96 keys: HashSet::new(),
97 typed: String::new(),
98 key_events: Vec::new(),
99 esc_down: None,
100 mouse: MouseState::default(),
101 applied_mode: 0,
102 pointer_flags: 0,
103 gilrs: Gilrs::new().ok(),
104 next: Instant::now(),
105 fps_t: Instant::now(),
106 fps_n: 0,
107 result: Ok(0),
108 frames_run: 0,
109 audio_stream: None,
110 splash: None,
111 copy_pending: false,
112 clipboard: None,
113 paste: None,
114 };
115 event_loop.run_app(&mut app).map_err(|e| e.to_string())?;
116 app.result
117}
118
119fn gamepad_pads(gilrs: &mut Gilrs) -> ([u32; 4], usize) {
121 const MAP: &[(Button, u32)] = &[
122 (Button::East, 1 << 0),
123 (Button::South, 1 << 1),
124 (Button::North, 1 << 2),
125 (Button::West, 1 << 3),
126 (Button::LeftTrigger, 1 << 4),
127 (Button::RightTrigger, 1 << 5),
128 (Button::Select, 1 << 6),
129 (Button::Start, 1 << 7),
130 (Button::DPadUp, 1 << 8),
131 (Button::DPadDown, 1 << 9),
132 (Button::DPadLeft, 1 << 10),
133 (Button::DPadRight, 1 << 11),
134 ];
135 while gilrs.next_event().is_some() {}
136 let mut pads = [0u32; 4];
137 let connected = gilrs.gamepads().count();
138 for (i, (_, pad)) in gilrs.gamepads().take(4).enumerate() {
139 pads[i] = MAP.iter().filter(|(b, _)| pad.is_pressed(*b)).fold(0, |m, (_, bit)| m | bit);
140 let (x, y) = (pad.value(gilrs::Axis::LeftStickX), pad.value(gilrs::Axis::LeftStickY));
141 if x < -0.5 { pads[i] |= 1 << 10 }
142 if x > 0.5 { pads[i] |= 1 << 11 }
143 if y > 0.5 { pads[i] |= 1 << 8 }
144 if y < -0.5 { pads[i] |= 1 << 9 }
145 }
146 (pads, connected)
147}
148
149#[derive(Default)]
151struct MouseState {
152 x: f32,
153 y: f32,
154 inside: bool,
155 buttons: u32,
156 pressed: u32,
158 released: u32,
159 dx: f32,
160 dy: f32,
161 wheel_x: f32,
162 wheel_y: f32,
163}
164
165const ESC_HOLD: Duration = Duration::from_millis(1000);
167
168fn mouse_bit(b: MouseButton) -> u32 {
169 match b {
170 MouseButton::Left => 1,
171 MouseButton::Right => 2,
172 MouseButton::Middle => 4,
173 MouseButton::Back => 8,
174 MouseButton::Forward => 16,
175 MouseButton::Other(_) => 0,
176 }
177}
178
179fn gamepads_raw(gilrs: &Gilrs) -> [Gamepad; 4] {
182 use gilrs::{Axis, MappingSource};
183 const BUTTONS: [Button; 17] = [
184 Button::South, Button::East, Button::West, Button::North, Button::LeftTrigger, Button::RightTrigger,
185 Button::LeftTrigger2, Button::RightTrigger2, Button::Select, Button::Start, Button::LeftThumb,
186 Button::RightThumb, Button::DPadUp, Button::DPadDown, Button::DPadLeft, Button::DPadRight, Button::Mode,
187 ];
188 let mut out: [Gamepad; 4] = Default::default();
189 for (slot, (_, pad)) in gilrs.gamepads().take(4).enumerate() {
190 let g = &mut out[slot];
191 g.connected = true;
192 g.name = pad.name().to_owned();
193 if pad.mapping_source() != MappingSource::None {
194 g.standard = true;
195 g.buttons = BUTTONS.iter().map(|b| pad.button_data(*b).map_or(0.0, |d| d.value())).collect();
196 g.axes = vec![pad.value(Axis::LeftStickX), -pad.value(Axis::LeftStickY), pad.value(Axis::RightStickX), -pad.value(Axis::RightStickY)];
198 } else {
199 let mut b: Vec<_> = pad.state().buttons().map(|(c, d)| (c.into_u32(), d.value())).collect();
200 let mut a: Vec<_> = pad.state().axes().map(|(c, d)| (c.into_u32(), d.value())).collect();
201 b.sort_by_key(|x| x.0);
202 a.sort_by_key(|x| x.0);
203 g.buttons = b.into_iter().map(|x| x.1).take(host::GAMEPAD_BUTTONS).collect();
204 g.axes = a.into_iter().map(|x| x.1).take(host::GAMEPAD_AXES).collect();
205 }
206 }
207 out
208}
209
210struct App {
211 frames_run: u64,
213 title: String,
215 default_title: String,
216 fps: u32,
218 opts: Options,
219 session: Option<Session>,
221 window: Option<Arc<Window>>,
222 game: Option<Game>,
223 keys: HashSet<KeyCode>,
224 typed: String,
226 key_events: Vec<(u16, bool)>,
228 esc_down: Option<Instant>,
230 mouse: MouseState,
231 applied_mode: u32,
233 pointer_flags: u32,
235 gilrs: Option<Gilrs>,
236 next: Instant,
237 fps_t: Instant,
238 fps_n: u32,
239 result: Result<i32, String>,
240 audio_stream: Option<AudioStream>,
242 splash: Option<Splash>,
243 copy_pending: bool,
245 clipboard: Option<arboard::Clipboard>,
247 paste: Option<String>,
249 consent: Option<crate::consent::Consent>,
251 asking: Option<crate::consent::Subject>,
253 swallowed: HashSet<KeyCode>,
255}
256
257fn open_clipboard(clipboard: &mut Option<arboard::Clipboard>) -> Option<&mut arboard::Clipboard> {
259 if clipboard.is_none() {
260 match arboard::Clipboard::new() {
261 Ok(c) => *clipboard = Some(c),
262 Err(e) => eprintln!("[gasm] clipboard: unavailable ({e})"),
263 }
264 }
265 clipboard.as_mut()
266}
267
268fn copy_frame(clipboard: &mut Option<arboard::Clipboard>, w: u32, h: u32, mut rgba: Vec<u8>) {
270 for px in rgba.chunks_exact_mut(4) {
272 px[3] = 255;
273 }
274 if open_clipboard(clipboard).is_none() {
275 return;
276 }
277 let img = arboard::ImageData { width: w as usize, height: h as usize, bytes: rgba.into() };
278 match clipboard.as_mut().map(|c| c.set_image(img)) {
279 Some(Ok(())) => eprintln!("[gasm] copied the frame ({w}x{h}) to the clipboard"),
280 Some(Err(e)) => eprintln!("[gasm] copy: {e}"),
281 None => {}
282 }
283}
284
285impl App {
286
287 fn stop(&mut self, el: &ActiveEventLoop, result: Result<i32, String>) {
288 self.result = result;
289 self.game = None;
292 el.exit();
293 }
294
295 fn show_title(&self) {
299 if let Some(w) = &self.window {
300 w.set_title(&format!("{} — gasm — {} fps", self.title, self.fps));
301 }
302 }
303
304 fn apply_input_mode(&mut self) {
305 let (Some(game), Some(w)) = (&mut self.game, &self.window) else { return };
306 let want = game.with_host(|h| h.input_mode) & 6;
307 if want == self.applied_mode {
308 return;
309 }
310 self.applied_mode = want;
311 let locked = want & 4 != 0
312 && w.set_cursor_grab(CursorGrabMode::Locked).or_else(|_| w.set_cursor_grab(CursorGrabMode::Confined)).is_ok();
313 if want & 4 == 0 {
314 let _ = w.set_cursor_grab(CursorGrabMode::None);
315 }
316 w.set_cursor_visible(want == 0);
318 self.pointer_flags = if want != 0 { 2 } else { 0 } | if locked { 4 } else { 0 };
319 }
320
321 fn quit(&mut self, el: &ActiveEventLoop) {
323 if let Some(g) = &mut self.game {
324 g.exit();
325 }
326 self.stop(el, Ok(0));
327 }
328
329 fn tick_splash(&mut self, el: &ActiveEventLoop) {
332 let Some(sp) = &mut self.splash else { return };
333 if Instant::now() < sp.next {
334 return el.set_control_flow(ControlFlow::WaitUntil(sp.next));
335 }
336 if sp.compiling.as_ref().is_some_and(|t| t.is_finished()) {
337 let t = sp.compiling.take().expect("compile thread");
338 sp.module = Some(t.join().unwrap_or_else(|_| Err(Stop::Trap("the compiler thread panicked".into()))));
339 }
340 let ready = sp.module.is_some();
341 if sp.skip {
342 sp.frame = if ready { splash::FRAMES } else { sp.frame.max(splash::HOLD) };
343 }
344 if sp.frame >= splash::FRAMES && ready {
345 if let Some(q) = self.consent.as_ref().and_then(|c| c.lock().unwrap().question().cloned()) {
347 if self.asking.as_ref() != Some(&q) {
348 let (wants, what) = crate::consent::describe(&q);
349 eprintln!("[gasm] consent: {} {wants} {what}? (1: this time, 2: always, 3: not now, 4: never)", self.default_title);
350 }
351 let img = crate::prompt::image(&self.default_title, &q);
352 let (w, h) = (crate::prompt::W as u32, crate::prompt::H as u32);
353 match &sp.gl {
354 Some(angle) => angle.show_image(&img, w, h),
355 None => sp.gfx.present_video(&img, w, h, None),
356 }
357 self.asking = Some(q);
358 return el.set_control_flow(ControlFlow::WaitUntil(Instant::now() + Duration::from_millis(50)));
359 }
360 self.asking = None;
361 let sp = self.splash.take().expect("splash");
362 return self.start_game(el, sp.module.expect("compiled"), sp.audio, sp.gfx, sp.gl);
363 }
364 let rgba = splash::frame(sp.frame);
365 match &sp.gl {
366 Some(angle) => angle.show_image(&rgba, splash::W as u32, splash::H as u32),
367 None => sp.gfx.present_video(&rgba, splash::W as u32, splash::H as u32, None),
368 }
369 if sp.frame != splash::HOLD || ready {
370 sp.frame += 1;
371 }
372 sp.next += Duration::from_secs_f64(1.0 / 60.0);
373 if sp.next < Instant::now() {
374 sp.next = Instant::now();
375 }
376 el.set_control_flow(ControlFlow::WaitUntil(sp.next));
377 }
378
379 fn start_game(&mut self, el: &ActiveEventLoop, module: Result<wasmtime::Module, Stop>, audio: Option<Box<dyn AudioOut>>, gfx: Gfx, gl: Option<crate::angle::Angle>) {
381 let Some(session) = self.session.take() else { return };
382 let started = match module {
383 Ok(m) => session.start_compiled(m, audio, gfx, gl),
384 Err(e) => Err(e),
385 };
386 match started {
387 Ok(g) => self.game = Some(g),
388 Err(Stop::Exit(code)) => return self.stop(el, Ok(code)),
389 Err(Stop::Trap(e)) => return self.stop(el, Err(e)),
390 }
391 self.typed.clear();
393 self.key_events.clear();
394 self.mouse.pressed = 0;
395 self.mouse.released = 0;
396 self.next = Instant::now();
397 el.set_control_flow(ControlFlow::WaitUntil(self.next));
398 }
399
400 fn tick(&mut self, el: &ActiveEventLoop) {
401 if self.splash.is_some() {
402 if self.esc_down.is_some_and(|t| t.elapsed() >= ESC_HOLD) {
403 return self.quit(el);
404 }
405 return self.tick_splash(el);
406 }
407 let Some(game) = &mut self.game else { return };
408 let period = Duration::from_secs_f64(1.0 / game.with_host(|h| h.frame_rate));
409 let now = Instant::now();
410 if let Some(q) = self.consent.as_ref().and_then(|c| c.lock().unwrap().question().cloned()) {
412 if self.esc_down.is_some_and(|t| t.elapsed() >= ESC_HOLD) {
413 return self.quit(el);
414 }
415 if self.asking.as_ref() != Some(&q) {
416 let (wants, what) = crate::consent::describe(&q);
417 eprintln!("[gasm] consent: {} {wants} {what}? (1: this time, 2: always, 3: not now, 4: never)", self.default_title);
418 }
419 let img = crate::prompt::image(&self.default_title, &q);
420 let (w, h) = (crate::prompt::W as u32, crate::prompt::H as u32);
421 game.with_host(|host| {
422 if !host.gl.show_over_game(&img, w, h) {
423 host.gfx.present_video(&img, w, h, None);
424 }
425 });
426 self.asking = Some(q);
427 self.next = now + period; return el.set_control_flow(ControlFlow::WaitUntil(now + Duration::from_millis(50)));
429 }
430 self.asking = None;
431 if now >= self.next {
432 let behind = ((now - self.next).as_secs_f64() / period.as_secs_f64()) as u32 + 1;
434 let steps = behind.min(4);
435 if self.esc_down.is_some_and(|t| t.elapsed() >= ESC_HOLD) {
437 return self.quit(el);
438 }
439 let mut keys = [0u8; KEY_STATE_BYTES];
440 for &c in &self.keys {
441 let k = keymap::gasm_key(c);
442 if k != 0 {
443 keys[k as usize / 8] |= 1 << (k % 8);
444 }
445 }
446 let (mut pads, gamepads) = self.gilrs.as_mut().map(gamepad_pads).unwrap_or_default();
448 let raw_pads = self.gilrs.as_ref().map(gamepads_raw).unwrap_or_default();
449 if game.with_host(|h| h.input_mode) & 1 == 0 {
451 let kb = keymap::pads(&self.opts.keymap, &self.keys, gamepads);
452 for (p, k) in pads.iter_mut().zip(kb) {
453 *p |= k;
454 }
455 }
456 let drawable = game.with_host(|h| h.gl.size().unwrap_or_else(|| h.gfx.size()));
457 for k in 0..steps {
458 game.with_host(|host| {
459 host.pads = pads;
460 let first = k == 0;
462 host.text = Some(if first { std::mem::take(&mut self.typed) } else { String::new() });
463 let m = &mut self.mouse;
464 let pointer = Pointer {
465 x: m.x,
466 y: m.y,
467 dx: if first { m.dx } else { 0.0 },
468 dy: if first { m.dy } else { 0.0 },
469 wheel_x: if first { m.wheel_x } else { 0.0 },
470 wheel_y: if first { m.wheel_y } else { 0.0 },
471 buttons: m.buttons,
472 pressed: if first { m.pressed } else { 0 },
473 released: if first { m.released } else { 0 },
474 flags: m.inside as u32 | self.pointer_flags,
475 drawable: (drawable.0 as f32, drawable.1 as f32),
476 integer_scale: self.opts.present.integer_scale,
477 };
478 if first {
479 (m.dx, m.dy, m.wheel_x, m.wheel_y, m.pressed, m.released) = (0.0, 0.0, 0.0, 0.0, 0, 0);
480 }
481 if first {
482 host.input = RawInput { keys: Some(keys), key_events: std::mem::take(&mut self.key_events), pointer: Some(pointer), gamepads: Some(raw_pads.clone()) };
483 host.clipboard.pasted = self.paste.take();
485 } else {
486 host.input.key_events.clear();
487 host.clipboard.pasted = None;
488 host.input.pointer = Some(pointer);
489 }
490 host.show_frame = k + 1 == steps;
491 host.catch_up = k + 1 < steps;
492 host.gfx.used = false;
493 });
494 let r = game.frame();
495 self.frames_run += 1;
496 let shot = self.opts.screenshot.as_ref().filter(|(n, _)| *n == self.frames_run).map(|(_, p)| p.clone());
497 let copy = std::mem::take(&mut self.copy_pending);
498 let (img, copied, text) = game.with_host(|h| {
499 let img = shot.as_ref().and_then(|_| h.gl.read_frame());
500 let copied = copy.then(|| h.gl.read_frame().or_else(|| (h.width > 0).then(|| (h.width as u32, h.height as u32, h.rgba.clone())))).flatten();
502 let show = h.show_frame;
503 h.gl.end_frame(show);
504 (img, copied, h.clipboard.copied.take())
505 });
506 if let Some(t) = text {
507 if let Some(Err(e)) = open_clipboard(&mut self.clipboard).map(|c| c.set_text(t)) {
508 eprintln!("[gasm] clipboard: {e}");
509 }
510 }
511 match copied {
512 Some((w, h, rgba)) => copy_frame(&mut self.clipboard, w, h, rgba),
513 None if copy => eprintln!("[gasm] copy: nothing to copy (gasm:gfx games can't be copied yet)"),
514 None => {}
515 }
516 if let Some(path) = shot {
517 let r = match img {
518 Some((w, h, rgba)) => crate::headless::write_png(&path, w, h, &rgba).map(|_| eprintln!("[gasm] wrote {path}")),
519 None => Err("--window-screenshot: only gasm:gl games".into()),
520 };
521 return match r {
522 Ok(()) => self.quit(el),
523 Err(e) => self.stop(el, Err(e)),
524 };
525 }
526 match r {
527 Ok(()) => {}
528 Err(Stop::Exit(code)) => return self.stop(el, Ok(code)),
529 Err(Stop::Trap(e)) => return self.stop(el, Err(e)),
530 }
531 self.next += period;
532 self.fps_n += 1;
533 }
534 if behind > 4 {
535 self.next = Instant::now() + period;
536 }
537 let title = game.with_host(|host| {
539 if !host.gfx.used && host.width > 0 {
540 let (w, h) = (host.width as u32, host.height as u32);
541 let rgba = std::mem::take(&mut host.rgba);
542 host.gfx.present_video(&rgba, w, h, host.aspect);
543 host.rgba = rgba;
544 }
545 std::mem::take(&mut host.title_changed).then(|| host.title.clone())
546 });
547 if let Some(t) = title {
548 self.title = t.unwrap_or_else(|| self.default_title.clone());
549 self.show_title();
550 }
551 }
552 self.apply_input_mode();
553 if let Some(s) = &mut self.audio_stream {
554 s.check(); }
556 if self.fps_t.elapsed() >= Duration::from_secs(1) {
557 self.fps = self.fps_n;
558 self.show_title();
559 self.fps_n = 0;
560 self.fps_t = Instant::now();
561 }
562 el.set_control_flow(ControlFlow::WaitUntil(self.next));
563 }
564}
565
566impl ApplicationHandler for App {
567 fn resumed(&mut self, el: &ActiveEventLoop) {
568 if self.window.is_some() {
569 return;
570 }
571 let attrs = Window::default_attributes()
572 .with_title(format!("{} — gasm", self.title))
573 .with_inner_size(LogicalSize::new(self.opts.size.0, self.opts.size.1));
574 let window = match el.create_window(attrs) {
575 Ok(w) => Arc::new(w),
576 Err(e) => return self.stop(el, Err(format!("cannot create window: {e}"))),
577 };
578 let uses_gl = self.session.as_ref().is_some_and(|s| s.uses_gl());
580 let mut gl = None;
581 let gfx = if uses_gl {
582 let native = match native_window(&window) {
583 Ok(n) => n,
584 Err(e) => return self.stop(el, Err(e)),
585 };
586 match self.session.as_ref().map(|s| s.open_gl(Some(native), (0, 0))) {
587 Some(Ok(a)) => gl = Some(a),
588 Some(Err(e)) => return self.stop(el, Err(format!("cannot initialise OpenGL ES (gasm:gl): {e}"))),
589 None => return,
590 }
591 Gfx::null()
592 } else {
593 match Gfx::for_window(window.clone()) {
594 Ok(mut g) => {
595 g.present = self.opts.present;
596 g
597 }
598 Err(e) => return self.stop(el, Err(format!("cannot initialise GPU: {e}"))),
599 }
600 };
601 let audio: Option<Box<dyn AudioOut>> = if self.opts.mute {
602 None
603 } else {
604 match AudioSink::open() {
605 Ok((a, stream)) => {
606 eprintln!("[gasm] audio: {} Hz", a.device_rate());
607 self.audio_stream = Some(stream);
608 Some(Box::new(a))
609 }
610 Err(e) => {
611 eprintln!("[gasm] audio disabled: {e}");
612 None
613 }
614 }
615 };
616 self.window = Some(window);
617 let Some(session) = self.session.as_ref() else { return };
618 let asking = self.consent.as_ref().is_some_and(|c| c.lock().unwrap().asking());
620 if self.opts.splash || asking {
621 let compiling = Some(session.compile_in_background());
623 let next = Instant::now();
624 let frame = if self.opts.splash { 0 } else { splash::FRAMES };
625 self.splash = Some(Splash { frame, next, compiling, module: None, skip: false, audio, gfx, gl });
626 el.set_control_flow(ControlFlow::WaitUntil(next));
627 } else {
628 let module = crate::host::Game::compile(&session.wasm, session.load.allow_precompiled);
629 self.start_game(el, module, audio, gfx, gl);
630 }
631 }
632
633 fn window_event(&mut self, el: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
634 match event {
635 WindowEvent::CloseRequested => self.quit(el),
636 WindowEvent::Resized(size) => {
637 if let Some(g) = &mut self.game {
638 g.with_host(|h| h.gfx.resize(size.width, size.height));
639 }
640 if let Some(sp) = &mut self.splash {
641 sp.gfx.resize(size.width, size.height);
642 }
643 }
644 WindowEvent::Focused(false) => {
645 for c in self.keys.drain() {
647 let k = keymap::gasm_key(c);
648 if k != 0 {
649 self.key_events.push((k, false));
650 }
651 }
652 self.esc_down = None;
653 self.mouse.inside = false;
654 self.mouse.released |= self.mouse.buttons;
655 self.mouse.buttons = 0;
656 }
657 WindowEvent::CursorMoved { position, .. } => {
658 self.mouse.x = position.x as f32;
659 self.mouse.y = position.y as f32;
660 self.mouse.inside = true;
661 }
662 WindowEvent::CursorEntered { .. } => self.mouse.inside = true,
663 WindowEvent::CursorLeft { .. } => self.mouse.inside = false,
664 WindowEvent::MouseInput { state, button, .. } => {
665 if let Some(sp) = &mut self.splash {
666 sp.skip |= state == ElementState::Pressed;
667 }
668 let bit = mouse_bit(button);
669 match state {
670 ElementState::Pressed => {
671 self.mouse.buttons |= bit;
672 self.mouse.pressed |= bit;
673 }
674 ElementState::Released => {
675 self.mouse.buttons &= !bit;
676 self.mouse.released |= bit;
677 }
678 }
679 }
680 WindowEvent::MouseWheel { delta, .. } => {
681 let (x, y) = match delta {
683 MouseScrollDelta::LineDelta(x, y) => (x, y),
684 MouseScrollDelta::PixelDelta(p) => (p.x as f32 / 100.0, p.y as f32 / 100.0),
685 };
686 self.mouse.wheel_x -= x;
687 self.mouse.wheel_y -= y;
688 }
689 WindowEvent::KeyboardInput { event, .. } => {
690 if let Some(sp) = &mut self.splash {
691 sp.skip |= event.state == ElementState::Pressed;
692 }
693 if let PhysicalKey::Code(code) = event.physical_key {
695 if event.state == ElementState::Released && self.swallowed.remove(&code) {
696 if code == KeyCode::Escape {
697 self.esc_down = None;
698 }
699 return;
700 }
701 if let (Some(q), ElementState::Pressed) = (self.asking.clone(), event.state) {
702 use crate::consent::Answer;
703 let answer = match code {
704 KeyCode::Digit1 | KeyCode::Numpad1 => Some(Answer::ThisTime),
705 KeyCode::Digit2 | KeyCode::Numpad2 => Some(Answer::Always),
706 KeyCode::Digit3 | KeyCode::Numpad3 | KeyCode::Escape => Some(Answer::No),
707 KeyCode::Digit4 | KeyCode::Numpad4 => Some(Answer::Never),
708 _ => None,
709 };
710 if code == KeyCode::Escape && !event.repeat {
711 self.esc_down = Some(Instant::now());
712 }
713 self.swallowed.insert(code);
714 if let (Some(a), Some(c)) = (answer, &self.consent) {
715 if !event.repeat {
716 c.lock().unwrap().answer(&q, a);
717 }
718 }
719 return;
720 }
721 }
722 if let PhysicalKey::Code(code) = event.physical_key {
723 if code == KeyCode::Escape && !event.repeat {
724 self.esc_down = (event.state == ElementState::Pressed).then(Instant::now);
725 }
726 if Some(code) == self.opts.copy_key && event.state == ElementState::Pressed && !event.repeat {
728 self.copy_pending = true;
729 }
730 let modifier = [KeyCode::ControlLeft, KeyCode::ControlRight, KeyCode::SuperLeft, KeyCode::SuperRight].iter().any(|k| self.keys.contains(k));
732 if code == KeyCode::KeyV && modifier && event.state == ElementState::Pressed {
733 self.paste = open_clipboard(&mut self.clipboard)
734 .and_then(|c| c.get_text().ok())
735 .filter(|t| t.len() <= host::CLIPBOARD_MAX);
736 }
737 let k = keymap::gasm_key(code);
738 if !event.repeat && k != 0 {
739 self.key_events.push((k, event.state == ElementState::Pressed));
740 }
741 match event.state {
742 ElementState::Pressed => self.keys.insert(code),
743 ElementState::Released => self.keys.remove(&code),
744 };
745 }
746 if event.state == ElementState::Pressed {
747 match &event.logical_key {
748 Key::Named(NamedKey::Enter) => self.typed.push('\n'),
749 Key::Named(NamedKey::Backspace) => self.typed.push('\u{8}'),
750 _ => {
751 if let Some(t) = &event.text {
752 self.typed.extend(t.chars().filter(|c| !c.is_control()));
753 }
754 }
755 }
756 }
757 }
758 WindowEvent::RedrawRequested => self.tick(el),
759 _ => {}
760 }
761 }
762
763 fn device_event(&mut self, _el: &ActiveEventLoop, _id: DeviceId, event: DeviceEvent) {
764 if let DeviceEvent::MouseMotion { delta } = event
766 && self.window.as_ref().is_some_and(|w| w.has_focus())
767 {
768 self.mouse.dx += delta.0 as f32;
769 self.mouse.dy += delta.1 as f32;
770 }
771 }
772
773 fn about_to_wait(&mut self, el: &ActiveEventLoop) {
774 self.tick(el);
775 }
776}
777
778
779#[cfg(test)]
780mod tests {
781 #[test]
783 #[ignore]
784 fn copy_frame_puts_the_image_on_the_clipboard() {
785 let rgba: Vec<u8> = (0..4 * 3 * 4).map(|i| (i * 7) as u8 | 3).collect();
786 let mut clipboard = None;
787 super::copy_frame(&mut clipboard, 4, 3, rgba.clone());
788 let img = clipboard.as_mut().unwrap().get_image().unwrap();
789 assert_eq!((img.width, img.height), (4, 3));
790 let opaque: Vec<u8> = rgba.chunks(4).flat_map(|p| [p[0], p[1], p[2], 255]).collect();
791 assert_eq!(img.bytes.into_owned(), opaque);
792 }
793}