Skip to main content

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}