1use azul_core::{
20 dom::DomNodeId,
21 selection::{OptionSelectionRange, SelectionRange},
22 task::Instant,
23 window::CursorPosition,
24};
25use azul_css::AzString;
26
27use crate::managers::selection::ClipboardContent;
28
29pub type ChangesetId = usize;
31
32#[derive(Debug, Clone)]
34#[repr(C)]
35pub struct TextChangeset {
36 pub id: ChangesetId,
38 pub target: DomNodeId,
40 pub operation: TextOperation,
42 pub timestamp: Instant,
44}
45
46#[derive(Debug, Clone)]
48#[repr(C)]
49pub struct TextOpInsertText {
50 pub text: AzString,
51 pub position: CursorPosition,
52 pub new_cursor: CursorPosition,
53}
54
55#[derive(Debug, Clone)]
57#[repr(C)]
58pub struct TextOpDeleteText {
59 pub range: SelectionRange,
60 pub deleted_text: AzString,
61 pub new_cursor: CursorPosition,
62}
63
64#[derive(Debug, Clone)]
66#[repr(C)]
67pub struct TextOpReplaceText {
68 pub range: SelectionRange,
69 pub old_text: AzString,
70 pub new_text: AzString,
71 pub new_cursor: CursorPosition,
72}
73
74#[derive(Debug, Clone)]
76#[repr(C)]
77pub struct TextOpSetSelection {
78 pub old_range: OptionSelectionRange,
79 pub new_range: SelectionRange,
80}
81
82#[derive(Debug, Clone)]
84#[repr(C)]
85pub struct TextOpExtendSelection {
86 pub old_range: SelectionRange,
87 pub new_range: SelectionRange,
88 pub direction: SelectionDirection,
89}
90
91#[derive(Debug, Clone)]
93#[repr(C)]
94pub struct TextOpClearSelection {
95 pub old_range: SelectionRange,
96}
97
98#[derive(Debug, Clone)]
100#[repr(C)]
101pub struct TextOpMoveCursor {
102 pub old_position: CursorPosition,
103 pub new_position: CursorPosition,
104 pub movement: CursorMovement,
105}
106
107#[derive(Debug, Clone)]
109#[repr(C)]
110pub struct TextOpCopy {
111 pub range: SelectionRange,
112 pub content: ClipboardContent,
113}
114
115#[derive(Debug, Clone)]
117#[repr(C)]
118pub struct TextOpCut {
119 pub range: SelectionRange,
120 pub content: ClipboardContent,
121 pub new_cursor: CursorPosition,
122}
123
124#[derive(Debug, Clone)]
126#[repr(C)]
127pub struct TextOpPaste {
128 pub content: ClipboardContent,
129 pub position: CursorPosition,
130 pub new_cursor: CursorPosition,
131}
132
133#[derive(Debug, Clone)]
135#[repr(C)]
136pub struct TextOpSelectAll {
137 pub old_range: OptionSelectionRange,
138 pub new_range: SelectionRange,
139}
140
141#[derive(Debug, Clone)]
143#[repr(C, u8)]
144pub enum TextOperation {
145 InsertText(TextOpInsertText),
147 DeleteText(TextOpDeleteText),
149 ReplaceText(TextOpReplaceText),
151 SetSelection(TextOpSetSelection),
153 ExtendSelection(TextOpExtendSelection),
155 ClearSelection(TextOpClearSelection),
157 MoveCursor(TextOpMoveCursor),
159 Copy(TextOpCopy),
161 Cut(TextOpCut),
163 Paste(TextOpPaste),
165 SelectAll(TextOpSelectAll),
167}
168
169pub use azul_core::events::SelectionDirection;
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174#[repr(C)]
175pub enum CursorMovement {
176 Left,
178 Right,
180 Up,
182 Down,
184 WordLeft,
186 WordRight,
188 LineStart,
190 LineEnd,
192 DocumentStart,
194 DocumentEnd,
196 Absolute,
198}
199
200impl TextChangeset {
201 pub fn new(target: DomNodeId, operation: TextOperation, timestamp: Instant) -> Self {
203 use std::sync::atomic::{AtomicUsize, Ordering};
204 static CHANGESET_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
205
206 Self {
207 id: CHANGESET_ID_COUNTER.fetch_add(1, Ordering::Relaxed),
208 target,
209 operation,
210 timestamp,
211 }
212 }
213
214 pub fn mutates_text(&self) -> bool {
216 matches!(
217 self.operation,
218 TextOperation::InsertText { .. }
219 | TextOperation::DeleteText { .. }
220 | TextOperation::ReplaceText { .. }
221 | TextOperation::Cut { .. }
222 | TextOperation::Paste { .. }
223 )
224 }
225
226 pub fn changes_selection(&self) -> bool {
228 matches!(
229 self.operation,
230 TextOperation::SetSelection { .. }
231 | TextOperation::ExtendSelection { .. }
232 | TextOperation::ClearSelection { .. }
233 | TextOperation::MoveCursor { .. }
234 | TextOperation::SelectAll { .. }
235 )
236 }
237
238 pub fn uses_clipboard(&self) -> bool {
240 matches!(
241 self.operation,
242 TextOperation::Copy { .. } | TextOperation::Cut { .. } | TextOperation::Paste { .. }
243 )
244 }
245
246 pub fn resulting_cursor_position(&self) -> Option<CursorPosition> {
248 match &self.operation {
249 TextOperation::InsertText(op) => Some(op.new_cursor),
250 TextOperation::DeleteText(op) => Some(op.new_cursor),
251 TextOperation::ReplaceText(op) => Some(op.new_cursor),
252 TextOperation::Cut(op) => Some(op.new_cursor),
253 TextOperation::Paste(op) => Some(op.new_cursor),
254 TextOperation::MoveCursor(op) => Some(op.new_position),
255 _ => None,
256 }
257 }
258
259 pub fn resulting_selection_range(&self) -> Option<SelectionRange> {
261 match &self.operation {
262 TextOperation::SetSelection(op) => Some(op.new_range),
263 TextOperation::ExtendSelection(op) => Some(op.new_range),
264 TextOperation::SelectAll(op) => Some(op.new_range),
265 _ => None,
266 }
267 }
268}
269
270pub fn create_copy_changeset(
275 target: DomNodeId,
276 timestamp: Instant,
277 layout_window: &crate::window::LayoutWindow,
278) -> Option<TextChangeset> {
279 let dom_id = &target.dom;
281 let content = layout_window.get_selected_content_for_clipboard(dom_id)?;
282
283 let ranges: Vec<azul_core::selection::SelectionRange> = layout_window.text_edit_manager.multi_cursor.as_ref()
285 .map(|mc| mc.selections.iter().filter_map(|s| match &s.selection {
286 azul_core::selection::Selection::Range(r) => Some(*r),
287 _ => None,
288 }).collect()).unwrap_or_default();
289 let range = ranges.first()?;
290
291 Some(TextChangeset::new(
292 target,
293 TextOperation::Copy(TextOpCopy {
294 range: *range,
295 content,
296 }),
297 timestamp,
298 ))
299}
300
301pub fn create_cut_changeset(
307 target: DomNodeId,
308 timestamp: Instant,
309 layout_window: &crate::window::LayoutWindow,
310) -> Option<TextChangeset> {
311 let dom_id = &target.dom;
313 let content = layout_window.get_selected_content_for_clipboard(dom_id)?;
314
315 let ranges: Vec<azul_core::selection::SelectionRange> = layout_window.text_edit_manager.multi_cursor.as_ref()
317 .map(|mc| mc.selections.iter().filter_map(|s| match &s.selection {
318 azul_core::selection::Selection::Range(r) => Some(*r),
319 _ => None,
320 }).collect()).unwrap_or_default();
321 let range = ranges.first()?;
322
323 let new_cursor_position = azul_core::window::CursorPosition::Uninitialized;
326
327 Some(TextChangeset::new(
328 target,
329 TextOperation::Cut(TextOpCut {
330 range: *range,
331 content,
332 new_cursor: new_cursor_position,
333 }),
334 timestamp,
335 ))
336}
337
338pub fn create_paste_changeset(
345 target: DomNodeId,
346 timestamp: Instant,
347 layout_window: &crate::window::LayoutWindow,
348) -> Option<TextChangeset> {
349 None
352}
353
354pub fn create_select_all_changeset(
359 target: DomNodeId,
360 timestamp: Instant,
361 layout_window: &crate::window::LayoutWindow,
362) -> Option<TextChangeset> {
363 use azul_core::selection::{CursorAffinity, GraphemeClusterId, TextCursor};
364
365 let dom_id = &target.dom;
366 let node_id = target.node.into_crate_internal()?;
367
368 let old_range: Option<azul_core::selection::SelectionRange> = layout_window.text_edit_manager.multi_cursor.as_ref()
370 .and_then(|mc| mc.selections.iter().find_map(|s| match &s.selection {
371 azul_core::selection::Selection::Range(r) => Some(*r),
372 _ => None,
373 }));
374
375 let content = layout_window.get_text_before_textinput(*dom_id, node_id);
377 let text = layout_window.extract_text_from_inline_content(&content);
378
379 let start_cursor = TextCursor {
381 cluster_id: GraphemeClusterId {
382 source_run: 0,
383 start_byte_in_run: 0,
384 },
385 affinity: CursorAffinity::Leading,
386 };
387
388 let end_cursor = TextCursor {
389 cluster_id: GraphemeClusterId {
390 source_run: 0,
391 start_byte_in_run: text.len() as u32,
392 },
393 affinity: CursorAffinity::Leading,
394 };
395
396 let new_range = azul_core::selection::SelectionRange {
397 start: start_cursor,
398 end: end_cursor,
399 };
400
401 Some(TextChangeset::new(
402 target,
403 TextOperation::SelectAll(TextOpSelectAll {
404 old_range: old_range.into(),
405 new_range,
406 }),
407 timestamp,
408 ))
409}
410
411pub fn create_delete_selection_changeset(
420 target: DomNodeId,
421 forward: bool,
422 timestamp: Instant,
423 layout_window: &crate::window::LayoutWindow,
424) -> Option<TextChangeset> {
425 use azul_core::selection::{CursorAffinity, GraphemeClusterId, TextCursor};
426
427 let dom_id = &target.dom;
428 let node_id = target.node.into_crate_internal()?;
429
430 let ranges: Vec<azul_core::selection::SelectionRange> = layout_window.text_edit_manager.multi_cursor.as_ref()
432 .map(|mc| mc.selections.iter().filter_map(|s| match &s.selection {
433 azul_core::selection::Selection::Range(r) => Some(*r),
434 _ => None,
435 }).collect()).unwrap_or_default();
436 let cursor = layout_window.text_edit_manager.get_primary_cursor();
437
438 let (delete_range, deleted_text) = if let Some(range) = ranges.first() {
440 let content = layout_window.get_text_before_textinput(*dom_id, node_id);
442 let text = layout_window.extract_text_from_inline_content(&content);
443
444 let deleted = String::new(); (*range, deleted)
450 } else if let Some(cursor_pos) = cursor {
451 let content = layout_window.get_text_before_textinput(*dom_id, node_id);
453 let text = layout_window.extract_text_from_inline_content(&content);
454
455 let byte_pos = cursor_pos.cluster_id.start_byte_in_run as usize;
456
457 let (range, deleted) = if forward {
458 if byte_pos >= text.len() {
460 return None; }
462 let end_pos = (byte_pos + 1).min(text.len());
464 let deleted = text[byte_pos..end_pos].to_string();
465
466 let range = azul_core::selection::SelectionRange {
467 start: cursor_pos,
468 end: TextCursor {
469 cluster_id: GraphemeClusterId {
470 source_run: cursor_pos.cluster_id.source_run,
471 start_byte_in_run: end_pos as u32,
472 },
473 affinity: CursorAffinity::Leading,
474 },
475 };
476 (range, deleted)
477 } else {
478 if byte_pos == 0 {
480 return None; }
482 let start_pos = byte_pos.saturating_sub(1);
484 let deleted = text[start_pos..byte_pos].to_string();
485
486 let range = azul_core::selection::SelectionRange {
487 start: TextCursor {
488 cluster_id: GraphemeClusterId {
489 source_run: cursor_pos.cluster_id.source_run,
490 start_byte_in_run: start_pos as u32,
491 },
492 affinity: CursorAffinity::Leading,
493 },
494 end: cursor_pos,
495 };
496 (range, deleted)
497 };
498
499 (range, deleted)
500 } else {
501 return None; };
503
504 let new_cursor = azul_core::window::CursorPosition::Uninitialized;
506
507 Some(TextChangeset::new(
508 target,
509 TextOperation::DeleteText(TextOpDeleteText {
510 range: delete_range,
511 deleted_text: deleted_text.into(),
512 new_cursor,
513 }),
514 timestamp,
515 ))
516}