bevy_react/event.rs
1//! Bevy → React named events: an app-level signal pushed from the ECS to the
2//! listeners the React app registers with `bevy.on(name, cb)`.
3//!
4//! Unlike a [`UiEvent`](crate::protocol::UiEvent) (a click on a specific node),
5//! a [`ReactEvent`] is a named, typed broadcast. Send one from any system with
6//! the [`ReactEvents`] param:
7//!
8//! ```ignore
9//! use bevy::prelude::*;
10//! use bevy_react::{react_event, ReactEvents};
11//!
12//! #[react_event(name = "user.disconnected")]
13//! struct UserDisconnected { user_id: String }
14//!
15//! fn on_drop(mut events: ReactEvents) {
16//! events.send(&UserDisconnected { user_id: "abc".into() });
17//! }
18//! ```
19
20use std::any::TypeId;
21use std::collections::HashMap;
22
23use bevy::ecs::system::SystemParam;
24use bevy::prelude::*;
25use serde::Serialize;
26use ts_rs::TS;
27
28use crate::bridge::OutboundResource;
29use crate::protocol::Outbound;
30use crate::registry::{NamedEntry, register_entry};
31use crate::ts_codegen::TsCollector;
32
33/// A typed payload Bevy sends to React as a named event. Out-only — it is never
34/// deserialized on the Rust side, so it derives `Serialize` (not `Deserialize`).
35///
36/// Usually derived with [`#[react_event]`](crate::react_event).
37pub trait ReactEvent: Serialize + TS + Send + Sync + 'static {
38 /// The event name React listens for, e.g. `"user.disconnected"`. Defaults to
39 /// the struct ident with its first letter lowercased.
40 const NAME: &'static str;
41}
42
43/// System param for sending [`ReactEvent`]s to the React app.
44#[derive(SystemParam)]
45pub struct ReactEvents<'w> {
46 out: Res<'w, OutboundResource>,
47}
48
49impl ReactEvents<'_> {
50 /// Push `event` to every React listener registered for `E::NAME`.
51 // TODO(review): `send` works whether or not `E` was registered via `add_react_event`, but
52 // the generated TS typings only include REGISTERED events — so you can ship an event that
53 // never appears in `bevy.on`'s types (silent drift, in the untyped-works direction).
54 // Consider a debug-only warning when sending an unregistered event.
55 pub fn send<E: ReactEvent>(&self, event: &E) {
56 match serde_json::to_value(event) {
57 Ok(value) => {
58 let _ = self.out.0.send(Outbound::Event {
59 name: E::NAME.to_string(),
60 value,
61 });
62 }
63 Err(e) => error!("serialize react event {:?}: {e}", E::NAME),
64 }
65 }
66}
67
68/// TypeScript metadata for one registered event type (export-only).
69pub(crate) struct EventRegistration {
70 type_id: TypeId,
71 /// The event payload's TypeScript reference name.
72 pub(crate) ts_name: fn() -> String,
73 /// Collects the payload's type declaration (and its dependencies).
74 pub(crate) ts_collect: fn(&mut TsCollector),
75}
76
77/// Known Bevy → React event types, keyed by name. Used only by the TypeScript
78/// exporter — sending an event does not require registration, but registering
79/// makes it appear in the generated `bevy.on` typing.
80#[derive(Resource, Default)]
81pub(crate) struct ReactEventRegistry {
82 pub(crate) handlers: HashMap<&'static str, EventRegistration>,
83}
84
85impl NamedEntry for EventRegistration {
86 fn type_id(&self) -> TypeId {
87 self.type_id
88 }
89}
90
91impl ReactEventRegistry {
92 /// Record event type `E` for export. Idempotent per type; warns only if a
93 /// different type already owns `E::NAME`.
94 pub(crate) fn register<E: ReactEvent>(&mut self) {
95 register_entry(
96 &mut self.handlers,
97 E::NAME,
98 "event",
99 EventRegistration {
100 type_id: TypeId::of::<E>(),
101 ts_name: <E as TS>::name,
102 ts_collect: |c| c.add::<E>(),
103 },
104 );
105 }
106}