Skip to main content

azul_layout/text3/
edit.rs

1//! Pure functions for editing a `Vec<InlineContent>` based on selections.
2
3use std::sync::Arc;
4
5use azul_core::selection::{
6    CursorAffinity, GraphemeClusterId, Selection, SelectionRange, TextCursor,
7};
8
9use crate::text3::cache::{ContentIndex, InlineContent, StyledRun};
10
11/// An enum representing a single text editing action.
12#[derive(Debug, Clone)]
13pub enum TextEdit {
14    Insert(String),
15    DeleteBackward,
16    DeleteForward,
17}
18
19/// The primary entry point for text modification. Takes the current content and selections,
20/// applies an edit, and returns the new content and the resulting cursor positions.
21pub fn edit_text(
22    content: &[InlineContent],
23    selections: &[Selection],
24    edit: &TextEdit,
25) -> (Vec<InlineContent>, Vec<Selection>) {
26    if selections.is_empty() {
27        return (content.to_vec(), Vec::new());
28    }
29
30    let mut new_content = content.to_vec();
31    let mut new_selections = Vec::new();
32
33    // To handle multiple cursors correctly, we must process edits
34    // from the end of the document to the beginning. This ensures that
35    // earlier edits do not invalidate the indices of later edits.
36    let mut sorted_selections = selections.to_vec();
37    sorted_selections.sort_by(|a, b| {
38        let cursor_a = match a {
39            Selection::Cursor(c) => c,
40            Selection::Range(r) => &r.start,
41        };
42        let cursor_b = match b {
43            Selection::Cursor(c) => c,
44            Selection::Range(r) => &r.start,
45        };
46        cursor_b.cluster_id.cmp(&cursor_a.cluster_id) // Reverse sort
47    });
48
49    for selection in sorted_selections {
50        let (mut temp_content, new_cursor) =
51            apply_edit_to_selection(&new_content, &selection, edit);
52
53        // When we insert/delete text, we need to adjust all previously-processed cursors
54        // that come after this edit position in the same run
55        let edit_run = match selection {
56            Selection::Cursor(c) => c.cluster_id.source_run,
57            Selection::Range(r) => r.start.cluster_id.source_run,
58        };
59        let edit_byte = match selection {
60            Selection::Cursor(c) => c.cluster_id.start_byte_in_run,
61            Selection::Range(r) => r.start.cluster_id.start_byte_in_run,
62        };
63
64        // Calculate the byte offset change
65        let byte_offset_change: i32 = match edit {
66            TextEdit::Insert(text) => text.len() as i32,
67            TextEdit::DeleteBackward | TextEdit::DeleteForward => {
68                // For simplicity, assume 1 grapheme deleted = some bytes
69                // A full implementation would track actual bytes deleted
70                -1
71            }
72        };
73
74        // Adjust all previously-processed cursors in the same run that come after this position
75        for prev_selection in new_selections.iter_mut() {
76            if let Selection::Cursor(cursor) = prev_selection {
77                if cursor.cluster_id.source_run == edit_run
78                    && cursor.cluster_id.start_byte_in_run >= edit_byte
79                {
80                    cursor.cluster_id.start_byte_in_run =
81                        (cursor.cluster_id.start_byte_in_run as i32 + byte_offset_change).max(0)
82                            as u32;
83                }
84            }
85        }
86
87        new_content = temp_content;
88        new_selections.push(Selection::Cursor(new_cursor));
89    }
90
91    // The new selections were added in reverse order, so we reverse them back.
92    new_selections.reverse();
93
94    (new_content, new_selections)
95}
96
97/// Applies a single edit to a single selection.
98pub fn apply_edit_to_selection(
99    content: &[InlineContent],
100    selection: &Selection,
101    edit: &TextEdit,
102) -> (Vec<InlineContent>, TextCursor) {
103    let mut new_content = content.to_vec();
104
105    // First, if the selection is a range, we perform a deletion.
106    // The result of a deletion is always a single cursor.
107    let cursor_after_delete = match selection {
108        Selection::Range(range) => {
109            let (content_after_delete, cursor_pos) = delete_range(&new_content, range);
110            new_content = content_after_delete;
111            cursor_pos
112        }
113        Selection::Cursor(cursor) => *cursor,
114    };
115
116    // Now, apply the edit at the collapsed cursor position.
117    match edit {
118        TextEdit::Insert(text_to_insert) => {
119            insert_text(&mut new_content, &cursor_after_delete, text_to_insert)
120        }
121        TextEdit::DeleteBackward => delete_backward(&mut new_content, &cursor_after_delete),
122        TextEdit::DeleteForward => delete_forward(&mut new_content, &cursor_after_delete),
123    }
124}
125
126/// Deletes the content within a given range.
127pub fn delete_range(
128    content: &[InlineContent],
129    range: &SelectionRange,
130) -> (Vec<InlineContent>, TextCursor) {
131    // This is a highly complex function. A full implementation needs to handle:
132    //
133    // - Deletions within a single text run.
134    // - Deletions that span across multiple text runs.
135    // - Deletions that include non-text items like images.
136    //
137    // For now, we provide a simplified version that handles deletion within a
138    // single run.
139
140    let mut new_content = content.to_vec();
141    let start_run_idx = range.start.cluster_id.source_run as usize;
142    let end_run_idx = range.end.cluster_id.source_run as usize;
143
144    if start_run_idx == end_run_idx {
145        if let Some(InlineContent::Text(run)) = new_content.get_mut(start_run_idx) {
146            let start_byte = range.start.cluster_id.start_byte_in_run as usize;
147            let end_byte = range.end.cluster_id.start_byte_in_run as usize;
148            if start_byte <= end_byte && end_byte <= run.text.len() {
149                run.text.drain(start_byte..end_byte);
150            }
151        }
152    } else {
153        // TODO: Handle multi-run deletion
154    }
155
156    (new_content, range.start) // Return cursor at the start of the deleted range
157}
158
159/// Inserts text at a cursor position.
160/// 
161/// The cursor's affinity determines the exact insertion point:
162/// - `Leading`: Insert at the start of the referenced cluster (start_byte_in_run)
163/// - `Trailing`: Insert at the end of the referenced cluster (after the grapheme)
164pub fn insert_text(
165    content: &mut Vec<InlineContent>,
166    cursor: &TextCursor,
167    text_to_insert: &str,
168) -> (Vec<InlineContent>, TextCursor) {
169    use unicode_segmentation::UnicodeSegmentation;
170    
171    let mut new_content = content.clone();
172    let run_idx = cursor.cluster_id.source_run as usize;
173    let cluster_start_byte = cursor.cluster_id.start_byte_in_run as usize;
174
175    if let Some(InlineContent::Text(run)) = new_content.get_mut(run_idx) {
176        // Calculate the actual insertion byte offset based on affinity
177        let byte_offset = match cursor.affinity {
178            CursorAffinity::Leading => {
179                // Insert at the start of the cluster
180                cluster_start_byte
181            },
182            CursorAffinity::Trailing => {
183                // Insert at the end of the cluster - find the next grapheme boundary
184                // We need to find where this grapheme cluster ends
185                if cluster_start_byte >= run.text.len() {
186                    // Cursor is at/past end of run - insert at end
187                    run.text.len()
188                } else {
189                    // Find the grapheme that starts at cluster_start_byte and get its end
190                    run.text[cluster_start_byte..]
191                        .grapheme_indices(true)
192                        .next()
193                        .map(|(_, grapheme)| cluster_start_byte + grapheme.len())
194                        .unwrap_or(run.text.len())
195                }
196            },
197        };
198        
199        if byte_offset <= run.text.len() {
200            run.text.insert_str(byte_offset, text_to_insert);
201
202            let new_cursor = TextCursor {
203                cluster_id: GraphemeClusterId {
204                    source_run: run_idx as u32,
205                    start_byte_in_run: (byte_offset + text_to_insert.len()) as u32,
206                },
207                affinity: CursorAffinity::Leading,
208            };
209            return (new_content, new_cursor);
210        }
211    }
212
213    // If insertion failed, return original state
214    (content.to_vec(), *cursor)
215}
216
217/// Deletes one grapheme cluster backward from the cursor.
218/// 
219/// The cursor's affinity determines the actual cursor position:
220/// - `Leading`: Cursor is at start of cluster, delete the previous grapheme
221/// - `Trailing`: Cursor is at end of cluster, delete the current grapheme
222pub fn delete_backward(
223    content: &mut Vec<InlineContent>,
224    cursor: &TextCursor,
225) -> (Vec<InlineContent>, TextCursor) {
226    use unicode_segmentation::UnicodeSegmentation;
227    let mut new_content = content.clone();
228    let run_idx = cursor.cluster_id.source_run as usize;
229    let cluster_start_byte = cursor.cluster_id.start_byte_in_run as usize;
230
231    if let Some(InlineContent::Text(run)) = new_content.get_mut(run_idx) {
232        // Calculate the actual cursor byte offset based on affinity
233        let byte_offset = match cursor.affinity {
234            CursorAffinity::Leading => cluster_start_byte,
235            CursorAffinity::Trailing => {
236                // Cursor is at end of cluster - find the next grapheme boundary
237                if cluster_start_byte >= run.text.len() {
238                    run.text.len()
239                } else {
240                    run.text[cluster_start_byte..]
241                        .grapheme_indices(true)
242                        .next()
243                        .map(|(_, grapheme)| cluster_start_byte + grapheme.len())
244                        .unwrap_or(run.text.len())
245                }
246            },
247        };
248        
249        if byte_offset > 0 {
250            let prev_grapheme_start = run.text[..byte_offset]
251                .grapheme_indices(true)
252                .last()
253                .map_or(0, |(i, _)| i);
254            run.text.drain(prev_grapheme_start..byte_offset);
255
256            let new_cursor = TextCursor {
257                cluster_id: GraphemeClusterId {
258                    source_run: run_idx as u32,
259                    start_byte_in_run: prev_grapheme_start as u32,
260                },
261                affinity: CursorAffinity::Leading,
262            };
263            return (new_content, new_cursor);
264        } else if run_idx > 0 {
265            // Handle deleting across run boundaries (merge with previous run)
266            if let Some(InlineContent::Text(prev_run)) = content.get(run_idx - 1).cloned() {
267                let mut merged_text = prev_run.text;
268                let new_cursor_byte_offset = merged_text.len();
269                merged_text.push_str(&run.text);
270
271                new_content[run_idx - 1] = InlineContent::Text(StyledRun {
272                    text: merged_text,
273                    style: prev_run.style,
274                    logical_start_byte: prev_run.logical_start_byte,
275                    source_node_id: prev_run.source_node_id,
276                });
277                new_content.remove(run_idx);
278
279                let new_cursor = TextCursor {
280                    cluster_id: GraphemeClusterId {
281                        source_run: (run_idx - 1) as u32,
282                        start_byte_in_run: new_cursor_byte_offset as u32,
283                    },
284                    affinity: CursorAffinity::Leading,
285                };
286                return (new_content, new_cursor);
287            }
288        }
289    }
290
291    (content.to_vec(), *cursor)
292}
293
294/// Deletes one grapheme cluster forward from the cursor.
295/// 
296/// The cursor's affinity determines the actual cursor position:
297/// - `Leading`: Cursor is at start of cluster, delete the current grapheme
298/// - `Trailing`: Cursor is at end of cluster, delete the next grapheme
299pub fn delete_forward(
300    content: &mut Vec<InlineContent>,
301    cursor: &TextCursor,
302) -> (Vec<InlineContent>, TextCursor) {
303    use unicode_segmentation::UnicodeSegmentation;
304    let mut new_content = content.clone();
305    let run_idx = cursor.cluster_id.source_run as usize;
306    let cluster_start_byte = cursor.cluster_id.start_byte_in_run as usize;
307
308    if let Some(InlineContent::Text(run)) = new_content.get_mut(run_idx) {
309        // Calculate the actual cursor byte offset based on affinity
310        let byte_offset = match cursor.affinity {
311            CursorAffinity::Leading => cluster_start_byte,
312            CursorAffinity::Trailing => {
313                // Cursor is at end of cluster - find the next grapheme boundary
314                if cluster_start_byte >= run.text.len() {
315                    run.text.len()
316                } else {
317                    run.text[cluster_start_byte..]
318                        .grapheme_indices(true)
319                        .next()
320                        .map(|(_, grapheme)| cluster_start_byte + grapheme.len())
321                        .unwrap_or(run.text.len())
322                }
323            },
324        };
325        
326        if byte_offset < run.text.len() {
327            let next_grapheme_end = run.text[byte_offset..]
328                .grapheme_indices(true)
329                .nth(1)
330                .map_or(run.text.len(), |(i, _)| byte_offset + i);
331            run.text.drain(byte_offset..next_grapheme_end);
332
333            // Cursor position stays at the same byte offset but with Leading affinity
334            let new_cursor = TextCursor {
335                cluster_id: GraphemeClusterId {
336                    source_run: run_idx as u32,
337                    start_byte_in_run: byte_offset as u32,
338                },
339                affinity: CursorAffinity::Leading,
340            };
341            return (new_content, new_cursor);
342        } else if run_idx < content.len() - 1 {
343            // Handle deleting across run boundaries (merge with next run)
344            if let Some(InlineContent::Text(next_run)) = content.get(run_idx + 1).cloned() {
345                let mut merged_text = run.text.clone();
346                merged_text.push_str(&next_run.text);
347
348                new_content[run_idx] = InlineContent::Text(StyledRun {
349                    text: merged_text,
350                    style: run.style.clone(),
351                    logical_start_byte: run.logical_start_byte,
352                    source_node_id: run.source_node_id,
353                });
354                new_content.remove(run_idx + 1);
355
356                return (new_content, *cursor);
357            }
358        }
359    }
360
361    (content.to_vec(), *cursor)
362}
363
364/// Inspect what would be deleted by a delete operation without actually deleting
365///
366/// Returns (range_that_would_be_deleted, text_that_would_be_deleted).
367/// This is useful for callbacks to inspect pending delete operations.
368///
369/// # Arguments
370///
371/// - `content` - The current text content
372/// - `selection` - The current selection (cursor or range)
373/// - `forward` - If true, delete forward (Delete key); if false, delete backward (Backspace key)
374///
375/// # Returns
376///
377/// - `Some((range, deleted_text))` - The range and text that would be deleted
378/// - `None` - Nothing would be deleted (e.g., cursor at start/end of document)
379pub fn inspect_delete(
380    content: &[InlineContent],
381    selection: &Selection,
382    forward: bool,
383) -> Option<(SelectionRange, String)> {
384    match selection {
385        Selection::Range(range) => {
386            // If there's already a selection, that's what would be deleted
387            let deleted_text = extract_text_in_range(content, range);
388            Some((*range, deleted_text))
389        }
390        Selection::Cursor(cursor) => {
391            // No selection - would delete one grapheme cluster
392            if forward {
393                inspect_delete_forward(content, cursor)
394            } else {
395                inspect_delete_backward(content, cursor)
396            }
397        }
398    }
399}
400
401/// Inspect what would be deleted by delete-forward (Delete key)
402fn inspect_delete_forward(
403    content: &[InlineContent],
404    cursor: &TextCursor,
405) -> Option<(SelectionRange, String)> {
406    use unicode_segmentation::UnicodeSegmentation;
407
408    let run_idx = cursor.cluster_id.source_run as usize;
409    let byte_offset = cursor.cluster_id.start_byte_in_run as usize;
410
411    if let Some(InlineContent::Text(run)) = content.get(run_idx) {
412        if byte_offset < run.text.len() {
413            // Delete within same run
414            let next_grapheme_end = run.text[byte_offset..]
415                .grapheme_indices(true)
416                .nth(1)
417                .map_or(run.text.len(), |(i, _)| byte_offset + i);
418
419            let deleted_text = run.text[byte_offset..next_grapheme_end].to_string();
420
421            let range = SelectionRange {
422                start: *cursor,
423                end: TextCursor {
424                    cluster_id: GraphemeClusterId {
425                        source_run: run_idx as u32,
426                        start_byte_in_run: next_grapheme_end as u32,
427                    },
428                    affinity: CursorAffinity::Leading,
429                },
430            };
431
432            return Some((range, deleted_text));
433        } else if run_idx < content.len() - 1 {
434            // Would delete across run boundary
435            if let Some(InlineContent::Text(next_run)) = content.get(run_idx + 1) {
436                let deleted_text = next_run.text.graphemes(true).next()?.to_string();
437
438                let next_grapheme_end = next_run
439                    .text
440                    .grapheme_indices(true)
441                    .nth(1)
442                    .map_or(next_run.text.len(), |(i, _)| i);
443
444                let range = SelectionRange {
445                    start: *cursor,
446                    end: TextCursor {
447                        cluster_id: GraphemeClusterId {
448                            source_run: (run_idx + 1) as u32,
449                            start_byte_in_run: next_grapheme_end as u32,
450                        },
451                        affinity: CursorAffinity::Leading,
452                    },
453                };
454
455                return Some((range, deleted_text));
456            }
457        }
458    }
459
460    None // At end of document, nothing to delete
461}
462
463/// Inspect what would be deleted by delete-backward (Backspace key)
464fn inspect_delete_backward(
465    content: &[InlineContent],
466    cursor: &TextCursor,
467) -> Option<(SelectionRange, String)> {
468    use unicode_segmentation::UnicodeSegmentation;
469
470    let run_idx = cursor.cluster_id.source_run as usize;
471    let byte_offset = cursor.cluster_id.start_byte_in_run as usize;
472
473    if let Some(InlineContent::Text(run)) = content.get(run_idx) {
474        if byte_offset > 0 {
475            // Delete within same run
476            let prev_grapheme_start = run.text[..byte_offset]
477                .grapheme_indices(true)
478                .last()
479                .map_or(0, |(i, _)| i);
480
481            let deleted_text = run.text[prev_grapheme_start..byte_offset].to_string();
482
483            let range = SelectionRange {
484                start: TextCursor {
485                    cluster_id: GraphemeClusterId {
486                        source_run: run_idx as u32,
487                        start_byte_in_run: prev_grapheme_start as u32,
488                    },
489                    affinity: CursorAffinity::Leading,
490                },
491                end: *cursor,
492            };
493
494            return Some((range, deleted_text));
495        } else if run_idx > 0 {
496            // Would delete across run boundary
497            if let Some(InlineContent::Text(prev_run)) = content.get(run_idx - 1) {
498                let deleted_text = prev_run.text.graphemes(true).last()?.to_string();
499
500                let prev_grapheme_start = prev_run.text[..]
501                    .grapheme_indices(true)
502                    .last()
503                    .map_or(0, |(i, _)| i);
504
505                let range = SelectionRange {
506                    start: TextCursor {
507                        cluster_id: GraphemeClusterId {
508                            source_run: (run_idx - 1) as u32,
509                            start_byte_in_run: prev_grapheme_start as u32,
510                        },
511                        affinity: CursorAffinity::Leading,
512                    },
513                    end: *cursor,
514                };
515
516                return Some((range, deleted_text));
517            }
518        }
519    }
520
521    None // At start of document, nothing to delete
522}
523
524/// Extract the text within a selection range
525fn extract_text_in_range(content: &[InlineContent], range: &SelectionRange) -> String {
526    let start_run = range.start.cluster_id.source_run as usize;
527    let end_run = range.end.cluster_id.source_run as usize;
528    let start_byte = range.start.cluster_id.start_byte_in_run as usize;
529    let end_byte = range.end.cluster_id.start_byte_in_run as usize;
530
531    if start_run == end_run {
532        // Single run
533        if let Some(InlineContent::Text(run)) = content.get(start_run) {
534            if start_byte <= end_byte && end_byte <= run.text.len() {
535                return run.text[start_byte..end_byte].to_string();
536            }
537        }
538    } else {
539        // Multi-run selection (simplified - full implementation would handle images, etc.)
540        let mut result = String::new();
541
542        for (idx, item) in content.iter().enumerate() {
543            if let InlineContent::Text(run) = item {
544                if idx == start_run {
545                    // First run - from start_byte to end
546                    if start_byte < run.text.len() {
547                        result.push_str(&run.text[start_byte..]);
548                    }
549                } else if idx > start_run && idx < end_run {
550                    // Middle runs - entire text
551                    result.push_str(&run.text);
552                } else if idx == end_run {
553                    // Last run - from 0 to end_byte
554                    if end_byte <= run.text.len() {
555                        result.push_str(&run.text[..end_byte]);
556                    }
557                    break;
558                }
559            }
560        }
561
562        return result;
563    }
564
565    String::new()
566}