Skip to main content

blizz_ui/
input_buffer.rs

1#[derive(Debug, Clone)]
2pub struct InputBuffer {
3  text: String,
4  cursor: usize,
5  selection_anchor: Option<usize>,
6}
7
8pub fn new() -> InputBuffer {
9  InputBuffer {
10    text: String::new(),
11    cursor: 0,
12    selection_anchor: None,
13  }
14}
15
16pub fn text(buf: &InputBuffer) -> &str {
17  &buf.text
18}
19
20pub fn cursor(buf: &InputBuffer) -> usize {
21  buf.cursor
22}
23
24pub fn selection_range(buf: &InputBuffer) -> Option<(usize, usize)> {
25  buf.selection_anchor.map(|anchor| {
26    let start = anchor.min(buf.cursor);
27    let end = anchor.max(buf.cursor);
28    (start, end)
29  })
30}
31
32pub fn has_selection(buf: &InputBuffer) -> bool {
33  buf.selection_anchor.is_some()
34}
35
36pub fn insert_char(buf: &mut InputBuffer, c: char) {
37  delete_selection_if_active(buf);
38  buf.text.insert(byte_offset(&buf.text, buf.cursor), c);
39  buf.cursor += 1;
40}
41
42pub fn backspace(buf: &mut InputBuffer) {
43  if has_selection(buf) {
44    delete_selection_if_active(buf);
45    return;
46  }
47  if buf.cursor == 0 {
48    return;
49  }
50  let offset = byte_offset(&buf.text, buf.cursor - 1);
51  buf.text.remove(offset);
52  buf.cursor -= 1;
53}
54
55pub fn delete(buf: &mut InputBuffer) {
56  if has_selection(buf) {
57    delete_selection_if_active(buf);
58    return;
59  }
60  let len = char_count(&buf.text);
61  if buf.cursor >= len {
62    return;
63  }
64  let offset = byte_offset(&buf.text, buf.cursor);
65  buf.text.remove(offset);
66}
67
68pub fn move_left(buf: &mut InputBuffer) {
69  buf.selection_anchor = None;
70  if buf.cursor > 0 {
71    buf.cursor -= 1;
72  }
73}
74
75pub fn move_right(buf: &mut InputBuffer) {
76  buf.selection_anchor = None;
77  let len = char_count(&buf.text);
78  if buf.cursor < len {
79    buf.cursor += 1;
80  }
81}
82
83pub fn move_home(buf: &mut InputBuffer) {
84  buf.selection_anchor = None;
85  buf.cursor = 0;
86}
87
88pub fn move_end(buf: &mut InputBuffer) {
89  buf.selection_anchor = None;
90  buf.cursor = char_count(&buf.text);
91}
92
93pub fn select_left(buf: &mut InputBuffer) {
94  if buf.cursor == 0 {
95    return;
96  }
97  if buf.selection_anchor.is_none() {
98    buf.selection_anchor = Some(buf.cursor);
99  }
100  buf.cursor -= 1;
101  collapse_if_empty(buf);
102}
103
104pub fn select_right(buf: &mut InputBuffer) {
105  let len = char_count(&buf.text);
106  if buf.cursor >= len {
107    return;
108  }
109  if buf.selection_anchor.is_none() {
110    buf.selection_anchor = Some(buf.cursor);
111  }
112  buf.cursor += 1;
113  collapse_if_empty(buf);
114}
115
116pub fn select_home(buf: &mut InputBuffer) {
117  if buf.cursor == 0 {
118    return;
119  }
120  if buf.selection_anchor.is_none() {
121    buf.selection_anchor = Some(buf.cursor);
122  }
123  buf.cursor = 0;
124  collapse_if_empty(buf);
125}
126
127pub fn select_end(buf: &mut InputBuffer) {
128  let len = char_count(&buf.text);
129  if buf.cursor >= len {
130    return;
131  }
132  if buf.selection_anchor.is_none() {
133    buf.selection_anchor = Some(buf.cursor);
134  }
135  buf.cursor = len;
136  collapse_if_empty(buf);
137}
138
139pub fn select_all(buf: &mut InputBuffer) {
140  let len = char_count(&buf.text);
141  if len == 0 {
142    return;
143  }
144  buf.selection_anchor = Some(0);
145  buf.cursor = len;
146}
147
148pub fn move_word_left(buf: &mut InputBuffer) {
149  buf.selection_anchor = None;
150  buf.cursor = prev_word_boundary(&buf.text, buf.cursor);
151}
152
153pub fn move_word_right(buf: &mut InputBuffer) {
154  buf.selection_anchor = None;
155  buf.cursor = next_word_boundary(&buf.text, buf.cursor);
156}
157
158pub fn select_word_left(buf: &mut InputBuffer) {
159  let target = prev_word_boundary(&buf.text, buf.cursor);
160  if target == buf.cursor {
161    return;
162  }
163  if buf.selection_anchor.is_none() {
164    buf.selection_anchor = Some(buf.cursor);
165  }
166  buf.cursor = target;
167  collapse_if_empty(buf);
168}
169
170pub fn select_word_right(buf: &mut InputBuffer) {
171  let target = next_word_boundary(&buf.text, buf.cursor);
172  if target == buf.cursor {
173    return;
174  }
175  if buf.selection_anchor.is_none() {
176    buf.selection_anchor = Some(buf.cursor);
177  }
178  buf.cursor = target;
179  collapse_if_empty(buf);
180}
181
182fn delete_selection_if_active(buf: &mut InputBuffer) {
183  let Some(anchor) = buf.selection_anchor.take() else {
184    return;
185  };
186  let start = anchor.min(buf.cursor);
187  let end = anchor.max(buf.cursor);
188
189  let start_byte = byte_offset(&buf.text, start);
190  let end_byte = byte_offset(&buf.text, end);
191  buf.text.replace_range(start_byte..end_byte, "");
192  buf.cursor = start;
193}
194
195fn collapse_if_empty(buf: &mut InputBuffer) {
196  if buf.selection_anchor == Some(buf.cursor) {
197    buf.selection_anchor = None;
198  }
199}
200
201fn byte_offset(text: &str, char_idx: usize) -> usize {
202  text
203    .char_indices()
204    .nth(char_idx)
205    .map(|(i, _)| i)
206    .unwrap_or(text.len())
207}
208
209fn char_count(text: &str) -> usize {
210  text.chars().count()
211}
212
213fn prev_word_boundary(text: &str, cursor: usize) -> usize {
214  if cursor == 0 {
215    return 0;
216  }
217  let chars: Vec<char> = text.chars().collect();
218  let mut pos = cursor - 1;
219
220  while pos > 0 && !chars[pos].is_alphanumeric() {
221    pos -= 1;
222  }
223  while pos > 0 && chars[pos - 1].is_alphanumeric() {
224    pos -= 1;
225  }
226  pos
227}
228
229fn next_word_boundary(text: &str, cursor: usize) -> usize {
230  let chars: Vec<char> = text.chars().collect();
231  let len = chars.len();
232  if cursor >= len {
233    return len;
234  }
235  let mut pos = cursor;
236
237  while pos < len && !chars[pos].is_alphanumeric() {
238    pos += 1;
239  }
240  while pos < len && chars[pos].is_alphanumeric() {
241    pos += 1;
242  }
243  pos
244}
245
246#[cfg(test)]
247mod tests {
248  use super::*;
249
250  #[test]
251  fn insert_appends_at_end() {
252    let mut buf = new();
253    insert_char(&mut buf, 'h');
254    insert_char(&mut buf, 'i');
255
256    assert_eq!(text(&buf), "hi");
257    assert_eq!(cursor(&buf), 2);
258  }
259
260  #[test]
261  fn insert_at_cursor_position() {
262    let mut buf = new();
263    insert_char(&mut buf, 'a');
264    insert_char(&mut buf, 'c');
265    move_left(&mut buf);
266    insert_char(&mut buf, 'b');
267
268    assert_eq!(text(&buf), "abc");
269    assert_eq!(cursor(&buf), 2);
270  }
271
272  #[test]
273  fn backspace_removes_before_cursor() {
274    let mut buf = new();
275    insert_char(&mut buf, 'a');
276    insert_char(&mut buf, 'b');
277    insert_char(&mut buf, 'c');
278    move_left(&mut buf);
279    backspace(&mut buf);
280
281    assert_eq!(text(&buf), "ac");
282    assert_eq!(cursor(&buf), 1);
283  }
284
285  #[test]
286  fn backspace_at_start_does_nothing() {
287    let mut buf = new();
288    insert_char(&mut buf, 'a');
289    move_home(&mut buf);
290    backspace(&mut buf);
291
292    assert_eq!(text(&buf), "a");
293    assert_eq!(cursor(&buf), 0);
294  }
295
296  #[test]
297  fn delete_removes_at_cursor() {
298    let mut buf = new();
299    insert_char(&mut buf, 'a');
300    insert_char(&mut buf, 'b');
301    insert_char(&mut buf, 'c');
302    move_home(&mut buf);
303    delete(&mut buf);
304
305    assert_eq!(text(&buf), "bc");
306    assert_eq!(cursor(&buf), 0);
307  }
308
309  #[test]
310  fn delete_at_end_does_nothing() {
311    let mut buf = new();
312    insert_char(&mut buf, 'a');
313    delete(&mut buf);
314
315    assert_eq!(text(&buf), "a");
316  }
317
318  #[test]
319  fn move_left_right_navigates() {
320    let mut buf = new();
321    insert_char(&mut buf, 'a');
322    insert_char(&mut buf, 'b');
323    insert_char(&mut buf, 'c');
324
325    move_left(&mut buf);
326    assert_eq!(cursor(&buf), 2);
327    move_left(&mut buf);
328    assert_eq!(cursor(&buf), 1);
329    move_right(&mut buf);
330    assert_eq!(cursor(&buf), 2);
331  }
332
333  #[test]
334  fn move_left_clamps_at_zero() {
335    let mut buf = new();
336    move_left(&mut buf);
337    assert_eq!(cursor(&buf), 0);
338  }
339
340  #[test]
341  fn move_right_clamps_at_end() {
342    let mut buf = new();
343    insert_char(&mut buf, 'a');
344    move_right(&mut buf);
345    assert_eq!(cursor(&buf), 1);
346  }
347
348  #[test]
349  fn home_and_end() {
350    let mut buf = new();
351    insert_char(&mut buf, 'a');
352    insert_char(&mut buf, 'b');
353    insert_char(&mut buf, 'c');
354    move_home(&mut buf);
355    assert_eq!(cursor(&buf), 0);
356    move_end(&mut buf);
357    assert_eq!(cursor(&buf), 3);
358  }
359
360  #[test]
361  fn select_left_creates_selection() {
362    let mut buf = new();
363    insert_char(&mut buf, 'a');
364    insert_char(&mut buf, 'b');
365    insert_char(&mut buf, 'c');
366    select_left(&mut buf);
367    select_left(&mut buf);
368
369    assert_eq!(selection_range(&buf), Some((1, 3)));
370    assert_eq!(cursor(&buf), 1);
371  }
372
373  #[test]
374  fn select_right_creates_selection() {
375    let mut buf = new();
376    insert_char(&mut buf, 'a');
377    insert_char(&mut buf, 'b');
378    insert_char(&mut buf, 'c');
379    move_home(&mut buf);
380    select_right(&mut buf);
381
382    assert_eq!(selection_range(&buf), Some((0, 1)));
383    assert_eq!(cursor(&buf), 1);
384  }
385
386  #[test]
387  fn select_all_selects_entire_text() {
388    let mut buf = new();
389    insert_char(&mut buf, 'h');
390    insert_char(&mut buf, 'i');
391    select_all(&mut buf);
392
393    assert_eq!(selection_range(&buf), Some((0, 2)));
394  }
395
396  #[test]
397  fn typing_replaces_selection() {
398    let mut buf = new();
399    insert_char(&mut buf, 'h');
400    insert_char(&mut buf, 'e');
401    insert_char(&mut buf, 'l');
402    insert_char(&mut buf, 'l');
403    insert_char(&mut buf, 'o');
404    select_all(&mut buf);
405    insert_char(&mut buf, 'x');
406
407    assert_eq!(text(&buf), "x");
408    assert_eq!(cursor(&buf), 1);
409    assert!(!has_selection(&buf));
410  }
411
412  #[test]
413  fn backspace_deletes_selection() {
414    let mut buf = new();
415    insert_char(&mut buf, 'a');
416    insert_char(&mut buf, 'b');
417    insert_char(&mut buf, 'c');
418    select_left(&mut buf);
419    select_left(&mut buf);
420    backspace(&mut buf);
421
422    assert_eq!(text(&buf), "a");
423    assert_eq!(cursor(&buf), 1);
424  }
425
426  #[test]
427  fn move_clears_selection() {
428    let mut buf = new();
429    insert_char(&mut buf, 'a');
430    insert_char(&mut buf, 'b');
431    select_left(&mut buf);
432    move_right(&mut buf);
433
434    assert!(!has_selection(&buf));
435  }
436
437  #[test]
438  fn select_home_from_end() {
439    let mut buf = new();
440    insert_char(&mut buf, 'a');
441    insert_char(&mut buf, 'b');
442    insert_char(&mut buf, 'c');
443    select_home(&mut buf);
444
445    assert_eq!(selection_range(&buf), Some((0, 3)));
446    assert_eq!(cursor(&buf), 0);
447  }
448
449  #[test]
450  fn select_end_from_start() {
451    let mut buf = new();
452    insert_char(&mut buf, 'a');
453    insert_char(&mut buf, 'b');
454    move_home(&mut buf);
455    select_end(&mut buf);
456
457    assert_eq!(selection_range(&buf), Some((0, 2)));
458    assert_eq!(cursor(&buf), 2);
459  }
460
461  #[test]
462  fn word_boundaries_navigate_words() {
463    let mut buf = new();
464    for c in "hello world foo".chars() {
465      insert_char(&mut buf, c);
466    }
467    move_home(&mut buf);
468
469    move_word_right(&mut buf);
470    assert_eq!(cursor(&buf), 5);
471    move_word_right(&mut buf);
472    assert_eq!(cursor(&buf), 11);
473    move_word_left(&mut buf);
474    assert_eq!(cursor(&buf), 6);
475    move_word_left(&mut buf);
476    assert_eq!(cursor(&buf), 0);
477  }
478
479  #[test]
480  fn select_word_left_selects_word() {
481    let mut buf = new();
482    for c in "hello world".chars() {
483      insert_char(&mut buf, c);
484    }
485    select_word_left(&mut buf);
486
487    assert_eq!(selection_range(&buf), Some((6, 11)));
488  }
489
490  #[test]
491  fn select_word_right_selects_word() {
492    let mut buf = new();
493    for c in "hello world".chars() {
494      insert_char(&mut buf, c);
495    }
496    move_home(&mut buf);
497    select_word_right(&mut buf);
498
499    assert_eq!(selection_range(&buf), Some((0, 5)));
500  }
501
502  #[test]
503  fn multibyte_chars_handled_correctly() {
504    let mut buf = new();
505    for c in "café".chars() {
506      insert_char(&mut buf, c);
507    }
508    assert_eq!(cursor(&buf), 4);
509    move_left(&mut buf);
510    insert_char(&mut buf, 'x');
511
512    assert_eq!(text(&buf), "cafxé");
513    assert_eq!(cursor(&buf), 4);
514  }
515
516  #[test]
517  fn selection_collapes_when_cursor_meets_anchor() {
518    let mut buf = new();
519    insert_char(&mut buf, 'a');
520    insert_char(&mut buf, 'b');
521    select_left(&mut buf);
522    select_right(&mut buf);
523
524    assert!(!has_selection(&buf));
525  }
526
527  // ── Edge-case coverage for early returns ──────────────────
528
529  #[test]
530  fn delete_with_selection_removes_selected() {
531    let mut buf = new();
532    for c in "abc".chars() {
533      insert_char(&mut buf, c);
534    }
535    move_home(&mut buf);
536    select_right(&mut buf);
537    select_right(&mut buf);
538    delete(&mut buf);
539    assert_eq!(text(&buf), "c");
540  }
541
542  #[test]
543  fn select_left_at_zero_is_noop() {
544    let mut buf = new();
545    insert_char(&mut buf, 'a');
546    move_home(&mut buf);
547    select_left(&mut buf);
548    assert_eq!(cursor(&buf), 0);
549    assert!(!has_selection(&buf));
550  }
551
552  #[test]
553  fn select_right_at_end_is_noop() {
554    let mut buf = new();
555    insert_char(&mut buf, 'a');
556    select_right(&mut buf);
557    assert_eq!(cursor(&buf), 1);
558    assert!(!has_selection(&buf));
559  }
560
561  #[test]
562  fn select_home_at_zero_is_noop() {
563    let mut buf = new();
564    select_home(&mut buf);
565    assert_eq!(cursor(&buf), 0);
566    assert!(!has_selection(&buf));
567  }
568
569  #[test]
570  fn select_end_at_end_is_noop() {
571    let mut buf = new();
572    insert_char(&mut buf, 'a');
573    select_end(&mut buf);
574    assert_eq!(cursor(&buf), 1);
575    assert!(!has_selection(&buf));
576  }
577
578  #[test]
579  fn select_all_on_empty_is_noop() {
580    let mut buf = new();
581    select_all(&mut buf);
582    assert!(!has_selection(&buf));
583  }
584
585  #[test]
586  fn select_word_left_at_boundary_is_noop() {
587    let mut buf = new();
588    insert_char(&mut buf, 'a');
589    move_home(&mut buf);
590    select_word_left(&mut buf);
591    assert_eq!(cursor(&buf), 0);
592    assert!(!has_selection(&buf));
593  }
594
595  #[test]
596  fn select_word_right_at_boundary_is_noop() {
597    let mut buf = new();
598    insert_char(&mut buf, 'a');
599    select_word_right(&mut buf);
600    assert_eq!(cursor(&buf), 1);
601    assert!(!has_selection(&buf));
602  }
603
604  #[test]
605  fn prev_word_boundary_at_zero_returns_zero() {
606    assert_eq!(prev_word_boundary("hello", 0), 0);
607  }
608
609  #[test]
610  fn next_word_boundary_at_end_returns_len() {
611    assert_eq!(next_word_boundary("hello", 5), 5);
612  }
613}