Skip to main content

blizz_ui/
input_buffer.rs

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