use std::sync::Arc;
use bevy::camera::RenderTarget;
use bevy::prelude::*;
use bevy::reflect::TypeRegistry;
use bevy::window::{EnabledButtons, PrimaryWindow, WindowRef, WindowResolution, WindowTheme};
use bevy_cobweb::prelude::*;
use bevy_cobweb_ui_core::editor::*;
use serde::de::DeserializeSeed;
use super::*;
use crate::cob::*;
use crate::prelude::*;
use crate::sickle::*;
#[derive(Resource, Default, Clone, Deref, DerefMut)]
struct EditorFileSelection(Option<CobFile>);
fn build_widgets(
h: &mut UiSceneHandle,
widgets: &CobWidgetRegistry,
file_hash: CobFileHash,
scene_ref: SceneRef,
longname: &'static str,
shortname: &'static str,
loadable: Box<dyn PartialReflect + 'static>,
death_signal: DeathSignal,
)
{
if let Some(spawn_fn) = widgets.get(longname) {
let content_entity = h.id();
let editor_ref = CobEditorRef {
file_hash,
scene_ref,
loadable_name: shortname,
structure_path: ReflectStructurePath { path: Arc::from([]) },
death_signal,
};
let (loader, builder) = h.inner();
if !(spawn_fn)(
builder.commands(),
loader,
content_entity,
&editor_ref,
loadable.as_ref(),
) {
h.spawn_scene_simple(("editor.frame", "destructure_unsupported"));
}
return;
}
h.spawn_scene_simple(("editor.frame", "destructure_unsupported"));
}
fn build_loadable(
h: &mut UiSceneHandle,
registry: &TypeRegistry,
loadables: &LoadableRegistry,
widgets: &CobWidgetRegistry,
file_hash: CobFileHash,
scene_ref: SceneRef,
loadable: &CobLoadable,
)
{
let name = loadable.id.to_canonical(None);
let Some((deserializer, _, longname, shortname)) = get_deserializer(registry, name.as_str(), loadables) else {
h.spawn_scene_simple(("editor.frame", "unsupported"));
return;
};
h.spawn_scene(("editor.frame", "loadable"), |h| {
h.get("name")
.update(move |id: TargetId, mut e: TextEditor| {
write_text!(e, id, "{}", shortname);
});
match deserializer.deserialize(loadable) {
Ok(reflected) => {
h.edit("content", |h| {
let (signaler, signal) = DeathSignaler::new();
h.insert(signaler);
build_widgets(h, widgets, file_hash, scene_ref, longname, shortname, reflected, signal);
});
}
Err(_) => {
h.get("content")
.spawn_scene_simple(("editor.frame", "reflect_fail"));
}
}
});
}
fn spawn_scene_simple_layer(
h: &mut UiSceneHandle,
registry: &TypeRegistry,
loadables: &LoadableRegistry,
widgets: &CobWidgetRegistry,
file_hash: CobFileHash,
scene_ref: SceneRef,
layer: &CobSceneLayer,
)
{
let scene_ref = scene_ref + layer.name.as_str();
h.spawn_scene(("editor.frame", "scene_node"), |h| {
let ref_path = scene_ref.path.clone();
h.get("name")
.update(move |id: TargetId, mut e: TextEditor| {
write_text!(e, id, "\"{}\"", ref_path.iter().rev().next().unwrap());
});
h.edit("content", |h| {
for entry in layer.entries.iter() {
match entry {
CobSceneLayerEntry::Loadable(loadable) => {
build_loadable(h, registry, loadables, widgets, file_hash, scene_ref.clone(), loadable);
}
CobSceneLayerEntry::Layer(scene_layer) => {
spawn_scene_simple_layer(
h,
registry,
loadables,
widgets,
file_hash,
scene_ref.clone(),
scene_layer,
);
}
_ => {
h.spawn_scene_simple(("editor.frame", "unsupported"));
}
}
}
});
});
}
fn build_file_view(In((base_entity, file)): In<(Entity, CobFile)>, mut c: Commands)
{
let mut ec = c.entity(base_entity);
ec.update_on(
(broadcast::<EditorFileExternalChange>(), broadcast::<EditorFileSaved>()),
move | _: TargetId,
mut tracked_hash: Local<Option<CobFileHash>>,
external_change: BroadcastEvent<EditorFileExternalChange>,
file_saved: BroadcastEvent<EditorFileSaved>,
mut c: Commands,
mut s: SceneBuilder,
registry: Res<AppTypeRegistry>,
loadables: Res<LoadableRegistry>,
widgets: Res<CobWidgetRegistry>,
editor: Res<CobEditor>, | {
if let Ok(external) = external_change.try_read() {
if external.file != file {
return;
}
}
if let Ok(file_saved) = file_saved.try_read() {
if file_saved.file != file || Some(file_saved.hash) == *tracked_hash {
return;
}
}
c.entity(base_entity).despawn_related::<Children>();
let Some(file_data) = editor.get_file(&file) else { return };
if !file_data.is_editable() {
c.ui_builder(base_entity).spawn_scene_simple(("editor.frame", "file_not_editable"), &mut s);
return;
}
*tracked_hash = Some(file_data.last_save_hash);
let registry = registry.read();
c.ui_builder(base_entity).spawn_scene(("editor.frame", "file_frame"), &mut s, |h| {
h.edit("commands", |h| {
let commands_ref = SceneRef{ file: file.clone().into(), path: ScenePath::new("#commands") };
for commands_section in file_data.data.sections.iter().filter_map(|s| {
let CobSection::Commands(commands) = s else { return None };
Some(commands)
}) {
for command in commands_section.entries.iter() {
build_loadable(
h,
®istry,
&loadables,
&widgets,
file_data.last_save_hash,
commands_ref.clone(),
&command.0,
);
}
}
});
h.edit("scenes", |h| {
let scene_ref = SceneRef{ file: file.clone().into(), path: ScenePath::empty() };
for scenes_section in file_data.data.sections.iter().filter_map(|s| {
let CobSection::Scenes(scenes) = s else { return None };
Some(scenes)
}) {
for scene_layer in scenes_section.scenes.iter() {
spawn_scene_simple_layer(
h,
®istry,
&loadables,
&widgets,
file_data.last_save_hash,
scene_ref.clone(),
scene_layer
);
}
}
});
});
},
);
}
fn build_editor_view(mut c: Commands, mut s: SceneBuilder, camera: Query<Entity, With<EditorCamera>>)
{
let camera_entity = camera.single().unwrap();
let scene = ("editor.frame", "base");
c.ui_root().spawn_scene(scene, &mut s, |h| {
h.insert(UiTargetCamera(camera_entity));
let content_entity = h.get("content").id();
h.edit("dropdown", |h| {
let dropdown_entity = h.id();
h.on_event::<Option<CobFile>>().r(
move | event: bevy_cobweb::prelude::EntityEvent<Option<CobFile>>,
mut c: Commands,
mut selection: ResMut<EditorFileSelection> | {
let (_, maybe_file) = event.read();
if **selection == *maybe_file { return }
**selection = maybe_file.clone();
c.entity(content_entity).despawn_related::<Children>();
let Some(file) = maybe_file else { return };
c.syscall((content_entity, file.clone()), build_file_view);
},
);
h.on_open(
move | mut c: Commands,
mut s: SceneBuilder,
editor: Res<CobEditor>,
selection: Res<EditorFileSelection> | {
c.entity(dropdown_entity).despawn_related::<Children>();
let mut builder = c.ui_builder(dropdown_entity);
builder.spawn_scene(("editor.frame", "empty_dropdown_entry"), &mut s, |h| {
h.on_pressed(move |mut c: Commands| {
c.react().entity_event(dropdown_entity, Close);
});
});
let mut entries: Vec<Option<CobFile>> =
editor.iter_files().filter(|(_, d)| d.is_editable()).map(|(f, _)| Some(f.clone())).collect();
entries.push(None);
entries.sort_unstable_by(|a, b| {
let a = a.as_ref().map(|f| f.as_str()).unwrap_or("");
let b = b.as_ref().map(|f| f.as_str()).unwrap_or("");
a.cmp(b)
});
for entry in entries {
builder.spawn_scene(("editor.frame", "dropdown_entry"), &mut s, |h| {
let entry_clone = entry.clone();
h.on_pressed(move |mut c: Commands| {
c.react().entity_event(dropdown_entity, entry_clone.clone());
c.react().entity_event(dropdown_entity, Close);
});
let entry_clone = entry.clone();
h.get("text").update(
move |id: TargetId, mut e: TextEditor| {
let text = entry_clone.as_ref().map(|f| f.as_str()).unwrap_or("<none>");
write_text!(e, id, "{}", text);
}
);
if entry == **selection {
let entry_entity = h.id();
h.react().entity_event(entry_entity, Select);
}
});
}
},
);
h.on_close(
move |mut c: Commands, mut s: SceneBuilder, selection: Res<EditorFileSelection>| {
c.entity(dropdown_entity).despawn_related::<Children>();
c.ui_builder(dropdown_entity).spawn_scene(
("editor.frame", "dropdown_entry"),
&mut s,
|h| {
let entry_entity = h.id();
h.react().entity_event(entry_entity, Fold);
let text: EditorFileSelection = selection.deref().clone();
h.get("text").update(
move |id: TargetId, mut e: TextEditor| {
let text = text.as_ref().map(|f| f.as_str()).unwrap_or("<none>");
write_text!(e, id, "{}", text);
}
);
h.on_pressed(move |mut c: Commands| {
c.react().entity_event(dropdown_entity, Open);
});
},
);
},
);
h.on_event::<EditorNewFile>()
.r(move |mut c: Commands, p: PseudoStateParam| {
if p.entity_has(dropdown_entity, PseudoState::Open) {
c.react().entity_event(dropdown_entity, Close);
c.react().entity_event(dropdown_entity, Open);
}
});
h.commands()
.syscall_once((), move |mut c: Commands, editor: Res<CobEditor>| {
let file = CobFile::try_new("main.cob").unwrap();
let init = if editor.get_file(&file).is_some() {
Some(file)
} else {
None
};
c.react().entity_event(dropdown_entity, init);
});
h.react().entity_event(dropdown_entity, Close);
});
let unsaved = h.get("footer::unsaved").id();
h.react().on(
(broadcast::<EditorFileUnsaved>(), broadcast::<EditorFileSaved>()),
move |mut c: Commands, p: PseudoStateParam, editor: Res<CobEditor>| {
if editor.any_unsaved() {
p.try_enable(&mut c, unsaved);
} else {
p.try_disable(&mut c, unsaved);
}
},
);
h.react().entity_event(unsaved, Disable);
h.get("footer::save")
.on_pressed(|w: &mut World| SaveEditor.apply(w));
});
}
fn refresh_editor_window(
primary_win: Query<&Window, (With<PrimaryWindow>, Without<EditorWindow>)>,
mut editor_win: Query<&mut Window, (With<EditorWindow>, Without<PrimaryWindow>)>,
)
{
let primary_window = primary_win.single().unwrap();
let mut editor_window = editor_win.single_mut().unwrap();
let WindowPosition::At(primary_pos) = primary_window.position else { return };
let mut desired_pos = primary_pos;
desired_pos.x -= (EDITOR_WIDTH * primary_window.resolution.scale_factor()) as i32;
desired_pos.x = desired_pos.x.max(0);
if WindowPosition::At(desired_pos) != editor_window.position {
editor_window.position = WindowPosition::At(desired_pos);
}
let primary_height = primary_window.resolution.size().y;
if primary_height != editor_window.resolution.size().y {
editor_window.resolution.set(EDITOR_WIDTH, primary_height);
}
}
const EDITOR_WIDTH: f32 = 300.0;
#[derive(Component, Debug)]
pub(crate) struct EditorWindow;
#[derive(Component, Debug)]
pub(crate) struct EditorCamera;
pub(crate) struct CobEditorBuildPlugin;
impl Plugin for CobEditorBuildPlugin
{
fn build(&self, app: &mut App)
{
let mut query = app
.world_mut()
.query_filtered::<&Window, With<PrimaryWindow>>();
let primary_window = query.single(app.world()).unwrap();
let initial_height = primary_window.resolution.size().y;
let mut resolution = WindowResolution::new(0, 0);
resolution.set(EDITOR_WIDTH, initial_height); let editor_window = app
.world_mut()
.spawn((
Window {
title: "Cob Editor".into(),
resolution,
resizable: false,
enabled_buttons: EnabledButtons { minimize: false, maximize: false, close: false },
window_theme: Some(WindowTheme::Dark), ..default()
},
EditorWindow,
))
.id();
app.world_mut().spawn((
Camera2d,
Camera {
target: RenderTarget::Window(WindowRef::Entity(editor_window)),
..default()
},
EditorCamera,
));
app.add_plugins(CobEditorTemplatePlugin)
.init_resource::<EditorFileSelection>()
.add_systems(First, refresh_editor_window)
.add_systems(OnEnter(LoadState::Done), build_editor_view);
}
}