nightshade-api 0.48.0

Procedural high level API for the nightshade game engine
Documentation
//! A reusable undo and redo stack over the component snapshots in
//! [`reflect`](crate::reflect). A tool that edits a scene captures the state it
//! is about to change, commits a transaction, and can step back and forth. This
//! is the stack the standalone editor builds on the same primitives.

use crate::reflect::{ComponentSnapshot, SnapshotKind, restore_component, snapshot_component};
use nightshade::prelude::{Entity, World};

struct Change {
    entity: Entity,
    kind: SnapshotKind,
    snapshot: ComponentSnapshot,
}

/// A dual stack of edit transactions. Each transaction batches the before-state
/// of every component it touched, so one undo reverses a whole edit and one redo
/// reapplies it.
#[derive(Default)]
pub struct UndoStack {
    undo: Vec<Vec<Change>>,
    redo: Vec<Vec<Change>>,
    pending: Vec<Change>,
}

impl UndoStack {
    /// A fresh empty stack.
    pub fn new() -> Self {
        Self::default()
    }

    /// Captures an entity's current value for `kind` into the open transaction,
    /// before applying the change. Call once per component about to change, then
    /// make the change, then [`commit`](Self::commit).
    pub fn capture(&mut self, world: &World, entity: Entity, kind: SnapshotKind) {
        if let Some(snapshot) = snapshot_component(world, entity, kind) {
            self.pending.push(Change {
                entity,
                kind,
                snapshot,
            });
        }
    }

    /// Closes the open transaction and pushes it onto the undo stack, clearing
    /// the redo stack since a new edit invalidates any redo history. A commit
    /// with nothing captured is a no-op.
    pub fn commit(&mut self) {
        if self.pending.is_empty() {
            return;
        }
        let transaction = std::mem::take(&mut self.pending);
        self.undo.push(transaction);
        self.redo.clear();
    }

    /// Reverses the most recent committed transaction, restoring every component
    /// it captured and recording the current state for [`redo`](Self::redo).
    /// Returns false when there is nothing to undo.
    pub fn undo(&mut self, world: &mut World) -> bool {
        let Some(transaction) = self.undo.pop() else {
            return false;
        };
        let mut redo_transaction = Vec::with_capacity(transaction.len());
        for change in &transaction {
            if let Some(current) = snapshot_component(world, change.entity, change.kind) {
                redo_transaction.push(Change {
                    entity: change.entity,
                    kind: change.kind,
                    snapshot: current,
                });
            }
            restore_component(&change.snapshot, world, change.entity);
        }
        self.redo.push(redo_transaction);
        true
    }

    /// Reapplies the most recently undone transaction. Returns false when there
    /// is nothing to redo.
    pub fn redo(&mut self, world: &mut World) -> bool {
        let Some(transaction) = self.redo.pop() else {
            return false;
        };
        let mut undo_transaction = Vec::with_capacity(transaction.len());
        for change in &transaction {
            if let Some(current) = snapshot_component(world, change.entity, change.kind) {
                undo_transaction.push(Change {
                    entity: change.entity,
                    kind: change.kind,
                    snapshot: current,
                });
            }
            restore_component(&change.snapshot, world, change.entity);
        }
        self.undo.push(undo_transaction);
        true
    }

    /// Whether an undo is available.
    pub fn can_undo(&self) -> bool {
        !self.undo.is_empty()
    }

    /// Whether a redo is available.
    pub fn can_redo(&self) -> bool {
        !self.redo.is_empty()
    }

    /// Drops all history and any open transaction.
    pub fn clear(&mut self) {
        self.undo.clear();
        self.redo.clear();
        self.pending.clear();
    }
}