Skip to content

Initial check-in of bevy_undo.#23373

Draft
viridia wants to merge 5 commits intobevyengine:mainfrom
viridia:bevy_undo
Draft

Initial check-in of bevy_undo.#23373
viridia wants to merge 5 commits intobevyengine:mainfrom
viridia:bevy_undo

Conversation

@viridia
Copy link
Contributor

@viridia viridia commented Mar 15, 2026

Objective

Fixes #23354

Solution

A basic undo / redo framework (WiP)

Testing

Unit tests so far.

@github-actions
Copy link
Contributor

You added a new feature but didn't update the readme. Please run cargo run -p build-templated-pages -- update features to update it, and commit the file change.

@alice-i-cecile alice-i-cecile added A-Editor Graphical tools to make Bevy games X-Needs-SME This type of work requires an SME to approve it. M-Release-Note Work that should be called out in the blog due to impact S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Mar 15, 2026
@alice-i-cecile alice-i-cecile self-requested a review March 15, 2026 21:32
@alice-i-cecile alice-i-cecile added the A-UI Graphical user interfaces, styles, layouts, and widgets label Mar 15, 2026
@github-project-automation github-project-automation bot moved this to Needs SME Triage in UI Mar 15, 2026
@andriyDev
Copy link
Contributor

bevy_undo already exists as a crate. Is this an upstreaming? Did we get publish permission for this crate?

@viridia
Copy link
Contributor Author

viridia commented Mar 16, 2026

bevy_undo already exists as a crate. Is this an upstreaming? Did we get publish permission for this crate?

There is an existing crate, bevy-undo with a dash instead of an underscore. There is no bevy_undo on crates.io. I don't know enough about crates.io to know whether or not this will cause a conflict.

The bevy-undo crate is deprecated, and the link to the repository is a dead link.

@viridia
Copy link
Contributor Author

viridia commented Mar 16, 2026

OK so it looks like crates.io treats dashes and underscores as equivalent, so there will in fact be a conflict. We have three choices:

  • Pick a different name (commence bikeshedding)
  • Try to contact the crate owner
  • Put the undo code in bevy_ui or somewhere else instead of making it a separate crate.

@andriyDev
Copy link
Contributor

Another day, another "why the heck did they allow dash in crate names if it's not allowed in the language".

I feel strongly that making it a separate crate is the correct move, so my preference is try to contact the crate owner then pick a different name.

If we want to make it fun, I propose bevy_forwardback, or less fun bevy_history. Both of these are much less obvious than "undo" though haha.

Copy link
Contributor

@andriyDev andriyDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like how hyper focused on editing UI elements this is, and I'm not even sure it's sufficient for that.

I've tried implementing my own undo system before, and the problem is always entities. We need a way to map entities before and after an undo. If an action is spawn an entity, then undoing that action should despawn that entity, and redoing that action should spawn a new entity with the same components. However that also means we need to fix all the references in the world we can find.

I don't think this is something we can leave to implementors, or patch on later.

Maybe I'm ballooning the scope here though? idk. If the idea is to never deal with entities directly (e.g., only touch BSN or something), then maybe it'll be fine?

/// a new [`UndoAction`] that represents the combination of the two actions, which will replace
/// the current top of stack; otherwise, no merge will occur and the new action will be pushed
/// separately.
fn coalesce(&self, prev: &dyn UndoAction) -> Option<Box<dyn UndoAction>>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should take these as owned, and then have the return type return Result<Box<dyn UndoAction>, (Box<dyn UndoAction>, Box<dyn UndoAction>)>. It is definitely a more complex signature, but consider that the contents of these undo actions might be "every component that included a particular Entity" since we need to rename the Entity.

/// Apply the action. This undoes the modification to the app state. This consumes the action,
/// and returns an inverse action, one that reverses the changes. So an "undo" yields a
/// "redo" and vice versa.
fn apply(self: Box<Self>, world: &mut World) -> Box<dyn UndoAction>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this is not sufficient. For example, if your undo action spawns an entity (you previously despawned the entity), you also need to modify every other undo action to point at the new entity, else the references will be broken.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm. I haven't run into this problem before, because all my editors edit assets, not entities (although some assets produce entities when loaded, the entities are not the source of truth). Still, worth thinking about.

Can you talk more about your previous experience? What kinds of entities were you undoing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to make a generic undo/redo system (which ideally would be the case for a Bevy official undo/redo system). So it could be any kind of entity, including one with references.

In particular, I was trying to make an editor whose workflow was: spawn entities, add components to those entities, and then serialize those entities. Loading the editor would just mean deserializing those entities and spawning them. But that also means the entities are effectively the SOT.

@github-actions
Copy link
Contributor

You added a new feature but didn't update the readme. Please run cargo run -p build-templated-pages -- update features to update it, and commit the file change.


/// Push a new [`UndoAction`] on to the undo stack. This also clears the redo stack.
/// This should be done whenever we make a change to the app's document state.
pub fn push(&mut self, ctx: &ContextId, action: Box<dyn UndoAction>) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we apply the action to the world first, then push the inverse onto the undo stack?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends on whether you are using snapshot or delta style. For a snapshot, you'll want to push the current undo state first.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense! Thanks for the clarification

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Editor Graphical tools to make Bevy games A-UI Graphical user interfaces, styles, layouts, and widgets M-Release-Note Work that should be called out in the blog due to impact S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged X-Needs-SME This type of work requires an SME to approve it.

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

Undo Framework

4 participants