cranpose_foundation/text/
buffer.rs1use super::TextRange;
2
3#[derive(Debug, Clone)]
27pub struct TextFieldBuffer {
28 text: String,
29 selection: TextRange,
30 composition: Option<TextRange>,
31 has_changes: bool,
32}
33
34impl TextFieldBuffer {
35 pub fn new(initial_text: impl Into<String>) -> Self {
38 let text: String = initial_text.into();
39 let len = text.len();
40 Self {
41 text,
42 selection: TextRange::cursor(len),
43 composition: None,
44 has_changes: false,
45 }
46 }
47
48 pub fn with_selection(text: impl Into<String>, selection: TextRange) -> Self {
50 let text: String = text.into();
51 let selection = selection.coerce_in(text.len());
52 Self {
53 text,
54 selection,
55 composition: None,
56 has_changes: false,
57 }
58 }
59
60 pub fn text(&self) -> &str {
62 &self.text
63 }
64
65 pub fn len(&self) -> usize {
67 self.text.len()
68 }
69
70 pub fn is_empty(&self) -> bool {
72 self.text.is_empty()
73 }
74
75 pub fn selection(&self) -> TextRange {
77 self.selection
78 }
79
80 pub fn composition(&self) -> Option<TextRange> {
82 self.composition
83 }
84
85 pub fn has_selection(&self) -> bool {
87 !self.selection.collapsed()
88 }
89
90 pub fn has_changes(&self) -> bool {
92 self.has_changes
93 }
94
95 pub fn replace(&mut self, range: TextRange, replacement: &str) {
101 let min = range.min().min(self.text.len());
102 let max = range.max().min(self.text.len());
103
104 self.text.replace_range(min..max, replacement);
105
106 let new_end = min + replacement.len();
107 self.selection = TextRange::cursor(new_end);
108
109 self.composition = None;
110 self.has_changes = true;
111 }
112
113 pub fn insert(&mut self, text: &str) {
115 if self.has_selection() {
116 self.replace(self.selection, text);
117 } else {
118 let pos = self.selection.start.min(self.text.len());
119 self.text.insert_str(pos, text);
120 self.selection = TextRange::cursor(pos + text.len());
121 self.composition = None;
122 self.has_changes = true;
123 }
124 }
125
126 pub fn append(&mut self, text: &str) {
128 self.text.push_str(text);
129 self.has_changes = true;
130 }
131
132 pub fn delete(&mut self, range: TextRange) {
134 self.replace(range, "");
135 }
136
137 pub fn delete_before_cursor(&mut self) {
139 if self.has_selection() {
140 self.delete(self.selection);
141 } else if self.selection.start > 0 {
142 let pos = self.selection.start;
143 let prev_pos = self.prev_char_boundary(pos);
144 self.delete(TextRange::new(prev_pos, pos));
145 }
146 }
147
148 pub fn delete_after_cursor(&mut self) {
150 if self.has_selection() {
151 self.delete(self.selection);
152 } else if self.selection.start < self.text.len() {
153 let pos = self.selection.start;
154 let next_pos = self.next_char_boundary(pos);
155 self.delete(TextRange::new(pos, next_pos));
156 }
157 }
158
159 pub fn delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) {
164 if self.text.is_empty() || (before_bytes == 0 && after_bytes == 0) {
165 return;
166 }
167
168 let selection = self.selection;
169 let mut start = selection.min().saturating_sub(before_bytes);
170 let mut end = selection
171 .max()
172 .saturating_add(after_bytes)
173 .min(self.text.len());
174
175 start = self.clamp_prev_boundary(start);
176 end = self.clamp_next_boundary(end);
177
178 if start >= end {
179 return;
180 }
181
182 let mut ranges = Vec::new();
183 if let Some(comp) = self.composition {
184 let comp_start = comp.min();
185 let comp_end = comp.max();
186
187 if end <= comp_start || start >= comp_end {
188 ranges.push((start, end));
189 } else {
190 if start < comp_start {
191 ranges.push((start, comp_start));
192 }
193 if end > comp_end {
194 ranges.push((comp_end, end));
195 }
196 }
197 } else {
198 ranges.push((start, end));
199 }
200
201 if ranges.is_empty() {
202 return;
203 }
204
205 ranges.sort_by_key(|(range_start, _)| *range_start);
206 let total_removed: usize = ranges.iter().map(|(s, e)| e - s).sum();
207 if total_removed == 0 {
208 return;
209 }
210
211 let original_text = self.text.clone();
212 let mut new_text = String::with_capacity(original_text.len().saturating_sub(total_removed));
213 let mut last = 0usize;
214 for (range_start, range_end) in &ranges {
215 if last < *range_start {
216 new_text.push_str(&original_text[last..*range_start]);
217 }
218 last = *range_end;
219 }
220 new_text.push_str(&original_text[last..]);
221
222 let removed_before = |pos: usize| -> usize {
223 let mut removed = 0usize;
224 for (range_start, range_end) in &ranges {
225 if pos <= *range_start {
226 break;
227 }
228 let clamped_end = pos.min(*range_end);
229 if clamped_end > *range_start {
230 removed += clamped_end - *range_start;
231 }
232 }
233 removed
234 };
235
236 let cursor_pos = selection.min();
237 let new_cursor = cursor_pos
238 .saturating_sub(removed_before(cursor_pos))
239 .min(new_text.len());
240
241 self.text = new_text;
242 self.selection = TextRange::cursor(new_cursor);
243 self.composition = self.composition.map(|comp| {
244 let comp_start = comp.min().saturating_sub(removed_before(comp.min()));
245 let comp_end = comp.max().saturating_sub(removed_before(comp.max()));
246 TextRange::new(comp_start, comp_end).coerce_in(self.text.len())
247 });
248 self.has_changes = true;
249 }
250
251 pub fn clear(&mut self) {
253 self.text.clear();
254 self.selection = TextRange::zero();
255 self.composition = None;
256 self.has_changes = true;
257 }
258
259 pub fn place_cursor_at_end(&mut self) {
261 self.selection = TextRange::cursor(self.text.len());
262 }
263
264 pub fn place_cursor_at_start(&mut self) {
266 self.selection = TextRange::zero();
267 }
268
269 pub fn place_cursor_before_char(&mut self, index: usize) {
271 let pos = index.min(self.text.len());
272 self.selection = TextRange::cursor(pos);
273 }
274
275 pub fn select_all(&mut self) {
277 self.selection = TextRange::all(self.text.len());
278 }
279
280 pub fn extend_selection_left(&mut self) {
292 if self.selection.end > 0 {
293 let cursor = self.prev_char_boundary(self.selection.end);
294 self.selection = TextRange::new(self.selection.start, cursor);
295 }
296 }
297
298 pub fn extend_selection_right(&mut self) {
303 if self.selection.end < self.text.len() {
304 let cursor = self.next_char_boundary(self.selection.end);
305 self.selection = TextRange::new(self.selection.start, cursor);
306 }
307 }
308
309 pub fn select(&mut self, range: TextRange) {
311 self.selection = range.coerce_in(self.text.len());
312 }
313
314 pub fn set_composition(&mut self, range: Option<TextRange>) {
316 self.composition = range.map(|r| r.coerce_in(self.text.len()));
317 }
318
319 fn prev_char_boundary(&self, from: usize) -> usize {
320 let mut pos = from.saturating_sub(1);
321 while pos > 0 && !self.text.is_char_boundary(pos) {
322 pos -= 1;
323 }
324 pos
325 }
326
327 fn next_char_boundary(&self, from: usize) -> usize {
328 let mut pos = from + 1;
329 while pos < self.text.len() && !self.text.is_char_boundary(pos) {
330 pos += 1;
331 }
332 pos.min(self.text.len())
333 }
334
335 fn clamp_prev_boundary(&self, from: usize) -> usize {
336 if self.text.is_char_boundary(from) {
337 from
338 } else {
339 self.prev_char_boundary(from)
340 }
341 }
342
343 fn clamp_next_boundary(&self, from: usize) -> usize {
344 if self.text.is_char_boundary(from) {
345 from
346 } else {
347 self.next_char_boundary(from)
348 }
349 }
350
351 pub fn copy_selection(&self) -> Option<String> {
354 if !self.has_selection() {
355 return None;
356 }
357
358 let sel_start = self.selection.min();
359 let sel_end = self.selection.max();
360 Some(self.text[sel_start..sel_end].to_string())
361 }
362
363 pub fn cut_selection(&mut self) -> Option<String> {
366 let copied = self.copy_selection();
367 if copied.is_some() {
368 self.delete(self.selection);
369 self.has_changes = true;
370 }
371 copied
372 }
373}
374
375impl Default for TextFieldBuffer {
376 fn default() -> Self {
377 Self::new("")
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn new_buffer_has_cursor_at_end() {
387 let buffer = TextFieldBuffer::new("Hello");
388 assert_eq!(buffer.text(), "Hello");
389 assert_eq!(buffer.selection(), TextRange::cursor(5));
390 }
391
392 #[test]
393 fn insert_at_cursor() {
394 let mut buffer = TextFieldBuffer::new("Hello");
395 buffer.place_cursor_at_end();
396 buffer.insert(", World!");
397 assert_eq!(buffer.text(), "Hello, World!");
398 assert_eq!(buffer.selection(), TextRange::cursor(13));
399 }
400
401 #[test]
402 fn insert_in_middle() {
403 let mut buffer = TextFieldBuffer::new("Helo");
404 buffer.place_cursor_before_char(2);
405 buffer.insert("l");
406 assert_eq!(buffer.text(), "Hello");
407 }
408
409 #[test]
410 fn delete_selection() {
411 let mut buffer = TextFieldBuffer::new("Hello World");
412 buffer.select(TextRange::new(5, 11));
413 buffer.delete(buffer.selection());
414 assert_eq!(buffer.text(), "Hello");
415 }
416
417 #[test]
418 fn delete_before_cursor() {
419 let mut buffer = TextFieldBuffer::new("Hello");
420 buffer.place_cursor_at_end();
421 buffer.delete_before_cursor();
422 assert_eq!(buffer.text(), "Hell");
423 }
424
425 #[test]
426 fn select_all() {
427 let mut buffer = TextFieldBuffer::new("Hello");
428 buffer.select_all();
429 assert_eq!(buffer.selection(), TextRange::new(0, 5));
430 }
431
432 #[test]
433 fn replace_selection() {
434 let mut buffer = TextFieldBuffer::new("Hello World");
435 buffer.select(TextRange::new(6, 11));
436 buffer.insert("Rust");
437 assert_eq!(buffer.text(), "Hello Rust");
438 }
439
440 #[test]
441 fn clear_buffer() {
442 let mut buffer = TextFieldBuffer::new("Hello");
443 buffer.clear();
444 assert!(buffer.is_empty());
445 assert_eq!(buffer.selection(), TextRange::zero());
446 }
447
448 #[test]
449 fn unicode_handling() {
450 let mut buffer = TextFieldBuffer::new("Hello 🌍");
451 buffer.place_cursor_at_end();
452 buffer.delete_before_cursor();
453 assert_eq!(buffer.text(), "Hello ");
454 }
455
456 #[test]
457 fn delete_surrounding_collapsed_cursor() {
458 let mut buffer = TextFieldBuffer::new("abcdef");
459 buffer.place_cursor_before_char(3);
460 buffer.delete_surrounding(2, 2);
461 assert_eq!(buffer.text(), "af");
462 assert_eq!(buffer.selection(), TextRange::cursor(1));
463 }
464
465 #[test]
466 fn delete_surrounding_preserves_composition() {
467 let mut buffer = TextFieldBuffer::new("abcdef");
468 buffer.place_cursor_before_char(3);
469 buffer.set_composition(Some(TextRange::new(2, 4)));
470 buffer.delete_surrounding(3, 3);
471 assert_eq!(buffer.text(), "cd");
472 assert_eq!(buffer.selection(), TextRange::cursor(1));
473 assert_eq!(buffer.composition(), Some(TextRange::new(0, 2)));
474 }
475}