mod asset_handle;
mod behavior;
#[cfg(target_os = "macos")]
mod app_menu;
mod code_font;
mod create_menu;
mod entry_list;
mod file_dialog;
mod filter;
mod gltf_export;
mod history;
mod hook;
mod hud;
mod hud_ids;
mod inject;
mod live;
mod map;
mod modal;
pub(crate) mod notify;
mod outlines;
mod owned_files;
mod overrides;
mod palette;
mod panels;
mod select_related;
mod selection;
mod session_store;
mod sim;
mod text_area;
mod theme;
mod thumbs;
mod toast_overlay;
mod view_menu;
mod viewport;
mod visibility;
mod widget;
mod widget_check;
mod widget_menu;
mod widget_slider;
mod worlds;
use concinnity_cook::authoring::world::WORLD_JSONL;
use concinnity_core::ecs::World;
use concinnity_engine::app::run::LaunchRequest;
use concinnity_engine::app::runtime::Runtime;
use concinnity_engine::shutdown::ShutdownToken;
use hook::EditorHook;
use crate::frame_hook::FrameHook;
const SEED_WINDOW: &str = "[\"Window\",{\"$id\":\"editor_default_window\"}]";
pub fn run_editor(
launch: LaunchRequest,
json_path: Option<&str>,
debug_port: Option<u16>,
) -> std::io::Result<()> {
let console_sink = panels::console::ConsoleSink::default();
panels::console::install_tracing(console_sink.clone());
let (world_path, pick_a_world) = resolve_edit_target(json_path);
let (entries, previewing) = if pick_a_world {
(entry_list::EntryList::default(), start_screen_pick())
} else {
let entries = worlds::files::read_entries(std::path::Path::new(&world_path))
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
(entries, None)
};
let mut runtime = crate::project::runtime().with_launch(launch);
boot_world(&mut runtime, &entries)?;
inject::editor_hud(runtime.world_mut());
let mut editor_hook = EditorHook::new(world_path, entries)
.with_console_sink(console_sink)
.with_app_menu();
if pick_a_world {
editor_hook = editor_hook.with_start_screen(previewing);
}
let hook: Box<dyn FrameHook> = match debug_port {
Some(port) => {
let server = crate::debug::DebugServer::start(port)?
.with_notifier(editor_hook.notifier())
.with_world_path(editor_hook.world_path_handle())
.with_reload_reports(editor_hook.reload_reports());
MultiHook::boxed(vec![Box::new(editor_hook), Box::new(server)])
}
None => {
let reload = crate::debug::hot_reload::HotReloadDriver::new()
.with_notifier(editor_hook.notifier())
.with_world_path(editor_hook.world_path_handle())
.with_reload_reports(editor_hook.reload_reports());
MultiHook::boxed(vec![Box::new(editor_hook), Box::new(reload)])
}
};
crate::run::start_app(runtime, Some(hook))
}
fn resolve_edit_target(json_path: Option<&str>) -> (String, bool) {
match json_path {
Some(p) => (p.to_string(), false),
None => (unsaved_world_path(), true),
}
}
fn start_screen_pick() -> Option<String> {
let world = worlds::files::newest(
crate::project::worlds_dir().as_deref(),
crate::project::content_root().as_deref(),
)?;
Some(world.path.to_string_lossy().into_owned())
}
pub(crate) fn unsaved_world_path() -> String {
crate::project::worlds_dir()
.map(|dir| dir.join(WORLD_JSONL).to_string_lossy().into_owned())
.unwrap_or_else(|| WORLD_JSONL.to_string())
}
fn boot_world(runtime: &mut Runtime, entries: &[serde_json::Value]) -> std::io::Result<()> {
let jsonl = entry_list::build_text(entries)?;
let (world, _) = build_renderable(&jsonl)?;
runtime.load_world(world);
Ok(())
}
fn build_renderable(
jsonl: &str,
) -> std::io::Result<(World, Vec<concinnity_cook::build_only::ShadowedAsset>)> {
match crate::authoring::build_world_and_shadows(jsonl) {
Ok(built) if built.0.renders() => Ok(built),
_ => crate::authoring::build_world_and_shadows(&seeded_content(jsonl)),
}
}
fn seeded_content(base: &str) -> String {
if base.trim().is_empty() {
SEED_WINDOW.to_string()
} else {
format!("{base}\n{SEED_WINDOW}")
}
}
struct MultiHook {
hooks: Vec<Box<dyn FrameHook>>,
}
impl MultiHook {
fn boxed(hooks: Vec<Box<dyn FrameHook>>) -> Box<dyn FrameHook> {
Box::new(Self { hooks })
}
}
impl FrameHook for MultiHook {
fn tick(&mut self, world: &mut World) {
for hook in &mut self.hooks {
hook.tick(world);
}
}
fn apply_world_swap(&mut self, runtime: &mut Runtime) {
for hook in &mut self.hooks {
hook.apply_world_swap(runtime);
}
}
fn attach_shutdown(&mut self, shutdown: ShutdownToken) {
for hook in &mut self.hooks {
hook.attach_shutdown(shutdown.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use concinnity_cook::authoring::world::{parse_world_jsonl, write_world_jsonl};
use concinnity_core::components::TextLabel;
#[test]
fn seeded_content_of_empty_is_the_render_marker() {
assert_eq!(seeded_content(""), SEED_WINDOW);
assert_eq!(seeded_content(" \n"), SEED_WINDOW);
}
#[test]
fn seeded_content_appends_marker_to_authored_content() {
let base = "[\"PhysicsConfig\",{\"$id\":\"phys\"}]";
let seeded = seeded_content(base);
let parsed = parse_world_jsonl(&seeded).unwrap();
assert_eq!(parsed.len(), 2, "authored entry plus the seed marker");
assert_eq!(parsed[0]["args"]["$id"], "phys");
assert_eq!(parsed[1]["type"], "Window");
}
#[test]
fn seed_marker_is_a_window() {
let parsed = parse_world_jsonl(SEED_WINDOW).unwrap();
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0]["type"], "Window");
}
fn open_project(dir: &std::path::Path) -> std::path::PathBuf {
let build_root = dir.join(".concinnity");
crate::project::open(
concinnity_host::store::paths::StateTree::at(dir)
.with_build(&build_root)
.with_cache(concinnity_testing::shared_cache_dir(
"concinnity-dev-tests-cache",
)),
);
build_root
}
fn entry(name: &str, ty: &str, args: serde_json::Value) -> serde_json::Value {
serde_json::json!({"type": ty, "args": concinnity_cook::authoring::world::args_with_id(args, name)})
}
fn renderable_entries(label: &str) -> Vec<serde_json::Value> {
vec![
entry("cam", "Camera3D", serde_json::json!({})),
entry("room", "Room", serde_json::json!({})),
entry("hint", "TextLabel", serde_json::json!({"content": label})),
]
}
fn booted_label(runtime: &Runtime) -> String {
runtime
.world()
.query::<TextLabel>()
.next()
.expect("the authored label is in the booted world")
.content
.clone()
}
#[test]
fn boot_compiles_the_authored_entries_without_touching_the_build_root() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
let build_root = open_project(dir.path());
let mut runtime = crate::project::runtime();
boot_world(&mut runtime, &renderable_entries("authored")).expect("the world builds");
assert!(runtime.world().renders());
assert_eq!(booted_label(&runtime), "authored");
assert!(
!build_root.join("data").exists() && !build_root.join("world-lock.json").exists(),
"boot writes no blobs and no lock"
);
crate::test_support::isolate_state_dir();
}
#[test]
fn boot_ignores_blobs_that_no_longer_match_the_entries() {
let _guard = crate::test_support::lock();
let dir = concinnity_testing::TempTree::new();
let build_root = open_project(dir.path());
let world_path = dir.path().join("worlds").join(WORLD_JSONL);
std::fs::create_dir_all(world_path.parent().unwrap()).unwrap();
std::fs::write(
&world_path,
write_world_jsonl(&renderable_entries("stale")).unwrap(),
)
.unwrap();
let content = std::fs::read_to_string(&world_path).unwrap();
crate::authoring::build_world_str_to_disk(&content, None).expect("the build writes blobs");
let blob = concinnity_host::store::blob::primary_in(&build_root.join("data"));
let before = std::fs::read(&blob).expect("the build wrote a primary blob");
let mut runtime = crate::project::runtime();
boot_world(&mut runtime, &renderable_entries("edited")).expect("the world builds");
assert_eq!(
booted_label(&runtime),
"edited",
"the entries win over the blobs the last build left"
);
assert_eq!(
std::fs::read(&blob).unwrap(),
before,
"boot leaves the build output untouched"
);
crate::test_support::isolate_state_dir();
}
#[test]
fn boot_seeds_a_render_marker_for_an_empty_entry_list() {
let _guard = crate::test_support::lock();
crate::test_support::isolate_state_dir();
let mut runtime = crate::project::runtime();
boot_world(&mut runtime, &[]).expect("an empty world seeds");
assert!(runtime.world().renders());
}
#[test]
fn resolve_edit_target_honors_an_explicit_path() {
let (path, pick) = resolve_edit_target(Some("/no/such/cn-editor-world.jsonl"));
assert_eq!(path, "/no/such/cn-editor-world.jsonl");
assert!(!pick, "a named world loads instead of the Worlds panel");
}
#[test]
fn resolve_edit_target_without_a_path_opens_the_worlds_panel() {
let _guard = crate::test_support::lock();
crate::test_support::isolate_state_dir();
let (path, pick) = resolve_edit_target(None);
assert!(pick, "no named world opens the Worlds panel");
assert_eq!(path, unsaved_world_path());
}
#[test]
fn an_unsaved_world_is_named_inside_the_projects_worlds_directory() {
let _guard = crate::test_support::lock();
crate::test_support::isolate_state_dir();
let worlds = crate::project::worlds_dir().expect("the harness opened a project");
assert_eq!(
unsaved_world_path(),
worlds.join(WORLD_JSONL).to_string_lossy()
);
}
}