use ponko_ecs::Entity;
use serde_json::{Value, json};
use crate::{Ctx, Vec2, WorldReflect};
const MAX_UNDO: usize = 200;
#[derive(Debug)]
enum Undo {
Component {
e: Entity,
name: String,
before: Option<Value>,
},
Spawned(Entity),
Despawned { e: Entity, components: Value },
}
#[derive(Default)]
pub(crate) struct Editor {
undo: Vec<Undo>,
played_from: Option<(Value, usize)>,
}
impl Editor {
pub fn playing(&self) -> bool {
self.played_from.is_some()
}
pub fn play(&mut self, ctx: &mut Ctx) {
if self.played_from.is_none() {
self.played_from = Some((ctx.world.save(), self.undo.len()));
}
ctx.resume();
}
pub fn stop(&mut self, ctx: &mut Ctx) -> Result<(), String> {
ctx.pause();
let Some((save, steps)) = self.played_from.take() else {
return Ok(());
};
self.undo.truncate(steps);
ctx.load_scene_assets(&save)?;
ctx.world.load(&save).map_err(|e| e.to_string())
}
pub fn keep(&mut self) {
self.played_from = None;
}
pub fn can_undo(&self) -> bool {
!self.undo.is_empty()
}
fn record(&mut self, step: Undo) {
if self.undo.len() == MAX_UNDO {
self.undo.remove(0);
}
self.undo.push(step);
}
pub fn set(&mut self, ctx: &mut Ctx, e: Entity, name: &str, value: Value) -> Result<(), String> {
let before = ctx.world.get_json(e, name).ok();
ctx.load_scene_assets(&json!({ name: &value }))?;
ctx.world.set_json(e, name, value).map_err(|e| e.to_string())?;
self.record(Undo::Component {
e,
name: name.to_string(),
before,
});
Ok(())
}
pub fn add(&mut self, ctx: &mut Ctx, e: Entity, name: &str) -> Result<(), String> {
self.set(ctx, e, name, json!({}))
}
pub fn remove(&mut self, ctx: &mut Ctx, e: Entity, name: &str) -> Result<(), String> {
let before = ctx.world.get_json(e, name).map_err(|e| e.to_string())?;
ctx.world.remove_json(e, name).map_err(|e| e.to_string())?;
self.record(Undo::Component {
e,
name: name.to_string(),
before: Some(before),
});
Ok(())
}
pub fn moved(&mut self, e: Entity, before: Value) {
self.record(Undo::Component {
e,
name: "Transform2D".into(),
before: Some(before),
});
}
pub fn spawn_empty(&mut self, ctx: &mut Ctx, at: Vec2) -> Entity {
let e = ctx.world.spawn((crate::Transform2D::at(at),));
self.record(Undo::Spawned(e));
e
}
pub fn spawn_prefab(&mut self, ctx: &mut Ctx, path: &str, at: Vec2) -> Result<Entity, String> {
let e = ctx.spawn_prefab(path, at)?;
self.record(Undo::Spawned(e));
Ok(e)
}
pub fn duplicate(&mut self, ctx: &mut Ctx, e: Entity, offset: Vec2) -> Result<Entity, String> {
let mut components = ctx.world.entity_json(e).map_err(|e| e.to_string())?;
if let Some(p) = components["Transform2D"]["position"].as_array_mut()
&& let [x, y] = &mut p[..]
{
*x = json!(x.as_f64().unwrap_or(0.0) + f64::from(offset.x));
*y = json!(y.as_f64().unwrap_or(0.0) + f64::from(offset.y));
}
let copy = ctx.world.spawn_json(components).map_err(|e| e.to_string())?;
self.record(Undo::Spawned(copy));
Ok(copy)
}
pub fn delete(&mut self, ctx: &mut Ctx, e: Entity) -> Result<(), String> {
let components = ctx.world.entity_json(e).map_err(|e| e.to_string())?;
ctx.world.despawn(e);
if components.as_object().is_some_and(|c| !c.is_empty()) {
self.record(Undo::Despawned { e, components });
}
Ok(())
}
pub fn undo(&mut self, ctx: &mut Ctx) -> Result<bool, String> {
let Some(step) = self.undo.pop() else {
return Ok(false);
};
match step {
Undo::Component { e, name, before } => {
match before {
Some(value) => ctx.world.set_json(e, &name, value).map(drop),
None => ctx.world.remove_json(e, &name).map(drop),
}
.map_err(|e| e.to_string())?;
}
Undo::Spawned(e) => {
ctx.world.despawn(e);
}
Undo::Despawned { e, components } => {
let back = ctx.world.spawn_json(components).map_err(|e| e.to_string())?;
for step in &mut self.undo {
match step {
Undo::Component { e: old, .. } | Undo::Spawned(old) | Undo::Despawned { e: old, .. } => {
if *old == e {
*old = back;
}
}
}
}
}
}
Ok(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Transform2D, vec2};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Default, Clone, Copy, PartialEq, Debug)]
#[serde(default)]
struct Hp(f32);
fn ctx() -> Ctx {
let mut ctx = Ctx::new(1.0 / 60.0, vec2(800.0, 600.0));
ctx.world.reflect::<Hp>();
ctx
}
fn at(ctx: &Ctx, e: Entity) -> Vec2 {
ctx.world.get::<Transform2D>(e).unwrap().position
}
#[test]
fn every_edit_is_undone_in_reverse_order() {
let mut ctx = ctx();
let mut ed = Editor::default();
let a = ed.spawn_empty(&mut ctx, vec2(1.0, 1.0));
ed.set(&mut ctx, a, "Hp", json!(5.0)).unwrap();
let b = ed.duplicate(&mut ctx, a, vec2(10.0, 0.0)).unwrap();
assert_eq!((at(&ctx, b), ctx.world.get::<Hp>(b)), (vec2(11.0, 1.0), Some(&Hp(5.0))));
ed.remove(&mut ctx, a, "Hp").unwrap();
ed.delete(&mut ctx, b).unwrap();
assert_eq!(ctx.world.len(), 1);
assert!(ed.undo(&mut ctx).unwrap()); assert!(!ctx.world.is_alive(b), "old handles stay dead");
let back = ctx
.world
.query::<(Entity, &Hp)>()
.iter()
.map(|(e, _)| e)
.next()
.unwrap();
assert_eq!(at(&ctx, back), vec2(11.0, 1.0));
assert!(ed.undo(&mut ctx).unwrap()); assert_eq!(ctx.world.get::<Hp>(a), Some(&Hp(5.0)));
assert!(ed.undo(&mut ctx).unwrap()); assert!(!ctx.world.is_alive(back));
assert!(ed.undo(&mut ctx).unwrap()); assert_eq!(ctx.world.get::<Hp>(a), None);
assert!(ed.undo(&mut ctx).unwrap()); assert_eq!(ctx.world.len(), 0);
assert!(!ed.undo(&mut ctx).unwrap());
}
#[test]
fn a_drag_undoes_in_one_step() {
let mut ctx = ctx();
let mut ed = Editor::default();
let e = ctx.world.spawn((Transform2D::at(vec2(1.0, 2.0)),));
let before = ctx.world.get_json(e, "Transform2D").unwrap();
for x in 0..10 {
ctx.world.get_mut::<Transform2D>(e).unwrap().position.x = x as f32; }
ed.moved(e, before);
ed.undo(&mut ctx).unwrap();
assert_eq!(at(&ctx, e), vec2(1.0, 2.0));
}
#[test]
fn stop_puts_the_world_back_as_it_was_at_play() {
let mut ctx = ctx();
let mut ed = Editor::default();
let e = ed.spawn_empty(&mut ctx, vec2(1.0, 2.0));
ed.play(&mut ctx);
assert!(ed.playing() && !ctx.is_paused());
ctx.world.get_mut::<Transform2D>(e).unwrap().position = vec2(50.0, 50.0);
ctx.world.spawn((Transform2D::default(),));
ed.set(&mut ctx, e, "Hp", json!(1.0)).unwrap();
ctx.pause();
ed.play(&mut ctx); ed.stop(&mut ctx).unwrap();
assert!(ctx.is_paused() && !ed.playing());
assert_eq!(ctx.world.len(), 1);
assert_eq!((at(&ctx, e), ctx.world.get::<Hp>(e)), (vec2(1.0, 2.0), None));
assert!(ed.undo(&mut ctx).unwrap());
assert_eq!(ctx.world.len(), 0);
assert!(!ed.can_undo());
}
#[test]
fn keep_makes_stop_do_nothing() {
let mut ctx = ctx();
let mut ed = Editor::default();
ed.play(&mut ctx);
ctx.world.spawn((Transform2D::default(),));
ed.keep();
ed.stop(&mut ctx).unwrap();
assert_eq!(ctx.world.len(), 1);
}
#[test]
fn a_failed_edit_changes_nothing_and_is_not_recorded() {
let mut ctx = ctx();
let mut ed = Editor::default();
let e = ctx.world.spawn((Transform2D::default(),));
assert!(ed.set(&mut ctx, e, "Hp", json!("not a number")).is_err());
assert!(ed.add(&mut ctx, e, "NoSuchThing").is_err());
assert!(!ed.can_undo());
}
}