1use repose_core::*;
41use std::ops::Range;
42use web_time::Duration;
43use web_time::Instant;
44
45use unicode_segmentation::UnicodeSegmentation;
46
47pub const TF_FONT_DP: f32 = 16.0;
49pub const TF_PADDING_X_DP: f32 = 8.0;
51
52pub struct TextMetrics {
53 pub positions: Vec<f32>, pub byte_offsets: Vec<usize>,
57}
58
59pub fn measure_text(text: &str, font_px: f32) -> TextMetrics {
62 let m = repose_text::metrics_for_textfield(text, font_px);
63 TextMetrics {
64 positions: m.positions,
65 byte_offsets: m.byte_offsets,
66 }
67}
68
69pub fn byte_to_char_index(m: &TextMetrics, byte: usize) -> usize {
70 match m.byte_offsets.binary_search(&byte) {
71 Ok(i) | Err(i) => i,
72 }
73}
74
75pub fn index_for_x_bytes(text: &str, font_px: f32, x_px: f32) -> usize {
77 let m = measure_text(text, font_px);
78
79 let mut best_i = 0usize;
80 let mut best_d = f32::INFINITY;
81 for i in 0..m.positions.len() {
82 let d = (m.positions[i] - x_px).abs();
83 if d < best_d {
84 best_d = d;
85 best_i = i;
86 }
87 }
88 m.byte_offsets[best_i]
89}
90
91fn prev_grapheme_boundary(text: &str, byte: usize) -> usize {
93 let mut last = 0usize;
94 for (i, _) in text.grapheme_indices(true) {
95 if i >= byte {
96 break;
97 }
98 last = i;
99 }
100 last
101}
102
103fn next_grapheme_boundary(text: &str, byte: usize) -> usize {
104 for (i, _) in text.grapheme_indices(true) {
105 if i > byte {
106 return i;
107 }
108 }
109 text.len()
110}
111
112#[derive(Clone, Debug)]
113pub struct TextFieldState {
114 pub text: String,
115 pub selection: Range<usize>,
116 pub composition: Option<Range<usize>>, pub scroll_offset: f32, pub drag_anchor: Option<usize>, pub blink_start: Instant, pub inner_width: f32, }
122
123impl Default for TextFieldState {
124 fn default() -> Self {
125 Self::new()
126 }
127}
128
129impl TextFieldState {
130 pub fn new() -> Self {
131 Self {
132 text: String::new(),
133 selection: 0..0,
134 composition: None,
135 scroll_offset: 0.0,
136 drag_anchor: None,
137 blink_start: Instant::now(),
138 inner_width: 0.0,
139 }
140 }
141
142 pub fn insert_text(&mut self, text: &str) {
143 let start = self.selection.start.min(self.text.len());
144 let end = self.selection.end.min(self.text.len());
145
146 self.text.replace_range(start..end, text);
147 let new_pos = start + text.len();
148 self.selection = new_pos..new_pos;
149 self.reset_caret_blink();
150 }
151
152 pub fn delete_backward(&mut self) {
153 if self.selection.start == self.selection.end {
154 let pos = self.selection.start.min(self.text.len());
155 if pos > 0 {
156 let prev = prev_grapheme_boundary(&self.text, pos);
157 self.text.replace_range(prev..pos, "");
158 self.selection = prev..prev;
159 }
160 } else {
161 self.insert_text("");
162 }
163 self.reset_caret_blink();
164 }
165
166 pub fn delete_forward(&mut self) {
167 if self.selection.start == self.selection.end {
168 let pos = self.selection.start.min(self.text.len());
169 if pos < self.text.len() {
170 let next = next_grapheme_boundary(&self.text, pos);
171 self.text.replace_range(pos..next, "");
172 }
173 } else {
174 self.insert_text("");
175 }
176 self.reset_caret_blink();
177 }
178
179 pub fn move_cursor(&mut self, delta: isize, extend_selection: bool) {
180 let mut pos = self.selection.end.min(self.text.len());
181 if delta < 0 {
182 for _ in 0..delta.unsigned_abs() {
183 pos = prev_grapheme_boundary(&self.text, pos);
184 }
185 } else if delta > 0 {
186 for _ in 0..(delta as usize) {
187 pos = next_grapheme_boundary(&self.text, pos);
188 }
189 }
190 if extend_selection {
191 self.selection.end = pos;
192 } else {
193 self.selection = pos..pos;
194 }
195 self.reset_caret_blink();
196 }
197
198 pub fn selected_text(&self) -> String {
199 if self.selection.start == self.selection.end {
200 String::new()
201 } else {
202 self.text[self.selection.clone()].to_string()
203 }
204 }
205
206 pub fn set_composition(&mut self, text: String, cursor: Option<(usize, usize)>) {
207 if text.is_empty() {
208 if let Some(range) = self.composition.take() {
209 let s = clamp_to_char_boundary(&self.text, range.start.min(self.text.len()));
210 let e = clamp_to_char_boundary(&self.text, range.end.min(self.text.len()));
211 if s <= e {
212 self.text.replace_range(s..e, "");
213 self.selection = s..s;
214 }
215 }
216 self.reset_caret_blink();
217 return;
218 }
219
220 let anchor_start;
221 if let Some(r) = self.composition.take() {
222 let mut s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
223 let mut e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
224 if e < s {
225 std::mem::swap(&mut s, &mut e);
226 }
227 self.text.replace_range(s..e, &text);
228 anchor_start = s;
229 } else {
230 let pos = clamp_to_char_boundary(&self.text, self.selection.start.min(self.text.len()));
231 self.text.insert_str(pos, &text);
232 anchor_start = pos;
233 }
234
235 self.composition = Some(anchor_start..(anchor_start + text.len()));
236
237 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()));
258 self.text.insert_str(pos, &text);
259 let new_pos = pos + text.len();
260 self.selection = new_pos..new_pos;
261 }
262 self.reset_caret_blink();
263 }
264
265 pub fn cancel_composition(&mut self) {
266 if let Some(r) = self.composition.take() {
267 let s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
268 let e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
269 if s <= e {
270 self.text.replace_range(s..e, "");
271 self.selection = s..s;
272 }
273 }
274 self.reset_caret_blink();
275 }
276
277 pub fn delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) {
278 if self.selection.start != self.selection.end {
279 let start = self.selection.start.min(self.text.len());
280 let end = self.selection.end.min(self.text.len());
281 self.text.replace_range(start..end, "");
282 self.selection = start..start;
283 self.reset_caret_blink();
284 return;
285 }
286
287 let caret = self.selection.end.min(self.text.len());
288 let start_raw = caret.saturating_sub(before_bytes);
289 let end_raw = (caret + after_bytes).min(self.text.len());
290
291 let start = prev_grapheme_boundary(&self.text, start_raw);
292 let end = next_grapheme_boundary(&self.text, end_raw);
293 if start < end {
294 self.text.replace_range(start..end, "");
295 self.selection = start..start;
296 }
297 self.reset_caret_blink();
298 }
299
300 pub fn begin_drag(&mut self, idx_byte: usize, extend: bool) {
301 let idx = idx_byte.min(self.text.len());
302 if extend {
303 let anchor = self.selection.start;
304 self.selection = anchor.min(idx)..anchor.max(idx);
305 self.drag_anchor = Some(anchor);
306 } else {
307 self.selection = idx..idx;
308 self.drag_anchor = Some(idx);
309 }
310 self.reset_caret_blink();
311 }
312
313 pub fn drag_to(&mut self, idx_byte: usize) {
314 if let Some(anchor) = self.drag_anchor {
315 let i = idx_byte.min(self.text.len());
316 self.selection = anchor.min(i)..anchor.max(i);
317 }
318 self.reset_caret_blink();
319 }
320 pub fn end_drag(&mut self) {
321 self.drag_anchor = None;
322 }
323
324 pub fn caret_index(&self) -> usize {
325 self.selection.end
326 }
327
328 pub fn ensure_caret_visible(&mut self, caret_x_px: f32, inner_width_px: f32, inset_px: f32) {
331 let inset_px = inset_px.max(0.0);
332 let left_px = self.scroll_offset + inset_px;
333 let right_px = self.scroll_offset + inner_width_px - inset_px;
334 if caret_x_px < left_px {
335 self.scroll_offset = (caret_x_px - inset_px).max(0.0);
336 } else if caret_x_px > right_px {
337 self.scroll_offset = (caret_x_px - inner_width_px + inset_px).max(0.0);
338 }
339 }
340
341 pub fn reset_caret_blink(&mut self) {
342 self.blink_start = Instant::now();
343 }
344 pub fn caret_visible(&self) -> bool {
345 const PERIOD: Duration = Duration::from_millis(500);
346 ((Instant::now() - self.blink_start).as_millis() / PERIOD.as_millis()).is_multiple_of(2)
347 }
348
349 pub fn set_inner_width(&mut self, w_px: f32) {
350 self.inner_width = w_px.max(0.0);
351 }
352}
353
354pub fn TextField(
356 hint: impl Into<String>,
357 modifier: repose_core::Modifier,
358 on_change: Option<impl Fn(String) + 'static>,
359 on_submit: Option<impl Fn(String) + 'static>,
360) -> repose_core::View {
361 repose_core::View::new(
362 0,
363 repose_core::ViewKind::TextField {
364 state_key: 0,
365 hint: hint.into(),
366 on_change: on_change.map(|f| std::rc::Rc::new(f) as _),
367 on_submit: on_submit.map(|f| std::rc::Rc::new(f) as _),
368 },
369 )
370 .modifier(modifier)
371 .semantics(repose_core::Semantics {
372 role: repose_core::Role::TextField,
373 label: None,
374 focused: false,
375 enabled: true,
376 })
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn test_index_for_x_bytes_grapheme() {
385 let t = "Ašš½B";
386 let font_px = 16.0; let m = measure_text(t, font_px);
388 for i in 0..m.byte_offsets.len() - 1 {
389 let b = m.byte_offsets[i];
390 let _ = &t[..b];
391 }
392 }
393}
394
395fn clamp_to_char_boundary(s: &str, i: usize) -> usize {
396 if i >= s.len() {
397 return s.len();
398 }
399 if s.is_char_boundary(i) {
400 return i;
401 }
402 let mut j = i;
403 while j > 0 && !s.is_char_boundary(j) {
404 j -= 1;
405 }
406 j
407}
408
409fn char_to_byte(s: &str, ci: usize) -> usize {
410 if ci == 0 {
411 0
412 } else {
413 s.char_indices().nth(ci).map(|(i, _)| i).unwrap_or(s.len())
414 }
415}