use std::path::PathBuf;
use concinnity_core::ecs::World;
use super::edit::sdf_form::SdfExtras;
use super::edit::shader_form::ShaderExtras;
use super::{EditorHook, FormTarget, declared_id};
use crate::editor::entry_list::{EntryId, EntryList};
use crate::editor::panels::form_extras::ExtraRow;
use crate::editor::panels::form_panel;
use crate::editor::widget;
pub(in crate::editor::hook) struct ExtrasCx<'a> {
pub(in crate::editor::hook) entries: &'a EntryList,
pub(in crate::editor::hook) target: &'a FormTarget,
pub(in crate::editor::hook) name: &'a str,
}
impl ExtrasCx<'_> {
pub(in crate::editor::hook) fn name_taken(&self, name: &str) -> bool {
let own = self.target.entry().and_then(|k| self.entries.index_of(k));
self.entries
.iter()
.enumerate()
.any(|(i, e)| Some(i) != own && declared_id(e) == Some(name))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::editor::hook) struct NewFile {
pub(in crate::editor::hook) path: PathBuf,
pub(in crate::editor::hook) text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::editor::hook) struct Committed {
pub(in crate::editor::hook) key: EntryId,
pub(in crate::editor::hook) before: Option<String>,
pub(in crate::editor::hook) name: Option<String>,
pub(in crate::editor::hook) moved: usize,
}
pub(in crate::editor::hook) trait FormExtras:
std::fmt::Debug + Send
{
fn hidden_fields(&self) -> &'static [&'static str] {
&[]
}
fn rows(&self, cx: &ExtrasCx) -> Vec<ExtraRow>;
fn press(&mut self, id: usize);
fn switch_type(&self) -> Option<&'static str> {
None
}
fn blocked(&self, _cx: &ExtrasCx) -> Option<String> {
None
}
fn fill_args(
&self,
_cx: &ExtrasCx,
_args: &mut serde_json::Map<String, serde_json::Value>,
) -> Vec<NewFile> {
Vec::new()
}
fn hold(&self, _hook: &mut EditorHook) -> bool {
false
}
fn edit_entries(&mut self, _entries: &mut EntryList, _c: &Committed) {}
fn after(&self, _hook: &mut EditorHook, _c: &Committed) {}
}
pub(in crate::editor::hook) fn for_type(
ty: &str,
entries: &EntryList,
target: &FormTarget,
) -> Option<Box<dyn FormExtras>> {
match (ty, target) {
(_, FormTarget::Promote(_)) => None,
("Shader", _) => Some(Box::new(ShaderExtras::open(entries, target))),
("SdfVolume", _) => Some(Box::new(SdfExtras::open(entries, target))),
_ => None,
}
}
impl EditorHook {
pub(in crate::editor::hook) fn extras_cx<'a>(&'a self, name: &'a str) -> ExtrasCx<'a> {
ExtrasCx {
entries: &self.entries,
target: &self.form.target,
name: name.trim(),
}
}
pub(in crate::editor::hook) fn form_extras_data(
&self,
world: &World,
) -> (Vec<ExtraRow>, Option<String>) {
let Some(extras) = &self.form.extras else {
return (Vec::new(), None);
};
let typed = widget::field_text(world, form_panel::NAME_INPUT);
let cx = self.extras_cx(&typed);
(extras.rows(&cx), extras.blocked(&cx))
}
pub(in crate::editor::hook) fn switch_form_type(&mut self, world: &mut World, ty: &str) {
let typed = widget::field_text(world, form_panel::NAME_INPUT);
let host = self.form.host;
self.open_form(world, ty.to_string(), FormTarget::New);
self.form.host = host;
self.form.touched = true;
self.place_form_at_view(world);
widget::focus_field_with(world, form_panel::NAME_INPUT, &typed);
}
pub(in crate::editor::hook) fn place_form_at_view(&mut self, world: &mut World) {
let Some(ty) = self.form.selected_type.clone() else {
return;
};
let key = crate::editor::create_menu::position_key(&ty);
if !self.form.args.contains_key(key) {
return;
}
let vp = self.viewport;
let middle = [vp[0] * 0.5, (crate::editor::hud::body_top() + vp[1]) * 0.5];
let Some(pos) = self.drop_point(world, vp, middle, false) else {
return;
};
let pos = pos.map(super::drag::gizmo::round3);
self.form
.args
.insert(key.to_string(), serde_json::json!(pos));
self.refresh_form(world);
}
pub(in crate::editor::hook) fn form_row_count(&self) -> usize {
let extras = self
.form
.extras
.as_ref()
.map_or(0, |x| x.rows(&self.extras_cx("")).len());
self.form.fields.len() + extras
}
pub(in crate::editor::hook) fn commit_with_extras(
&mut self,
ty: &str,
typed: &str,
args: serde_json::Value,
files: Vec<NewFile>,
) {
let Some(mut extras) = self.form.extras.take() else {
return;
};
if extras.hold(self) {
self.form.extras = Some(extras);
return;
}
let committed = self.commit_form_entry(ty, typed, args);
extras.edit_entries(&mut self.entries, &committed);
let stood = self.entries.changed_read_only(&self.baseline).is_none();
let written = stood && self.write_new_files(&files);
if !stood {
self.mark_changed();
} else if !written {
self.entries = self.baseline.clone();
}
if !written {
self.form.extras = Some(extras);
return;
}
self.mark_changed();
extras.after(self, &committed);
self.form.close();
}
fn commit_form_entry(&mut self, ty: &str, typed: &str, args: serde_json::Value) -> Committed {
match self.form.target.entry() {
Some(key) => {
let renamed = self.rename_entry(key, typed);
if let Some(obj) = self.entries.by_key_mut(key).and_then(|e| e.as_object_mut()) {
obj.insert(
"args".to_string(),
with_optional_id(args, renamed.name.as_deref()),
);
}
Committed {
key,
before: renamed.before,
name: renamed.name,
moved: renamed.moved,
}
}
None => {
let name = self.finalize_name(typed);
let key = self.entries.push(serde_json::json!({
"type": ty, "args": with_optional_id(args, name.as_deref()),
}));
Committed {
key,
before: None,
name,
moved: 0,
}
}
}
}
fn write_new_files(&mut self, files: &[NewFile]) -> bool {
for file in files {
let written = file
.path
.parent()
.map_or(Ok(()), std::fs::create_dir_all)
.and_then(|()| std::fs::write(&file.path, &file.text));
if let Err(e) = written {
self.form.error = Some(format!("Could not write {}: {e}", file.path.display()));
return false;
}
}
true
}
}
pub(in crate::editor::hook) fn with_optional_id(
args: serde_json::Value,
id: Option<&str>,
) -> serde_json::Value {
match id {
Some(id) => concinnity_cook::authoring::world::args_with_id(args, id),
None => args,
}
}