Skip to main content

nightshade_api/
wire.rs

1//! Leptos-free wire types spoken between a page and its render worker,
2//! compiled under either the `leptos` or the `offscreen` feature. Pixel
3//! quantities are physical surface pixels (CSS pixels times the device pixel
4//! ratio), origin at the canvas top-left.
5//!
6//! [`ToWorker`] and [`FromWorker`] are the control plane the host owns:
7//! init, resize, input, picking, and status. Application traffic rides a
8//! separate envelope field as the message pair a [`WorkerProtocol`] impl
9//! declares, with optional binary [`Attachments`] transferred alongside.
10
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14/// Envelope field carrying the serialized control message in every
15/// `postMessage`.
16pub const MESSAGE_KEY: &str = "message";
17/// Envelope field carrying the transferred `OffscreenCanvas` (on `Init` only).
18pub const CANVAS_KEY: &str = "canvas";
19/// Envelope field carrying a serialized application message: the
20/// [`WorkerProtocol::Incoming`] type page to worker, the
21/// [`WorkerProtocol::Outgoing`] type worker to page.
22pub const APP_KEY: &str = "app";
23/// Envelope field carrying named binary payloads as a `{ name: Uint8Array }`
24/// object whose buffers ride the transfer list.
25pub const ATTACHMENTS_KEY: &str = "attachments";
26
27/// Named binary payloads accompanying an application message: dropped asset
28/// bytes, a glTF with its resources, a saved file.
29pub type Attachments = Vec<(String, Vec<u8>)>;
30
31/// The application message pair a page and its worker speak over the
32/// [`APP_KEY`] envelope field, from the worker's point of view:
33/// `Incoming` arrives from the page, `Outgoing` returns to it. The control
34/// plane (init, resize, input, picking, status) is not part of the
35/// protocol; the host owns it. Both types serialize and deserialize
36/// because each crosses the wire in one direction and is decoded on the
37/// other side.
38pub trait WorkerProtocol {
39    type Incoming: Serialize + serde::de::DeserializeOwned + 'static;
40    type Outgoing: Serialize + serde::de::DeserializeOwned + Send + Sync + 'static;
41}
42
43/// The untyped protocol: JSON values both ways. What `run_offscreen` and
44/// the default `use_engine` speak, so a small app never declares message
45/// enums.
46pub struct ValueProtocol;
47
48impl WorkerProtocol for ValueProtocol {
49    type Incoming = Value;
50    type Outgoing = Value;
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
54pub enum TouchPhase {
55    Started,
56    Moved,
57    Ended,
58    Cancelled,
59}
60
61/// The selected entity as reported to the page: display data only. The
62/// worker keeps the live selection and hands it to the custom message
63/// handler; nothing here resolves back into an entity.
64#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
65pub struct SelectedEntity {
66    /// Raw entity index without its generation, for display.
67    pub id: u32,
68    /// The entity name, empty when unnamed.
69    pub name: String,
70}
71
72#[derive(Clone, Debug, Serialize, Deserialize)]
73pub enum ToWorker {
74    Init {
75        width: f32,
76        height: f32,
77    },
78    Resize {
79        width: f32,
80        height: f32,
81    },
82    PointerMove {
83        x: f32,
84        y: f32,
85    },
86    /// Raw relative motion for first-person look while the pointer is
87    /// locked, in physical pixels.
88    PointerMotion {
89        dx: f32,
90        dy: f32,
91    },
92    PointerButton {
93        button: u8,
94        pressed: bool,
95    },
96    Wheel {
97        delta: f32,
98    },
99    Touch {
100        id: u64,
101        phase: TouchPhase,
102        x: f32,
103        y: f32,
104    },
105    Key {
106        code: String,
107        pressed: bool,
108        text: Option<String>,
109    },
110    Pick {
111        x: f32,
112        y: f32,
113    },
114}
115
116#[derive(Clone, Debug, Serialize, Deserialize)]
117pub enum FromWorker {
118    Ready { adapter: String },
119    Stats { fps: f32, entity_count: u32 },
120    Selected { detail: Option<SelectedEntity> },
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    fn to_worker_roundtrips(message: ToWorker) {
128        let value = serde_json::to_value(&message).expect("serialize");
129        let back: ToWorker = serde_json::from_value(value).expect("deserialize");
130        assert_eq!(format!("{message:?}"), format!("{back:?}"));
131    }
132
133    #[test]
134    fn to_worker_survives_the_wire() {
135        to_worker_roundtrips(ToWorker::Init {
136            width: 800.0,
137            height: 600.0,
138        });
139        to_worker_roundtrips(ToWorker::PointerButton {
140            button: 2,
141            pressed: true,
142        });
143        to_worker_roundtrips(ToWorker::PointerMotion { dx: -3.5, dy: 1.0 });
144        to_worker_roundtrips(ToWorker::Touch {
145            id: 7,
146            phase: TouchPhase::Moved,
147            x: 1.0,
148            y: 2.0,
149        });
150        to_worker_roundtrips(ToWorker::Key {
151            code: "KeyW".to_string(),
152            pressed: true,
153            text: Some("w".to_string()),
154        });
155    }
156
157    #[test]
158    fn from_worker_survives_the_wire() {
159        let message = FromWorker::Selected {
160            detail: Some(SelectedEntity {
161                id: 3,
162                name: "Cube 3".to_string(),
163            }),
164        };
165        let value = serde_json::to_value(&message).expect("serialize");
166        let back: FromWorker = serde_json::from_value(value).expect("deserialize");
167        assert_eq!(format!("{message:?}"), format!("{back:?}"));
168    }
169}