use crate::transfer::{app_value_from, attachments_from, control_value_from};
use crate::wire::{
Attachments, FromWorker, SelectedEntity, ToWorker, ValueProtocol, WorkerProtocol,
};
use leptos::prelude::*;
use serde::Serialize;
use wasm_bindgen::{JsCast, JsValue};
use crate::web::viewport::{Bridge, Viewport, ViewportEvent};
#[derive(Clone, Copy)]
pub struct EngineState {
pub ready: RwSignal<bool>,
pub adapter: RwSignal<String>,
pub fps: RwSignal<f32>,
pub entity_count: RwSignal<u32>,
pub selected: RwSignal<Option<SelectedEntity>>,
pub grabbing: RwSignal<bool>,
pub surface_size: RwSignal<(f32, f32)>,
}
impl EngineState {
fn new() -> Self {
Self {
ready: RwSignal::new(false),
adapter: RwSignal::new(String::new()),
fps: RwSignal::new(0.0),
entity_count: RwSignal::new(0),
selected: RwSignal::new(None),
grabbing: RwSignal::new(false),
surface_size: RwSignal::new((0.0, 0.0)),
}
}
}
enum Queued<P: WorkerProtocol> {
Control(ToWorker),
App(P::Incoming, Attachments),
}
type AppHandler<P> = std::rc::Rc<dyn Fn(<P as WorkerProtocol>::Outgoing, Attachments)>;
pub struct Engine<P: WorkerProtocol + 'static = ValueProtocol> {
pub state: EngineState,
bridge: StoredValue<Option<Bridge>, LocalStorage>,
app_handler: StoredValue<Option<AppHandler<P>>, LocalStorage>,
queue: StoredValue<Vec<Queued<P>>, LocalStorage>,
worker_url: StoredValue<String>,
}
impl<P: WorkerProtocol + 'static> Clone for Engine<P> {
fn clone(&self) -> Self {
*self
}
}
impl<P: WorkerProtocol + 'static> Copy for Engine<P> {}
impl<P: WorkerProtocol + 'static> Engine<P> {
pub fn send_app(&self, message: P::Incoming) {
self.send_app_with_attachments(message, Vec::new());
}
pub fn send_app_with_attachments(&self, message: P::Incoming, attachments: Attachments) {
if let Some(bridge) = self.bridge.get_value() {
bridge.send_app(&message, &attachments);
} else {
self.queue
.update_value(|queue| queue.push(Queued::App(message, attachments)));
}
}
pub fn on_app_message(&self, handler: impl Fn(P::Outgoing, Attachments) + 'static) {
self.app_handler.set_value(Some(std::rc::Rc::new(handler)));
}
pub fn send_key(&self, code: String, pressed: bool, text: Option<String>) {
self.dispatch(ToWorker::Key {
code,
pressed,
text,
});
}
pub fn install_key_forwarding(&self) {
let engine = *self;
let _ = window_event_listener(leptos::ev::keydown, move |event| {
if event.ctrl_key() || event.meta_key() || typing_in_field(&event) {
return;
}
let text = (event.key().chars().count() == 1).then(|| event.key());
engine.send_key(event.code(), true, text);
});
let _ = window_event_listener(leptos::ev::keyup, move |event| {
if event.ctrl_key() || event.meta_key() || typing_in_field(&event) {
return;
}
engine.send_key(event.code(), false, None);
});
}
fn dispatch(&self, message: ToWorker) {
if let Some(bridge) = self.bridge.get_value() {
bridge.send(&message);
} else {
self.queue
.update_value(|queue| queue.push(Queued::Control(message)));
}
}
fn connect(&self, bridge: Bridge, canvas: &web_sys::OffscreenCanvas, width: f32, height: f32) {
self.state.surface_size.set((width, height));
bridge.send_with_canvas(&ToWorker::Init { width, height }, canvas);
let backlog = self
.queue
.try_update_value(std::mem::take)
.unwrap_or_default();
for queued in backlog {
match queued {
Queued::Control(message) => bridge.send(&message),
Queued::App(message, attachments) => bridge.send_app(&message, &attachments),
}
}
self.bridge.set_value(Some(bridge));
}
}
impl Engine<ValueProtocol> {
pub fn send<Message: Serialize>(&self, message: &Message) {
if let Ok(value) = serde_json::to_value(message) {
self.send_app(value);
}
}
pub fn on_custom(&self, handler: Callback<serde_json::Value>) {
self.on_app_message(move |value, _attachments| {
handler.run(value);
});
}
}
pub fn use_engine(worker_url: impl Into<String>) -> Engine<ValueProtocol> {
let engine = use_engine_typed(worker_url);
engine.install_key_forwarding();
engine
}
pub fn use_engine_typed<P: WorkerProtocol + 'static>(worker_url: impl Into<String>) -> Engine<P> {
Engine {
state: EngineState::new(),
bridge: StoredValue::new_local(None),
app_handler: StoredValue::new_local(None),
queue: StoredValue::new_local(Vec::new()),
worker_url: StoredValue::new(worker_url.into()),
}
}
#[component]
pub fn EngineViewport<P: WorkerProtocol + 'static>(engine: Engine<P>) -> impl IntoView {
let state = engine.state;
let app_handler = engine.app_handler;
let on_input = Callback::new(move |event: ViewportEvent| {
let message = match event {
ViewportEvent::Resize { width, height } => {
state.surface_size.set((width, height));
ToWorker::Resize { width, height }
}
ViewportEvent::PointerMove { x, y } => ToWorker::PointerMove { x, y },
ViewportEvent::PointerMotion { dx, dy } => ToWorker::PointerMotion { dx, dy },
ViewportEvent::PointerButton { button, pressed } => {
ToWorker::PointerButton { button, pressed }
}
ViewportEvent::Wheel { delta } => ToWorker::Wheel { delta },
ViewportEvent::Touch { id, phase, x, y } => ToWorker::Touch { id, phase, x, y },
ViewportEvent::Pick { x, y } => ToWorker::Pick { x, y },
};
engine.dispatch(message);
});
let on_connect = Callback::new(
move |(connected, canvas, width, height): (Bridge, web_sys::OffscreenCanvas, f32, f32)| {
engine.connect(connected, &canvas, width, height);
},
);
let on_message = Callback::new(move |data: JsValue| {
if let Some(payload) = control_value_from(&data) {
let Ok(message) = serde_wasm_bindgen::from_value::<FromWorker>(payload) else {
return;
};
match message {
FromWorker::Ready { adapter } => {
state.adapter.set(adapter);
state.ready.set(true);
}
FromWorker::Stats { fps, entity_count } => {
state.fps.set(fps);
state.entity_count.set(entity_count);
}
FromWorker::Selected { detail } => state.selected.set(detail),
}
return;
}
if let Some(payload) = app_value_from(&data)
&& let Ok(message) = serde_wasm_bindgen::from_value::<P::Outgoing>(payload)
&& let Some(handler) = app_handler.get_value()
{
handler(message, attachments_from(&data));
}
});
view! {
<Viewport
worker_url=engine.worker_url.get_value()
grabbing=state.grabbing
on_input=on_input
on_connect=on_connect
on_message=on_message
/>
}
}
fn typing_in_field(event: &web_sys::KeyboardEvent) -> bool {
event
.target()
.and_then(|target| target.dyn_into::<web_sys::HtmlElement>().ok())
.map(|element| {
let tag = element.tag_name();
tag.eq_ignore_ascii_case("input")
|| tag.eq_ignore_ascii_case("textarea")
|| tag.eq_ignore_ascii_case("select")
|| element.is_content_editable()
})
.unwrap_or(false)
}