1use web_time::Instant;
2
3use loam_runtime::{
4 ActionEvent, Bindings, Input, Key, Pointer, PointerButton, PointerPhase as RuntimePointerPhase,
5};
6
7use crate::wasm::input_queue::{InputMessage, PointerPhase};
8
9pub struct InputMap {
10 input: Input,
11 spare: Input,
12 held_keys: Vec<(Key, loam_runtime::ActionId)>,
13 width: u32,
14 height: u32,
15 scale: f32,
16 display: Option<[f32; 2]>,
17 cursor: [f32; 2],
18 look: [f32; 2],
19 cursor_locked: bool,
20 mouse_buttons: [bool; 3],
21 active_touches: Vec<u32>,
22 started: Instant,
23 clock_offset: f64,
24 latest_time: f64,
25}
26
27#[cfg(any(all(target_arch = "wasm32", feature = "egui"), test))]
28pub(crate) struct TouchCapture {
29 active: Vec<CapturedTouch>,
30 pointer: Option<u64>,
31}
32
33#[cfg(any(all(target_arch = "wasm32", feature = "egui"), test))]
34struct CapturedTouch {
35 id: u64,
36 captured: bool,
37 pos: [f32; 2],
38}
39
40#[cfg(any(all(target_arch = "wasm32", feature = "egui"), test))]
41impl Default for TouchCapture {
42 fn default() -> Self {
43 Self {
44 active: Vec::with_capacity(8),
45 pointer: None,
46 }
47 }
48}
49
50#[cfg(any(all(target_arch = "wasm32", feature = "egui"), test))]
51impl TouchCapture {
52 pub(crate) fn route(
53 &mut self,
54 id: u64,
55 phase: PointerPhase,
56 over_ui: bool,
57 pos: [f32; 2],
58 ) -> bool {
59 match phase {
60 PointerPhase::Down => {
61 if let Some(touch) = self.active.iter_mut().find(|touch| touch.id == id) {
62 touch.pos = pos;
63 return touch.captured;
64 }
65 if over_ui && self.pointer.is_none() {
66 self.pointer = Some(id);
67 }
68 self.active.push(CapturedTouch {
69 id,
70 captured: over_ui,
71 pos,
72 });
73 over_ui
74 }
75 PointerPhase::Move => {
76 let Some(touch) = self.active.iter_mut().find(|touch| touch.id == id) else {
77 return false;
78 };
79 touch.pos = pos;
80 touch.captured
81 }
82 PointerPhase::Up | PointerPhase::Cancel => {
83 let captured = self
84 .active
85 .iter()
86 .position(|touch| touch.id == id)
87 .map(|index| self.active.remove(index).captured)
88 .unwrap_or(false);
89 if self.pointer == Some(id) {
90 self.pointer = None;
91 }
92 captured
93 }
94 }
95 }
96
97 #[cfg(feature = "egui")]
98 pub(super) fn cancel_all(&mut self) -> impl Iterator<Item = (u64, [f32; 2])> + '_ {
99 self.pointer = None;
100 self.active
101 .drain(..)
102 .filter_map(|touch| touch.captured.then_some((touch.id, touch.pos)))
103 }
104
105 #[cfg(feature = "egui")]
106 pub(crate) fn is_pointer(&self, id: u64) -> bool {
107 self.pointer == Some(id)
108 }
109}
110
111impl Default for InputMap {
112 fn default() -> Self {
113 Self {
114 input: Input::default(),
115 spare: Input::default(),
116 held_keys: Vec::new(),
117 width: 1,
118 height: 1,
119 scale: 1.0,
120 display: None,
121 cursor: [0.0; 2],
122 look: [0.0; 2],
123 cursor_locked: false,
124 mouse_buttons: [false; 3],
125 active_touches: Vec::with_capacity(8),
126 started: Instant::now(),
127 clock_offset: 0.0,
128 latest_time: 0.0,
129 }
130 }
131}
132
133impl InputMap {
134 pub fn resize(&mut self, width: u32, height: u32, scale: f32) {
135 self.width = width.max(1);
136 self.height = height.max(1);
137 if scale.is_finite() && scale > 0.0 {
138 self.scale = scale;
139 }
140 }
141
142 pub fn set_display(&mut self, width: f32, height: f32) {
144 if width.is_finite() && height.is_finite() && width >= 1.0 && height >= 1.0 {
145 self.display = Some([width, height]);
146 }
147 }
148
149 pub fn display_aspect(&self) -> Option<f32> {
150 self.display.map(|[width, height]| width / height)
151 }
152
153 fn css_size(&self) -> [f32; 2] {
154 self.display.unwrap_or([
155 self.width as f32 / self.scale,
156 self.height as f32 / self.scale,
157 ])
158 }
159
160 pub fn size(&self) -> (u32, u32) {
161 (self.width, self.height)
162 }
163
164 pub fn scale(&self) -> f32 {
165 self.scale
166 }
167
168 pub fn cursor(&self) -> [f32; 2] {
169 self.cursor
170 }
171
172 pub fn cursor_locked(&self) -> bool {
173 self.cursor_locked
174 }
175
176 pub fn set_cursor_locked(&mut self, locked: bool) {
177 if self.cursor_locked == locked {
178 return;
179 }
180 self.cursor_locked = locked;
181 self.look = [0.0; 2];
182 self.release_pointers();
183 }
184
185 pub fn ndc(&self, physical_x: f64, physical_y: f64) -> [f32; 2] {
186 [
187 (physical_x as f32 / self.width as f32) * 2.0 - 1.0,
188 1.0 - (physical_y as f32 / self.height as f32) * 2.0,
189 ]
190 }
191
192 pub fn css_ndc(&self, css_x: f32, css_y: f32) -> [f32; 2] {
193 let [width, height] = self.css_size();
194 [css_x / width * 2.0 - 1.0, 1.0 - css_y / height * 2.0]
195 }
196
197 pub fn action(&mut self, bindings: &Bindings, key: Key, pressed: bool) {
198 let held = self.held_keys.iter().position(|(held, _)| *held == key);
199 if pressed {
200 if held.is_some() {
201 return;
202 }
203 let Some(action) = bindings.action(key) else {
204 return;
205 };
206 let starts_action = !self
207 .held_keys
208 .iter()
209 .any(|(_, held_action)| *held_action == action);
210 self.held_keys.push((key, action));
211 if starts_action {
212 self.input.actions.push(ActionEvent {
213 action,
214 pressed: true,
215 });
216 }
217 } else {
218 let Some(held) = held else {
219 return;
220 };
221 let (_, action) = self.held_keys.remove(held);
222 if !self
223 .held_keys
224 .iter()
225 .any(|(_, held_action)| *held_action == action)
226 {
227 self.input.actions.push(ActionEvent {
228 action,
229 pressed: false,
230 });
231 }
232 }
233 }
234
235 pub(crate) fn host_action(
236 &mut self,
237 bindings: &Bindings,
238 key: Key,
239 pressed: bool,
240 consumed: bool,
241 ) {
242 if !consumed || !pressed {
243 self.action(bindings, key, pressed);
244 }
245 }
246
247 pub fn release_all(&mut self) {
248 while let Some((_, action)) = self.held_keys.pop() {
249 if !self
250 .held_keys
251 .iter()
252 .any(|(_, held_action)| *held_action == action)
253 {
254 self.input.actions.push(ActionEvent {
255 action,
256 pressed: false,
257 });
258 }
259 }
260 self.release_pointers();
261 }
262
263 fn now(&self) -> f64 {
264 (self.started.elapsed().as_secs_f64() + self.clock_offset).max(self.latest_time)
265 }
266
267 fn sync_clock(&mut self, time: f64) {
268 self.clock_offset = time - self.started.elapsed().as_secs_f64();
269 }
270
271 fn release_pointers(&mut self) {
272 let time = self.now();
273 for button in [
274 PointerButton::Primary,
275 PointerButton::Secondary,
276 PointerButton::Middle,
277 ] {
278 let index = Self::button_index(button);
279 if self.mouse_buttons[index] {
280 self.push(
281 self.cursor,
282 [0.0; 2],
283 Some(button),
284 RuntimePointerPhase::Cancelled,
285 time,
286 );
287 }
288 }
289 self.mouse_buttons = [false; 3];
290 for id in std::mem::take(&mut self.active_touches) {
291 self.push_with(
292 id,
293 self.cursor,
294 [0.0; 2],
295 Some(PointerButton::Primary),
296 RuntimePointerPhase::Cancelled,
297 time,
298 );
299 }
300 }
301
302 pub fn raw_motion(&mut self, physical_dx: f32, physical_dy: f32) {
303 if !self.cursor_locked {
304 return;
305 }
306 self.look[0] += physical_dx / self.scale;
307 self.look[1] -= physical_dy / self.scale;
308 }
309
310 pub fn moved(&mut self, ndc: [f32; 2]) {
311 self.moved_at(ndc, self.now());
312 }
313
314 fn moved_at(&mut self, ndc: [f32; 2], time: f64) {
315 let delta = [ndc[0] - self.cursor[0], ndc[1] - self.cursor[1]];
316 self.cursor = ndc;
317 let mut moved = false;
318 for button in [
319 PointerButton::Primary,
320 PointerButton::Secondary,
321 PointerButton::Middle,
322 ] {
323 if self.mouse_buttons[Self::button_index(button)] {
324 self.push(ndc, delta, Some(button), RuntimePointerPhase::Moved, time);
325 moved = true;
326 }
327 }
328 if !moved {
329 self.push(ndc, delta, None, RuntimePointerPhase::Moved, time);
330 }
331 }
332
333 pub fn sync_mouse_buttons(&mut self, buttons: u8) {
334 self.mouse_buttons = [buttons & 1 != 0, buttons & 2 != 0, buttons & 4 != 0];
335 }
336
337 pub fn button(&mut self, ndc: [f32; 2], button: PointerButton, pressed: bool) {
338 self.button_at(ndc, button, pressed, self.now());
339 }
340
341 fn button_at(&mut self, ndc: [f32; 2], button: PointerButton, pressed: bool, time: f64) {
342 self.cursor = ndc;
343 self.mouse_buttons[Self::button_index(button)] = pressed;
344 let phase = if pressed {
345 RuntimePointerPhase::Began
346 } else {
347 RuntimePointerPhase::Ended
348 };
349 self.push(ndc, [0.0; 2], Some(button), phase, time);
350 }
351
352 #[cfg(any(not(target_arch = "wasm32"), test))]
353 pub(crate) fn host_button(
354 &mut self,
355 ndc: [f32; 2],
356 button: PointerButton,
357 pressed: bool,
358 consumed: bool,
359 ) {
360 let held = self.mouse_buttons[Self::button_index(button)];
361 if !consumed || (!pressed && held) {
362 self.button(ndc, button, pressed);
363 }
364 }
365
366 pub fn touch(&mut self, id: u32, ndc: [f32; 2], phase: RuntimePointerPhase) {
367 self.touch_at(id, ndc, phase, self.now());
368 }
369
370 fn touch_at(&mut self, id: u32, ndc: [f32; 2], phase: RuntimePointerPhase, time: f64) {
371 let delta = match phase {
372 RuntimePointerPhase::Moved => [ndc[0] - self.cursor[0], ndc[1] - self.cursor[1]],
373 _ => [0.0; 2],
374 };
375 self.cursor = ndc;
376 self.push_with(id, ndc, delta, Some(PointerButton::Primary), phase, time);
377 }
378
379 fn host_touch_at(
380 &mut self,
381 id: u32,
382 ndc: [f32; 2],
383 phase: RuntimePointerPhase,
384 consumed: bool,
385 time: f64,
386 ) {
387 let held = self.active_touches.contains(&id);
388 match phase {
389 RuntimePointerPhase::Began if !consumed => {
390 if !held {
391 self.active_touches.push(id);
392 }
393 self.touch_at(id, ndc, phase, time);
394 }
395 RuntimePointerPhase::Moved if !consumed => self.touch_at(id, ndc, phase, time),
396 RuntimePointerPhase::Ended | RuntimePointerPhase::Cancelled if !consumed || held => {
397 self.active_touches.retain(|active| *active != id);
398 self.touch_at(id, ndc, phase, time);
399 }
400 _ => {}
401 }
402 }
403
404 pub fn wheel(&mut self, delta: [f32; 2]) {
405 self.input.scroll[0] += delta[0];
406 self.input.scroll[1] += delta[1];
407 }
408
409 pub fn host_message(&mut self, topic: &str, values: &[f32]) {
410 self.input.host.push(topic, values);
411 }
412
413 fn button_index(button: PointerButton) -> usize {
414 match button {
415 PointerButton::Primary => 0,
416 PointerButton::Secondary => 1,
417 PointerButton::Middle => 2,
418 }
419 }
420
421 fn push(
422 &mut self,
423 ndc: [f32; 2],
424 delta: [f32; 2],
425 button: Option<PointerButton>,
426 phase: RuntimePointerPhase,
427 time: f64,
428 ) {
429 self.push_with(0, ndc, delta, button, phase, time);
430 }
431
432 fn push_with(
433 &mut self,
434 id: u32,
435 ndc: [f32; 2],
436 delta: [f32; 2],
437 button: Option<PointerButton>,
438 phase: RuntimePointerPhase,
439 time: f64,
440 ) {
441 self.latest_time = self.latest_time.max(time);
442 let [width, height] = self.css_size();
443 self.input.pointers.push(Pointer {
444 id,
445 button,
446 ndc,
447 delta: [delta[0] * width / 2.0, delta[1] * height / 2.0],
448 phase,
449 time,
450 });
451 }
452
453 pub fn take(&mut self) -> Input {
454 self.input.held.clear();
455 for (_, action) in &self.held_keys {
456 if !self.input.held.contains(action) {
457 self.input.held.push(*action);
458 }
459 }
460 self.input.look = std::mem::take(&mut self.look);
461 self.input.cursor_locked = self.cursor_locked;
462 self.input.time = self.now();
463 std::mem::replace(&mut self.input, std::mem::take(&mut self.spare))
464 }
465
466 pub fn reclaim(&mut self, mut reclaimed: Input) {
467 reclaimed.pointers.clear();
468 reclaimed.actions.clear();
469 reclaimed.held.clear();
470 reclaimed.scroll = [0.0; 2];
471 reclaimed.look = [0.0; 2];
472 reclaimed.cursor_locked = false;
473 reclaimed.time = 0.0;
474 reclaimed.host.clear();
475 self.spare = reclaimed;
476 }
477}
478
479#[cfg(not(target_arch = "wasm32"))]
480pub fn winit_key(event: &winit::event::KeyEvent) -> Option<Key> {
481 use winit::keyboard::{Key as Logical, NamedKey};
482
483 match &event.logical_key {
484 Logical::Named(NamedKey::Space) => Some(Key::Space),
485 Logical::Named(NamedKey::Escape) => Some(Key::Escape),
486 Logical::Character(text) => match text.chars().next() {
487 Some(letter) if letter.is_ascii_digit() => Some(Key::Digit(letter as u8 - b'0')),
488 Some(letter) => Some(Key::Letter(letter.to_ascii_lowercase())),
489 None => None,
490 },
491 _ => None,
492 }
493}
494
495#[cfg(not(target_arch = "wasm32"))]
496pub fn winit_alt(event: &winit::event::KeyEvent) -> Option<usize> {
497 use winit::keyboard::{KeyCode, PhysicalKey};
498
499 match event.physical_key {
500 PhysicalKey::Code(KeyCode::AltLeft) => Some(0),
501 PhysicalKey::Code(KeyCode::AltRight) => Some(1),
502 _ => None,
503 }
504}
505
506pub fn dom_key(code: &str) -> Option<Key> {
507 match code {
508 "Space" => Some(Key::Space),
509 "Escape" => Some(Key::Escape),
510 _ => {
511 let digit = code
512 .strip_prefix("Digit")
513 .or_else(|| code.strip_prefix("Numpad"));
514 if let Some(digit) = digit.and_then(|rest| rest.parse::<u8>().ok()) {
515 return Some(Key::Digit(digit));
516 }
517 let mut letters = code.strip_prefix("Key")?.chars();
518 let letter = letters.next().filter(char::is_ascii_alphabetic)?;
519 letters
520 .next()
521 .is_none()
522 .then(|| Key::Letter(letter.to_ascii_lowercase()))
523 }
524 }
525}
526
527fn dom_phase(phase: PointerPhase) -> RuntimePointerPhase {
528 match phase {
529 PointerPhase::Down => RuntimePointerPhase::Began,
530 PointerPhase::Move => RuntimePointerPhase::Moved,
531 PointerPhase::Up => RuntimePointerPhase::Ended,
532 PointerPhase::Cancel => RuntimePointerPhase::Cancelled,
533 }
534}
535
536pub fn apply(map: &mut InputMap, bindings: &Bindings, message: &InputMessage, consumed: bool) {
537 match message {
538 InputMessage::Resize { width, height, dpr } => map.resize(*width, *height, *dpr),
539 InputMessage::Viewport { width, height } => map.set_display(*width, *height),
540 InputMessage::MouseMove {
541 x,
542 y,
543 buttons,
544 dx,
545 dy,
546 time,
547 } => {
548 map.sync_clock(time.as_secs_f64());
549 if consumed {
550 return;
551 }
552 if map.cursor_locked() {
553 map.raw_motion(*dx * map.scale(), *dy * map.scale());
554 } else {
555 let ndc = map.css_ndc(*x, *y);
556 map.sync_mouse_buttons(*buttons);
557 map.moved_at(ndc, time.as_secs_f64());
558 }
559 }
560 InputMessage::MouseButton {
561 x,
562 y,
563 button,
564 pressed,
565 time,
566 } => {
567 map.sync_clock(time.as_secs_f64());
568 let button = match button {
569 0 => Some(PointerButton::Primary),
570 1 => Some(PointerButton::Middle),
571 2 => Some(PointerButton::Secondary),
572 _ => None,
573 };
574 if let Some(button) = button {
575 let ndc = map.css_ndc(*x, *y);
576 let held = map.mouse_buttons[InputMap::button_index(button)];
577 if !consumed || (!pressed && held) {
578 map.button_at(ndc, button, *pressed, time.as_secs_f64());
579 }
580 }
581 }
582 InputMessage::Key { code, pressed, .. } => {
583 if let Some(key) = dom_key(code) {
584 map.host_action(bindings, key, *pressed, consumed);
585 }
586 }
587 InputMessage::Focus(false) => map.release_all(),
588 InputMessage::Pointer {
589 id,
590 x,
591 y,
592 phase,
593 time,
594 } => {
595 map.sync_clock(time.as_secs_f64());
596 let ndc = map.css_ndc(*x, *y);
597 map.host_touch_at(
598 *id as u32,
599 ndc,
600 dom_phase(*phase),
601 consumed,
602 time.as_secs_f64(),
603 );
604 }
605 InputMessage::MouseWheel { dx, dy } if !consumed => map.wheel([-*dx, -*dy]),
606 InputMessage::MouseWheel { .. } => {}
607 InputMessage::Host { topic, values } => map.host_message(topic, values),
608 InputMessage::Focus(true)
609 | InputMessage::Visibility(_)
610 | InputMessage::Start
611 | InputMessage::PointerLockChanged { .. } => {}
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use loam_runtime::{ActionId, Ctx, Phase, Session, SimConfig};
618
619 use super::*;
620
621 #[global_allocator]
622 static COUNTING_ALLOCATOR: loam_time::alloc::CountingAllocator<std::alloc::System> =
623 loam_time::alloc::CountingAllocator::new(std::alloc::System);
624
625 const WALK: ActionId = ActionId(0);
626
627 loam_runtime::stores! {
628 #[derive(Default)]
629 pub struct Watched {
630 walked: Value<bool>,
631 }
632 }
633
634 fn key_message(code: &str, pressed: bool) -> InputMessage {
635 InputMessage::Key {
636 code: code.to_string(),
637 key: code.to_lowercase(),
638 pressed,
639 repeat: false,
640 ctrl: false,
641 shift: false,
642 alt: false,
643 meta: false,
644 }
645 }
646
647 #[test]
648 fn a_browser_key_message_reaches_the_session_as_its_bound_action() {
649 let bindings = Bindings::new().key(Key::Letter('w'), WALK);
650 let mut session = Session::new(Watched::default(), SimConfig::default());
651 session.system(Phase::Dispatch, "watch", |ctx: Ctx<'_, Watched>| {
652 if ctx.input.pressed(WALK) {
653 ctx.app.walked.set(true);
654 }
655 Ok(())
656 });
657
658 let mut map = InputMap::default();
659 apply(
660 &mut map,
661 &bindings,
662 &InputMessage::Resize {
663 width: 800,
664 height: 600,
665 dpr: 2.0,
666 },
667 false,
668 );
669 apply(&mut map, &bindings, &key_message("KeyW", true), false);
670 session.boundary(map.take()).expect("boundary");
671
672 assert!(
673 *session.app.walked.get(),
674 "the DOM key code never became the action bound to it"
675 );
676 }
677
678 #[test]
679 fn the_boundary_stamp_follows_the_pointer_clock_of_host_messages() {
680 let bindings = Bindings::new();
681 let mut map = InputMap::default();
682 map.resize(800, 600, 1.0);
683 apply(
684 &mut map,
685 &bindings,
686 &InputMessage::MouseMove {
687 x: 10.0,
688 y: 10.0,
689 buttons: 0,
690 dx: 0.0,
691 dy: 0.0,
692 time: std::time::Duration::from_secs(100),
693 },
694 false,
695 );
696 let input = map.take();
697 let pointer = input.pointers[0].time;
698 assert!(
699 pointer == 100.0 && input.time >= pointer && input.time < pointer + 1.0,
700 "the boundary was stamped {} on a clock where the pointer read {pointer}",
701 input.time
702 );
703 }
704
705 #[test]
706 fn a_warmed_frame_of_host_input_conversion_asks_the_allocator_for_nothing() {
707 let bindings = Bindings::new().key(Key::Letter('w'), WALK);
708 let mut session = Session::new(Watched::default(), SimConfig::default());
709 let mut map = InputMap::default();
710 map.resize(800, 600, 1.0);
711 let cycle = |map: &mut InputMap, session: &mut Session<Watched>| {
712 map.action(&bindings, Key::Letter('w'), true);
713 map.button([0.1, 0.2], PointerButton::Primary, true);
714 map.moved([0.2, 0.2]);
715 map.button([0.2, 0.2], PointerButton::Primary, false);
716 map.action(&bindings, Key::Letter('w'), false);
717 map.host_message("scroll", &[0.5]);
718 let input = map.take();
719 session.boundary(input).expect("boundary");
720 session.tick().expect("tick");
721 map.reclaim(session.take_input());
722 };
723 for _ in 0..8 {
724 cycle(&mut map, &mut session);
725 }
726
727 let bytes = loam_time::alloc::bytes_allocated_by(|| {
728 for _ in 0..16 {
729 cycle(&mut map, &mut session);
730 }
731 })
732 .expect("the counting allocator is installed");
733 assert_eq!(
734 bytes, 0,
735 "16 warmed frames of input asked the allocator for {bytes} bytes"
736 );
737 }
738
739 #[test]
740 fn a_reclaimed_input_carries_no_host_messages_from_the_previous_boundary() {
741 let mut map = InputMap::default();
742 map.host_message("reset", &[]);
743 let first = map.take();
744 assert_eq!(first.host.iter().count(), 1);
745
746 map.reclaim(first);
747 let second = map.take();
748 map.reclaim(second);
749 let reused = map.take();
750 assert!(reused.host.iter().next().is_none());
751 }
752
753 #[test]
754 fn pointers_map_to_the_shown_canvas_while_its_physical_size_lags() {
755 let mut map = InputMap::default();
756 map.resize(1600, 900, 1.0);
757 map.set_display(2560.0, 900.0);
758 assert_eq!(map.css_ndc(2560.0, 450.0), [1.0, 0.0]);
759 assert_eq!(map.css_ndc(640.0, 0.0), [-0.5, 1.0]);
760 assert_eq!(map.display_aspect(), Some(2560.0 / 900.0));
761 }
762
763 #[test]
764 fn camera_turns_do_not_depend_on_viewport_shape_or_pixel_scale() {
765 for (width, height, scale) in [(640, 480, 1.0), (1280, 480, 1.0), (1280, 960, 2.0)] {
766 let mut map = InputMap::default();
767 map.resize(width, height, scale);
768 map.button(map.css_ndc(50.0, 50.0), PointerButton::Secondary, true);
769 map.moved(map.css_ndc(70.0, 70.0));
770 let input = map.take();
771 let mut orbit = crate::session::camera::Orbit::around([0.0; 3], 5.0);
772 orbit.drag(input.drag(PointerButton::Secondary));
773 assert!((orbit.yaw + 0.12).abs() < 1e-6);
774 assert!((orbit.pitch + 0.12).abs() < 1e-6);
775
776 map.set_cursor_locked(true);
777 map.raw_motion(20.0 * scale, 20.0 * scale);
778 let mut camera = crate::session::camera::FreeCamera::default();
779 camera.look(map.take().look);
780 let [x, y, z] = camera.eye.forward;
781 assert!(((-x).atan2(-z) + 0.04).abs() < 1e-6);
782 assert!((y.asin() + 0.04).abs() < 1e-6);
783 }
784 }
785
786 #[test]
787 fn raw_look_requires_a_confirmed_lock_and_survives_a_leased_input() {
788 let mut map = InputMap::default();
789 map.resize(800, 400, 2.0);
790 map.raw_motion(20.0, 10.0);
791 map.set_cursor_locked(true);
792 map.raw_motion(20.0, 10.0);
793
794 let first = map.take();
795 assert_eq!(first.look, [10.0, -5.0]);
796 assert!(first.cursor_locked);
797
798 map.raw_motion(40.0, -20.0);
799 map.reclaim(first);
800 let second = map.take();
801 assert_eq!(second.look, [20.0, 10.0]);
802 }
803
804 #[test]
805 fn a_consumed_release_clears_the_action_held_before_the_overlay_took_focus() {
806 let bindings = Bindings::new().key(Key::Letter('w'), WALK);
807 let mut map = InputMap::default();
808 map.host_action(&bindings, Key::Letter('w'), true, false);
809 let first = map.take();
810
811 map.host_action(&bindings, Key::Letter('w'), false, true);
812 map.reclaim(first);
813 let second = map.take();
814
815 assert!(second
816 .actions
817 .iter()
818 .any(|event| event.action == WALK && !event.pressed));
819 assert!(!second.held.contains(&WALK));
820 }
821
822 #[test]
823 fn consumed_pointer_releases_clear_scene_holds() {
824 let mut map = InputMap::default();
825 map.host_button([0.0; 2], PointerButton::Primary, true, false);
826 let first = map.take();
827 map.reclaim(first);
828
829 map.host_button([0.0; 2], PointerButton::Primary, false, true);
830 let released = map.take();
831
832 assert_eq!(released.pointers.len(), 1);
833 assert_eq!(released.pointers[0].phase, RuntimePointerPhase::Ended);
834 }
835
836 #[test]
837 fn browser_touch_capture_keeps_its_first_owner_and_source_times() {
838 let bindings = Bindings::new();
839 let mut map = InputMap::default();
840 map.resize(800, 600, 1.0);
841 let mut capture = TouchCapture::default();
842
843 for (message, over_ui) in [
844 (touch_message(7, PointerPhase::Down, 100, 40.0), true),
845 (touch_message(7, PointerPhase::Move, 120, 50.0), false),
846 (touch_message(7, PointerPhase::Up, 140, 60.0), false),
847 ] {
848 let InputMessage::Pointer {
849 id, x, y, phase, ..
850 } = &message
851 else {
852 unreachable!();
853 };
854 let consumed = capture.route(*id, *phase, over_ui, [*x, *y]);
855 apply(&mut map, &bindings, &message, consumed);
856 }
857 assert!(map.take().pointers.is_empty());
858
859 for (message, over_ui) in [
860 (touch_message(8, PointerPhase::Down, 200, 100.0), false),
861 (touch_message(8, PointerPhase::Move, 225, 110.0), true),
862 (touch_message(8, PointerPhase::Up, 250, 120.0), true),
863 ] {
864 let InputMessage::Pointer {
865 id, x, y, phase, ..
866 } = &message
867 else {
868 unreachable!();
869 };
870 let consumed = capture.route(*id, *phase, over_ui, [*x, *y]);
871 apply(&mut map, &bindings, &message, consumed);
872 }
873 let pointers = map.take().pointers;
874 assert_eq!(pointers.len(), 3);
875 assert_eq!(pointers[0].phase, RuntimePointerPhase::Began);
876 assert_eq!(pointers[1].phase, RuntimePointerPhase::Moved);
877 assert_eq!(pointers[2].phase, RuntimePointerPhase::Ended);
878 assert_eq!(
879 pointers
880 .iter()
881 .map(|pointer| pointer.time)
882 .collect::<Vec<_>>(),
883 [0.2, 0.225, 0.25]
884 );
885 }
886
887 fn touch_message(id: u64, phase: PointerPhase, millis: u64, x: f32) -> InputMessage {
888 InputMessage::Pointer {
889 id,
890 x,
891 y: 20.0,
892 phase,
893 time: std::time::Duration::from_millis(millis),
894 }
895 }
896}