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
76#[derive(Clone, Debug)]
77pub struct TextFieldState {
78 pub text: String,
79 pub selection: Range<usize>,
80 pub composition: Option<Range<usize>>, pub scroll_offset: f32,
82 pub drag_anchor: Option<usize>, pub blink_start: Instant, pub inner_width: f32,
85}
86
87impl TextFieldState {
88 pub fn new() -> Self {
89 Self {
90 text: String::new(),
91 selection: 0..0,
92 composition: None,
93 scroll_offset: 0.0,
94 drag_anchor: None,
95 blink_start: Instant::now(),
96 inner_width: 0.0,
97 }
98 }
99
100 pub fn insert_text(&mut self, text: &str) {
101 let start = self.selection.start.min(self.text.len());
102 let end = self.selection.end.min(self.text.len());
103
104 self.text.replace_range(start..end, text);
105 let new_pos = start + text.len();
106 self.selection = new_pos..new_pos;
107 self.reset_caret_blink();
108 }
109
110 pub fn delete_backward(&mut self) {
111 if self.selection.start == self.selection.end {
112 let pos = self.selection.start.min(self.text.len());
113 if pos > 0 {
114 let prev = prev_grapheme_boundary(&self.text, pos);
115 self.text.replace_range(prev..pos, "");
116 self.selection = prev..prev;
117 }
118 } else {
119 self.insert_text("");
120 }
121 self.reset_caret_blink();
122 }
123
124 pub fn delete_forward(&mut self) {
125 if self.selection.start == self.selection.end {
126 let pos = self.selection.start.min(self.text.len());
127 if pos < self.text.len() {
128 let next = next_grapheme_boundary(&self.text, pos);
129 self.text.replace_range(pos..next, "");
130 }
131 } else {
132 self.insert_text("");
133 }
134 self.reset_caret_blink();
135 }
136
137 pub fn move_cursor(&mut self, delta: isize, extend_selection: bool) {
138 let mut pos = self.selection.end.min(self.text.len());
139 if delta < 0 {
140 for _ in 0..delta.unsigned_abs() {
141 pos = prev_grapheme_boundary(&self.text, pos);
142 }
143 } else if delta > 0 {
144 for _ in 0..(delta as usize) {
145 pos = next_grapheme_boundary(&self.text, pos);
146 }
147 }
148 if extend_selection {
149 self.selection.end = pos;
150 } else {
151 self.selection = pos..pos;
152 }
153 self.reset_caret_blink();
154 }
155
156 pub fn selected_text(&self) -> String {
157 if self.selection.start == self.selection.end {
158 String::new()
159 } else {
160 self.text[self.selection.clone()].to_string()
161 }
162 }
163
164 pub fn set_composition(&mut self, text: String, cursor: Option<(usize, usize)>) {
165 if text.is_empty() {
166 if let Some(range) = self.composition.take() {
167 let s = clamp_to_char_boundary(&self.text, range.start.min(self.text.len()));
168 let e = clamp_to_char_boundary(&self.text, range.end.min(self.text.len()));
169 if s <= e {
170 self.text.replace_range(s..e, "");
171 self.selection = s..s;
172 }
173 }
174 self.reset_caret_blink();
175 return;
176 }
177
178 let anchor_start;
179 if let Some(r) = self.composition.take() {
180 let mut s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
182 let mut e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
183 if e < s {
184 std::mem::swap(&mut s, &mut e);
185 }
186 self.text.replace_range(s..e, &text);
187 anchor_start = s;
188 } else {
189 let pos = clamp_to_char_boundary(&self.text, self.selection.start.min(self.text.len()));
191 self.text.insert_str(pos, &text);
192 anchor_start = pos;
193 }
194
195 self.composition = Some(anchor_start..(anchor_start + text.len()));
196
197 if let Some((c0, c1)) = cursor {
199 let b0 = char_to_byte(&text, c0);
200 let b1 = char_to_byte(&text, c1);
201 self.selection = (anchor_start + b0)..(anchor_start + b1);
202 } else {
203 let end = anchor_start + text.len();
204 self.selection = end..end;
205 }
206
207 self.reset_caret_blink();
208 }
209
210 pub fn commit_composition(&mut self, text: String) {
211 if let Some(r) = self.composition.take() {
212 let s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
213 let e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
214 self.text.replace_range(s..e, &text);
215 let new_pos = s + text.len();
216 self.selection = new_pos..new_pos;
217 } else {
218 let pos = clamp_to_char_boundary(&self.text, self.selection.end.min(self.text.len()));
220 self.text.insert_str(pos, &text);
221 let new_pos = pos + text.len();
222 self.selection = new_pos..new_pos;
223 }
224 self.reset_caret_blink();
225 }
226
227 pub fn cancel_composition(&mut self) {
228 if let Some(r) = self.composition.take() {
229 let s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
230 let e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
231 if s <= e {
232 self.text.replace_range(s..e, "");
233 self.selection = s..s;
234 }
235 }
236 self.reset_caret_blink();
237 }
238
239 pub fn delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) {
240 if self.selection.start != self.selection.end {
241 let start = self.selection.start.min(self.text.len());
242 let end = self.selection.end.min(self.text.len());
243 self.text.replace_range(start..end, "");
244 self.selection = start..start;
245 self.reset_caret_blink();
246 return;
247 }
248
249 let caret = self.selection.end.min(self.text.len());
250 let start_raw = caret.saturating_sub(before_bytes);
251 let end_raw = (caret + after_bytes).min(self.text.len());
252 let start = prev_grapheme_boundary(&self.text, start_raw);
254 let end = next_grapheme_boundary(&self.text, end_raw);
255 if start < end {
256 self.text.replace_range(start..end, "");
257 self.selection = start..start;
258 }
259 self.reset_caret_blink();
260 }
261
262 pub fn begin_drag(&mut self, idx_byte: usize, extend: bool) {
264 let idx = idx_byte.min(self.text.len());
265 if extend {
266 let anchor = self.selection.start;
267 self.selection = anchor.min(idx)..anchor.max(idx);
268 self.drag_anchor = Some(anchor);
269 } else {
270 self.selection = idx..idx;
271 self.drag_anchor = Some(idx);
272 }
273 self.reset_caret_blink();
274 }
275
276 pub fn drag_to(&mut self, idx_byte: usize) {
277 if let Some(anchor) = self.drag_anchor {
278 let i = idx_byte.min(self.text.len());
279 self.selection = anchor.min(i)..anchor.max(i);
280 }
281 self.reset_caret_blink();
282 }
283 pub fn end_drag(&mut self) {
284 self.drag_anchor = None;
285 }
286
287 pub fn caret_index(&self) -> usize {
288 self.selection.end
289 }
290
291 pub fn ensure_caret_visible(&mut self, caret_x_px: f32, inner_width_px: f32) {
293 let inset_px = dp_to_px(2.0);
295 let left_px = self.scroll_offset + inset_px;
296 let right_px = self.scroll_offset + inner_width_px - inset_px;
297 if caret_x_px < left_px {
298 self.scroll_offset = (caret_x_px - inset_px).max(0.0);
299 } else if caret_x_px > right_px {
300 self.scroll_offset = (caret_x_px - inner_width_px + inset_px).max(0.0);
301 }
302 }
303
304 pub fn reset_caret_blink(&mut self) {
305 self.blink_start = Instant::now();
306 }
307 pub fn caret_visible(&self) -> bool {
308 const PERIOD: Duration = Duration::from_millis(500);
309 ((Instant::now() - self.blink_start).as_millis() / PERIOD.as_millis() as u128) % 2 == 0
310 }
311
312 pub fn set_inner_width(&mut self, w_px: f32) {
313 self.inner_width = w_px.max(0.0);
314 }
315}
316
317pub fn TextField(
319 hint: impl Into<String>,
320 modifier: repose_core::Modifier,
321 on_change: Option<impl Fn(String) + 'static>,
322 on_submit: Option<impl Fn(String) + 'static>,
323) -> repose_core::View {
324 repose_core::View::new(
325 0,
326 repose_core::ViewKind::TextField {
327 state_key: 0,
328 hint: hint.into(),
329 on_change: on_change.map(|f| std::rc::Rc::new(f) as _),
330 on_submit: on_submit.map(|f| std::rc::Rc::new(f) as _),
331 },
332 )
333 .modifier(modifier)
334 .semantics(repose_core::Semantics {
335 role: repose_core::Role::TextField,
336 label: None,
337 focused: false,
338 enabled: true,
339 })
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn test_textfield_insert() {
348 let mut state = TextFieldState::new();
349 state.insert_text("Hello");
350 assert_eq!(state.text, "Hello");
351 assert_eq!(state.selection, 5..5);
352 }
353
354 #[test]
355 fn test_textfield_delete_backward() {
356 let mut state = TextFieldState::new();
357 state.insert_text("Hello");
358 state.delete_backward();
359 assert_eq!(state.text, "Hell");
360 assert_eq!(state.selection, 4..4);
361 }
362
363 #[test]
364 fn test_textfield_selection() {
365 let mut state = TextFieldState::new();
366 state.insert_text("Hello");
367 state.selection = 0..5; state.insert_text("Hi");
369 assert_eq!(state.text, "Hi World".replacen("World", "", 1)); assert_eq!(state.selection, 2..2);
371 }
372
373 #[test]
374 fn test_textfield_ime_composition() {
375 let mut state = TextFieldState::new();
376 state.insert_text("Test ");
377 state.set_composition("日本".to_string(), Some((0, 2)));
378 assert!(state.composition.is_some());
379
380 state.commit_composition("日本語".to_string());
381 assert!(state.composition.is_none());
382 }
383
384 #[test]
385 fn test_textfield_cursor_movement() {
386 let mut state = TextFieldState::new();
387 state.insert_text("Hello");
388 state.move_cursor(-2, false);
389 assert_eq!(state.selection, 3..3);
390
391 state.move_cursor(1, false);
392 assert_eq!(state.selection, 4..4);
393 }
394
395 #[test]
396 fn test_delete_surrounding() {
397 let mut state = TextFieldState::new();
398 state.insert_text("Hello");
399 state.delete_surrounding(2, 1); assert_eq!(state.text, "Hel");
402 assert_eq!(state.selection, 3..3);
403 }
404
405 #[test]
406 fn test_index_for_x_bytes_grapheme() {
407 let t = "A👍🏽B";
409 let px_dp = 16u32;
410 let m = measure_text(t, px_dp);
411 for i in 0..m.byte_offsets.len() - 1 {
413 let b = m.byte_offsets[i];
414 let _ = &t[..b];
415 }
416 }
417}
418
419fn clamp_to_char_boundary(s: &str, i: usize) -> usize {
420 if i >= s.len() {
421 return s.len();
422 }
423 if s.is_char_boundary(i) {
424 return i;
425 }
426 let mut j = i;
428 while j > 0 && !s.is_char_boundary(j) {
429 j -= 1;
430 }
431 j
432}
433
434fn char_to_byte(s: &str, ci: usize) -> usize {
435 if ci == 0 {
436 0
437 } else {
438 s.char_indices().nth(ci).map(|(i, _)| i).unwrap_or(s.len())
439 }
440}