Skip to main content

lumen_scene/
script_host.rs

1//! The per-host half of the script wiring, installed once for each active
2//! [`ScriptHost`].
3//!
4//! A host's own plugin ([`lumen_script::ScriptPlugin`]) installs the systems
5//! that drive it every tick. What it leaves to the scene is what needs the
6//! scene's tree: `on_ready` once the document is queryable, the component use
7//! sites the build left for the script, delivering DOM events into the host,
8//! and, in an app with more than one language, the late mirror sync that
9//! keeps one host's signals current with another's writes.
10//!
11//! Every system here joins a [`ScriptSet`], so the host-neutral edges the
12//! assembly installs cover it without naming its concrete type. The same call
13//! serves a desktop window, a page, and a server render.
14
15use bevy_ecs::component::Mutable;
16use bevy_ecs::prelude::*;
17use lumen_core::prelude::{App, TickStage};
18use lumen_script::{ScriptHost, ScriptSet};
19
20use crate::dom::build_dom_index;
21use crate::script_commands::apply_scene_script_commands;
22
23/// The systems that publish per-node detail (text, attributes, inline style,
24/// the cascade inputs) for a script's reads. A host's lifecycle dispatches run
25/// after them, so `on_ready` reads the tree it mounted into. An assembly with
26/// no such publisher leaves the set empty and the edge inert.
27#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
28pub struct PublishNodeDetails;
29
30/// Install the per-host half of the script wiring for host `H`.
31///
32/// `multi_host` is true when the app runs more than one script language.
33pub fn install<H: ScriptHost + Resource<Mutability = Mutable>>(app: &mut App, multi_host: bool) {
34    // Cross-host signal reads. A host keeps its own mirror current as its
35    // builtins run, so with one host the early `ScriptSet::SyncSignals` pass
36    // is all that is needed and this stays unregistered. With two, a signal
37    // written in one language reaches `PropertyStore` only when
38    // `apply_scene_script_commands` runs, and its dirty flag is cleared at
39    // end of tick - so the other host's mirror must be refreshed here, inside
40    // that one-tick window, or the write is invisible to it forever.
41    if multi_host {
42        app.add_systems(
43            TickStage::Systems,
44            lumen_script::sync_signals_into_host::<H>
45                .in_set(ScriptSet::SyncSignalsLate)
46                .after(apply_scene_script_commands)
47                .before(ScriptSet::Derivations),
48        );
49    }
50    // Post-mount lifecycle: dispatch `on_ready` once per host, after the first
51    // `build_dom_index` publish so a DOM query inside it sees the mounted
52    // static tree, and before `collect_dom_commands` so any tree the handler
53    // builds is materialized on the same first tick. A missing `on_ready` is a
54    // no-op, so `on_start`-only apps are unaffected.
55    //
56    // `.after(ScriptSet::SyncSignals)`: both write the host's signal mirror on
57    // the tick where a value is still dirty, and the sync rewrites entries from
58    // the store. Unordered, the sync can run after the dispatch and overwrite
59    // the values `on_ready` just wrote with the pre-dispatch store state,
60    // leaving the mirror stale for every later handler read.
61    app.add_systems(
62        TickStage::Systems,
63        lumen_script::fire_on_ready::<H>
64            .in_set(ScriptSet::Ready)
65            .after(build_dom_index)
66            .after(PublishNodeDetails)
67            .after(ScriptSet::SyncSignals),
68    );
69    // The use sites the build left for the script to fill, on the same terms
70    // as `on_ready`: after the tree is queryable, before the command collector,
71    // so what the call builds lands on the tick the call ran. Every tick, not
72    // once - a subtree spawned while the app runs can carry a marker too.
73    app.add_systems(
74        TickStage::Systems,
75        lumen_script::fill_components::<H>
76            .in_set(ScriptSet::Fill)
77            .after(ScriptSet::Ready)
78            .after(build_dom_index)
79            .after(PublishNodeDetails)
80            .after(ScriptSet::SyncSignals),
81    );
82    // The handlers the script binds with `on(type, handler)`: the one DOM
83    // event dispatch reaches this host through its entry here.
84    lumen_script::register_event_host::<H>(&mut app.world);
85}