Skip to content

Commit 5201c04

Browse files
authored
egui: add redo support to Undoer (#3478)
* Closes #3447 * Closes #3448 Better implementation than #3448. (by accident since I did not see that PR)
1 parent c0b14f4 commit 5201c04

File tree

2 files changed

+50
-6
lines changed

2 files changed

+50
-6
lines changed

crates/egui/src/util/undoer.rs

+30-4
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,22 @@ pub struct Undoer<State> {
5757
/// The latest undo point may (often) be the current state.
5858
undos: VecDeque<State>,
5959

60+
/// Stores redos immediately after a sequence of undos.
61+
/// Gets cleared every time the state changes.
62+
/// Does not need to be a deque, because there can only be up to undos.len() redos,
63+
/// which is already limited to settings.max_undos.
64+
redos: Vec<State>,
65+
6066
#[cfg_attr(feature = "serde", serde(skip))]
6167
flux: Option<Flux<State>>,
6268
}
6369

6470
impl<State> std::fmt::Debug for Undoer<State> {
6571
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66-
let Self { undos, .. } = self;
72+
let Self { undos, redos, .. } = self;
6773
f.debug_struct("Undoer")
6874
.field("undo count", &undos.len())
75+
.field("redo count", &redos.len())
6976
.finish()
7077
}
7178
}
@@ -91,6 +98,10 @@ where
9198
}
9299
}
93100

101+
pub fn has_redo(&self, current_state: &State) -> bool {
102+
!self.redos.is_empty() && self.undos.back() == Some(current_state)
103+
}
104+
94105
/// Return true if the state is currently changing
95106
pub fn is_in_flux(&self) -> bool {
96107
self.flux.is_some()
@@ -101,7 +112,9 @@ where
101112
self.flux = None;
102113

103114
if self.undos.back() == Some(current_state) {
104-
self.undos.pop_back();
115+
self.redos.push(self.undos.pop_back().unwrap());
116+
} else {
117+
self.redos.push(current_state.clone());
105118
}
106119

107120
// Note: we keep the undo point intact.
@@ -111,9 +124,20 @@ where
111124
}
112125
}
113126

127+
pub fn redo(&mut self, current_state: &State) -> Option<&State> {
128+
if !self.undos.is_empty() && self.undos.back() != Some(current_state) {
129+
// state changed since the last undo, redos should be cleared.
130+
self.redos.clear();
131+
None
132+
} else if let Some(state) = self.redos.pop() {
133+
self.undos.push_back(state);
134+
self.undos.back()
135+
} else {
136+
None
137+
}
138+
}
139+
114140
/// Add an undo point if, and only if, there has been a change since the latest undo point.
115-
///
116-
/// * `time`: current time in seconds.
117141
pub fn add_undo(&mut self, current_state: &State) {
118142
if self.undos.back() != Some(current_state) {
119143
self.undos.push_back(current_state.clone());
@@ -139,6 +163,8 @@ where
139163
if latest_undo == current_state {
140164
self.flux = None;
141165
} else {
166+
self.redos.clear();
167+
142168
match self.flux.as_mut() {
143169
None => {
144170
self.flux = Some(Flux {

crates/egui/src/widgets/text_edit/builder.rs

+20-2
Original file line numberDiff line numberDiff line change
@@ -986,8 +986,7 @@ fn events(
986986
pressed: true,
987987
modifiers,
988988
..
989-
} if modifiers.command && !modifiers.shift => {
990-
// TODO(emilk): redo
989+
} if modifiers.matches(Modifiers::COMMAND) => {
991990
if let Some((undo_ccursor_range, undo_txt)) = state
992991
.undoer
993992
.lock()
@@ -999,6 +998,25 @@ fn events(
999998
None
1000999
}
10011000
}
1001+
Event::Key {
1002+
key,
1003+
pressed: true,
1004+
modifiers,
1005+
..
1006+
} if (modifiers.matches(Modifiers::COMMAND) && *key == Key::Y)
1007+
|| (modifiers.matches(Modifiers::SHIFT | Modifiers::COMMAND) && *key == Key::Z) =>
1008+
{
1009+
if let Some((redo_ccursor_range, redo_txt)) = state
1010+
.undoer
1011+
.lock()
1012+
.redo(&(cursor_range.as_ccursor_range(), text.as_str().to_owned()))
1013+
{
1014+
text.replace(redo_txt);
1015+
Some(*redo_ccursor_range)
1016+
} else {
1017+
None
1018+
}
1019+
}
10021020

10031021
Event::Key {
10041022
key,

0 commit comments

Comments
 (0)