Implement undo for TextEdit
This commit is contained in:
parent
83444af862
commit
b17e6b3260
3 changed files with 215 additions and 3 deletions
|
@ -3,6 +3,7 @@
|
||||||
pub(crate) mod cache;
|
pub(crate) mod cache;
|
||||||
mod history;
|
mod history;
|
||||||
pub mod mutex;
|
pub mod mutex;
|
||||||
|
pub mod undoer;
|
||||||
|
|
||||||
pub(crate) use cache::Cache;
|
pub(crate) use cache::Cache;
|
||||||
pub use history::History;
|
pub use history::History;
|
||||||
|
|
172
egui/src/util/undoer.rs
Normal file
172
egui/src/util/undoer.rs
Normal file
|
@ -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<State> {
|
||||||
|
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<State>,
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
|
flux: Option<Flux<State>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State> std::fmt::Debug for Undoer<State> {
|
||||||
|
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<State> {
|
||||||
|
start_time: f64,
|
||||||
|
latest_change_time: f64,
|
||||||
|
latest_state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State> Undoer<State>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
pub(crate) struct State {
|
pub(crate) struct State {
|
||||||
cursorp: Option<CursorPair>,
|
cursorp: Option<CursorPair>,
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
|
undoer: Undoer<(CCursorPair, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[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 {
|
fn is_empty(&self) -> bool {
|
||||||
self.primary.ccursor == self.secondary.ccursor
|
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))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
struct CCursorPair {
|
struct CCursorPair {
|
||||||
/// When selecting with a mouse, this is where the mouse was released.
|
/// 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()));
|
.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 {
|
for event in &ui.input().events {
|
||||||
let did_mutate_text = match event {
|
let did_mutate_text = match event {
|
||||||
Event::Copy => {
|
Event::Copy => {
|
||||||
|
@ -370,11 +386,29 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
ui.memory().surrender_kb_focus(id);
|
ui.memory().surrender_kb_focus(id);
|
||||||
break;
|
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 {
|
Event::Key {
|
||||||
key,
|
key,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
modifiers,
|
modifiers,
|
||||||
} => on_key_press(&mut cursorp, text, &galley, *key, modifiers),
|
} => on_key_press(&mut cursorp, text, &galley, *key, modifiers),
|
||||||
|
|
||||||
Event::Key { .. } => None,
|
Event::Key { .. } => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -395,6 +429,10 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.cursorp = Some(cursorp);
|
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());
|
.unwrap_or_else(|| ui.style().visuals.widgets.inactive.text_color());
|
||||||
ui.painter()
|
ui.painter()
|
||||||
.galley(response.rect.min, galley, text_style, text_color);
|
.galley(response.rect.min, galley, text_style, text_color);
|
||||||
|
|
||||||
ui.memory().text_edit.insert(id, state);
|
ui.memory().text_edit.insert(id, state);
|
||||||
|
|
||||||
Response {
|
Response {
|
||||||
|
|
Loading…
Reference in a new issue