cranpose_foundation/text/
buffer.rs1use super::TextRange;
7
8#[derive(Debug, Clone)]
32pub struct TextFieldBuffer {
33 text: String,
35 selection: TextRange,
37 composition: Option<TextRange>,
39 has_changes: bool,
41}
42
43impl TextFieldBuffer {
44 pub fn new(initial_text: impl Into<String>) -> Self {
47 let text: String = initial_text.into();
48 let len = text.len();
49 Self {
50 text,
51 selection: TextRange::cursor(len),
52 composition: None,
53 has_changes: false,
54 }
55 }
56
57 pub fn with_selection(text: impl Into<String>, selection: TextRange) -> Self {
59 let text: String = text.into();
60 let selection = selection.coerce_in(text.len());
61 Self {
62 text,
63 selection,
64 composition: None,
65 has_changes: false,
66 }
67 }
68
69 pub fn text(&self) -> &str {
71 &self.text
72 }
73
74 pub fn len(&self) -> usize {
76 self.text.len()
77 }
78
79 pub fn is_empty(&self) -> bool {
81 self.text.is_empty()
82 }
83
84 pub fn selection(&self) -> TextRange {
86 self.selection
87 }
88
89 pub fn composition(&self) -> Option<TextRange> {
91 self.composition
92 }
93
94 pub fn has_selection(&self) -> bool {
96 !self.selection.collapsed()
97 }
98
99 pub fn has_changes(&self) -> bool {
101 self.has_changes
102 }
103
104 pub fn replace(&mut self, range: TextRange, replacement: &str) {
112 let min = range.min().min(self.text.len());
113 let max = range.max().min(self.text.len());
114
115 self.text.replace_range(min..max, replacement);
117
118 let new_end = min + replacement.len();
120 self.selection = TextRange::cursor(new_end);
121
122 self.composition = None;
124 self.has_changes = true;
125 }
126
127 pub fn insert(&mut self, text: &str) {
129 if self.has_selection() {
130 self.replace(self.selection, text);
132 } else {
133 let pos = self.selection.start.min(self.text.len());
135 self.text.insert_str(pos, text);
136 self.selection = TextRange::cursor(pos + text.len());
137 self.composition = None;
138 self.has_changes = true;
139 }
140 }
141
142 pub fn append(&mut self, text: &str) {
144 self.text.push_str(text);
145 self.has_changes = true;
146 }
147
148 pub fn delete(&mut self, range: TextRange) {
150 self.replace(range, "");
151 }
152
153 pub fn delete_before_cursor(&mut self) {
155 if self.has_selection() {
156 self.delete(self.selection);
158 } else if self.selection.start > 0 {
159 let pos = self.selection.start;
162 let prev_pos = self.prev_char_boundary(pos);
163 self.delete(TextRange::new(prev_pos, pos));
164 }
165 }
166
167 pub fn delete_after_cursor(&mut self) {
169 if self.has_selection() {
170 self.delete(self.selection);
171 } else if self.selection.start < self.text.len() {
172 let pos = self.selection.start;
173 let next_pos = self.next_char_boundary(pos);
174 self.delete(TextRange::new(pos, next_pos));
175 }
176 }
177
178 pub fn delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) {
183 if self.text.is_empty() || (before_bytes == 0 && after_bytes == 0) {
184 return;
185 }
186
187 let selection = self.selection;
188 let mut start = selection.min().saturating_sub(before_bytes);
189 let mut end = selection
190 .max()
191 .saturating_add(after_bytes)
192 .min(self.text.len());
193
194 start = self.clamp_prev_boundary(start);
195 end = self.clamp_next_boundary(end);
196
197 if start >= end {
198 return;
199 }
200
201 let mut ranges = Vec::new();
202 if let Some(comp) = self.composition {
203 let comp_start = comp.min();
204 let comp_end = comp.max();
205
206 if end <= comp_start || start >= comp_end {
207 ranges.push((start, end));
208 } else {
209 if start < comp_start {
210 ranges.push((start, comp_start));
211 }
212 if end > comp_end {
213 ranges.push((comp_end, end));
214 }
215 }
216 } else {
217 ranges.push((start, end));
218 }
219
220 if ranges.is_empty() {
221 return;
222 }
223
224 ranges.sort_by_key(|(range_start, _)| *range_start);
225 let total_removed: usize = ranges.iter().map(|(s, e)| e - s).sum();
226 if total_removed == 0 {
227 return;
228 }
229
230 let original_text = self.text.clone();
231 let mut new_text = String::with_capacity(original_text.len().saturating_sub(total_removed));
232 let mut last = 0usize;
233 for (range_start, range_end) in &ranges {
234 if last < *range_start {
235 new_text.push_str(&original_text[last..*range_start]);
236 }
237 last = *range_end;
238 }
239 new_text.push_str(&original_text[last..]);
240
241 let removed_before = |pos: usize| -> usize {
242 let mut removed = 0usize;
243 for (range_start, range_end) in &ranges {
244 if pos <= *range_start {
245 break;
246 }
247 let clamped_end = pos.min(*range_end);
248 if clamped_end > *range_start {
249 removed += clamped_end - *range_start;
250 }
251 }
252 removed
253 };
254
255 let cursor_pos = selection.min();
256 let new_cursor = cursor_pos
257 .saturating_sub(removed_before(cursor_pos))
258 .min(new_text.len());
259
260 self.text = new_text;
261 self.selection = TextRange::cursor(new_cursor);
262 self.composition = self.composition.map(|comp| {
263 let comp_start = comp.min().saturating_sub(removed_before(comp.min()));
264 let comp_end = comp.max().saturating_sub(removed_before(comp.max()));
265 TextRange::new(comp_start, comp_end).coerce_in(self.text.len())
266 });
267 self.has_changes = true;
268 }
269
270 pub fn clear(&mut self) {
272 self.text.clear();
273 self.selection = TextRange::zero();
274 self.composition = None;
275 self.has_changes = true;
276 }
277
278 pub fn place_cursor_at_end(&mut self) {
282 self.selection = TextRange::cursor(self.text.len());
283 }
284
285 pub fn place_cursor_at_start(&mut self) {
287 self.selection = TextRange::zero();
288 }
289
290 pub fn place_cursor_before_char(&mut self, index: usize) {
292 let pos = index.min(self.text.len());
293 self.selection = TextRange::cursor(pos);
294 }
295
296 pub fn place_cursor_after_char(&mut self, index: usize) {
298 let pos = (index + 1).min(self.text.len());
299 self.selection = TextRange::cursor(pos);
300 }
301
302 pub fn select_all(&mut self) {
304 self.selection = TextRange::all(self.text.len());
305 }
306
307 pub fn extend_selection_left(&mut self) {
311 if self.selection.start > 0 {
312 let new_start = self.prev_char_boundary(self.selection.start);
313 self.selection = TextRange::new(new_start, self.selection.end);
314 }
315 }
316
317 pub fn extend_selection_right(&mut self) {
321 if self.selection.end < self.text.len() {
322 let new_end = self.next_char_boundary(self.selection.end);
323 self.selection = TextRange::new(self.selection.start, new_end);
324 }
325 }
326
327 pub fn select(&mut self, range: TextRange) {
329 self.selection = range.coerce_in(self.text.len());
330 }
331
332 pub fn set_composition(&mut self, range: Option<TextRange>) {
334 self.composition = range.map(|r| r.coerce_in(self.text.len()));
335 }
336
337 fn prev_char_boundary(&self, from: usize) -> usize {
341 let mut pos = from.saturating_sub(1);
342 while pos > 0 && !self.text.is_char_boundary(pos) {
343 pos -= 1;
344 }
345 pos
346 }
347
348 fn next_char_boundary(&self, from: usize) -> usize {
350 let mut pos = from + 1;
351 while pos < self.text.len() && !self.text.is_char_boundary(pos) {
352 pos += 1;
353 }
354 pos.min(self.text.len())
355 }
356
357 fn clamp_prev_boundary(&self, from: usize) -> usize {
358 if self.text.is_char_boundary(from) {
359 from
360 } else {
361 self.prev_char_boundary(from)
362 }
363 }
364
365 fn clamp_next_boundary(&self, from: usize) -> usize {
366 if self.text.is_char_boundary(from) {
367 from
368 } else {
369 self.next_char_boundary(from)
370 }
371 }
372
373 pub fn copy_selection(&self) -> Option<String> {
380 if !self.has_selection() {
381 return None;
382 }
383
384 let sel_start = self.selection.min();
385 let sel_end = self.selection.max();
386 Some(self.text[sel_start..sel_end].to_string())
387 }
388
389 pub fn cut_selection(&mut self) -> Option<String> {
392 let copied = self.copy_selection();
393 if copied.is_some() {
394 self.delete(self.selection);
395 self.has_changes = true;
396 }
397 copied
398 }
399}
400
401impl Default for TextFieldBuffer {
402 fn default() -> Self {
403 Self::new("")
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 #[test]
412 fn new_buffer_has_cursor_at_end() {
413 let buffer = TextFieldBuffer::new("Hello");
414 assert_eq!(buffer.text(), "Hello");
415 assert_eq!(buffer.selection(), TextRange::cursor(5));
416 }
417
418 #[test]
419 fn insert_at_cursor() {
420 let mut buffer = TextFieldBuffer::new("Hello");
421 buffer.place_cursor_at_end();
422 buffer.insert(", World!");
423 assert_eq!(buffer.text(), "Hello, World!");
424 assert_eq!(buffer.selection(), TextRange::cursor(13));
425 }
426
427 #[test]
428 fn insert_in_middle() {
429 let mut buffer = TextFieldBuffer::new("Helo");
430 buffer.place_cursor_before_char(2);
431 buffer.insert("l");
432 assert_eq!(buffer.text(), "Hello");
433 }
434
435 #[test]
436 fn delete_selection() {
437 let mut buffer = TextFieldBuffer::new("Hello World");
438 buffer.select(TextRange::new(5, 11)); buffer.delete(buffer.selection());
440 assert_eq!(buffer.text(), "Hello");
441 }
442
443 #[test]
444 fn delete_before_cursor() {
445 let mut buffer = TextFieldBuffer::new("Hello");
446 buffer.place_cursor_at_end();
447 buffer.delete_before_cursor();
448 assert_eq!(buffer.text(), "Hell");
449 }
450
451 #[test]
452 fn select_all() {
453 let mut buffer = TextFieldBuffer::new("Hello");
454 buffer.select_all();
455 assert_eq!(buffer.selection(), TextRange::new(0, 5));
456 }
457
458 #[test]
459 fn replace_selection() {
460 let mut buffer = TextFieldBuffer::new("Hello World");
461 buffer.select(TextRange::new(6, 11)); buffer.insert("Rust");
463 assert_eq!(buffer.text(), "Hello Rust");
464 }
465
466 #[test]
467 fn clear_buffer() {
468 let mut buffer = TextFieldBuffer::new("Hello");
469 buffer.clear();
470 assert!(buffer.is_empty());
471 assert_eq!(buffer.selection(), TextRange::zero());
472 }
473
474 #[test]
475 fn unicode_handling() {
476 let mut buffer = TextFieldBuffer::new("Hello 🌍");
477 buffer.place_cursor_at_end();
478 buffer.delete_before_cursor();
479 assert_eq!(buffer.text(), "Hello ");
480 }
481
482 #[test]
483 fn delete_surrounding_collapsed_cursor() {
484 let mut buffer = TextFieldBuffer::new("abcdef");
485 buffer.place_cursor_before_char(3); buffer.delete_surrounding(2, 2); assert_eq!(buffer.text(), "af");
488 assert_eq!(buffer.selection(), TextRange::cursor(1));
489 }
490
491 #[test]
492 fn delete_surrounding_preserves_composition() {
493 let mut buffer = TextFieldBuffer::new("abcdef");
494 buffer.place_cursor_before_char(3);
495 buffer.set_composition(Some(TextRange::new(2, 4))); buffer.delete_surrounding(3, 3);
497 assert_eq!(buffer.text(), "cd");
498 assert_eq!(buffer.selection(), TextRange::cursor(1));
499 assert_eq!(buffer.composition(), Some(TextRange::new(0, 2)));
500 }
501}