Skip to main content

cranpose_foundation/text/
buffer.rs

1use smallvec::SmallVec;
2
3use super::{TextFieldValue, TextRange, next_grapheme_boundary, previous_grapheme_boundary};
4
5/// A mutable text buffer that can be edited.
6///
7/// This provides methods for changing text content:
8/// - [`replace`](Self::replace) - Replace a range with new text
9/// - [`append`](Self::append) - Add text at the end
10/// - [`insert`](Self::insert) - Insert text at cursor position
11/// - [`delete`](Self::delete) - Delete a range of text
12///
13/// And for manipulating cursor/selection:
14/// - [`place_cursor_at_end`](Self::place_cursor_at_end)
15/// - [`place_cursor_before_char`](Self::place_cursor_before_char)
16/// - [`move_cursor_left`](Self::move_cursor_left)
17/// - [`select_all`](Self::select_all)
18///
19/// Keyboard editing steps over whole user-perceived characters, Unicode's
20/// extended grapheme clusters: a flag, an emoji joined with zero-width
21/// joiners, a letter with its combining marks. Offsets are UTF-8 bytes.
22///
23/// # Example
24///
25/// ```
26/// use cranpose_foundation::text::{TextFieldBuffer, TextRange};
27///
28/// let mut buffer = TextFieldBuffer::new("Hello");
29/// buffer.place_cursor_at_end();
30/// buffer.insert(", World!");
31/// assert_eq!(buffer.text(), "Hello, World!");
32/// ```
33#[derive(Debug, Clone)]
34pub struct TextFieldBuffer {
35    text: String,
36    selection: TextRange,
37    composition: Option<TextRange>,
38    has_changes: bool,
39}
40
41impl TextFieldBuffer {
42    /// Creates a new buffer with the given initial text.
43    /// Cursor is placed at the end of the text.
44    pub fn new(initial_text: impl Into<String>) -> Self {
45        let text: String = initial_text.into();
46        let len = text.len();
47        Self {
48            text,
49            selection: TextRange::cursor(len),
50            composition: None,
51            has_changes: false,
52        }
53    }
54
55    /// Creates a buffer with text and specified selection.
56    pub fn with_selection(text: impl Into<String>, selection: TextRange) -> Self {
57        let text: String = text.into();
58        let selection = selection.coerce_in(text.len());
59        Self {
60            text,
61            selection,
62            composition: None,
63            has_changes: false,
64        }
65    }
66
67    /// Returns the current text content.
68    pub fn text(&self) -> &str {
69        &self.text
70    }
71
72    /// Returns the length of the text in bytes.
73    pub fn len(&self) -> usize {
74        self.text.len()
75    }
76
77    /// Returns true if the buffer is empty.
78    pub fn is_empty(&self) -> bool {
79        self.text.is_empty()
80    }
81
82    /// Returns the current selection range.
83    pub fn selection(&self) -> TextRange {
84        self.selection
85    }
86
87    /// Returns the current composition (IME) range, if any.
88    pub fn composition(&self) -> Option<TextRange> {
89        self.composition
90    }
91
92    /// Returns true if there's a non-collapsed selection.
93    pub fn has_selection(&self) -> bool {
94        !self.selection.collapsed()
95    }
96
97    /// Returns true if any changes have been made.
98    pub fn has_changes(&self) -> bool {
99        self.has_changes
100    }
101
102    /// Replaces text in the given range with new text.
103    ///
104    /// The selection is adjusted based on the replacement:
105    /// - If replacing before selection, selection shifts
106    /// - If replacing within selection, cursor moves to end of replacement
107    pub fn replace(&mut self, range: TextRange, replacement: &str) {
108        let min = range.min().min(self.text.len());
109        let max = range.max().min(self.text.len());
110
111        self.text.replace_range(min..max, replacement);
112
113        let new_end = min + replacement.len();
114        self.selection = TextRange::cursor(new_end);
115
116        self.composition = None;
117        self.has_changes = true;
118    }
119
120    /// Inserts text at the current cursor position (or replaces selection).
121    pub fn insert(&mut self, text: &str) {
122        if self.has_selection() {
123            self.replace(self.selection, text);
124        } else {
125            let pos = self.selection.start.min(self.text.len());
126            self.text.insert_str(pos, text);
127            self.selection = TextRange::cursor(pos + text.len());
128            self.composition = None;
129            self.has_changes = true;
130        }
131    }
132
133    /// Appends text at the end of the buffer.
134    pub fn append(&mut self, text: &str) {
135        self.text.push_str(text);
136        self.has_changes = true;
137    }
138
139    /// Deletes text in the given range.
140    pub fn delete(&mut self, range: TextRange) {
141        self.replace(range, "");
142    }
143
144    /// Deletes the selection, or the character before the cursor (backspace).
145    pub fn delete_before_cursor(&mut self) {
146        if self.has_selection() {
147            self.delete(self.selection);
148        } else if self.selection.start > 0 {
149            let pos = self.selection.start;
150            let prev_pos = previous_grapheme_boundary(&self.text, pos);
151            self.delete(TextRange::new(prev_pos, pos));
152        }
153    }
154
155    /// Deletes the selection, or the character after the cursor (delete key).
156    pub fn delete_after_cursor(&mut self) {
157        if self.has_selection() {
158            self.delete(self.selection);
159        } else if self.selection.start < self.text.len() {
160            let pos = self.selection.start;
161            let next_pos = next_grapheme_boundary(&self.text, pos);
162            self.delete(TextRange::new(pos, next_pos));
163        }
164    }
165
166    /// Deletes text surrounding the cursor or selection.
167    ///
168    /// `before_bytes` and `after_bytes` are byte counts in UTF-8.
169    /// The deletion respects character boundaries and preserves any IME composition range.
170    pub fn delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) {
171        if self.text.is_empty() || (before_bytes == 0 && after_bytes == 0) {
172            return;
173        }
174
175        let selection = self.selection;
176        let mut start = selection.min().saturating_sub(before_bytes);
177        let mut end = selection
178            .max()
179            .saturating_add(after_bytes)
180            .min(self.text.len());
181
182        start = self.text.floor_char_boundary(start);
183        end = self.text.ceil_char_boundary(end);
184
185        if start >= end {
186            return;
187        }
188
189        // At most two ranges, either side of a composition, in text order.
190        let mut ranges: SmallVec<[(usize, usize); 2]> = SmallVec::new();
191        if let Some(comp) = self.composition {
192            let comp_start = comp.min();
193            let comp_end = comp.max();
194
195            if end <= comp_start || start >= comp_end {
196                ranges.push((start, end));
197            } else {
198                if start < comp_start {
199                    ranges.push((start, comp_start));
200                }
201                if end > comp_end {
202                    ranges.push((comp_end, end));
203                }
204            }
205        } else {
206            ranges.push((start, end));
207        }
208
209        if ranges.is_empty() {
210            return;
211        }
212
213        for &(range_start, range_end) in ranges.iter().rev() {
214            self.text.replace_range(range_start..range_end, "");
215        }
216
217        let removed_before = |pos: usize| -> usize {
218            let mut removed = 0usize;
219            for (range_start, range_end) in &ranges {
220                if pos <= *range_start {
221                    break;
222                }
223                let clamped_end = pos.min(*range_end);
224                if clamped_end > *range_start {
225                    removed += clamped_end - *range_start;
226                }
227            }
228            removed
229        };
230
231        let cursor_pos = selection.min();
232        let new_cursor = cursor_pos
233            .saturating_sub(removed_before(cursor_pos))
234            .min(self.text.len());
235
236        self.selection = TextRange::cursor(new_cursor);
237        self.composition = self.composition.map(|comp| {
238            let comp_start = comp.min().saturating_sub(removed_before(comp.min()));
239            let comp_end = comp.max().saturating_sub(removed_before(comp.max()));
240            TextRange::new(comp_start, comp_end).coerce_in(self.text.len())
241        });
242        self.has_changes = true;
243    }
244
245    /// Replaces the buffer's text, selection and composition with `value`'s:
246    /// how an input transformation rejects an edit.
247    pub fn reset_to(&mut self, value: &TextFieldValue) {
248        self.text.clear();
249        self.text.push_str(&value.text);
250        self.selection = value.selection;
251        self.composition = value.composition;
252    }
253
254    /// Clears all text.
255    pub fn clear(&mut self) {
256        self.text.clear();
257        self.selection = TextRange::zero();
258        self.composition = None;
259        self.has_changes = true;
260    }
261
262    /// Places the cursor at the end of the text.
263    pub fn place_cursor_at_end(&mut self) {
264        self.selection = TextRange::cursor(self.text.len());
265    }
266
267    /// Places the cursor at the start of the text.
268    pub fn place_cursor_at_start(&mut self) {
269        self.selection = TextRange::zero();
270    }
271
272    /// Places the cursor before the character at the given index.
273    pub fn place_cursor_before_char(&mut self, index: usize) {
274        let pos = index.min(self.text.len());
275        self.selection = TextRange::cursor(pos);
276    }
277
278    /// Collapses a selection to its start, or moves the cursor one character
279    /// to the left (the left arrow key).
280    pub fn move_cursor_left(&mut self) {
281        let target = if self.has_selection() {
282            self.selection.min()
283        } else {
284            previous_grapheme_boundary(&self.text, self.selection.start)
285        };
286        self.selection = TextRange::cursor(target);
287    }
288
289    /// Collapses a selection to its end, or moves the cursor one character to
290    /// the right (the right arrow key).
291    pub fn move_cursor_right(&mut self) {
292        let target = if self.has_selection() {
293            self.selection.max()
294        } else {
295            next_grapheme_boundary(&self.text, self.selection.end)
296        };
297        self.selection = TextRange::cursor(target);
298    }
299
300    /// Selects all text.
301    pub fn select_all(&mut self) {
302        self.selection = TextRange::all(self.text.len());
303    }
304
305    /// Extends the selection one character to the left.
306    ///
307    /// `start` is the anchor and `end` is the moving cursor, which is what
308    /// makes this and [`Self::extend_selection_right`] inverses of each other:
309    /// shift-left followed by shift-right returns to where it began. Moving
310    /// whichever end happens to be smaller instead would leave the two growing
311    /// from opposite ends, and shift-right after shift-left would grow the
312    /// selection rather than shrink it.
313    ///
314    /// A reverse range (`start > end`) is how an anchor to the right of the
315    /// cursor is represented; see [`TextRange`].
316    pub fn extend_selection_left(&mut self) {
317        if self.selection.end > 0 {
318            let cursor = previous_grapheme_boundary(&self.text, self.selection.end);
319            self.selection = TextRange::new(self.selection.start, cursor);
320        }
321    }
322
323    /// Extends the selection one character to the right.
324    ///
325    /// The anchor stays where it is and the cursor moves; see
326    /// [`Self::extend_selection_left`].
327    pub fn extend_selection_right(&mut self) {
328        if self.selection.end < self.text.len() {
329            let cursor = next_grapheme_boundary(&self.text, self.selection.end);
330            self.selection = TextRange::new(self.selection.start, cursor);
331        }
332    }
333
334    /// Selects the given range.
335    pub fn select(&mut self, range: TextRange) {
336        self.selection = range.coerce_in(self.text.len());
337    }
338
339    /// Sets the composition (IME) range.
340    pub fn set_composition(&mut self, range: Option<TextRange>) {
341        self.composition = range.map(|r| r.coerce_in(self.text.len()));
342    }
343
344    pub(crate) fn into_value(self) -> TextFieldValue {
345        TextFieldValue {
346            text: self.text,
347            selection: self.selection,
348            composition: self.composition,
349        }
350    }
351
352    /// Returns the selected text for copy operations.
353    /// Returns None if no selection.
354    pub fn copy_selection(&self) -> Option<String> {
355        if !self.has_selection() {
356            return None;
357        }
358
359        let sel_start = self.selection.min();
360        let sel_end = self.selection.max();
361        Some(self.text[sel_start..sel_end].to_string())
362    }
363
364    /// Cuts the selected text (returns it and deletes from buffer).
365    /// Returns the cut text, or None if no selection.
366    pub fn cut_selection(&mut self) -> Option<String> {
367        let copied = self.copy_selection();
368        if copied.is_some() {
369            self.delete(self.selection);
370            self.has_changes = true;
371        }
372        copied
373    }
374}
375
376impl Default for TextFieldBuffer {
377    fn default() -> Self {
378        Self::new("")
379    }
380}
381
382#[cfg(test)]
383#[path = "tests/buffer_tests.rs"]
384mod tests;