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