lumen_runtime/run/app_build.rs
1use super::*;
2use crate::app_layout::AppLayout;
3use lumen_core::window::{Menu, MenuEntry, MenuModel, WindowGeometry};
4use lumen_ir::layout_ir::MenuEntrySpec;
5
6use lumen_capability::Phase;
7use lumen_modules::language::{LanguageTable, ScriptGrouping};
8use lumen_script::{ScriptFnAppExt, ScriptLanguages, ScriptProgram};
9
10/// Construct the fully-configured [`App`] and the [`WindowSetup`] the
11/// windowed path would run it with - everything [`run_app`] does short
12/// of entering the event loop. Split out so [`run_app_headless`] can
13/// reuse the identical build without duplicating the plugin / system
14/// wiring.
15pub fn build_app(mut opts: RunOptions) -> Result<(App, WindowSetup), RunError> {
16 // Host-neutral native functions (the C-ABI's `lumen_app_expose`, the Rust
17 // SDK). They go into the registry every host drains, so an exposed
18 // function is callable from whichever languages the app ships.
19 let native_fns = std::mem::take(&mut opts.native_fns);
20 let app_hooks = std::mem::take(&mut opts.app_hooks);
21 // The injected markup/CSS front-end (the runtime links no parser itself).
22 // Shared as an `Arc` so the initial load, the hot-reload resource, and the
23 // devtools-overlay parse can all reach the same impl.
24 let parser: Option<std::sync::Arc<dyn SourceParser>> =
25 opts.parser.take().map(std::sync::Arc::from);
26 let dir = opts.dir.clone();
27 let cfg = crate::config::LumenToml::load_or_default(&dir).map_err(RunError::Config)?;
28 // The tags this app's modules bring, published before the markup is
29 // parsed. A module registers its own tag when it installs, which covers
30 // the run path; the declaration is what covers a compile, where nothing
31 // is loaded at all.
32 register_declared_tags(&cfg, &[lumen_modules::Target::Desktop], &[]);
33 // The dependencies this desktop run loads: its own target table laid over
34 // the shared one. Each is a library; a module's web half is a web
35 // build's business.
36 let dependencies = cfg.dependencies_for(lumen_modules::Target::Desktop);
37 // Where this app lives and what it is called, published before anything
38 // that resolves a path runs: every path a script names is resolved
39 // against these, and a script's `on_start` fires on the first tick.
40 let app_id = cfg
41 .app
42 .id
43 .clone()
44 .unwrap_or_else(|| lumen_capability::derive_app_id(&dir));
45 lumen_core::app_paths::set_app(dir.clone(), app_id.clone());
46 // Where this app's code is. In-memory markup has no source tree to check,
47 // so it takes the paths without one.
48 let layout = if opts.markup.is_some() {
49 AppLayout::of(&dir, &cfg)
50 } else {
51 AppLayout::resolve(&dir, &cfg)?
52 };
53 // File-based pages: discover the page set up front. The entry file is
54 // `index.lmn` (else the `[app] entry` stem, else `main.lmn`). Single-file
55 // apps come back `multipage = false` and take the untouched legacy path.
56 // In-memory (`markup`) sources bypass discovery entirely.
57 let page_plan = if opts.markup.is_some() {
58 None
59 } else {
60 Some(crate::pages::discover(&layout.src_dir, &cfg))
61 };
62 let html_path = match &page_plan {
63 Some(plan) => plan.entry_file.clone(),
64 None => layout.entry_path,
65 };
66 let css_path = layout.css_path;
67 let lib_dir = layout.lib_dir;
68 // The script libraries the app depends on, resolved by lumenc before the
69 // build; the runtime resolves nothing itself.
70 let import_roots = std::mem::take(&mut opts.import_roots);
71 let asset_roots = cfg.resolved_asset_roots(&dir);
72 let skin_override = cfg.skin.name.clone();
73 // The injected compiler-plugin chain (loaded by lumenc, like the parser),
74 // shared with hot reload as a resource; a reload pays only the hook calls.
75 let compiler_plugins: std::sync::Arc<dyn crate::compiler_plugins::CompilerPlugins> = opts
76 .compiler_plugins
77 .take()
78 .unwrap_or_else(|| std::sync::Arc::new(crate::compiler_plugins::NoCompilerPlugins));
79
80 // What the optional subsystems are installed with: the app's location,
81 // identity and config, the run mode, and a bounded view of its sources
82 // for the ones that gate on use (see `capability_env`). The embedder's
83 // hooks are Rust the scan cannot read, so with any present every
84 // use query answers yes.
85 let mut env = capability_env(&opts, &dir, &cfg);
86 if !app_hooks.is_empty() {
87 env = env.opaque();
88 }
89 if let Some(parser) = parser.clone() {
90 env.provide(parser);
91 }
92
93 let mut app = App::new();
94 // The run mode, before anything is installed: a plugin that only makes
95 // sense for a person at a window reads this from `Plugin::build`. Same bit
96 // the capability and portable-plugin environments carry, from the same
97 // source.
98 app.world.insert_resource(lumen_core::app::RunMode {
99 headless: opts.bounded,
100 });
101 // `[runtime] threads` overrides the `min(cores, 4)` default budget
102 // (the `LUMEN_THREADS` env var still wins over this at first tick).
103 if let Some(n) = cfg.runtime.threads.filter(|n| *n > 0) {
104 app.desired_threads = n;
105 }
106 // The core stack (see `run/subsystems.rs`), then the optional
107 // subsystems this binary carries, at the phase each asked for. The core
108 // is unconditional; an optional subsystem decides for itself whether an
109 // app uses it. `build_app` names none of them.
110 // The backends the stack is built on register into their registries
111 // first, so the core stack finds the layout engine it installs.
112 lumen_capability::install_phase(&mut app, &env, Phase::Backends);
113 // Text shaping first: the layout engine measures through the shaper
114 // installed here, and the renderer gets the sibling returned here.
115 let render_shaper = register_text(&mut app);
116 register_core(&mut app)?;
117 // Host services: OS integration, the introspection server, the render
118 // backends.
119 lumen_capability::install_phase(&mut app, &env, Phase::Platform);
120 // A rendered headless run starts its offscreen renderer now: the
121 // backends registered above, and bringing one up (a GPU device and its
122 // pipelines) needs nothing from the rest of the build it overlaps.
123 if let Some((width, height)) = opts.offscreen_prestart.take() {
124 let candidates = crate::run::render_backends(&app, &cfg)?;
125 app.world
126 .insert_resource(crate::run_headless::OffscreenPrestart::spawn(
127 candidates, width, height,
128 )?);
129 }
130 // Reactive bindings, reconcilers, dialog lifecycle, error overlay - the
131 // always-on reactive core.
132 register_reactive(&mut app);
133 // Translation. Runs before the tree is spawned so a
134 // `translatable="key"` element resolves its text on the first frame,
135 // and before the script host loads so `t("key")` works from `on_start`.
136 register_i18n(&mut app, &dir, &cfg)?;
137
138 // Command-bus drain, the FFI typed-read mirror, and the
139 // `set_color_scheme` `Command::Typed` handler. Always-on reactive plumbing.
140 register_commands(&mut app);
141 // What a script host binds to at construction, the HTTP client behind
142 // `fetch()` for one: the first host to build installs a `FetchRegistry`
143 // if none exists yet, so a client has to be in place before it. Ahead of
144 // the module phase, which is where it has always been: a module that
145 // installs a client of its own replaces this one, and moving the modules
146 // would have silently reversed that.
147 lumen_capability::install_phase(&mut app, &env, Phase::BeforeScripts);
148 // The registration order is the shadowing order: a plugin's functions
149 // first, then the embedder's. Every host drains this one registry as it
150 // loads and seals it afterwards, so a registration that arrives too late
151 // to bind says so instead of going quiet. The builtin table is not in it;
152 // each host binds that itself when it is constructed, so a name registered
153 // here shadows a builtin of the same name.
154 // Runtime dependencies (`[dependencies]`): prebuilt dylibs of either
155 // kind - engine-locked modules installed as ordinary Plugins, portable
156 // plugins bound through the C-ABI loader - each told apart by its
157 // exports. Before the embedder's plugin phase on purpose: registrations
158 // shadow in arrival order, so a later embedder registration of the same
159 // script-fn name wins over a module's. Any load failure is a stderr
160 // banner plus a `LoadedModules` failure entry, and the app keeps booting
161 // without that module.
162 //
163 // Before the markup is parsed, too: a module that brings an element
164 // registers its tag from `Plugin::build`, and a parse that ran first
165 // would have refused the element the module exists to provide. Everything
166 // below this point needs the parse to have happened, so this is the last
167 // moment a module can be installed and still be ahead of it.
168 //
169 // A compiled app is read before them instead: its tree was parsed when it
170 // was compiled, so no module's tag waits on this order.
171 //
172 // Script hosts are modules too, loaded beside the declared ones: the
173 // module each of the app's script languages runs on. A compiled app names
174 // them; an app run from source has them picked from the language
175 // descriptors in reach, by the `<script>` elements its markup names. The
176 // descriptor table is only consulted on the source path: a shipped app
177 // runs from the artifact and carries none.
178 let table = if opts.artifact_bytes.is_some() || opts.artifact.is_some() {
179 LanguageTable::default()
180 } else {
181 language_table()
182 };
183 let grouping = ScriptGrouping {
184 table: &table,
185 engine: cfg.script.engine(),
186 };
187 let no_languages = ScriptLanguages::default();
188 let compiled_first = if opts.artifact_bytes.is_some() || opts.artifact.is_some() {
189 Some(load_inputs(
190 &opts,
191 parser.as_deref(),
192 &*compiler_plugins,
193 &html_path,
194 &css_path,
195 &dir,
196 &asset_roots,
197 skin_override.as_deref(),
198 page_plan.as_ref(),
199 &Languages {
200 grouping,
201 registered: &no_languages,
202 },
203 )?)
204 } else {
205 None
206 };
207 let host_modules: Vec<String> = match &compiled_first {
208 Some(loaded) => loaded
209 .scripts
210 .iter()
211 .map(|script| script.module.clone())
212 .filter(|module| !module.is_empty())
213 .collect(),
214 None => script_languages(
215 &opts,
216 parser.as_deref(),
217 grouping,
218 &html_path,
219 page_plan.as_ref(),
220 )
221 .iter()
222 .filter_map(|language| table.source_provider(language))
223 .map(|provider| provider.module.clone())
224 .collect(),
225 };
226 let dependencies = lumen_modules::language::with_implied(&dependencies, &host_modules);
227 #[cfg(feature = "modules")]
228 {
229 let env = crate::modules::InitEnv {
230 app_dir: dir.clone(),
231 app_id: cfg
232 .app
233 .id
234 .clone()
235 .unwrap_or_else(|| lumen_capability::derive_app_id(&opts.dir)),
236 // The same bit the `RunMode` resource above carries, for the
237 // portable arm, which reads this environment rather than the
238 // world.
239 headless: opts.bounded,
240 hot_reload: opts.hot_reload && !opts.bounded,
241 };
242 crate::modules::load_modules(&mut app, &dir, &dependencies, &opts.resolved_modules, &env);
243 }
244 #[cfg(not(feature = "modules"))]
245 for dep in &dependencies.0 {
246 eprintln!(
247 "lumen-runtime: dependency '{}' is declared but this runtime was built without \
248 the `modules` feature; the app runs without it",
249 dep.name
250 );
251 }
252 // The embedder's plugins (`RunOptions::with_plugin`, the Rust SDK's
253 // `add_plugin`), at the same point as the modules and for the same
254 // reason: a plugin that brings an element registers its tag from
255 // `Plugin::build`, and the parse below refuses a tag nobody registered.
256 // After the modules, so a plugin's script functions shadow a module's of
257 // the same name; before the script hosts load, which bind every function
258 // registered by then.
259 for install in std::mem::take(&mut opts.plugins) {
260 install(&mut app);
261 }
262
263 // Initial load runs before the window exists, so there's no real
264 // OS theme / viewport yet. Apply with the best-guess default
265 // context; `detect_media_change` re-applies with the live context on
266 // the first tick after the window seeds `StyleManager` / `Viewport`.
267 //
268 // Either deserialize a precompiled AOT artifact (parser-free path) or
269 // parse `main.lmn` + `main.css` from source (`runtime-parse`).
270 let registered = app
271 .world
272 .get_resource::<ScriptLanguages>()
273 .cloned()
274 .unwrap_or_default();
275 let loaded = match compiled_first {
276 Some(loaded) => loaded,
277 None => load_inputs(
278 &opts,
279 parser.as_deref(),
280 &*compiler_plugins,
281 &html_path,
282 &css_path,
283 &dir,
284 &asset_roots,
285 skin_override.as_deref(),
286 page_plan.as_ref(),
287 &Languages {
288 grouping,
289 registered: ®istered,
290 },
291 )?,
292 };
293 let LoadResult {
294 ir,
295 html_mtime,
296 css_mtime,
297 script_paths,
298 script_mtimes,
299 include_paths,
300 include_mtimes,
301 css_import_paths,
302 css_import_mtimes,
303 scripts: compiled_scripts,
304 pages: compiled_pages,
305 fragments,
306 i18n: compiled_i18n,
307 } = loaded;
308 // The catalogues a compiled app carries fill in every locale the app
309 // directory has no loose file for, before the tree spawns and before a
310 // script's `on_start` calls `t()`.
311 add_compiled_catalogues(&mut app.world, &cfg, &compiled_i18n)?;
312 // The app's declared fragments, reachable by key for the rest of the
313 // run: a script instantiates one, and the applier builds it here.
314 crate::fragments::install(&mut app.world, fragments);
315 // Hot-reload watch fields are only consumed by the (feature-gated)
316 // watcher below; in a parser-free build they are always empty.
317 #[cfg(not(feature = "runtime-parse"))]
318 let _ = (
319 &html_mtime,
320 &css_mtime,
321 &script_paths,
322 &script_mtimes,
323 &include_paths,
324 &include_mtimes,
325 &css_import_paths,
326 &css_import_mtimes,
327 );
328 // Script hosts. Each part of the program runs on the host its language's
329 // module registered; an app that ships two languages runs two hosts side
330 // by side, reaching each other only through the shared `PropertyStore`
331 // signal bus, and `[script] engine` collapses everything onto one.
332 //
333 // `register_script_common` installs the host-neutral half once, ordering
334 // it against `lumen_script::ScriptSet` so its RC-critical edges cover
335 // every active host; each language's install adds the per-host half. The
336 // native functions an app adds - a plugin's and the embedder's
337 // `RunOptions::native_fns` - go into one `ScriptFnRegistry` below, which
338 // each host drains as it loads; the shared builtin surface
339 // (`lumen_script::builtin_script_fns`) is bound by each host itself.
340 //
341 // A precompiled artifact carries the split the AOT compiler recorded, and
342 // it is the only source of it: a compiled app ships no script files for
343 // the grouping to read, and a language with a bytecode form ships no
344 // source at all.
345 let programs: Vec<(String, ScriptProgram)> = if compiled_scripts.is_empty() {
346 grouped_script_sources(&ir, &dir, grouping)?
347 .into_iter()
348 .map(|script| {
349 (
350 script.language,
351 ScriptProgram {
352 source: script.source,
353 source_map: script.map,
354 ..ScriptProgram::default()
355 },
356 )
357 })
358 .collect()
359 } else {
360 compiled_scripts
361 .into_iter()
362 .map(|script| {
363 (
364 script.engine,
365 ScriptProgram {
366 source: script.source,
367 bytecode: script.bytecode,
368 source_map: script.source_map,
369 ..ScriptProgram::default()
370 },
371 )
372 })
373 .collect()
374 };
375 let has_script = !programs.is_empty();
376 let mut reloaders = ScriptReloaders::default();
377 let multi_host = programs.len() > 1;
378 // The host-neutral half of the script wiring. It needs the parse: whether
379 // the app ships a script at all is read out of the document.
380 register_script_common(&mut app, has_script);
381 app.add_script_fns(native_fns);
382 for (language, mut program) in programs {
383 let Some(entry) = registered.get(&language).copied() else {
384 no_host_for(&mut app, &language, &table);
385 continue;
386 };
387 // The entry path names the program in an error, `lib/` is where a
388 // native library a script imports is looked for, and the script
389 // libraries and compile-time flags are this desktop run's.
390 program.uri = html_path.display().to_string();
391 program.lib_dir = Some(lib_dir.clone());
392 program.import_roots = import_roots.clone();
393 program.cfg_flags = lumen_modules::Target::Desktop
394 .cfg_flags()
395 .iter()
396 .map(|f| (*f).to_string())
397 .collect();
398 (entry.install)(&mut app, program, multi_host);
399 if let Some(reload) = entry.reload {
400 reloaders.push(language, reload);
401 }
402 }
403 // RC6: a script that failed to load leaves `ScriptLoadFailure` behind.
404 // Mirror it into the in-app error banner so the failure is visible in the
405 // window itself, not only in the stderr banner the host printed.
406 if let Some(fail) = app.world.get_resource::<lumen_script::ScriptLoadFailure>() {
407 let msg = format!("script load failed: {}", fail.0);
408 app.world.resource_mut::<ErrorBanner>().0 = Some(msg);
409 }
410 app.world.insert_resource(reloaders);
411 app.world
412 .insert_resource(ScriptLanguageTable(table.clone()));
413 use crate::spawn::SpawnIntoWorld;
414 let root = ir.spawn_into(&mut app.world);
415 crate::run::restyle::install_root_class_list(&mut app.world, root);
416 // The `<if>` / `<for>` bodies `on_start`'s writes open, mounted before the
417 // first tick so `on_ready` finds them.
418 crate::spawn::settle_tree(&mut app.world);
419
420 // Pages: install the page registry, in-memory history, the reserved
421 // `route.*` signal seeds, and the navigation systems (`apply_navigation`
422 // before the `<if>` reconciler; anchor-click -> navigate). Only when the
423 // app has more than one page.
424 //
425 // A compiled app carries its page set in the artifact and is authoritative
426 // about it: the `.lmn` files a directory scan would look for are compiled
427 // in and not shipped, so the scan above always comes back single-page for
428 // one. An app loaded from source uses the plan discovered from its files.
429 match (&compiled_pages, &page_plan) {
430 (Some(pages), _) => {
431 crate::pages::install_routing(
432 &mut app,
433 pages.entry.clone(),
434 pages.keys.clone(),
435 crate::pages::Location::page(&pages.entry),
436 );
437 }
438 (None, Some(plan)) if plan.multipage => crate::pages::install(&mut app, plan),
439 _ => {}
440 }
441
442 // Seed K9's class cache so the first `set_root_class` call has a
443 // baseline to compare against (avoids a respawn on the first tick
444 // if a theme detector wrote `theme-light` before anyone set it).
445 let initial_root_classes: Vec<String> = app
446 .world
447 .get::<lumen_core::components::LumenClasses>(root)
448 .map(|c| c.0.iter().map(|s| s.to_string()).collect())
449 .unwrap_or_default();
450 app.world
451 .insert_resource(RootClassesCache(initial_root_classes));
452
453 // Style-invalidation cache, style version tracking, the live combined
454 // stylesheet, the theme/media re-resolver systems, and the per-cache
455 // memory budget. Always-on styling plumbing.
456 register_styles(&mut app, &ir, &cfg);
457
458 // Hot reload re-parses source on file change, so it exists only in
459 // `runtime-parse` builds. In-memory sources (and artifact loads) have no
460 // Stash the injected markup front-end so `set_inner_markup` (design 4.4)
461 // can parse a fragment at runtime through the same seam hot reload uses.
462 // Present on the from-source run path (dev / SDK / CLI `run`); the
463 // precompiled-artifact path carries no parser, and `set_inner_markup` is a
464 // no-op there. The hot-reload block below re-inserts the same resource for
465 // its own re-parse; this makes it available regardless of hot-reload.
466 if let Some(parser) = parser.clone() {
467 app.world
468 .insert_resource(crate::source_parser::RuntimeParser(parser));
469 }
470 app.world
471 .insert_resource(crate::compiler_plugins::RuntimeCompilerPlugins(
472 compiler_plugins,
473 ));
474
475 // file to watch - force hot reload off so the watcher never despawns the
476 // tree in favour of stale disk state.
477 // Hot-reload gating. `[runtime] hot_reload` forces the answer; otherwise
478 // the watcher runs only for an interactive run from source - never for a
479 // headless / bounded automation run (no editing session, and the `notify`
480 // watcher would spawn a thread the bench pays for). Markup / artifact
481 // runs have no source files to watch regardless.
482 #[cfg(feature = "runtime-parse")]
483 let hot_reload_enabled = match cfg.runtime.hot_reload {
484 Some(v) => v,
485 None => opts.hot_reload && !opts.bounded,
486 };
487 #[cfg(feature = "runtime-parse")]
488 if hot_reload_enabled && opts.markup.is_none() && opts.artifact.is_none() {
489 // Parent directories of every tracked source file (deduplicated).
490 // Computed before the paths move into `HotReloadState`.
491 let locale_dir = crate::run::i18n::locale_dir(&dir);
492 let watch_dirs: std::collections::HashSet<PathBuf> = [&html_path, &css_path]
493 .into_iter()
494 .chain(&script_paths)
495 .chain(&include_paths)
496 .chain(&css_import_paths)
497 .filter_map(|p| p.parent())
498 .chain(locale_dir.is_dir().then_some(locale_dir.as_path()))
499 .filter(|d| d.is_dir())
500 .map(PathBuf::from)
501 .collect();
502 let authored_text = authored_texts(&mut app.world);
503 app.world.insert_resource(HotReloadState {
504 dir: dir.clone(),
505 html_path: html_path.clone(),
506 css_path: css_path.clone(),
507 html_mtime,
508 css_mtime,
509 script_paths,
510 script_mtimes,
511 include_paths,
512 include_mtimes,
513 css_import_paths,
514 css_import_mtimes,
515 locale_stamps: crate::run::i18n::locale_stamps(&dir),
516 asset_roots: asset_roots.clone(),
517 skin_override: skin_override.clone(),
518 root,
519 authored_text,
520 });
521 // Change detection driver: notify watcher by default (idle apps
522 // park with zero ticks; an fs event wakes the loop for one tick),
523 // mtime polling behind `LUMEN_HOT_RELOAD_POLL` or on watcher
524 // init failure.
525 let driver = if std::env::var_os("LUMEN_HOT_RELOAD_POLL").is_some() {
526 eprintln!("lumenc: hot reload using mtime polling (LUMEN_HOT_RELOAD_POLL set)");
527 HotReloadDriver::Poll
528 } else {
529 let flag = std::sync::Arc::new(HotReloadFlag::default());
530 match spawn_hot_reload_watcher(&watch_dirs, std::sync::Arc::clone(&flag)) {
531 Ok(watcher) => HotReloadDriver::Watch {
532 flag,
533 _watcher: std::sync::Arc::new(std::sync::Mutex::new(watcher)),
534 },
535 Err(e) => {
536 eprintln!(
537 "lumenc: hot-reload file watcher init failed ({e}); \
538 falling back to mtime polling"
539 );
540 HotReloadDriver::Poll
541 }
542 }
543 };
544 app.world.insert_resource(driver);
545 // Stash the injected parser so `hot_reload::<H>` (a `&mut World`
546 // system that can't take it as a param) can re-parse on change. The
547 // watcher is only wired for a from-source run, which always carries a
548 // parser (`load_inputs` above would have returned `ParserDisabled`
549 // otherwise), so this unwrap is unreachable in practice.
550 if let Some(parser) = parser.clone() {
551 app.world
552 .insert_resource(crate::source_parser::RuntimeParser(parser));
553 }
554 // One watcher system for the whole app: it respawns the tree once and
555 // then reloads each active host through the `ScriptReloaders` table.
556 //
557 // Ordered before the DOM index publish: the respawn replaces the root,
558 // and `on_ready` re-fires on this same tick. A publish that ran first
559 // would hand that dispatch the despawned root, so a script that mounts
560 // into the document would attach to nothing.
561 app.add_systems(
562 TickStage::Systems,
563 hot_reload.before(lumen_scene::dom::build_dom_index),
564 );
565 // An image or SVG file edited on disk reloads in place: the asset
566 // watcher reports the path, and this strips what every element on
567 // it shows so the next tick decodes the new bytes.
568 app.add_systems(
569 TickStage::Systems,
570 lumen_assets::reload_changed_images.after(lumen_assets::process_watch_events),
571 );
572 }
573
574 // RunOptions (set by the CLI / embedder) overrides lumen.toml,
575 // which overrides built-in defaults.
576 let title = opts
577 .title
578 .or_else(|| cfg.window.title.clone())
579 .unwrap_or_else(|| derive_title(&opts.dir));
580 let mut size = match cfg.window.size {
581 Some([w, h]) if opts.size == RunOptions::DEFAULT_SIZE => (w, h),
582 _ => opts.size,
583 };
584 let mut maximized = true;
585 let mut start_position: Option<(i32, i32)> = None;
586 let mut on_close_state: Option<Box<dyn FnOnce(WindowGeometry) + Send>> = None;
587 if cfg.window.remember_state.unwrap_or(false) {
588 let prev = crate::window_state::load(&app_id);
589 if let Some([w, h]) = prev.size {
590 size = (w, h);
591 }
592 start_position = prev.position.map(|[x, y]| (x, y));
593 maximized = prev.maximized;
594 on_close_state = Some(Box::new(move |g| {
595 crate::window_state::save(
596 &app_id,
597 &crate::window_state::WindowState {
598 position: g.position.map(|(x, y)| [x, y]),
599 size: Some([g.size.0, g.size.1]),
600 maximized: g.maximized,
601 },
602 );
603 }));
604 }
605 let menubar = ir.menubar.as_ref().map(|spec| MenuModel {
606 menus: spec
607 .menus
608 .iter()
609 .map(|m| Menu {
610 label: m.label.clone(),
611 items: m
612 .items
613 .iter()
614 .map(|entry| match entry {
615 MenuEntrySpec::Item {
616 id,
617 label,
618 accelerator,
619 } => MenuEntry::Item {
620 id: id.clone(),
621 label: label.clone(),
622 accelerator: accelerator.clone(),
623 },
624 MenuEntrySpec::Separator => MenuEntry::Separator,
625 })
626 .collect(),
627 })
628 .collect(),
629 });
630 // `--lumen-window-bg` resolved from the fully-combined (UA + skin +
631 // app) stylesheet paints the GPU clear behind the very first frame -
632 // what a user sees before the root element itself paints, and behind
633 // any pixel the tree doesn't cover. Only a plain solid color parses
634 // (the clear is a single RGBA, not a gradient); an app whose active
635 // layers don't define the token - or define it as something else -
636 // falls back to `opts.clear` (`lumen_core::window::DEFAULT_CLEAR`
637 // unless the caller overrode it), preserving today's behavior exactly.
638 let clear = ir
639 .combined_stylesheet
640 .as_ref()
641 .and_then(|sheet| sheet.resolve_root_var("lumen-window-bg"))
642 .and_then(|value| lumen_ir::values::parse_color("<root>", "lumen-window-bg", &value).ok())
643 .map(Into::into)
644 .unwrap_or(opts.clear);
645 let window = WindowSetup {
646 options: WindowOptions {
647 size,
648 title,
649 clear,
650 maximized,
651 frameless: ir.frameless,
652 start_position,
653 on_close_state,
654 menubar,
655 },
656 text_shaper: Some(render_shaper),
657 };
658 // What mounts into the built document: the devtools overlay, when the
659 // binary carries it and the run has a front end to parse it with.
660 lumen_capability::install_phase(&mut app, &env, Phase::AfterBuild);
661
662 // Embedder hooks run last so they can order their systems against
663 // everything the default stack registered above (script dispatch,
664 // binding readers, reconcilers).
665 for hook in app_hooks {
666 hook(&mut app);
667 }
668 Ok((app, window))
669}
670
671/// The language descriptors in reach of this run. A set that disagrees with
672/// itself (two default languages) is reported and read as empty: the app
673/// runs, and every script says it has no host.
674fn language_table() -> LanguageTable {
675 LanguageTable::discover(None).unwrap_or_else(|e| {
676 lumen_core::warn_line!("lumen-runtime: {e}");
677 LanguageTable::default()
678 })
679}
680
681/// The languages an app run from source needs a host for: read off the
682/// `<script>` elements of its markup, before the parse. A run with no parser
683/// has nothing to read them with, and the parse it cannot do fails on its own.
684#[cfg(feature = "runtime-parse")]
685fn script_languages(
686 opts: &RunOptions,
687 parser: Option<&dyn SourceParser>,
688 grouping: ScriptGrouping<'_>,
689 html_path: &Path,
690 plan: Option<&crate::pages::PagePlan>,
691) -> Vec<String> {
692 match parser {
693 Some(parser) => source_languages(parser, grouping, html_path, opts.markup.as_deref(), plan),
694 None => Vec::new(),
695 }
696}
697
698#[cfg(not(feature = "runtime-parse"))]
699fn script_languages(
700 _opts: &RunOptions,
701 _parser: Option<&dyn SourceParser>,
702 _grouping: ScriptGrouping<'_>,
703 _html_path: &Path,
704 _plan: Option<&crate::pages::PagePlan>,
705) -> Vec<String> {
706 Vec::new()
707}
708
709/// A part of the program whose language no loaded module registered: say so
710/// once, on stderr and in the window, and run the app without it. The module
711/// loader has already bannered a host module that failed to load; this names
712/// the language, and the module that would run it when a descriptor says.
713fn no_host_for(app: &mut App, language: &str, table: &LanguageTable) {
714 let module = table
715 .shipped_provider(language)
716 .map(|p| format!("the `{}` module", p.module))
717 .unwrap_or_else(|| "a module that provides it".to_string());
718 let reason = format!(
719 "no script host for the `{language}` language is loaded, so the app's `{language}` \
720 script does not run. Install {module} beside the engine."
721 );
722 lumen_core::warn_line!(
723 "\n\
724 ================================================================\n\
725 lumen-runtime: SCRIPT NOT RUN\n\
726 \n\
727 {reason}\n\
728 \n\
729 The window will still open, but every event handler, signal,\n\
730 and derivation in that script is DISABLED.\n\
731 ================================================================\n"
732 );
733 app.world
734 .insert_resource(lumen_script::ScriptLoadFailure(reason));
735}