Skip to main content

azul_layout/managers/
changeset.rs

1//! Text editing changeset system
2//!
3//! **STATUS:** The core types (`TextChangeset`, `TextOperation`, `TextOp*` structs) are
4//! actively used by `window.rs`, `undo_redo.rs`, `event.rs`, and platform code.
5//!
6//! The live copy/cut/select-all/delete paths run through `common/event.rs`
7//! (`SystemChange::CopyToClipboard`/`CutToClipboard`, `CallbackChange::SetSelectAllRange`,
8//! `LayoutWindow::delete_selection`), not through changeset constructors. The earlier
9//! `create_*_changeset` helpers were a never-wired parallel implementation (with
10//! placeholder `deleted_text`, `CursorPosition::Uninitialized` cursors, and byte±1
11//! UTF-8 deletion) and have been removed.
12//!
13//! ## Architecture
14//!
15//! This module implements a two-phase changeset system for all text editing operations:
16//! 1. **Create changesets** (pre-callback): Analyze what would change, don't mutate yet
17//! 2. **Apply changesets** (post-callback): Actually mutate state if !preventDefault
18//!
19//! This pattern enables:
20//! - preventDefault support for ALL operations (not just text input)
21//! - Undo/redo stack (record changesets before applying)
22//! - Validation (check bounds, permissions before mutation)
23//! - Inspection (user callbacks can see planned changes)
24
25use azul_core::{
26    dom::DomNodeId,
27    selection::{OptionSelectionRange, SelectionRange},
28    task::Instant,
29    window::CursorPosition,
30};
31use azul_css::AzString;
32
33use crate::managers::selection::ClipboardContent;
34
35/// Unique identifier for a changeset (for undo/redo)
36pub type ChangesetId = usize;
37
38/// A text editing changeset that can be inspected before application
39#[derive(Debug, Clone)]
40#[repr(C)]
41pub struct TextChangeset {
42    /// Unique ID for undo/redo tracking
43    pub id: ChangesetId,
44    /// Target DOM node
45    pub target: DomNodeId,
46    /// The operation to perform
47    pub operation: TextOperation,
48    /// When this changeset was created
49    pub timestamp: Instant,
50}
51
52/// Insert text at cursor position
53#[derive(Debug, Clone)]
54#[repr(C)]
55pub struct TextOpInsertText {
56    pub text: AzString,
57    pub position: CursorPosition,
58    pub new_cursor: CursorPosition,
59}
60
61/// Delete text in range
62#[derive(Debug, Clone)]
63#[repr(C)]
64pub struct TextOpDeleteText {
65    pub range: SelectionRange,
66    pub deleted_text: AzString,
67    pub new_cursor: CursorPosition,
68}
69
70/// Replace text in range with new text
71#[derive(Debug, Clone)]
72#[repr(C)]
73pub struct TextOpReplaceText {
74    pub range: SelectionRange,
75    pub old_text: AzString,
76    pub new_text: AzString,
77    pub new_cursor: CursorPosition,
78}
79
80/// Set selection to new range
81#[derive(Copy, Debug, Clone)]
82#[repr(C)]
83pub struct TextOpSetSelection {
84    pub old_range: OptionSelectionRange,
85    pub new_range: SelectionRange,
86}
87
88/// Extend selection in a direction
89#[derive(Copy, Debug, Clone)]
90#[repr(C)]
91pub struct TextOpExtendSelection {
92    pub old_range: SelectionRange,
93    pub new_range: SelectionRange,
94    pub direction: SelectionDirection,
95}
96
97/// Clear all selections
98#[derive(Copy, Debug, Clone)]
99#[repr(C)]
100pub struct TextOpClearSelection {
101    pub old_range: SelectionRange,
102}
103
104/// Move cursor to new position
105#[derive(Copy, Debug, Clone)]
106#[repr(C)]
107pub struct TextOpMoveCursor {
108    pub old_position: CursorPosition,
109    pub new_position: CursorPosition,
110    pub movement: CursorMovement,
111}
112
113/// Copy selection to clipboard (no text change)
114#[derive(Debug, Clone)]
115#[repr(C)]
116pub struct TextOpCopy {
117    pub range: SelectionRange,
118    pub content: ClipboardContent,
119}
120
121/// Cut selection to clipboard (deletes text)
122#[derive(Debug, Clone)]
123#[repr(C)]
124pub struct TextOpCut {
125    pub range: SelectionRange,
126    pub content: ClipboardContent,
127    pub new_cursor: CursorPosition,
128}
129
130/// Paste from clipboard (inserts text)
131#[derive(Debug, Clone)]
132#[repr(C)]
133pub struct TextOpPaste {
134    pub content: ClipboardContent,
135    pub position: CursorPosition,
136    pub new_cursor: CursorPosition,
137}
138
139/// Select all text in node
140#[derive(Copy, Debug, Clone)]
141#[repr(C)]
142pub struct TextOpSelectAll {
143    pub old_range: OptionSelectionRange,
144    pub new_range: SelectionRange,
145}
146
147/// Text editing operation (what will change)
148#[derive(Debug, Clone)]
149#[repr(C, u8)]
150pub enum TextOperation {
151    /// Insert text at cursor position
152    InsertText(TextOpInsertText),
153    /// Delete text in range
154    DeleteText(TextOpDeleteText),
155    /// Replace text in range with new text
156    ReplaceText(TextOpReplaceText),
157    /// Set selection to new range
158    SetSelection(TextOpSetSelection),
159    /// Extend selection in a direction
160    ExtendSelection(TextOpExtendSelection),
161    /// Clear all selections
162    ClearSelection(TextOpClearSelection),
163    /// Move cursor to new position
164    MoveCursor(TextOpMoveCursor),
165    /// Copy selection to clipboard (no text change)
166    Copy(TextOpCopy),
167    /// Cut selection to clipboard (deletes text)
168    Cut(TextOpCut),
169    /// Paste from clipboard (inserts text)
170    Paste(TextOpPaste),
171    /// Select all text in node
172    SelectAll(TextOpSelectAll),
173}
174
175/// Re-export from events module
176pub use azul_core::events::SelectionDirection;
177
178/// Type of cursor movement
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180#[repr(C)]
181pub enum CursorMovement {
182    /// Move left one character
183    Left,
184    /// Move right one character
185    Right,
186    /// Move up one line
187    Up,
188    /// Move down one line
189    Down,
190    /// Jump to previous word boundary
191    WordLeft,
192    /// Jump to next word boundary
193    WordRight,
194    /// Jump to start of line
195    LineStart,
196    /// Jump to end of line
197    LineEnd,
198    /// Jump to start of document
199    DocumentStart,
200    /// Jump to end of document
201    DocumentEnd,
202    /// Absolute position (not relative)
203    Absolute,
204}
205
206impl TextChangeset {
207    /// Create a new changeset with unique ID
208    pub fn new(target: DomNodeId, operation: TextOperation, timestamp: Instant) -> Self {
209        use std::sync::atomic::{AtomicUsize, Ordering};
210        static CHANGESET_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
211
212        Self {
213            id: CHANGESET_ID_COUNTER.fetch_add(1, Ordering::Relaxed),
214            target,
215            operation,
216            timestamp,
217        }
218    }
219
220    /// Check if this changeset actually mutates text (vs just selection/cursor)
221    #[must_use] pub const fn mutates_text(&self) -> bool {
222        matches!(
223            self.operation,
224            TextOperation::InsertText { .. }
225                | TextOperation::DeleteText { .. }
226                | TextOperation::ReplaceText { .. }
227                | TextOperation::Cut { .. }
228                | TextOperation::Paste { .. }
229        )
230    }
231
232    /// Check if this changeset changes selection (including cursor moves)
233    #[must_use] pub const fn changes_selection(&self) -> bool {
234        matches!(
235            self.operation,
236            TextOperation::SetSelection { .. }
237                | TextOperation::ExtendSelection { .. }
238                | TextOperation::ClearSelection { .. }
239                | TextOperation::MoveCursor { .. }
240                | TextOperation::SelectAll { .. }
241        )
242    }
243
244    /// Check if this changeset involves clipboard
245    #[must_use] pub const fn uses_clipboard(&self) -> bool {
246        matches!(
247            self.operation,
248            TextOperation::Copy { .. } | TextOperation::Cut { .. } | TextOperation::Paste { .. }
249        )
250    }
251
252    /// Get the target cursor position after this changeset is applied
253    #[must_use] pub const fn resulting_cursor_position(&self) -> Option<CursorPosition> {
254        match &self.operation {
255            TextOperation::InsertText(op) => Some(op.new_cursor),
256            TextOperation::DeleteText(op) => Some(op.new_cursor),
257            TextOperation::ReplaceText(op) => Some(op.new_cursor),
258            TextOperation::Cut(op) => Some(op.new_cursor),
259            TextOperation::Paste(op) => Some(op.new_cursor),
260            TextOperation::MoveCursor(op) => Some(op.new_position),
261            _ => None,
262        }
263    }
264
265    /// Get the target selection range after this changeset is applied
266    #[must_use] pub const fn resulting_selection_range(&self) -> Option<SelectionRange> {
267        match &self.operation {
268            TextOperation::SetSelection(op) => Some(op.new_range),
269            TextOperation::ExtendSelection(op) => Some(op.new_range),
270            TextOperation::SelectAll(op) => Some(op.new_range),
271            _ => None,
272        }
273    }
274}