1use crate::transfer::{app_value_from, attachments_from, control_value_from};
8use crate::wire::{
9 Attachments, FromWorker, SelectedEntity, ToWorker, ValueProtocol, WorkerProtocol,
10};
11use leptos::prelude::*;
12use serde::Serialize;
13use wasm_bindgen::{JsCast, JsValue};
14
15use crate::web::viewport::{Bridge, Viewport, ViewportEvent};
16
17#[derive(Clone, Copy)]
20pub struct EngineState {
21 pub ready: RwSignal<bool>,
23 pub adapter: RwSignal<String>,
25 pub fps: RwSignal<f32>,
27 pub entity_count: RwSignal<u32>,
29 pub selected: RwSignal<Option<SelectedEntity>>,
31 pub grabbing: RwSignal<bool>,
33 pub surface_size: RwSignal<(f32, f32)>,
36}
37
38impl EngineState {
39 fn new() -> Self {
40 Self {
41 ready: RwSignal::new(false),
42 adapter: RwSignal::new(String::new()),
43 fps: RwSignal::new(0.0),
44 entity_count: RwSignal::new(0),
45 selected: RwSignal::new(None),
46 grabbing: RwSignal::new(false),
47 surface_size: RwSignal::new((0.0, 0.0)),
48 }
49 }
50}
51
52enum Queued<P: WorkerProtocol> {
53 Control(ToWorker),
54 App(P::Incoming, Attachments),
55}
56
57type AppHandler<P> = std::rc::Rc<dyn Fn(<P as WorkerProtocol>::Outgoing, Attachments)>;
58
59pub struct Engine<P: WorkerProtocol + 'static = ValueProtocol> {
63 pub state: EngineState,
65 bridge: StoredValue<Option<Bridge>, LocalStorage>,
66 app_handler: StoredValue<Option<AppHandler<P>>, LocalStorage>,
67 queue: StoredValue<Vec<Queued<P>>, LocalStorage>,
68 worker_url: StoredValue<String>,
69}
70
71impl<P: WorkerProtocol + 'static> Clone for Engine<P> {
72 fn clone(&self) -> Self {
73 *self
74 }
75}
76
77impl<P: WorkerProtocol + 'static> Copy for Engine<P> {}
78
79impl<P: WorkerProtocol + 'static> Engine<P> {
80 pub fn send_app(&self, message: P::Incoming) {
83 self.send_app_with_attachments(message, Vec::new());
84 }
85
86 pub fn send_app_with_attachments(&self, message: P::Incoming, attachments: Attachments) {
89 if let Some(bridge) = self.bridge.get_value() {
90 bridge.send_app(&message, &attachments);
91 } else {
92 self.queue
93 .update_value(|queue| queue.push(Queued::App(message, attachments)));
94 }
95 }
96
97 pub fn on_app_message(&self, handler: impl Fn(P::Outgoing, Attachments) + 'static) {
100 self.app_handler.set_value(Some(std::rc::Rc::new(handler)));
101 }
102
103 pub fn send_key(&self, code: String, pressed: bool, text: Option<String>) {
106 self.dispatch(ToWorker::Key {
107 code,
108 pressed,
109 text,
110 });
111 }
112
113 pub fn install_key_forwarding(&self) {
117 let engine = *self;
118 let _ = window_event_listener(leptos::ev::keydown, move |event| {
119 if event.ctrl_key() || event.meta_key() || typing_in_field(&event) {
120 return;
121 }
122 let text = (event.key().chars().count() == 1).then(|| event.key());
123 engine.send_key(event.code(), true, text);
124 });
125 let _ = window_event_listener(leptos::ev::keyup, move |event| {
126 if event.ctrl_key() || event.meta_key() || typing_in_field(&event) {
127 return;
128 }
129 engine.send_key(event.code(), false, None);
130 });
131 }
132
133 fn dispatch(&self, message: ToWorker) {
134 if let Some(bridge) = self.bridge.get_value() {
135 bridge.send(&message);
136 } else {
137 self.queue
138 .update_value(|queue| queue.push(Queued::Control(message)));
139 }
140 }
141
142 fn connect(&self, bridge: Bridge, canvas: &web_sys::OffscreenCanvas, width: f32, height: f32) {
143 self.state.surface_size.set((width, height));
144 bridge.send_with_canvas(&ToWorker::Init { width, height }, canvas);
145 let backlog = self
146 .queue
147 .try_update_value(std::mem::take)
148 .unwrap_or_default();
149 for queued in backlog {
150 match queued {
151 Queued::Control(message) => bridge.send(&message),
152 Queued::App(message, attachments) => bridge.send_app(&message, &attachments),
153 }
154 }
155 self.bridge.set_value(Some(bridge));
156 }
157}
158
159impl Engine<ValueProtocol> {
160 pub fn send<Message: Serialize>(&self, message: &Message) {
163 if let Ok(value) = serde_json::to_value(message) {
164 self.send_app(value);
165 }
166 }
167
168 pub fn on_custom(&self, handler: Callback<serde_json::Value>) {
171 self.on_app_message(move |value, _attachments| {
172 handler.run(value);
173 });
174 }
175}
176
177pub fn use_engine(worker_url: impl Into<String>) -> Engine<ValueProtocol> {
181 let engine = use_engine_typed(worker_url);
182 engine.install_key_forwarding();
183 engine
184}
185
186pub fn use_engine_typed<P: WorkerProtocol + 'static>(worker_url: impl Into<String>) -> Engine<P> {
192 Engine {
193 state: EngineState::new(),
194 bridge: StoredValue::new_local(None),
195 app_handler: StoredValue::new_local(None),
196 queue: StoredValue::new_local(Vec::new()),
197 worker_url: StoredValue::new(worker_url.into()),
198 }
199}
200
201#[component]
205pub fn EngineViewport<P: WorkerProtocol + 'static>(engine: Engine<P>) -> impl IntoView {
206 let state = engine.state;
207 let app_handler = engine.app_handler;
208
209 let on_input = Callback::new(move |event: ViewportEvent| {
210 let message = match event {
211 ViewportEvent::Resize { width, height } => {
212 state.surface_size.set((width, height));
213 ToWorker::Resize { width, height }
214 }
215 ViewportEvent::PointerMove { x, y } => ToWorker::PointerMove { x, y },
216 ViewportEvent::PointerMotion { dx, dy } => ToWorker::PointerMotion { dx, dy },
217 ViewportEvent::PointerButton { button, pressed } => {
218 ToWorker::PointerButton { button, pressed }
219 }
220 ViewportEvent::Wheel { delta } => ToWorker::Wheel { delta },
221 ViewportEvent::Touch { id, phase, x, y } => ToWorker::Touch { id, phase, x, y },
222 ViewportEvent::Pick { x, y } => ToWorker::Pick { x, y },
223 };
224 engine.dispatch(message);
225 });
226
227 let on_connect = Callback::new(
228 move |(connected, canvas, width, height): (Bridge, web_sys::OffscreenCanvas, f32, f32)| {
229 engine.connect(connected, &canvas, width, height);
230 },
231 );
232
233 let on_message = Callback::new(move |data: JsValue| {
234 if let Some(payload) = control_value_from(&data) {
235 let Ok(message) = serde_wasm_bindgen::from_value::<FromWorker>(payload) else {
236 return;
237 };
238 match message {
239 FromWorker::Ready { adapter } => {
240 state.adapter.set(adapter);
241 state.ready.set(true);
242 }
243 FromWorker::Stats { fps, entity_count } => {
244 state.fps.set(fps);
245 state.entity_count.set(entity_count);
246 }
247 FromWorker::Selected { detail } => state.selected.set(detail),
248 }
249 return;
250 }
251 if let Some(payload) = app_value_from(&data)
252 && let Ok(message) = serde_wasm_bindgen::from_value::<P::Outgoing>(payload)
253 && let Some(handler) = app_handler.get_value()
254 {
255 handler(message, attachments_from(&data));
256 }
257 });
258
259 view! {
260 <Viewport
261 worker_url=engine.worker_url.get_value()
262 grabbing=state.grabbing
263 on_input=on_input
264 on_connect=on_connect
265 on_message=on_message
266 />
267 }
268}
269
270fn typing_in_field(event: &web_sys::KeyboardEvent) -> bool {
271 event
272 .target()
273 .and_then(|target| target.dyn_into::<web_sys::HtmlElement>().ok())
274 .map(|element| {
275 let tag = element.tag_name();
276 tag.eq_ignore_ascii_case("input")
277 || tag.eq_ignore_ascii_case("textarea")
278 || tag.eq_ignore_ascii_case("select")
279 || element.is_content_editable()
280 })
281 .unwrap_or(false)
282}