agg_gui/widgets/
text_field_core.rs1use std::cell::RefCell;
5use std::rc::Rc;
6
7use crate::text::measure_advance;
8use crate::text::Font;
9use crate::undo::UndoRedoCommand;
10
11pub fn prev_char_boundary(s: &str, byte_pos: usize) -> usize {
16 let mut pos = byte_pos;
17 loop {
18 if pos == 0 {
19 return 0;
20 }
21 pos -= 1;
22 if s.is_char_boundary(pos) {
23 return pos;
24 }
25 }
26}
27
28pub fn next_char_boundary(s: &str, byte_pos: usize) -> usize {
29 let mut pos = byte_pos + 1;
30 while pos <= s.len() {
31 if s.is_char_boundary(pos) {
32 return pos;
33 }
34 pos += 1;
35 }
36 s.len()
37}
38
39fn is_word_char(c: char) -> bool {
44 c.is_alphanumeric() || c == '_'
45}
46
47pub fn next_word_boundary(s: &str, pos: usize) -> usize {
49 let mut chars = s[pos..].char_indices().peekable();
50 let mut advanced = 0usize;
51 while let Some(&(i, c)) = chars.peek() {
53 if !c.is_whitespace() {
54 break;
55 }
56 advanced = i + c.len_utf8();
57 chars.next();
58 }
59 while let Some(&(i, c)) = chars.peek() {
61 if c.is_whitespace() {
62 break;
63 }
64 advanced = i + c.len_utf8();
65 chars.next();
66 }
67 pos + advanced
68}
69
70pub fn prev_word_boundary(s: &str, pos: usize) -> usize {
72 if pos == 0 {
73 return 0;
74 }
75 let chars: Vec<(usize, char)> = s[..pos].char_indices().collect();
76 let mut i = chars.len();
77 while i > 0 && chars[i - 1].1.is_whitespace() {
78 i -= 1;
79 }
80 while i > 0 && !chars[i - 1].1.is_whitespace() {
81 i -= 1;
82 }
83 if i < chars.len() {
84 chars[i].0
85 } else {
86 0
87 }
88}
89
90pub fn word_range_at(s: &str, byte_pos: usize) -> (usize, usize) {
93 let anchor_class = is_word_char(s[byte_pos..].chars().next().unwrap_or(' '));
94 let start = {
96 let mut p = byte_pos;
97 while p > 0 {
98 let prev = prev_char_boundary(s, p);
99 let c = s[prev..p].chars().next().unwrap_or(' ');
100 if is_word_char(c) != anchor_class {
101 break;
102 }
103 p = prev;
104 }
105 p
106 };
107 let end = {
109 let mut p = byte_pos;
110 for (_, c) in s[byte_pos..].char_indices() {
111 if is_word_char(c) != anchor_class {
112 break;
113 }
114 p += c.len_utf8();
115 }
116 p
117 };
118 (start, end)
119}
120
121pub fn paragraph_range_at(s: &str, byte_pos: usize) -> (usize, usize) {
127 let pos = byte_pos.min(s.len());
128 let start = s[..pos].rfind('\n').map(|i| i + 1).unwrap_or(0);
129 let end = s[pos..].find('\n').map(|i| pos + i).unwrap_or(s.len());
130 (start, end)
131}
132
133pub fn byte_at_x(font: &Font, text: &str, font_size: f64, target_x: f64) -> usize {
139 if target_x <= 0.0 {
140 return 0;
141 }
142 let mut prev_x = 0.0f64;
143 let mut prev_pos = 0usize;
144 for (i, c) in text.char_indices() {
145 let x = measure_advance(font, &text[..i], font_size);
146 let mid = (prev_x + x) * 0.5;
147 if target_x < mid {
148 return prev_pos;
149 }
150 prev_x = x;
151 prev_pos = i;
152 let _ = c;
153 }
154 let total = measure_advance(font, text, font_size);
155 let mid = (prev_x + total) * 0.5;
156 if target_x < mid {
157 prev_pos
158 } else {
159 text.len()
160 }
161}
162
163#[derive(Clone, Default)]
177pub struct TextEditState {
178 pub text: String,
179 pub cursor: usize,
180 pub anchor: usize,
181 pub epoch: u64,
182}
183
184impl TextEditState {
185 #[inline]
187 pub fn note_text_change(&mut self) {
188 self.epoch = self.epoch.wrapping_add(1);
189 }
190
191 #[inline]
194 pub fn selection_range(&self) -> Option<(usize, usize)> {
195 let lo = self.cursor.min(self.anchor);
196 let hi = self.cursor.max(self.anchor);
197 if hi > lo {
198 Some((lo, hi))
199 } else {
200 None
201 }
202 }
203}
204
205pub struct TextEditCommand {
212 pub name: &'static str,
213 pub before: TextEditState,
214 pub after: TextEditState,
215 pub target: Rc<RefCell<TextEditState>>,
216}
217
218impl UndoRedoCommand for TextEditCommand {
219 fn name(&self) -> &str {
220 self.name
221 }
222 fn do_it(&mut self) {
223 *self.target.borrow_mut() = self.after.clone();
224 }
225 fn undo_it(&mut self) {
226 *self.target.borrow_mut() = self.before.clone();
227 }
228 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
229 self
230 }
231}
232
233#[cfg(test)]
234mod word_line_tests {
235 use super::{paragraph_range_at, word_range_at};
236
237 #[test]
238 fn word_range_selects_alnum_run() {
239 let s = "hello world";
240 assert_eq!(word_range_at(s, 2), (0, 5));
242 assert_eq!(word_range_at(s, 8), (6, 11));
244 }
245
246 #[test]
247 fn word_range_stops_at_punctuation() {
248 let s = "foo.bar";
249 assert_eq!(word_range_at(s, 1), (0, 3));
251 assert_eq!(word_range_at(s, 5), (4, 7));
252 assert_eq!(word_range_at(s, 3), (3, 4));
254 }
255
256 #[test]
257 fn word_range_at_word_edge() {
258 let s = "cat dog";
259 assert_eq!(word_range_at(s, 3), (3, 4));
262 assert_eq!(word_range_at(s, 2), (0, 3));
264 }
265
266 #[test]
267 fn word_range_includes_underscore() {
268 let s = "snake_case here";
269 assert_eq!(word_range_at(s, 3), (0, 10));
270 }
271
272 #[test]
273 fn paragraph_range_single_line() {
274 let s = "just one line";
275 assert_eq!(paragraph_range_at(s, 4), (0, s.len()));
276 }
277
278 #[test]
279 fn paragraph_range_middle_line() {
280 let s = "first\nsecond\nthird";
281 assert_eq!(paragraph_range_at(s, 8), (6, 12));
283 }
284
285 #[test]
286 fn paragraph_range_last_line_no_trailing_newline() {
287 let s = "a\nbb\nccc";
288 assert_eq!(paragraph_range_at(s, 7), (5, 8));
289 }
290
291 #[test]
292 fn paragraph_range_on_blank_line() {
293 let s = "a\n\nb";
294 assert_eq!(paragraph_range_at(s, 2), (2, 2));
296 }
297}