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