From 46ff43ff36b97fbf1a3c1c2b251db8e5457ddb68 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 16 Aug 2024 17:04:11 +0800 Subject: [PATCH] Extract `History` to a common design to support more cases. (#162) Ref https://github.com/huacnlee/gpui-component/issues/161 For example: Used to do Back/Forward --- crates/ui/src/history.rs | 192 +++++++++++++++++++++++++++++++++ crates/ui/src/input/change.rs | 39 +++++++ crates/ui/src/input/history.rs | 116 -------------------- crates/ui/src/input/input.rs | 15 ++- crates/ui/src/input/mod.rs | 2 +- crates/ui/src/lib.rs | 2 +- 6 files changed, 243 insertions(+), 123 deletions(-) create mode 100644 crates/ui/src/history.rs create mode 100644 crates/ui/src/input/change.rs delete mode 100644 crates/ui/src/input/history.rs diff --git a/crates/ui/src/history.rs b/crates/ui/src/history.rs new file mode 100644 index 00000000..45179945 --- /dev/null +++ b/crates/ui/src/history.rs @@ -0,0 +1,192 @@ +use std::{ + fmt::Debug, + time::{Duration, Instant}, +}; + +pub trait HistoryItem: Clone { + fn version(&self) -> usize; + fn set_version(&mut self, version: usize); +} + +#[derive(Debug)] +pub struct History { + undos: Vec, + redos: Vec, + last_changed_at: Instant, + version: usize, + pub(crate) ignore: bool, + max_undo: usize, + group_interval: Option, +} + +impl History +where + I: HistoryItem, +{ + pub fn new() -> Self { + Self { + undos: Default::default(), + redos: Default::default(), + ignore: false, + last_changed_at: Instant::now(), + version: 0, + max_undo: 1000, + group_interval: None, + } + } + + /// Set the maximum number of undo steps to keep, defaults to 1000. + pub fn max_undo(mut self, max_undo: usize) -> Self { + self.max_undo = max_undo; + self + } + + /// Set the interval in milliseconds to group changes, defaults to None. + pub fn group_interval(mut self, group_interval: Duration) -> Self { + self.group_interval = Some(group_interval); + self + } + + /// Increment the version number if the last change was made more than `GROUP_INTERVAL` milliseconds ago. + fn inc_version(&mut self) -> usize { + let t = Instant::now(); + if Some(self.last_changed_at.elapsed()) > self.group_interval { + self.version += 1; + } + + self.last_changed_at = t; + self.version + } + + /// Get the current version number. + pub fn version(&self) -> usize { + self.version + } + + pub fn push(&mut self, item: I) { + let version = self.inc_version(); + + if self.undos.len() >= self.max_undo { + self.undos.remove(0); + } + + let mut item = item; + item.set_version(version); + self.undos.push(item); + } + + pub fn undo(&mut self) -> Option> { + if let Some(first_change) = self.undos.pop() { + let mut changes = vec![first_change.clone()]; + // pick the next all changes with the same version + while self + .undos + .iter() + .filter(|c| c.version() == first_change.version()) + .count() + > 0 + { + let change = self.undos.pop().unwrap(); + changes.push(change); + } + + self.redos.extend(changes.iter().rev().cloned()); + Some(changes) + } else { + None + } + } + + pub fn redo(&mut self) -> Option> { + if let Some(first_change) = self.redos.pop() { + let mut changes = vec![first_change.clone()]; + // pick the next all changes with the same version + while self + .redos + .iter() + .filter(|c| c.version() == first_change.version()) + .count() + > 0 + { + let change = self.redos.pop().unwrap(); + changes.push(change); + } + self.undos.extend(changes.iter().rev().cloned()); + Some(changes) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Clone)] + struct TabIndex { + tab_index: usize, + version: usize, + } + + impl From for TabIndex { + fn from(value: usize) -> Self { + TabIndex { + tab_index: value, + version: 0, + } + } + } + + impl HistoryItem for TabIndex { + fn version(&self) -> usize { + self.version + } + fn set_version(&mut self, version: usize) { + self.version = version; + } + } + + #[test] + fn test_history() { + let mut history: History = History::new().max_undo(100); + history.push(0.into()); + history.push(3.into()); + history.push(2.into()); + history.push(1.into()); + + assert_eq!(history.version(), 4); + let changes = history.undo().unwrap(); + assert_eq!(changes.len(), 1); + assert_eq!(changes[0].tab_index, 1); + + let changes = history.undo().unwrap(); + assert_eq!(changes.len(), 1); + assert_eq!(changes[0].tab_index, 2); + + history.push(5.into()); + + let changes = history.redo().unwrap(); + assert_eq!(changes[0].tab_index, 2); + + let changes = history.redo().unwrap(); + assert_eq!(changes[0].tab_index, 1); + + let changes = history.undo().unwrap(); + assert_eq!(changes[0].tab_index, 1); + + let changes = history.undo().unwrap(); + assert_eq!(changes[0].tab_index, 2); + + let changes = history.undo().unwrap(); + assert_eq!(changes[0].tab_index, 5); + + let changes = history.undo().unwrap(); + assert_eq!(changes[0].tab_index, 3); + + let changes = history.undo().unwrap(); + assert_eq!(changes[0].tab_index, 0); + + assert_eq!(history.undo().is_none(), true); + } +} diff --git a/crates/ui/src/input/change.rs b/crates/ui/src/input/change.rs new file mode 100644 index 00000000..4cafb3f6 --- /dev/null +++ b/crates/ui/src/input/change.rs @@ -0,0 +1,39 @@ +use std::{fmt::Debug, ops::Range}; + +use crate::history::HistoryItem; + +#[derive(Debug, Clone)] +pub struct Change { + pub(crate) old_range: Range, + pub(crate) old_text: String, + pub(crate) new_range: Range, + pub(crate) new_text: String, + version: usize, +} + +impl Change { + pub fn new( + old_range: Range, + old_text: &str, + new_range: Range, + new_text: &str, + ) -> Self { + Self { + old_range, + old_text: old_text.to_string(), + new_range, + new_text: new_text.to_string(), + version: 0, + } + } +} + +impl HistoryItem for Change { + fn version(&self) -> usize { + self.version + } + + fn set_version(&mut self, version: usize) { + self.version = version; + } +} diff --git a/crates/ui/src/input/history.rs b/crates/ui/src/input/history.rs deleted file mode 100644 index 0ba1ee6b..00000000 --- a/crates/ui/src/input/history.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::{ - fmt::Debug, - ops::Range, - time::{Duration, Instant}, -}; - -const MAX_UNDO: usize = 1000; -/// Group interval in milliseconds -const GROUP_INTERVAL: u64 = 1000; - -#[derive(Debug)] -pub struct History { - undos: Vec, - redos: Vec, - last_changed_at: Instant, - version: usize, - pub(crate) ignore: bool, -} - -#[derive(Debug, Clone)] -pub struct Change { - pub(crate) old_range: Range, - pub(crate) old_text: String, - pub(crate) new_range: Range, - pub(crate) new_text: String, - version: usize, -} - -impl History { - pub fn new() -> Self { - Self { - undos: Default::default(), - redos: Default::default(), - ignore: false, - last_changed_at: Instant::now(), - version: 0, - } - } - - /// Increment the version number if the last change was made more than `GROUP_INTERVAL` milliseconds ago. - fn inc_version(&mut self) -> usize { - let t = Instant::now(); - if self.last_changed_at.elapsed().as_millis() - > Duration::from_millis(GROUP_INTERVAL).as_millis() - { - self.version += 1; - } - - self.last_changed_at = t; - self.version - } - - pub fn push( - &mut self, - old_range: Range, - old_text: &str, - new_range: Range, - new_text: &str, - ) { - let version = self.inc_version(); - - if self.undos.len() >= MAX_UNDO { - self.undos.remove(0); - } - self.undos.push(Change { - old_range, - old_text: old_text.to_string(), - new_range, - new_text: new_text.to_string(), - version, - }); - } - - pub fn undo(&mut self) -> Option> { - if let Some(first_change) = self.undos.pop() { - let mut changes = vec![first_change.clone()]; - // pick the next all changes with the same version - while self - .undos - .iter() - .filter(|c| c.version == first_change.version) - .count() - > 0 - { - let change = self.undos.pop().unwrap(); - changes.push(change); - } - - self.redos.extend(changes.iter().rev().cloned()); - Some(changes) - } else { - None - } - } - - pub fn redo(&mut self) -> Option> { - if let Some(first_change) = self.redos.pop() { - let mut changes = vec![first_change.clone()]; - // pick the next all changes with the same version - while self - .redos - .iter() - .filter(|c| c.version == first_change.version) - .count() - > 0 - { - let change = self.redos.pop().unwrap(); - changes.push(change); - } - self.undos.extend(changes.iter().rev().cloned()); - Some(changes) - } else { - None - } - } -} diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index 3485e1d9..ca1e955a 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -6,8 +6,9 @@ use std::ops::Range; use super::blink_cursor::BlinkCursor; -use super::history::History; +use super::change::Change; use super::ClearButton; +use crate::history::History; use crate::indicator::Indicator; use crate::theme::ActiveTheme; use crate::StyledExt as _; @@ -110,7 +111,7 @@ pub fn init(cx: &mut AppContext) { pub struct TextInput { focus_handle: FocusHandle, text: SharedString, - history: History, + history: History, blink_cursor: Model, prefix: Option) -> AnyElement + 'static>>, suffix: Option) -> AnyElement + 'static>>, @@ -138,7 +139,7 @@ impl TextInput { pub fn new(cx: &mut ViewContext) -> Self { let focus_handle = cx.focus_handle(); let blink_cursor = cx.new_model(|_| BlinkCursor::new()); - let history = History::new(); + let history = History::new().group_interval(std::time::Duration::from_secs(1)); let input = Self { focus_handle: focus_handle.clone(), text: "".into(), @@ -445,8 +446,12 @@ impl TextInput { let new_range = range.start..range.start + new_text.len(); - self.history - .push(range.clone(), &old_text, new_range, new_text); + self.history.push(Change::new( + range.clone(), + &old_text, + new_range.clone(), + new_text, + )); } fn undo(&mut self, _: &Undo, cx: &mut ViewContext) { diff --git a/crates/ui/src/input/mod.rs b/crates/ui/src/input/mod.rs index 17fbc6fd..41312e06 100644 --- a/crates/ui/src/input/mod.rs +++ b/crates/ui/src/input/mod.rs @@ -1,6 +1,6 @@ mod blink_cursor; +mod change; mod clear_button; -mod history; mod input; mod otp_input; diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 453a6a63..bb0241bc 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -2,7 +2,6 @@ mod colors; mod event; mod focusable; mod icon; - mod root; mod styled; mod svg_img; @@ -15,6 +14,7 @@ pub mod context_menu; pub mod divider; pub mod drawer; pub mod dropdown; +pub mod history; pub mod indicator; pub mod input; pub mod label;