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 select_all(&mut self) {
298 self.selection = TextRange::all(self.text.len());
299 }
300
301 pub fn extend_selection_left(&mut self) {
313 if self.selection.end > 0 {
314 let cursor = self.prev_char_boundary(self.selection.end);
315 self.selection = TextRange::new(self.selection.start, cursor);
316 }
317 }
318
319 pub fn extend_selection_right(&mut self) {
324 if self.selection.end < self.text.len() {
325 let cursor = self.next_char_boundary(self.selection.end);
326 self.selection = TextRange::new(self.selection.start, cursor);
327 }
328 }
329
330 pub fn select(&mut self, range: TextRange) {
332 self.selection = range.coerce_in(self.text.len());
333 }
334
335 pub fn set_composition(&mut self, range: Option<TextRange>) {
337 self.composition = range.map(|r| r.coerce_in(self.text.len()));
338 }
339
340 fn prev_char_boundary(&self, from: usize) -> usize {
344 let mut pos = from.saturating_sub(1);
345 while pos > 0 && !self.text.is_char_boundary(pos) {
346 pos -= 1;
347 }
348 pos
349 }
350
351 fn next_char_boundary(&self, from: usize) -> usize {
353 let mut pos = from + 1;
354 while pos < self.text.len() && !self.text.is_char_boundary(pos) {
355 pos += 1;
356 }
357 pos.min(self.text.len())
358 }
359
360 fn clamp_prev_boundary(&self, from: usize) -> usize {
361 if self.text.is_char_boundary(from) {
362 from
363 } else {
364 self.prev_char_boundary(from)
365 }
366 }
367
368 fn clamp_next_boundary(&self, from: usize) -> usize {
369 if self.text.is_char_boundary(from) {
370 from
371 } else {
372 self.next_char_boundary(from)
373 }
374 }
375
376 pub fn copy_selection(&self) -> Option<String> {
383 if !self.has_selection() {
384 return None;
385 }
386
387 let sel_start = self.selection.min();
388 let sel_end = self.selection.max();
389 Some(self.text[sel_start..sel_end].to_string())
390 }
391
392 pub fn cut_selection(&mut self) -> Option<String> {
395 let copied = self.copy_selection();
396 if copied.is_some() {
397 self.delete(self.selection);
398 self.has_changes = true;
399 }
400 copied
401 }
402}
403
404impl Default for TextFieldBuffer {
405 fn default() -> Self {
406 Self::new("")
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413
414 #[test]
415 fn new_buffer_has_cursor_at_end() {
416 let buffer = TextFieldBuffer::new("Hello");
417 assert_eq!(buffer.text(), "Hello");
418 assert_eq!(buffer.selection(), TextRange::cursor(5));
419 }
420
421 #[test]
422 fn insert_at_cursor() {
423 let mut buffer = TextFieldBuffer::new("Hello");
424 buffer.place_cursor_at_end();
425 buffer.insert(", World!");
426 assert_eq!(buffer.text(), "Hello, World!");
427 assert_eq!(buffer.selection(), TextRange::cursor(13));
428 }
429
430 #[test]
431 fn insert_in_middle() {
432 let mut buffer = TextFieldBuffer::new("Helo");
433 buffer.place_cursor_before_char(2);
434 buffer.insert("l");
435 assert_eq!(buffer.text(), "Hello");
436 }
437
438 #[test]
439 fn delete_selection() {
440 let mut buffer = TextFieldBuffer::new("Hello World");
441 buffer.select(TextRange::new(5, 11)); buffer.delete(buffer.selection());
443 assert_eq!(buffer.text(), "Hello");
444 }
445
446 #[test]
447 fn delete_before_cursor() {
448 let mut buffer = TextFieldBuffer::new("Hello");
449 buffer.place_cursor_at_end();
450 buffer.delete_before_cursor();
451 assert_eq!(buffer.text(), "Hell");
452 }
453
454 #[test]
455 fn select_all() {
456 let mut buffer = TextFieldBuffer::new("Hello");
457 buffer.select_all();
458 assert_eq!(buffer.selection(), TextRange::new(0, 5));
459 }
460
461 #[test]
462 fn replace_selection() {
463 let mut buffer = TextFieldBuffer::new("Hello World");
464 buffer.select(TextRange::new(6, 11)); buffer.insert("Rust");
466 assert_eq!(buffer.text(), "Hello Rust");
467 }
468
469 #[test]
470 fn clear_buffer() {
471 let mut buffer = TextFieldBuffer::new("Hello");
472 buffer.clear();
473 assert!(buffer.is_empty());
474 assert_eq!(buffer.selection(), TextRange::zero());
475 }
476
477 #[test]
478 fn unicode_handling() {
479 let mut buffer = TextFieldBuffer::new("Hello 🌍");
480 buffer.place_cursor_at_end();
481 buffer.delete_before_cursor();
482 assert_eq!(buffer.text(), "Hello ");
483 }
484
485 #[test]
486 fn delete_surrounding_collapsed_cursor() {
487 let mut buffer = TextFieldBuffer::new("abcdef");
488 buffer.place_cursor_before_char(3); buffer.delete_surrounding(2, 2); assert_eq!(buffer.text(), "af");
491 assert_eq!(buffer.selection(), TextRange::cursor(1));
492 }
493
494 #[test]
495 fn delete_surrounding_preserves_composition() {
496 let mut buffer = TextFieldBuffer::new("abcdef");
497 buffer.place_cursor_before_char(3);
498 buffer.set_composition(Some(TextRange::new(2, 4))); buffer.delete_surrounding(3, 3);
500 assert_eq!(buffer.text(), "cd");
501 assert_eq!(buffer.selection(), TextRange::cursor(1));
502 assert_eq!(buffer.composition(), Some(TextRange::new(0, 2)));
503 }
504}