1#![allow(missing_docs)]
15
16use unicode_segmentation::UnicodeSegmentation;
17
18pub fn hit_test_text(text: &str, origin_x: f32, pointer_x: f32, avg_advance: f32) -> usize {
30 if text.is_empty() || avg_advance <= 0.0 {
31 return 0;
32 }
33 let dx = (pointer_x - origin_x).max(0.0);
34 let target_col = (dx / avg_advance).round() as usize;
35 let mut col = 0usize;
36 let mut last_idx = 0usize;
37 for (idx, _) in text.grapheme_indices(true) {
38 if col == target_col {
39 return idx;
40 }
41 last_idx = idx;
42 col += 1;
43 }
44 if target_col >= col {
46 text.len()
47 } else {
48 last_idx
49 }
50}
51
52pub fn select_word_at_byte(text: &str, byte: usize) -> (usize, usize) {
56 let byte = clamp_to_char_boundary(text, byte);
57 for (off, w) in text.split_word_bound_indices() {
58 let end = off + w.len();
59 if byte >= off && byte < end {
60 if w.chars().all(|c| c.is_whitespace()) {
62 return (byte, byte);
63 }
64 return (off, end);
65 }
66 if byte == end && end == text.len() {
67 return (off, end);
68 }
69 }
70 (byte, byte)
71}
72
73pub fn select_line_at_byte(text: &str, byte: usize) -> (usize, usize) {
77 let byte = clamp_to_char_boundary(text, byte);
78 let start = text[..byte].rfind('\n').map(|i| i + 1).unwrap_or(0);
79 let end = text[byte..]
80 .find('\n')
81 .map(|i| byte + i)
82 .unwrap_or(text.len());
83 (start, end)
84}
85
86fn clamp_to_char_boundary(s: &str, mut at: usize) -> usize {
87 if at > s.len() {
88 at = s.len();
89 }
90 while at > 0 && !s.is_char_boundary(at) {
91 at -= 1;
92 }
93 at
94}
95
96pub fn prev_word_boundary(text: &str, from: usize) -> usize {
107 let from = clamp_to_char_boundary(text, from);
108 let mut prev = 0usize;
109 for (off, _) in text.split_word_bound_indices() {
110 if off >= from {
111 return prev;
112 }
113 prev = off;
114 }
115 prev
116}
117
118pub fn next_word_boundary(text: &str, from: usize) -> usize {
121 let from = clamp_to_char_boundary(text, from);
122 for (off, w) in text.split_word_bound_indices() {
123 let end = off + w.len();
124 if end > from {
125 return end;
126 }
127 }
128 text.len()
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn hit_test_returns_zero_for_left_of_origin() {
137 let b = hit_test_text("hello", 100.0, 50.0, 8.0);
138 assert_eq!(b, 0);
139 }
140
141 #[test]
142 fn hit_test_lands_in_middle() {
143 let b = hit_test_text("hello", 0.0, 16.0, 8.0);
145 assert_eq!(b, 2);
146 }
147
148 #[test]
149 fn hit_test_past_end_snaps_to_end() {
150 let b = hit_test_text("hi", 0.0, 1000.0, 8.0);
151 assert_eq!(b, 2);
152 }
153
154 #[test]
155 fn select_word_expands_around_byte() {
156 let (s, e) = select_word_at_byte("hello world", 7);
157 assert_eq!(&"hello world"[s..e], "world");
158 }
159
160 #[test]
161 fn select_word_on_whitespace_collapses() {
162 let (s, e) = select_word_at_byte("hello world", 5);
163 assert_eq!(s, e);
164 }
165
166 #[test]
167 fn select_line_excludes_newline() {
168 let text = "line one\nline two\nline three";
169 let (s, e) = select_line_at_byte(text, 12);
170 assert_eq!(&text[s..e], "line two");
171 }
172
173 #[test]
174 fn select_line_first_line() {
175 let text = "first\nsecond";
176 let (s, e) = select_line_at_byte(text, 2);
177 assert_eq!(&text[s..e], "first");
178 }
179
180 #[test]
181 fn select_line_last_line_no_trailing_newline() {
182 let text = "a\nb\nc";
183 let (s, e) = select_line_at_byte(text, 4);
184 assert_eq!(&text[s..e], "c");
185 }
186
187 #[test]
188 fn prev_word_boundary_steps_back_one_word() {
189 let text = "hello world";
190 assert_eq!(prev_word_boundary(text, 11), 6); assert_eq!(prev_word_boundary(text, 6), 5); assert_eq!(prev_word_boundary(text, 5), 0); assert_eq!(prev_word_boundary(text, 0), 0); }
195
196 #[test]
197 fn next_word_boundary_steps_forward_one_word() {
198 let text = "hello world";
199 assert_eq!(next_word_boundary(text, 0), 5); assert_eq!(next_word_boundary(text, 5), 6); assert_eq!(next_word_boundary(text, 6), 11); assert_eq!(next_word_boundary(text, 11), 11); }
204
205 #[test]
206 fn word_boundary_respects_multibyte_char_boundaries() {
207 let text = "h\u{e9}llo w\u{f6}rld"; assert_eq!(prev_word_boundary(text, 2), 0);
212 assert_eq!(next_word_boundary(text, 2), "h\u{e9}llo".len());
213 let world_start = text.rfind(' ').unwrap() + 1;
215 assert_eq!(prev_word_boundary(text, text.len()), world_start);
216 }
217}