Skip to main content

loam_app/wasm/
input_queue.rs

1//! Input transitions preserve arrival order; overflow releases held input and keeps the latest control state.
2//! A pointer move replaces its pointer's newest queued move unless a transition sits between them.
3
4use std::cell::RefCell;
5use std::collections::VecDeque;
6use std::time::Duration;
7
8pub const SCROLL_PIXELS_PER_LINE: f32 = 50.0;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum PointerPhase {
12    Down,
13    Move,
14    Up,
15    Cancel,
16}
17
18#[derive(Debug)]
19pub enum InputMessage {
20    /// Physical canvas size and the DPR measured with it.
21    Resize {
22        width: u32,
23        height: u32,
24        dpr: f32,
25    },
26
27    /// Canvas CSS size, sent on every frame it changes; `Resize` follows once it settles.
28    Viewport {
29        width: f32,
30        height: f32,
31    },
32
33    /// Canvas-local CSS position and accumulated DOM `movementX/Y` deltas.
34    MouseMove {
35        x: f32,
36        y: f32,
37        buttons: u8,
38        dx: f32,
39        dy: f32,
40        time: Duration,
41    },
42
43    /// `button` is `MouseEvent.button` (0=primary, 1=middle, 2=secondary).
44    MouseButton {
45        x: f32,
46        y: f32,
47        button: u8,
48        pressed: bool,
49        time: Duration,
50    },
51
52    /// Lines in DOM convention: positive is right/down.
53    MouseWheel {
54        dx: f32,
55        dy: f32,
56    },
57
58    /// DOM `code` identifies the physical key; `key` carries logical text.
59    Key {
60        code: String,
61        key: String,
62        pressed: bool,
63        repeat: bool,
64        ctrl: bool,
65        shift: bool,
66        alt: bool,
67        meta: bool,
68    },
69
70    Focus(bool),
71
72    Visibility(bool),
73
74    Start,
75
76    /// Browser-confirmed lock state, including releases by Esc or focus loss.
77    PointerLockChanged {
78        locked: bool,
79        released: bool,
80    },
81
82    /// Canvas-local CSS position and the DOM `timeStamp`.
83    Pointer {
84        id: u64,
85        x: f32,
86        y: f32,
87        phase: PointerPhase,
88        time: Duration,
89    },
90
91    /// A page `loam-message` event, delivered whether or not the embed is paused.
92    Host {
93        topic: String,
94        values: Vec<f32>,
95    },
96}
97
98pub const MESSAGE_QUEUE_CAPACITY: usize = 256;
99
100thread_local! {
101    static MESSAGE_QUEUE: RefCell<VecDeque<InputMessage>> = const { RefCell::new(VecDeque::new()) };
102}
103
104fn control_kind(msg: &InputMessage) -> Option<usize> {
105    match msg {
106        InputMessage::Resize { .. } => Some(0),
107        InputMessage::Visibility(_) => Some(1),
108        InputMessage::Start => Some(2),
109        InputMessage::PointerLockChanged { .. } => Some(3),
110        InputMessage::Focus(_) => Some(4),
111        InputMessage::Viewport { .. } => Some(5),
112        _ => None,
113    }
114}
115
116fn coalesced_move(q: &VecDeque<InputMessage>, msg: &InputMessage) -> Option<usize> {
117    let InputMessage::Pointer {
118        id,
119        phase: PointerPhase::Move,
120        ..
121    } = msg
122    else {
123        return None;
124    };
125    q.iter()
126        .enumerate()
127        .rev()
128        .map_while(|(index, queued)| match queued {
129            InputMessage::Pointer {
130                id: queued_id,
131                phase: PointerPhase::Move,
132                ..
133            } => Some((index, queued_id == id)),
134            _ => None,
135        })
136        .find(|(_, same_pointer)| *same_pointer)
137        .map(|(index, _)| index)
138}
139
140pub fn enqueue(msg: InputMessage) {
141    MESSAGE_QUEUE.with(|q| {
142        let mut q = q.borrow_mut();
143        let msg = match msg {
144            InputMessage::Host { topic, values } if !values.is_empty() => {
145                let earlier = q
146                    .iter_mut()
147                    .rev()
148                    .filter_map(|queued| match queued {
149                        InputMessage::Host { topic, values } => Some((topic, values)),
150                        _ => None,
151                    })
152                    .take_while(|(_, queued_values)| !queued_values.is_empty())
153                    .find(|(queued_topic, _)| **queued_topic == topic)
154                    .map(|(_, queued_values)| queued_values);
155                if let Some(earlier) = earlier {
156                    *earlier = values;
157                    return;
158                }
159                InputMessage::Host { topic, values }
160            }
161            msg => msg,
162        };
163        if let Some(index) = coalesced_move(&q, &msg) {
164            q.remove(index);
165        }
166        if q.len() >= MESSAGE_QUEUE_CAPACITY {
167            let mut latest = [None; 6];
168            for (index, queued) in q.iter().enumerate() {
169                if let Some(kind) = control_kind(queued) {
170                    latest[kind] = Some(index);
171                }
172            }
173            let mut index = 0;
174            q.retain(|queued| {
175                let keep = control_kind(queued).is_some_and(|kind| latest[kind] == Some(index));
176                index += 1;
177                keep
178            });
179            q.push_front(InputMessage::Focus(false));
180            tracing::warn!("input queue overflow: released held input and discarded stale events");
181        }
182        q.push_back(msg);
183    });
184}
185
186pub fn drain_messages_into(batch: &mut VecDeque<InputMessage>) {
187    batch.clear();
188    MESSAGE_QUEUE.with(|q| std::mem::swap(&mut *q.borrow_mut(), batch));
189}