diff --git a/egui/src/util/mod.rs b/egui/src/util/mod.rs index f982d99d..e780c6c3 100644 --- a/egui/src/util/mod.rs +++ b/egui/src/util/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod cache; mod history; pub mod mutex; +pub mod undoer; pub(crate) use cache::Cache; pub use history::History; diff --git a/egui/src/util/undoer.rs b/egui/src/util/undoer.rs new file mode 100644 index 00000000..ec57173c --- /dev/null +++ b/egui/src/util/undoer.rs @@ -0,0 +1,172 @@ +use std::collections::VecDeque; + +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct Settings { + /// Maximum number of undos. + /// If your state is resource intensive, you should keep this low. + /// + /// Default: `100` + pub max_undos: usize, + + /// When that state hasn't changed for this many seconds, + /// create a new undo point (if one is needed). + /// + /// Default value: `1.0` seconds. + pub stable_time: f32, + + /// If the state is changing so often that we never get to `stable_time`, + /// then still create a save point every `auto_save_interval` seconds, + /// so we have something to undo to. + /// + /// Default value: `30` seconds. + pub auto_save_interval: f32, +} + +impl Default for Settings { + fn default() -> Self { + Self { + max_undos: 100, + stable_time: 1.0, + auto_save_interval: 30.0, + } + } +} + +/// Automatic undo system. +/// +/// Every frame you feed it the most recent state. +/// The `Undoer` compares it with the latest undo point +/// and if there is a change it may create a new undo point. +/// +/// `Undoer` follows two simple rules: +/// +/// 1) If the state has changed since the latest undo point, but has +/// remained stable for `stable_time` seconds, an new undo point is created. +/// 2) If the state does not stabilize within `auto_save_interval` seconds, an undo point is created. +/// +/// Rule 1) will make sure an undo point is not created until you _stop_ dragging that slider. +/// Rule 2) will make sure that you will get some undo points even if you are constantly changing the state. +#[derive(Clone, Default)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct Undoer { + settings: Settings, + + /// New undoes are added to the back. + /// Two adjacent undo points are never equal. + /// The latest undo point may (often) be the current state. + undos: VecDeque, + + #[cfg_attr(feature = "serde", serde(skip))] + flux: Option>, +} + +impl std::fmt::Debug for Undoer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { undos, .. } = self; + f.debug_struct("Undoer") + .field("undo count", &undos.len()) + .finish() + } +} + +/// Represents how the current state is changing +#[derive(Clone)] +struct Flux { + start_time: f64, + latest_change_time: f64, + latest_state: State, +} + +impl Undoer +where + State: Clone + PartialEq, +{ + /// Do we have an undo point different from the given state? + pub fn has_undo(&self, current_state: &State) -> bool { + match self.undos.len() { + 0 => false, + 1 => self.undos.back() != Some(current_state), + _ => true, + } + } + + /// Return true if the state is currently changing + pub fn is_in_flux(&self) -> bool { + self.flux.is_some() + } + + pub fn undo(&mut self, current_state: &State) -> Option<&State> { + if self.has_undo(current_state) { + self.flux = None; + + if self.undos.back() == Some(current_state) { + self.undos.pop_back(); + } + + // Note: we keep the undo point intact. + self.undos.back() + } else { + None + } + } + + /// Add an undo point if, and only if, there has been a change since the latest undo point. + /// + /// * `time`: current time in seconds. + pub fn add_undo(&mut self, current_state: &State) { + if self.undos.back() != Some(current_state) { + self.undos.push_back(current_state.clone()); + } + while self.undos.len() > self.settings.max_undos { + self.undos.pop_front(); + } + self.flux = None; + } + + /// Call this as often as you want (e.g. every frame) + /// and `Undoer` will determine if a new undo point should be created. + /// + /// * `current_time`: current time in seconds. + pub fn feed_state(&mut self, current_time: f64, current_state: &State) { + match self.undos.back() { + None => { + // First time feed_state is called. + // always create an undo point: + self.add_undo(current_state); + } + Some(latest_undo) => { + if latest_undo == current_state { + self.flux = None; + } else { + match self.flux.as_mut() { + None => { + self.flux = Some(Flux { + start_time: current_time, + latest_change_time: current_time, + latest_state: current_state.clone(), + }); + } + Some(flux) => { + if &flux.latest_state == current_state { + let time_since_latest_change = + (current_time - flux.latest_change_time) as f32; + if time_since_latest_change >= self.settings.stable_time { + self.add_undo(current_state); + } + } else { + let time_since_flux_start = (current_time - flux.start_time) as f32; + if time_since_flux_start >= self.settings.auto_save_interval { + self.add_undo(current_state); + } else { + flux.latest_change_time = current_time; + flux.latest_state = current_state.clone(); + } + } + } + } + } + } + } + } +} diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index 04821e40..2a8378e4 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -1,10 +1,13 @@ -use crate::{paint::*, *}; +use crate::{paint::*, util::undoer::Undoer, *}; -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Debug, Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub(crate) struct State { cursorp: Option, + + #[cfg_attr(feature = "serde", serde(skip))] + undoer: Undoer<(CCursorPair, String)>, } #[derive(Clone, Copy, Debug, Default)] @@ -35,6 +38,13 @@ impl CursorPair { } } + fn as_ccursorp(&self) -> CCursorPair { + CCursorPair { + primary: self.primary.ccursor, + secondary: self.secondary.ccursor, + } + } + fn is_empty(&self) -> bool { self.primary.ccursor == self.secondary.ccursor } @@ -64,7 +74,7 @@ impl CursorPair { } } -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct CCursorPair { /// When selecting with a mouse, this is where the mouse was released. @@ -315,6 +325,12 @@ impl<'t> Widget for TextEdit<'t> { }) .unwrap_or_else(|| CursorPair::one(galley.end())); + // We feed state to the undoer both before and after handling input + // so that the undoer creates automatic saves even when there are no events for a while. + state + .undoer + .feed_state(ui.input().time, &(cursorp.as_ccursorp(), text.clone())); + for event in &ui.input().events { let did_mutate_text = match event { Event::Copy => { @@ -370,11 +386,29 @@ impl<'t> Widget for TextEdit<'t> { ui.memory().surrender_kb_focus(id); break; } + + Event::Key { + key: Key::Z, + pressed: true, + modifiers, + } if modifiers.command && !modifiers.shift => { + // TODO: redo + if let Some((undo_ccursorp, undo_txt)) = + state.undoer.undo(&(cursorp.as_ccursorp(), text.clone())) + { + *text = undo_txt.clone(); + Some(*undo_ccursorp) + } else { + None + } + } + Event::Key { key, pressed: true, modifiers, } => on_key_press(&mut cursorp, text, &galley, *key, modifiers), + Event::Key { .. } => None, }; @@ -395,6 +429,10 @@ impl<'t> Widget for TextEdit<'t> { } } state.cursorp = Some(cursorp); + + state + .undoer + .feed_state(ui.input().time, &(cursorp.as_ccursorp(), text.clone())); } { @@ -425,6 +463,7 @@ impl<'t> Widget for TextEdit<'t> { .unwrap_or_else(|| ui.style().visuals.widgets.inactive.text_color()); ui.painter() .galley(response.rect.min, galley, text_style, text_color); + ui.memory().text_edit.insert(id, state); Response {