1use azul_core::{
24 dom::DomNodeId,
25 geom::LogicalPosition,
26 selection::{OptionSelectionRange, SelectionRange},
27 task::Instant,
28 window::CursorPosition,
29};
30use azul_css::AzString;
31
32use crate::managers::selection::ClipboardContent;
33
34pub type ChangesetId = usize;
36
37#[derive(Debug, Clone)]
39#[repr(C)]
40pub struct TextChangeset {
41 pub id: ChangesetId,
43 pub target: DomNodeId,
45 pub operation: TextOperation,
47 pub timestamp: Instant,
49}
50
51#[derive(Debug, Clone)]
53#[repr(C)]
54pub struct TextOpInsertText {
55 pub position: CursorPosition,
56 pub text: AzString,
57 pub new_cursor: CursorPosition,
58}
59
60#[derive(Debug, Clone)]
62#[repr(C)]
63pub struct TextOpDeleteText {
64 pub range: SelectionRange,
65 pub deleted_text: AzString,
66 pub new_cursor: CursorPosition,
67}
68
69#[derive(Debug, Clone)]
71#[repr(C)]
72pub struct TextOpReplaceText {
73 pub range: SelectionRange,
74 pub old_text: AzString,
75 pub new_text: AzString,
76 pub new_cursor: CursorPosition,
77}
78
79#[derive(Debug, Clone)]
81#[repr(C)]
82pub struct TextOpSetSelection {
83 pub old_range: OptionSelectionRange,
84 pub new_range: SelectionRange,
85}
86
87#[derive(Debug, Clone)]
89#[repr(C)]
90pub struct TextOpExtendSelection {
91 pub old_range: SelectionRange,
92 pub new_range: SelectionRange,
93 pub direction: SelectionDirection,
94}
95
96#[derive(Debug, Clone)]
98#[repr(C)]
99pub struct TextOpClearSelection {
100 pub old_range: SelectionRange,
101}
102
103#[derive(Debug, Clone)]
105#[repr(C)]
106pub struct TextOpMoveCursor {
107 pub old_position: CursorPosition,
108 pub new_position: CursorPosition,
109 pub movement: CursorMovement,
110}
111
112#[derive(Debug, Clone)]
114#[repr(C)]
115pub struct TextOpCopy {
116 pub range: SelectionRange,
117 pub content: ClipboardContent,
118}
119
120#[derive(Debug, Clone)]
122#[repr(C)]
123pub struct TextOpCut {
124 pub range: SelectionRange,
125 pub content: ClipboardContent,
126 pub new_cursor: CursorPosition,
127}
128
129#[derive(Debug, Clone)]
131#[repr(C)]
132pub struct TextOpPaste {
133 pub position: CursorPosition,
134 pub content: ClipboardContent,
135 pub new_cursor: CursorPosition,
136}
137
138#[derive(Debug, Clone)]
140#[repr(C)]
141pub struct TextOpSelectAll {
142 pub old_range: OptionSelectionRange,
143 pub new_range: SelectionRange,
144}
145
146#[derive(Debug, Clone)]
148#[repr(C, u8)]
149pub enum TextOperation {
150 InsertText(TextOpInsertText),
152 DeleteText(TextOpDeleteText),
154 ReplaceText(TextOpReplaceText),
156 SetSelection(TextOpSetSelection),
158 ExtendSelection(TextOpExtendSelection),
160 ClearSelection(TextOpClearSelection),
162 MoveCursor(TextOpMoveCursor),
164 Copy(TextOpCopy),
166 Cut(TextOpCut),
168 Paste(TextOpPaste),
170 SelectAll(TextOpSelectAll),
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176#[repr(C)]
177pub enum SelectionDirection {
178 Forward,
180 Backward,
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186#[repr(C)]
187pub enum CursorMovement {
188 Left,
190 Right,
192 Up,
194 Down,
196 WordLeft,
198 WordRight,
200 LineStart,
202 LineEnd,
204 DocumentStart,
206 DocumentEnd,
208 Absolute,
210}
211
212impl TextChangeset {
213 pub fn new(target: DomNodeId, operation: TextOperation, timestamp: Instant) -> Self {
215 use std::sync::atomic::{AtomicUsize, Ordering};
216 static CHANGESET_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
217
218 Self {
219 id: CHANGESET_ID_COUNTER.fetch_add(1, Ordering::Relaxed),
220 target,
221 operation,
222 timestamp,
223 }
224 }
225
226 pub fn mutates_text(&self) -> bool {
228 matches!(
229 self.operation,
230 TextOperation::InsertText { .. }
231 | TextOperation::DeleteText { .. }
232 | TextOperation::ReplaceText { .. }
233 | TextOperation::Cut { .. }
234 | TextOperation::Paste { .. }
235 )
236 }
237
238 pub fn changes_selection(&self) -> bool {
240 matches!(
241 self.operation,
242 TextOperation::SetSelection { .. }
243 | TextOperation::ExtendSelection { .. }
244 | TextOperation::ClearSelection { .. }
245 | TextOperation::MoveCursor { .. }
246 | TextOperation::SelectAll { .. }
247 )
248 }
249
250 pub fn uses_clipboard(&self) -> bool {
252 matches!(
253 self.operation,
254 TextOperation::Copy { .. } | TextOperation::Cut { .. } | TextOperation::Paste { .. }
255 )
256 }
257
258 pub fn resulting_cursor_position(&self) -> Option<CursorPosition> {
260 match &self.operation {
261 TextOperation::InsertText(op) => Some(op.new_cursor),
262 TextOperation::DeleteText(op) => Some(op.new_cursor),
263 TextOperation::ReplaceText(op) => Some(op.new_cursor),
264 TextOperation::Cut(op) => Some(op.new_cursor),
265 TextOperation::Paste(op) => Some(op.new_cursor),
266 TextOperation::MoveCursor(op) => Some(op.new_position),
267 _ => None,
268 }
269 }
270
271 pub fn resulting_selection_range(&self) -> Option<SelectionRange> {
273 match &self.operation {
274 TextOperation::SetSelection(op) => Some(op.new_range),
275 TextOperation::ExtendSelection(op) => Some(op.new_range),
276 TextOperation::SelectAll(op) => Some(op.new_range),
277 _ => None,
278 }
279 }
280}
281
282fn get_current_time() -> Instant {
284 let external = crate::callbacks::ExternalSystemCallbacks::rust_internal();
285 (external.get_system_time_fn.cb)().into()
286}
287
288pub fn create_copy_changeset(
293 target: DomNodeId,
294 timestamp: Instant,
295 layout_window: &crate::window::LayoutWindow,
296) -> Option<TextChangeset> {
297 let dom_id = &target.dom;
299 let content = layout_window.get_selected_content_for_clipboard(dom_id)?;
300
301 let ranges = layout_window.selection_manager.get_ranges(dom_id);
303 let range = ranges.first()?;
304
305 Some(TextChangeset::new(
306 target,
307 TextOperation::Copy(TextOpCopy {
308 range: *range,
309 content,
310 }),
311 timestamp,
312 ))
313}
314
315pub fn create_cut_changeset(
321 target: DomNodeId,
322 timestamp: Instant,
323 layout_window: &crate::window::LayoutWindow,
324) -> Option<TextChangeset> {
325 let dom_id = &target.dom;
327 let content = layout_window.get_selected_content_for_clipboard(dom_id)?;
328
329 let ranges = layout_window.selection_manager.get_ranges(dom_id);
331 let range = ranges.first()?;
332
333 let new_cursor_position = azul_core::window::CursorPosition::Uninitialized;
336
337 Some(TextChangeset::new(
338 target,
339 TextOperation::Cut(TextOpCut {
340 range: *range,
341 content,
342 new_cursor: new_cursor_position,
343 }),
344 timestamp,
345 ))
346}
347
348pub fn create_paste_changeset(
355 target: DomNodeId,
356 timestamp: Instant,
357 layout_window: &crate::window::LayoutWindow,
358) -> Option<TextChangeset> {
359 None
362}
363
364pub fn create_select_all_changeset(
369 target: DomNodeId,
370 timestamp: Instant,
371 layout_window: &crate::window::LayoutWindow,
372) -> Option<TextChangeset> {
373 use azul_core::selection::{CursorAffinity, GraphemeClusterId, TextCursor};
374
375 let dom_id = &target.dom;
376 let node_id = target.node.into_crate_internal()?;
377
378 let old_range = layout_window
380 .selection_manager
381 .get_ranges(dom_id)
382 .first()
383 .copied();
384
385 let content = layout_window.get_text_before_textinput(*dom_id, node_id);
387 let text = layout_window.extract_text_from_inline_content(&content);
388
389 let start_cursor = TextCursor {
391 cluster_id: GraphemeClusterId {
392 source_run: 0,
393 start_byte_in_run: 0,
394 },
395 affinity: CursorAffinity::Leading,
396 };
397
398 let end_cursor = TextCursor {
399 cluster_id: GraphemeClusterId {
400 source_run: 0,
401 start_byte_in_run: text.len() as u32,
402 },
403 affinity: CursorAffinity::Leading,
404 };
405
406 let new_range = azul_core::selection::SelectionRange {
407 start: start_cursor,
408 end: end_cursor,
409 };
410
411 Some(TextChangeset::new(
412 target,
413 TextOperation::SelectAll(TextOpSelectAll {
414 old_range: old_range.into(),
415 new_range,
416 }),
417 timestamp,
418 ))
419}
420
421pub fn create_delete_selection_changeset(
430 target: DomNodeId,
431 forward: bool,
432 timestamp: Instant,
433 layout_window: &crate::window::LayoutWindow,
434) -> Option<TextChangeset> {
435 use azul_core::selection::{CursorAffinity, GraphemeClusterId, TextCursor};
436
437 let dom_id = &target.dom;
438 let node_id = target.node.into_crate_internal()?;
439
440 let ranges = layout_window.selection_manager.get_ranges(dom_id);
442 let cursor = layout_window.cursor_manager.get_cursor();
443
444 let (delete_range, deleted_text) = if let Some(range) = ranges.first() {
446 let content = layout_window.get_text_before_textinput(*dom_id, node_id);
448 let text = layout_window.extract_text_from_inline_content(&content);
449
450 let deleted = String::new(); (*range, deleted)
456 } else if let Some(cursor_pos) = cursor {
457 let content = layout_window.get_text_before_textinput(*dom_id, node_id);
459 let text = layout_window.extract_text_from_inline_content(&content);
460
461 let byte_pos = cursor_pos.cluster_id.start_byte_in_run as usize;
462
463 let (range, deleted) = if forward {
464 if byte_pos >= text.len() {
466 return None; }
468 let end_pos = (byte_pos + 1).min(text.len());
470 let deleted = text[byte_pos..end_pos].to_string();
471
472 let range = azul_core::selection::SelectionRange {
473 start: *cursor_pos,
474 end: TextCursor {
475 cluster_id: GraphemeClusterId {
476 source_run: cursor_pos.cluster_id.source_run,
477 start_byte_in_run: end_pos as u32,
478 },
479 affinity: CursorAffinity::Leading,
480 },
481 };
482 (range, deleted)
483 } else {
484 if byte_pos == 0 {
486 return None; }
488 let start_pos = byte_pos.saturating_sub(1);
490 let deleted = text[start_pos..byte_pos].to_string();
491
492 let range = azul_core::selection::SelectionRange {
493 start: TextCursor {
494 cluster_id: GraphemeClusterId {
495 source_run: cursor_pos.cluster_id.source_run,
496 start_byte_in_run: start_pos as u32,
497 },
498 affinity: CursorAffinity::Leading,
499 },
500 end: *cursor_pos,
501 };
502 (range, deleted)
503 };
504
505 (range, deleted)
506 } else {
507 return None; };
509
510 let new_cursor = azul_core::window::CursorPosition::Uninitialized;
512
513 Some(TextChangeset::new(
514 target,
515 TextOperation::DeleteText(TextOpDeleteText {
516 range: delete_range,
517 deleted_text: deleted_text.into(),
518 new_cursor,
519 }),
520 timestamp,
521 ))
522}