Skip to main content

inset_services/
undo_manager.rs

1//! Flutter counterpart: `services/undo_manager.dart`.
2//!
3//! [`set_undo_state`](UndoManager::set_undo_state) is empty: there is no method channel.
4//! [`setChannel`](https://api.flutter.dev/flutter/services/UndoManager/setChannel.html) is omitted.
5
6use inset_foundation::{App, Handle, HandleId};
7
8/// The direction in which an undo action should be performed, whether undo or redo.
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10pub enum UndoDirection {
11    /// Perform an undo action.
12    Undo,
13    /// Perform a redo action.
14    Redo,
15}
16
17/// An interface to receive events from a native UndoManager.
18///
19/// Dart's `mixin UndoManagerClient`.
20pub trait UndoManagerClient: Sized + 'static {
21    /// Requests that the client perform an undo or redo operation.
22    ///
23    /// Currently only used on iOS 9+ when the undo or redo methods are invoked
24    /// by the platform. For example, when using three-finger swipe gestures,
25    /// the iPad keyboard, or voice control.
26    fn handle_platform_undo(self: Handle<Self>, app: &mut App, direction: UndoDirection);
27
28    /// Reverts the value on the stack to the previous value.
29    fn undo(self: Handle<Self>, app: &mut App);
30
31    /// Updates the value on the stack to the next value.
32    fn redo(self: Handle<Self>, app: &mut App);
33
34    /// Will be true if there are past values on the stack.
35    fn can_undo(self: Handle<Self>, app: &App) -> bool;
36
37    /// Will be true if there are future values on the stack.
38    fn can_redo(self: Handle<Self>, app: &App) -> bool;
39
40    /// This client as the erased [`AnyUndoManagerClient`].
41    fn as_undo_manager_client(self: Handle<Self>) -> AnyUndoManagerClient {
42        AnyUndoManagerClient {
43            id: self.id(),
44            vtable: const { &UndoManagerClientVTable::of::<Self>() },
45        }
46    }
47}
48
49struct UndoManagerClientVTable {
50    handle_platform_undo: fn(&mut App, HandleId, UndoDirection),
51    undo: fn(&mut App, HandleId),
52    redo: fn(&mut App, HandleId),
53    can_undo: fn(&App, HandleId) -> bool,
54    can_redo: fn(&App, HandleId) -> bool,
55}
56
57fn resolve<T: 'static>(id: HandleId) -> Handle<T> {
58    Handle::from_id(id)
59}
60
61impl UndoManagerClientVTable {
62    const fn of<C: UndoManagerClient>() -> UndoManagerClientVTable {
63        UndoManagerClientVTable {
64            handle_platform_undo: |app, id, direction| {
65                C::handle_platform_undo(resolve(id), app, direction)
66            },
67            undo: |app, id| C::undo(resolve(id), app),
68            redo: |app, id| C::redo(resolve(id), app),
69            can_undo: |app, id| C::can_undo(resolve(id), app),
70            can_redo: |app, id| C::can_redo(resolve(id), app),
71        }
72    }
73}
74
75/// Erased [`UndoManagerClient`]: one identity and a static vtable.
76#[derive(Clone, Copy)]
77pub struct AnyUndoManagerClient {
78    id: HandleId,
79    vtable: &'static UndoManagerClientVTable,
80}
81
82impl AnyUndoManagerClient {
83    /// See [`UndoManagerClient::handle_platform_undo`].
84    pub fn handle_platform_undo(self, app: &mut App, direction: UndoDirection) {
85        (self.vtable.handle_platform_undo)(app, self.id, direction);
86    }
87
88    /// See [`UndoManagerClient::undo`].
89    pub fn undo(self, app: &mut App) {
90        (self.vtable.undo)(app, self.id);
91    }
92
93    /// See [`UndoManagerClient::redo`].
94    pub fn redo(self, app: &mut App) {
95        (self.vtable.redo)(app, self.id);
96    }
97
98    /// See [`UndoManagerClient::can_undo`].
99    pub fn can_undo(self, app: &App) -> bool {
100        (self.vtable.can_undo)(app, self.id)
101    }
102
103    /// See [`UndoManagerClient::can_redo`].
104    pub fn can_redo(self, app: &App) -> bool {
105        (self.vtable.can_redo)(app, self.id)
106    }
107}
108
109impl PartialEq for AnyUndoManagerClient {
110    fn eq(&self, other: &AnyUndoManagerClient) -> bool {
111        self.id == other.id
112    }
113}
114
115impl Eq for AnyUndoManagerClient {}
116
117impl std::fmt::Debug for AnyUndoManagerClient {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.debug_struct("AnyUndoManagerClient")
120            .field("id", &self.id)
121            .finish()
122    }
123}
124
125/// A low-level interface to the system's undo manager.
126///
127/// To receive events from the system undo manager, create an
128/// [`UndoManagerClient`] and set it as the [`client`](Self::client) on [`UndoManager`].
129///
130/// The [`set_undo_state`](Self::set_undo_state) method can be used to update the system's undo
131/// manager using the `can_undo` and `can_redo` parameters.
132///
133/// When the system undo or redo button is tapped, the current
134/// [`UndoManagerClient`] will receive [`UndoManagerClient::handle_platform_undo`]
135/// with an [`UndoDirection`] representing whether the event is "undo" or "redo".
136///
137/// Currently, only iOS has an UndoManagerPlugin implemented on the engine side.
138/// On iOS, this can be used to listen to the keyboard undo/redo buttons and the
139/// undo/redo gestures.
140///
141/// See also:
142///
143///  * [NSUndoManager](https://developer.apple.com/documentation/foundation/nsundomanager)
144///
145/// Dart's `UndoManager._instance` is [`instance`](Self::instance), the App's singleton.
146#[derive(Default)]
147pub struct UndoManager {
148    current_client: Option<AnyUndoManagerClient>,
149}
150
151impl UndoManager {
152    /// Dart's `UndoManager._instance`.
153    pub fn instance(app: &mut App) -> Handle<UndoManager> {
154        app.singleton::<UndoManager>()
155    }
156
157    /// Receive undo and redo events from the system's [`UndoManager`].
158    ///
159    /// Setting the [`client`](Self::client) will cause [`UndoManagerClient::handle_platform_undo`]
160    /// to be called when a system undo or redo is triggered, such as by tapping
161    /// the undo/redo keyboard buttons or using the 3-finger swipe gestures.
162    pub fn set_client(app: &mut App, client: Option<AnyUndoManagerClient>) {
163        let this = Self::instance(app);
164        app.get_mut(this).current_client = client;
165    }
166
167    /// Return the current [`UndoManagerClient`].
168    pub fn client(app: &mut App) -> Option<AnyUndoManagerClient> {
169        let this = Self::instance(app);
170        app.get(this).current_client
171    }
172
173    /// Set the current state of the system UndoManager. [`can_undo`] and [`can_redo`]
174    /// control the respective "undo" and "redo" buttons of the system UndoManager.
175    ///
176    /// Empty: there is no method channel.
177    pub fn set_undo_state(_app: &mut App, _can_undo: bool, _can_redo: bool) {}
178
179    /// Flutter `UndoManagerClient.handleUndo`: the host asked for undo or redo.
180    pub fn handle_platform_undo(app: &mut App, direction: UndoDirection) {
181        let this = Self::instance(app);
182        let client = app.get(this).current_client;
183        debug_assert!(
184            client.is_some(),
185            "There must be a current UndoManagerClient."
186        );
187        client
188            .expect("There must be a current UndoManagerClient.")
189            .handle_platform_undo(app, direction);
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use inset_foundation::AppCell;
197
198    #[derive(Default)]
199    struct RecordingClient {
200        directions: Vec<UndoDirection>,
201        undos: usize,
202        redos: usize,
203    }
204
205    impl UndoManagerClient for RecordingClient {
206        fn handle_platform_undo(self: Handle<Self>, app: &mut App, direction: UndoDirection) {
207            app.get_mut(self).directions.push(direction);
208            match direction {
209                UndoDirection::Undo => self.undo(app),
210                UndoDirection::Redo => self.redo(app),
211            }
212        }
213
214        fn undo(self: Handle<Self>, app: &mut App) {
215            app.get_mut(self).undos += 1;
216        }
217
218        fn redo(self: Handle<Self>, app: &mut App) {
219            app.get_mut(self).redos += 1;
220        }
221
222        fn can_undo(self: Handle<Self>, _app: &App) -> bool {
223            false
224        }
225
226        fn can_redo(self: Handle<Self>, _app: &App) -> bool {
227            false
228        }
229    }
230
231    #[test]
232    fn set_client_receives_handle_platform_undo() {
233        let cell = AppCell::new();
234        let mut app = cell.borrow_mut();
235        let client = app.create(RecordingClient::default());
236        UndoManager::set_client(&mut app, Some(client.as_undo_manager_client()));
237        assert_eq!(
238            UndoManager::client(&mut app),
239            Some(client.as_undo_manager_client())
240        );
241
242        UndoManager::handle_platform_undo(&mut app, UndoDirection::Undo);
243        UndoManager::handle_platform_undo(&mut app, UndoDirection::Redo);
244        assert_eq!(
245            app.get(client).directions,
246            [UndoDirection::Undo, UndoDirection::Redo]
247        );
248        assert_eq!(app.get(client).undos, 1);
249        assert_eq!(app.get(client).redos, 1);
250
251        UndoManager::set_client(&mut app, None);
252        assert!(UndoManager::client(&mut app).is_none());
253        UndoManager::set_undo_state(&mut app, true, true);
254    }
255}