1use repose_core::*;
2use std::ops::Range;
3use std::rc::Rc;
4use std::time::Duration;
5use std::{cell::RefCell, time::Instant};
6
7use ab_glyph::{Font, FontArc, PxScale, ScaleFont};
8use fontdb::Database;
9use std::sync::OnceLock;
10
11pub const TF_FONT_PX: f32 = 16.0;
12pub const TF_PADDING_X: f32 = 8.0;
13
14use unicode_segmentation::UnicodeSegmentation;
15
16pub struct TextMetrics {
17 pub positions: Vec<f32>,
19 pub byte_offsets: Vec<usize>,
21}
22
23pub fn measure_text(text: &str, px: u32) -> TextMetrics {
24 let m = repose_text::metrics_for_textfield(text, px as f32);
25 TextMetrics {
26 positions: m.positions,
27 byte_offsets: m.byte_offsets,
28 }
29}
30
31pub fn byte_to_char_index(m: &TextMetrics, byte: usize) -> usize {
32 match m.byte_offsets.binary_search(&byte) {
34 Ok(i) | Err(i) => i,
35 }
36}
37
38pub fn index_for_x_bytes(text: &str, px: u32, x: f32) -> usize {
39 let m = measure_text(text, px);
40 let mut best_i = 0usize;
42 let mut best_d = f32::INFINITY;
43 for i in 0..m.positions.len() {
44 let d = (m.positions[i] - x).abs();
45 if d < best_d {
46 best_d = d;
47 best_i = i;
48 }
49 }
50 m.byte_offsets[best_i]
51}
52
53fn prev_grapheme_boundary(text: &str, byte: usize) -> usize {
55 let mut last = 0usize;
56 for (i, _) in text.grapheme_indices(true) {
57 if i >= byte {
58 break;
59 }
60 last = i;
61 }
62 last
63}
64
65fn next_grapheme_boundary(text: &str, byte: usize) -> usize {
66 for (i, _) in text.grapheme_indices(true) {
67 if i > byte {
68 return i;
69 }
70 }
71 text.len()
72}
73
74pub fn positions_for(text: &str, px: u32) -> Vec<f32> {
76 repose_text::metrics_for_textfield(text, px as f32).positions
77}
78
79pub fn index_for_x(text: &str, px: u32, x: f32) -> usize {
81 let m = repose_text::metrics_for_textfield(text, px as f32);
82 let mut best = 0usize;
84 let mut dmin = f32::INFINITY;
85 for (i, &xx) in m.positions.iter().enumerate() {
86 let d = (xx - x).abs();
87 if d < dmin {
88 dmin = d;
89 best = i;
90 }
91 }
92 best
93}
94
95#[derive(Clone, Debug)]
96pub struct TextFieldState {
97 pub text: String,
98 pub selection: Range<usize>,
99 pub composition: Option<Range<usize>>, pub scroll_offset: f32,
101 pub drag_anchor: Option<usize>, pub blink_start: Instant, pub inner_width: f32,
104}
105
106impl TextFieldState {
107 pub fn new() -> Self {
108 Self {
109 text: String::new(),
110 selection: 0..0,
111 composition: None,
112 scroll_offset: 0.0,
113 drag_anchor: None,
114 blink_start: Instant::now(),
115 inner_width: 0.0,
116 }
117 }
118
119 pub fn insert_text(&mut self, text: &str) {
120 let start = self.selection.start.min(self.text.len());
121 let end = self.selection.end.min(self.text.len());
122
123 self.text.replace_range(start..end, text);
124 let new_pos = start + text.len();
125 self.selection = new_pos..new_pos;
126 self.reset_caret_blink();
127 }
128
129 pub fn delete_backward(&mut self) {
130 if self.selection.start == self.selection.end {
131 let pos = self.selection.start.min(self.text.len());
132 if pos > 0 {
133 let prev = prev_grapheme_boundary(&self.text, pos);
134 self.text.replace_range(prev..pos, "");
135 self.selection = prev..prev;
136 }
137 } else {
138 self.insert_text("");
139 }
140 self.reset_caret_blink();
141 }
142
143 pub fn delete_forward(&mut self) {
144 if self.selection.start == self.selection.end {
145 let pos = self.selection.start.min(self.text.len());
146 if pos < self.text.len() {
147 let next = next_grapheme_boundary(&self.text, pos);
148 self.text.replace_range(pos..next, "");
149 }
150 } else {
151 self.insert_text("");
152 }
153 self.reset_caret_blink();
154 }
155
156 pub fn move_cursor(&mut self, delta: isize, extend_selection: bool) {
157 let mut pos = self.selection.end.min(self.text.len());
158 if delta < 0 {
159 for _ in 0..delta.unsigned_abs() {
160 pos = prev_grapheme_boundary(&self.text, pos);
161 }
162 } else if delta > 0 {
163 for _ in 0..(delta as usize) {
164 pos = next_grapheme_boundary(&self.text, pos);
165 }
166 }
167 if extend_selection {
168 self.selection.end = pos;
169 } else {
170 self.selection = pos..pos;
171 }
172 self.reset_caret_blink();
173 }
174
175 pub fn selected_text(&self) -> String {
176 if self.selection.start == self.selection.end {
177 String::new()
178 } else {
179 self.text[self.selection.clone()].to_string()
180 }
181 }
182
183 pub fn set_composition(&mut self, text: String, cursor: Option<(usize, usize)>) {
184 if text.is_empty() {
185 if let Some(range) = self.composition.take() {
186 let s = clamp_to_char_boundary(&self.text, range.start.min(self.text.len()));
187 let e = clamp_to_char_boundary(&self.text, range.end.min(self.text.len()));
188 if s <= e {
189 self.text.replace_range(s..e, "");
190 self.selection = s..s;
191 }
192 }
193 self.reset_caret_blink();
194 return;
195 }
196
197 let anchor_start;
198 if let Some(r) = self.composition.take() {
199 let mut s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
201 let mut e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
202 if e < s {
203 std::mem::swap(&mut s, &mut e);
204 }
205 self.text.replace_range(s..e, &text);
206 anchor_start = s;
207 } else {
208 let pos = clamp_to_char_boundary(&self.text, self.selection.start.min(self.text.len()));
210 self.text.insert_str(pos, &text);
211 anchor_start = pos;
212 }
213
214 self.composition = Some(anchor_start..(anchor_start + text.len()));
216
217 if let Some((c0, c1)) = cursor {
219 let b0 = char_to_byte(&text, c0);
220 let b1 = char_to_byte(&text, c1);
221 self.selection = (anchor_start + b0)..(anchor_start + b1);
222 } else {
223 let end = anchor_start + text.len();
224 self.selection = end..end;
225 }
226
227 self.reset_caret_blink();
228 }
229
230 pub fn commit_composition(&mut self, text: String) {
231 if let Some(r) = self.composition.take() {
232 let s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
233 let e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
234 self.text.replace_range(s..e, &text);
235 let new_pos = s + text.len();
236 self.selection = new_pos..new_pos;
237 } else {
238 let pos = clamp_to_char_boundary(&self.text, self.selection.end.min(self.text.len()));
240 self.text.insert_str(pos, &text);
241 let new_pos = pos + text.len();
242 self.selection = new_pos..new_pos;
243 }
244 self.reset_caret_blink();
245 }
246
247 pub fn cancel_composition(&mut self) {
248 if let Some(r) = self.composition.take() {
249 let s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
250 let e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
251 if s <= e {
252 self.text.replace_range(s..e, "");
253 self.selection = s..s;
254 }
255 }
256 self.reset_caret_blink();
257 }
258
259 pub fn delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) {
260 if self.selection.start != self.selection.end {
261 let start = self.selection.start.min(self.text.len());
262 let end = self.selection.end.min(self.text.len());
263 self.text.replace_range(start..end, "");
264 self.selection = start..start;
265 self.reset_caret_blink();
266 return;
267 }
268
269 let caret = self.selection.end.min(self.text.len());
270 let start_raw = caret.saturating_sub(before_bytes);
271 let end_raw = (caret + after_bytes).min(self.text.len());
272 let start = prev_grapheme_boundary(&self.text, start_raw);
274 let end = next_grapheme_boundary(&self.text, end_raw);
275 if start < end {
276 self.text.replace_range(start..end, "");
277 self.selection = start..start;
278 }
279 self.reset_caret_blink();
280 }
281
282 pub fn begin_drag(&mut self, idx_byte: usize, extend: bool) {
284 let idx = idx_byte.min(self.text.len());
285 if extend {
286 let anchor = self.selection.start;
287 self.selection = anchor.min(idx)..anchor.max(idx);
288 self.drag_anchor = Some(anchor);
289 } else {
290 self.selection = idx..idx;
291 self.drag_anchor = Some(idx);
292 }
293 self.reset_caret_blink();
294 }
295
296 pub fn drag_to(&mut self, idx_byte: usize) {
297 if let Some(anchor) = self.drag_anchor {
298 let i = idx_byte.min(self.text.len());
299 self.selection = anchor.min(i)..anchor.max(i);
300 }
301 self.reset_caret_blink();
302 }
303 pub fn end_drag(&mut self) {
304 self.drag_anchor = None;
305 }
306
307 pub fn caret_index(&self) -> usize {
308 self.selection.end
309 }
310
311 pub fn ensure_caret_visible(&mut self, caret_x: f32, inner_width: f32) {
313 let inset = 2.0;
315 let left = self.scroll_offset + inset;
316 let right = self.scroll_offset + inner_width - inset;
317 if caret_x < left {
318 self.scroll_offset = (caret_x - inset).max(0.0);
319 } else if caret_x > right {
320 self.scroll_offset = (caret_x - inner_width + inset).max(0.0);
321 }
322 }
323
324 pub fn reset_caret_blink(&mut self) {
325 self.blink_start = Instant::now();
326 }
327 pub fn caret_visible(&self) -> bool {
328 const PERIOD: Duration = Duration::from_millis(500);
329 ((Instant::now() - self.blink_start).as_millis() / PERIOD.as_millis() as u128) % 2 == 0
330 }
331
332 pub fn set_inner_width(&mut self, w: f32) {
333 self.inner_width = w.max(0.0);
334 }
335}
336
337pub fn TextField(
339 hint: impl Into<String>,
340 modifier: repose_core::Modifier,
341 on_change: Option<impl Fn(String) + 'static>,
342 on_submit: Option<impl Fn(String) + 'static>,
343) -> repose_core::View {
344 repose_core::View::new(
345 0,
346 repose_core::ViewKind::TextField {
347 state_key: 0,
348 hint: hint.into(),
349 on_change: on_change.map(|f| std::rc::Rc::new(f) as _),
350 on_submit: on_submit.map(|f| std::rc::Rc::new(f) as _),
351 },
352 )
353 .modifier(modifier)
354 .semantics(repose_core::Semantics {
355 role: repose_core::Role::TextField,
356 label: None,
357 focused: false,
358 enabled: true,
359 })
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn test_textfield_insert() {
368 let mut state = TextFieldState::new();
369 state.insert_text("Hello");
370 assert_eq!(state.text, "Hello");
371 assert_eq!(state.selection, 5..5);
372 }
373
374 #[test]
375 fn test_textfield_delete_backward() {
376 let mut state = TextFieldState::new();
377 state.insert_text("Hello");
378 state.delete_backward();
379 assert_eq!(state.text, "Hell");
380 assert_eq!(state.selection, 4..4);
381 }
382
383 #[test]
384 fn test_textfield_selection() {
385 let mut state = TextFieldState::new();
386 state.insert_text("Hello World");
387 state.selection = 0..5; state.insert_text("Hi");
389 assert_eq!(state.text, "Hi World");
390 assert_eq!(state.selection, 2..2);
391 }
392
393 #[test]
394 fn test_textfield_ime_composition() {
395 let mut state = TextFieldState::new();
396 state.insert_text("Test ");
397 state.set_composition("日本".to_string(), Some((0, 2)));
398 assert_eq!(state.text, "Test 日本");
399 assert!(state.composition.is_some());
400
401 state.commit_composition("日本語".to_string());
402 assert_eq!(state.text, "Test 日本語");
403 assert!(state.composition.is_none());
404 }
405
406 #[test]
407 fn test_textfield_cursor_movement() {
408 let mut state = TextFieldState::new();
409 state.insert_text("Hello");
410 state.move_cursor(-2, false);
411 assert_eq!(state.selection, 3..3);
412
413 state.move_cursor(1, false);
414 assert_eq!(state.selection, 4..4);
415 }
416
417 #[test]
418 fn test_delete_surrounding() {
419 let mut state = TextFieldState::new();
420 state.insert_text("Hello");
421 state.delete_surrounding(2, 1); assert_eq!(state.text, "Hel");
424 assert_eq!(state.selection, 3..3);
425 }
426
427 #[test]
428 fn test_grapheme_delete_and_move() {
429 let mut st = TextFieldState::new();
431 st.insert_text("A👍🏽B");
432 assert_eq!(st.text, "A👍🏽B");
433 st.move_cursor(-1, false);
435 assert_eq!(st.selection.end, "A👍🏽".len());
436 st.delete_backward();
437 assert_eq!(st.text, "AB");
438 assert_eq!(st.selection, "A".len().."A".len());
439 }
440
441 #[test]
442 fn test_index_for_x_bytes_grapheme() {
443 let t = "A👍🏽B";
445 let px = 16u32;
446 let m = measure_text(t, px);
447 for i in 0..m.byte_offsets.len() - 1 {
449 let b = m.byte_offsets[i];
450 let _ = &t[..b];
452 }
453 }
454}
455
456fn clamp_to_char_boundary(s: &str, i: usize) -> usize {
457 if i >= s.len() {
458 return s.len();
459 }
460 if s.is_char_boundary(i) {
461 return i;
462 }
463 let mut j = i;
465 while j > 0 && !s.is_char_boundary(j) {
466 j -= 1;
467 }
468 j
469}
470
471fn char_to_byte(s: &str, ci: usize) -> usize {
472 if ci == 0 {
473 0
474 } else {
475 s.char_indices().nth(ci).map(|(i, _)| i).unwrap_or(s.len())
476 }
477}