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}