Skip to main content

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: &registered,
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}