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
This commit is contained in:
parent
4490618c4a
commit
46ff43ff36
6 changed files with 243 additions and 123 deletions
192
crates/ui/src/history.rs
Normal file
192
crates/ui/src/history.rs
Normal file
|
|
@ -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<I: HistoryItem> {
|
||||||
|
undos: Vec<I>,
|
||||||
|
redos: Vec<I>,
|
||||||
|
last_changed_at: Instant,
|
||||||
|
version: usize,
|
||||||
|
pub(crate) ignore: bool,
|
||||||
|
max_undo: usize,
|
||||||
|
group_interval: Option<Duration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I> History<I>
|
||||||
|
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<Vec<I>> {
|
||||||
|
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<Vec<I>> {
|
||||||
|
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<usize> 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<TabIndex> = 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
crates/ui/src/input/change.rs
Normal file
39
crates/ui/src/input/change.rs
Normal file
|
|
@ -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<usize>,
|
||||||
|
pub(crate) old_text: String,
|
||||||
|
pub(crate) new_range: Range<usize>,
|
||||||
|
pub(crate) new_text: String,
|
||||||
|
version: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Change {
|
||||||
|
pub fn new(
|
||||||
|
old_range: Range<usize>,
|
||||||
|
old_text: &str,
|
||||||
|
new_range: Range<usize>,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Change>,
|
|
||||||
redos: Vec<Change>,
|
|
||||||
last_changed_at: Instant,
|
|
||||||
version: usize,
|
|
||||||
pub(crate) ignore: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Change {
|
|
||||||
pub(crate) old_range: Range<usize>,
|
|
||||||
pub(crate) old_text: String,
|
|
||||||
pub(crate) new_range: Range<usize>,
|
|
||||||
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<usize>,
|
|
||||||
old_text: &str,
|
|
||||||
new_range: Range<usize>,
|
|
||||||
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<Vec<Change>> {
|
|
||||||
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<Vec<Change>> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,8 +6,9 @@
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use super::blink_cursor::BlinkCursor;
|
use super::blink_cursor::BlinkCursor;
|
||||||
use super::history::History;
|
use super::change::Change;
|
||||||
use super::ClearButton;
|
use super::ClearButton;
|
||||||
|
use crate::history::History;
|
||||||
use crate::indicator::Indicator;
|
use crate::indicator::Indicator;
|
||||||
use crate::theme::ActiveTheme;
|
use crate::theme::ActiveTheme;
|
||||||
use crate::StyledExt as _;
|
use crate::StyledExt as _;
|
||||||
|
|
@ -110,7 +111,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
pub struct TextInput {
|
pub struct TextInput {
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
text: SharedString,
|
text: SharedString,
|
||||||
history: History,
|
history: History<Change>,
|
||||||
blink_cursor: Model<BlinkCursor>,
|
blink_cursor: Model<BlinkCursor>,
|
||||||
prefix: Option<Box<dyn Fn(&mut ViewContext<Self>) -> AnyElement + 'static>>,
|
prefix: Option<Box<dyn Fn(&mut ViewContext<Self>) -> AnyElement + 'static>>,
|
||||||
suffix: Option<Box<dyn Fn(&mut ViewContext<Self>) -> AnyElement + 'static>>,
|
suffix: Option<Box<dyn Fn(&mut ViewContext<Self>) -> AnyElement + 'static>>,
|
||||||
|
|
@ -138,7 +139,7 @@ impl TextInput {
|
||||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||||
let focus_handle = cx.focus_handle();
|
let focus_handle = cx.focus_handle();
|
||||||
let blink_cursor = cx.new_model(|_| BlinkCursor::new());
|
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 {
|
let input = Self {
|
||||||
focus_handle: focus_handle.clone(),
|
focus_handle: focus_handle.clone(),
|
||||||
text: "".into(),
|
text: "".into(),
|
||||||
|
|
@ -445,8 +446,12 @@ impl TextInput {
|
||||||
|
|
||||||
let new_range = range.start..range.start + new_text.len();
|
let new_range = range.start..range.start + new_text.len();
|
||||||
|
|
||||||
self.history
|
self.history.push(Change::new(
|
||||||
.push(range.clone(), &old_text, new_range, new_text);
|
range.clone(),
|
||||||
|
&old_text,
|
||||||
|
new_range.clone(),
|
||||||
|
new_text,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
|
fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
mod blink_cursor;
|
mod blink_cursor;
|
||||||
|
mod change;
|
||||||
mod clear_button;
|
mod clear_button;
|
||||||
mod history;
|
|
||||||
mod input;
|
mod input;
|
||||||
mod otp_input;
|
mod otp_input;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ mod colors;
|
||||||
mod event;
|
mod event;
|
||||||
mod focusable;
|
mod focusable;
|
||||||
mod icon;
|
mod icon;
|
||||||
|
|
||||||
mod root;
|
mod root;
|
||||||
mod styled;
|
mod styled;
|
||||||
mod svg_img;
|
mod svg_img;
|
||||||
|
|
@ -15,6 +14,7 @@ pub mod context_menu;
|
||||||
pub mod divider;
|
pub mod divider;
|
||||||
pub mod drawer;
|
pub mod drawer;
|
||||||
pub mod dropdown;
|
pub mod dropdown;
|
||||||
|
pub mod history;
|
||||||
pub mod indicator;
|
pub mod indicator;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod label;
|
pub mod label;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue