1use repose_core::*;
41use std::cell::RefCell;
42use std::ops::Range;
43use std::rc::Rc;
44use std::sync::Arc;
45use unicode_segmentation::UnicodeSegmentation;
46use web_time::Duration;
47use web_time::Instant;
48
49const TEXT_UNDO_CAPACITY: usize = 100;
51
52const SNAPSHOTS_INTERVAL_MILLIS: u128 = 5000;
54
55#[derive(Clone, Copy, Debug, PartialEq)]
57enum TextEditType {
58 Insert,
59 Delete,
60 Replace,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq)]
65enum TextDeleteType {
66 Start, End, Inner, NotByUser,
70}
71
72#[derive(Clone, Debug)]
74pub struct TextUndoOp {
75 pub index: usize,
77 pub pre_text: String,
79 pub post_text: String,
81 pub pre_selection: Range<usize>,
83 pub post_selection: Range<usize>,
85 pub time: Instant,
87 pub can_merge: bool,
89}
90
91impl TextUndoOp {
92 fn edit_type(&self) -> TextEditType {
93 match (self.pre_text.is_empty(), self.post_text.is_empty()) {
94 (true, true) => unreachable!("Both pre and post text cannot be empty"),
95 (true, false) => TextEditType::Insert,
96 (false, true) => TextEditType::Delete,
97 (false, false) => TextEditType::Replace,
98 }
99 }
100
101 fn is_newline(&self) -> bool {
102 self.post_text == "\n" || self.post_text == "\r\n"
103 }
104
105 fn try_merge(&self, next: &TextUndoOp) -> Option<TextUndoOp> {
107 if !self.can_merge || !next.can_merge {
108 return None;
109 }
110
111 let elapsed = next.time.saturating_duration_since(self.time);
112 if elapsed.as_millis() >= SNAPSHOTS_INTERVAL_MILLIS {
113 return None;
114 }
115
116 if self.is_newline() || next.is_newline() {
117 return None;
118 }
119
120 let self_type = self.edit_type();
121 if self_type != next.edit_type() {
122 return None;
123 }
124
125 match self_type {
126 TextEditType::Insert => {
127 if self.index + self.post_text.len() == next.index {
129 Some(TextUndoOp {
130 index: self.index,
131 pre_text: String::new(),
132 post_text: format!("{}{}", self.post_text, next.post_text),
133 pre_selection: self.pre_selection.clone(),
134 post_selection: next.post_selection.clone(),
135 time: self.time,
136 can_merge: true,
137 })
138 } else {
139 None
140 }
141 }
142 TextEditType::Delete => {
143 let self_del = self.deletion_type();
144 let next_del = next.deletion_type();
145 if self_del == next_del
147 && (self_del == TextDeleteType::Start || self_del == TextDeleteType::End)
148 {
149 if self.index == next.index + next.pre_text.len() {
150 Some(TextUndoOp {
152 index: next.index,
153 pre_text: format!("{}{}", next.pre_text, self.pre_text),
154 post_text: String::new(),
155 pre_selection: self.pre_selection.clone(),
156 post_selection: next.post_selection.clone(),
157 time: self.time,
158 can_merge: true,
159 })
160 } else if self.index == next.index {
161 Some(TextUndoOp {
163 index: self.index,
164 pre_text: format!("{}{}", self.pre_text, next.pre_text),
165 post_text: String::new(),
166 pre_selection: self.pre_selection.clone(),
167 post_selection: next.post_selection.clone(),
168 time: self.time,
169 can_merge: true,
170 })
171 } else {
172 None
173 }
174 } else {
175 None
176 }
177 }
178 TextEditType::Replace => None,
179 }
180 }
181
182 fn deletion_type(&self) -> TextDeleteType {
184 if self.edit_type() != TextEditType::Delete {
185 return TextDeleteType::NotByUser;
186 }
187 if !self.post_selection.start == self.post_selection.end {
188 return TextDeleteType::NotByUser;
189 }
190 if self.pre_selection.start == self.pre_selection.end {
191 if self.pre_selection.start > self.post_selection.start {
193 TextDeleteType::Start } else {
195 TextDeleteType::End }
197 } else if self.pre_selection.start == self.post_selection.start
198 && self.pre_selection.start == self.index
199 {
200 TextDeleteType::Inner
201 } else {
202 TextDeleteType::NotByUser
203 }
204 }
205}
206
207const SCROLL_STIFFNESS: f32 = 300.0;
209const SCROLL_DAMPING: f32 = 30.0;
210
211pub const TF_FONT_DP: f32 = 16.0;
213
214#[derive(Clone, Copy, Debug)]
216pub struct KeyboardOptions {
217 pub keyboard_type: repose_core::KeyboardType,
218 pub autocorrect: bool,
219}
220
221impl Default for KeyboardOptions {
222 fn default() -> Self {
223 Self {
224 keyboard_type: repose_core::KeyboardType::default(),
225 autocorrect: true,
226 }
227 }
228}
229pub const TF_PADDING_X_DP: f32 = 8.0;
231
232pub struct TextMetrics {
233 pub positions: Vec<f32>, pub byte_offsets: Vec<usize>,
237}
238
239pub fn measure_text(text: &str, font_px: f32, font_family: Option<&'static str>) -> TextMetrics {
243 let m = repose_text::metrics_for_textfield(text, font_px, font_family);
244 TextMetrics {
245 positions: m.positions,
246 byte_offsets: m.byte_offsets,
247 }
248}
249
250pub fn byte_to_char_index(m: &TextMetrics, byte: usize) -> usize {
251 match m.byte_offsets.binary_search(&byte) {
252 Ok(i) | Err(i) => i,
253 }
254}
255
256pub fn index_for_x_bytes(text: &str, font_px: f32, x_px: f32) -> usize {
258 let m = measure_text(text, font_px, None);
259
260 let mut best_i = 0usize;
261 let mut best_d = f32::INFINITY;
262 for i in 0..m.positions.len() {
263 let d = (m.positions[i] - x_px).abs();
264 if d < best_d {
265 best_d = d;
266 best_i = i;
267 }
268 }
269 m.byte_offsets[best_i]
270}
271
272fn prev_grapheme_boundary(text: &str, byte: usize) -> usize {
274 let mut last = 0usize;
275 for (i, _) in text.grapheme_indices(true) {
276 if i >= byte {
277 break;
278 }
279 last = i;
280 }
281 last
282}
283
284fn next_grapheme_boundary(text: &str, byte: usize) -> usize {
285 for (i, _) in text.grapheme_indices(true) {
286 if i > byte {
287 return i;
288 }
289 }
290 text.len()
291}
292
293#[derive(Clone)]
294pub struct TextFieldState {
295 pub text: String,
296 pub selection: Range<usize>,
297 pub composition: Option<Range<usize>>, pub scroll_offset: f32, pub scroll_offset_y: f32, pub drag_anchor: Option<usize>, pub blink_start: Instant, pub inner_width: f32, pub inner_height: f32, pub preferred_x_px: Option<f32>, pub offset_map: Option<Rc<dyn Fn(usize) -> usize>>,
308 pub visual_transformation: Option<Rc<dyn VisualTransformation>>,
310 pub(crate) scroll_target: f32,
312 pub(crate) scroll_target_y: f32,
314 scroll_vel: f32,
316 scroll_vel_y: f32,
318 last_scroll_tick: Option<Instant>,
320
321 undo_stack: Vec<TextUndoOp>,
324 redo_stack: Vec<TextUndoOp>,
326 staging_undo: Option<TextUndoOp>,
328}
329
330impl std::fmt::Debug for TextFieldState {
331 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332 f.debug_struct("TextFieldState")
333 .field("text", &self.text)
334 .field("selection", &self.selection)
335 .field("composition", &self.composition)
336 .field("scroll_offset", &self.scroll_offset)
337 .field("scroll_offset_y", &self.scroll_offset_y)
338 .field("drag_anchor", &self.drag_anchor)
339 .field("blink_start", &self.blink_start)
340 .field("inner_width", &self.inner_width)
341 .field("inner_height", &self.inner_height)
342 .field("preferred_x_px", &self.preferred_x_px)
343 .field("offset_map", &self.offset_map.as_ref().map(|_| "<fn>"))
344 .field(
345 "visual_transformation",
346 &self.visual_transformation.as_ref().map(|_| "<vt>"),
347 )
348 .field("scroll_target", &self.scroll_target)
349 .field("scroll_target_y", &self.scroll_target_y)
350 .field("can_undo", &self.can_undo())
351 .field("can_redo", &self.can_redo())
352 .field("undo_count", &self.undo_stack.len())
353 .field("redo_count", &self.redo_stack.len())
354 .finish()
355 }
356}
357
358impl Default for TextFieldState {
359 fn default() -> Self {
360 Self::new()
361 }
362}
363
364impl TextFieldState {
365 pub fn new() -> Self {
366 Self {
367 text: String::new(),
368 selection: 0..0,
369 composition: None,
370 scroll_offset: 0.0,
371 scroll_offset_y: 0.0,
372 drag_anchor: None,
373 blink_start: Instant::now(),
374 inner_width: 0.0,
375 inner_height: 0.0,
376 preferred_x_px: None,
377 offset_map: None,
378 visual_transformation: None,
379 scroll_target: 0.0,
380 scroll_target_y: 0.0,
381 scroll_vel: 0.0,
382 scroll_vel_y: 0.0,
383 last_scroll_tick: None,
384 undo_stack: Vec::new(),
385 redo_stack: Vec::new(),
386 staging_undo: None,
387 }
388 }
389
390 pub fn can_undo(&self) -> bool {
394 !self.undo_stack.is_empty() || self.staging_undo.is_some()
395 }
396
397 pub fn can_redo(&self) -> bool {
399 !self.redo_stack.is_empty()
400 }
401
402 pub fn undo(&mut self) -> bool {
404 self.flush_undo();
405 if let Some(op) = self.undo_stack.pop() {
406 let end = (op.index + op.post_text.len()).min(self.text.len());
407 self.text.replace_range(op.index..end, &op.pre_text);
408 self.selection = op.pre_selection.clone();
409 self.redo_stack.push(op);
410 self.preferred_x_px = None;
411 self.reset_caret_blink();
412 true
413 } else {
414 false
415 }
416 }
417
418 pub fn redo(&mut self) -> bool {
420 if let Some(op) = self.redo_stack.pop() {
421 let end = (op.index + op.pre_text.len()).min(self.text.len());
422 self.text.replace_range(op.index..end, &op.post_text);
423 self.selection = op.post_selection.clone();
424 self.undo_stack.push(op);
425 self.preferred_x_px = None;
426 self.reset_caret_blink();
427 true
428 } else {
429 false
430 }
431 }
432
433 pub fn clear_undo_history(&mut self) {
435 self.undo_stack.clear();
436 self.redo_stack.clear();
437 self.staging_undo = None;
438 }
439
440 fn record_edit(&mut self, op: TextUndoOp) {
444 if let Some(staging) = self.staging_undo.take() {
445 if let Some(merged) = staging.try_merge(&op) {
446 self.staging_undo = Some(merged);
447 return;
448 }
449 self.undo_stack.push(staging);
451 self.redo_stack.clear();
452 while self.undo_stack.len() + 1 > TEXT_UNDO_CAPACITY {
454 self.undo_stack.remove(0);
455 }
456 }
457 self.staging_undo = Some(op);
458 }
459
460 fn flush_undo(&mut self) {
462 if let Some(op) = self.staging_undo.take() {
463 self.undo_stack.push(op);
464 self.redo_stack.clear();
465 while self.undo_stack.len() > TEXT_UNDO_CAPACITY {
466 self.undo_stack.remove(0);
467 }
468 }
469 }
470
471 fn insert_text_impl(&mut self, text: &str, can_merge: bool) {
472 let start = self.selection.start.min(self.text.len());
473 let end = self.selection.end.min(self.text.len());
474 let pre_text = self.text[start..end].to_string();
475 let pre_selection = self.selection.clone();
476
477 self.text.replace_range(start..end, text);
478 let new_pos = start + text.len();
479 self.selection = new_pos..new_pos;
480 self.preferred_x_px = None;
481 self.reset_caret_blink();
482
483 if !pre_text.is_empty() || !text.is_empty() {
484 self.record_edit(TextUndoOp {
485 index: start,
486 pre_text,
487 post_text: text.to_string(),
488 pre_selection,
489 post_selection: self.selection.clone(),
490 time: Instant::now(),
491 can_merge,
492 });
493 }
494 }
495
496 pub fn insert_text(&mut self, text: &str) {
497 self.insert_text_impl(text, true);
498 }
499
500 pub fn insert_text_atomic(&mut self, text: &str) {
502 self.insert_text_impl(text, false);
503 }
504
505 pub fn delete_backward(&mut self) {
506 if self.selection.start == self.selection.end {
507 let pos = self.selection.start.min(self.text.len());
508 if pos > 0 {
509 let prev = prev_grapheme_boundary(&self.text, pos);
510 let pre_text = self.text[prev..pos].to_string();
511 let pre_selection = self.selection.clone();
512 self.text.replace_range(prev..pos, "");
513 self.selection = prev..prev;
514 self.preferred_x_px = None;
515 self.reset_caret_blink();
516 self.record_edit(TextUndoOp {
517 index: prev,
518 pre_text,
519 post_text: String::new(),
520 pre_selection,
521 post_selection: self.selection.clone(),
522 time: Instant::now(),
523 can_merge: true,
524 });
525 }
526 } else {
527 self.insert_text_impl("", true);
528 }
529 self.preferred_x_px = None;
530 self.reset_caret_blink();
531 }
532
533 pub fn delete_forward(&mut self) {
534 if self.selection.start == self.selection.end {
535 let pos = self.selection.start.min(self.text.len());
536 if pos < self.text.len() {
537 let next = next_grapheme_boundary(&self.text, pos);
538 let pre_text = self.text[pos..next].to_string();
539 let pre_selection = self.selection.clone();
540 self.text.replace_range(pos..next, "");
541 self.preferred_x_px = None;
542 self.reset_caret_blink();
543 self.record_edit(TextUndoOp {
544 index: pos,
545 pre_text,
546 post_text: String::new(),
547 pre_selection,
548 post_selection: self.selection.clone(),
549 time: Instant::now(),
550 can_merge: true,
551 });
552 }
553 } else {
554 self.insert_text_impl("", true);
555 }
556 self.preferred_x_px = None;
557 self.reset_caret_blink();
558 }
559
560 pub fn move_cursor(&mut self, delta: isize, extend_selection: bool) {
561 let mut pos = self.selection.end.min(self.text.len());
562 if delta < 0 {
563 for _ in 0..delta.unsigned_abs() {
564 pos = prev_grapheme_boundary(&self.text, pos);
565 }
566 } else if delta > 0 {
567 for _ in 0..(delta as usize) {
568 pos = next_grapheme_boundary(&self.text, pos);
569 }
570 }
571 if extend_selection {
572 self.selection.end = pos;
573 } else {
574 self.selection = pos..pos;
575 }
576 self.preferred_x_px = None;
577 self.reset_caret_blink();
578 }
579
580 pub fn selected_text(&self) -> String {
581 if self.selection.start == self.selection.end {
582 String::new()
583 } else {
584 self.text[self.selection.clone()].to_string()
585 }
586 }
587
588 pub fn set_composition(&mut self, text: String, cursor: Option<(usize, usize)>) {
589 if text.is_empty() {
590 if let Some(range) = self.composition.take() {
591 let s = clamp_to_char_boundary(&self.text, range.start.min(self.text.len()));
592 let e = clamp_to_char_boundary(&self.text, range.end.min(self.text.len()));
593 if s <= e {
594 self.text.replace_range(s..e, "");
595 self.selection = s..s;
596 }
597 }
598 self.preferred_x_px = None;
599 self.reset_caret_blink();
600 return;
601 }
602
603 let anchor_start;
604 if let Some(r) = self.composition.take() {
605 let mut s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
606 let mut e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
607 if e < s {
608 std::mem::swap(&mut s, &mut e);
609 }
610 self.text.replace_range(s..e, &text);
611 anchor_start = s;
612 } else {
613 let pos = clamp_to_char_boundary(&self.text, self.selection.start.min(self.text.len()));
614 self.text.insert_str(pos, &text);
615 anchor_start = pos;
616 }
617
618 self.composition = Some(anchor_start..(anchor_start + text.len()));
619
620 if let Some((c0, c1)) = cursor {
621 let b0 = char_to_byte(&text, c0);
622 let b1 = char_to_byte(&text, c1);
623 self.selection = (anchor_start + b0)..(anchor_start + b1);
624 } else {
625 let end = anchor_start + text.len();
626 self.selection = end..end;
627 }
628
629 self.preferred_x_px = None;
630 self.reset_caret_blink();
631 }
632
633 pub fn commit_composition(&mut self, text: String) {
634 let pre_selection = self.selection.clone();
635 if let Some(r) = self.composition.take() {
636 let s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
637 let e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
638 let pre_text = self.text[s..e].to_string();
639 self.text.replace_range(s..e, &text);
640 let new_pos = s + text.len();
641 self.selection = new_pos..new_pos;
642 self.preferred_x_px = None;
643 self.reset_caret_blink();
644 if !pre_text.is_empty() || !text.is_empty() {
645 self.record_edit(TextUndoOp {
646 index: s,
647 pre_text,
648 post_text: text,
649 pre_selection,
650 post_selection: self.selection.clone(),
651 time: Instant::now(),
652 can_merge: true,
653 });
654 }
655 } else {
656 let pos = clamp_to_char_boundary(&self.text, self.selection.end.min(self.text.len()));
657 self.text.insert_str(pos, &text);
658 let new_pos = pos + text.len();
659 self.selection = new_pos..new_pos;
660 self.preferred_x_px = None;
661 self.reset_caret_blink();
662 if !text.is_empty() {
663 self.record_edit(TextUndoOp {
664 index: pos,
665 pre_text: String::new(),
666 post_text: text,
667 pre_selection,
668 post_selection: self.selection.clone(),
669 time: Instant::now(),
670 can_merge: true,
671 });
672 }
673 }
674 }
675
676 pub fn cancel_composition(&mut self) {
677 if let Some(r) = self.composition.take() {
678 let s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
679 let e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
680 if s <= e {
681 self.text.replace_range(s..e, "");
682 self.selection = s..s;
683 }
684 }
685 self.preferred_x_px = None;
686 self.reset_caret_blink();
687 }
688
689 pub fn delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) {
690 if self.selection.start != self.selection.end {
691 let start = self.selection.start.min(self.text.len());
692 let end = self.selection.end.min(self.text.len());
693 self.text.replace_range(start..end, "");
694 self.selection = start..start;
695 self.preferred_x_px = None;
696 self.reset_caret_blink();
697 return;
698 }
699
700 let caret = self.selection.end.min(self.text.len());
701 let start_raw = caret.saturating_sub(before_bytes);
702 let end_raw = (caret + after_bytes).min(self.text.len());
703
704 let start = prev_grapheme_boundary(&self.text, start_raw);
705 let end = next_grapheme_boundary(&self.text, end_raw);
706 if start < end {
707 self.text.replace_range(start..end, "");
708 self.selection = start..start;
709 }
710 self.preferred_x_px = None;
711 self.reset_caret_blink();
712 }
713
714 pub fn begin_drag(&mut self, idx_byte: usize, extend: bool) {
715 let idx = idx_byte.min(self.text.len());
716 if extend {
717 let anchor = self.selection.start;
718 self.selection = anchor.min(idx)..anchor.max(idx);
719 self.drag_anchor = Some(anchor);
720 } else {
721 self.selection = idx..idx;
722 self.drag_anchor = Some(idx);
723 }
724 self.preferred_x_px = None;
725 self.reset_caret_blink();
726 }
727
728 pub fn drag_to(&mut self, idx_byte: usize) {
729 if let Some(anchor) = self.drag_anchor {
730 let i = idx_byte.min(self.text.len());
731 self.selection = anchor.min(i)..anchor.max(i);
732 }
733 self.preferred_x_px = None;
734 self.reset_caret_blink();
735 }
736 pub fn end_drag(&mut self) {
737 self.drag_anchor = None;
738 }
739
740 pub fn caret_index(&self) -> usize {
741 self.selection.end
742 }
743
744 pub fn ensure_caret_visible(&mut self, caret_x_px: f32, inner_width_px: f32, inset_px: f32) {
748 self.ensure_caret_visible_xy(caret_x_px, 0.0, inner_width_px, 1.0, inset_px);
749 }
750
751 pub fn ensure_caret_visible_xy(
754 &mut self,
755 caret_x_px: f32,
756 caret_y_px: f32,
757 inner_w_px: f32,
758 inner_h_px: f32,
759 inset_px: f32,
760 ) {
761 let inset_px = inset_px.max(0.0);
762
763 let left_px = self.scroll_offset + inset_px;
765 let right_px = self.scroll_offset + inner_w_px - inset_px;
766 if caret_x_px < left_px {
767 self.scroll_target = (caret_x_px - inset_px).max(0.0);
768 } else if caret_x_px > right_px {
769 self.scroll_target = (caret_x_px - inner_w_px + inset_px).max(0.0);
770 }
771
772 let top_px = self.scroll_offset_y + inset_px;
774 let bot_px = self.scroll_offset_y + inner_h_px - inset_px;
775 if caret_y_px < top_px {
776 self.scroll_target_y = (caret_y_px - inset_px).max(0.0);
777 } else if caret_y_px > bot_px {
778 self.scroll_target_y = (caret_y_px - inner_h_px + inset_px).max(0.0);
779 }
780 }
781
782 pub fn clamp_scroll(&mut self, content_h_px: f32) {
783 let max_y = (content_h_px - self.inner_height).max(0.0);
784 self.scroll_target_y = self.scroll_target_y.clamp(0.0, max_y);
785 if self.scroll_target_y.is_nan() {
786 self.scroll_target_y = 0.0;
787 }
788 }
789
790 pub fn reset_caret_blink(&mut self) {
791 self.blink_start = Instant::now();
792 }
793 pub fn caret_visible(&self) -> bool {
794 const PERIOD: Duration = Duration::from_millis(500);
795 ((Instant::now() - self.blink_start).as_millis() / PERIOD.as_millis()).is_multiple_of(2)
796 }
797
798 pub fn set_inner_width(&mut self, w_px: f32) {
799 self.inner_width = w_px.max(0.0);
800 if self.scroll_offset.is_nan() {
801 self.scroll_offset = 0.0;
802 }
803 if self.scroll_target.is_nan() {
804 self.scroll_target = 0.0;
805 }
806 }
807 pub fn set_inner_height(&mut self, h_px: f32) {
808 self.inner_height = h_px.max(0.0);
809 if self.scroll_offset_y.is_nan() {
810 self.scroll_offset_y = 0.0;
811 }
812 if self.scroll_target_y.is_nan() {
813 self.scroll_target_y = 0.0;
814 }
815 }
816
817 pub fn tick_scroll_animation(&mut self) {
821 let now = Instant::now();
822 let dt = match self.last_scroll_tick {
823 Some(prev) => {
824 let d = now.saturating_duration_since(prev).as_secs_f32();
825 d.min(0.05) }
827 None => {
828 self.last_scroll_tick = Some(now);
831 self.scroll_offset = self.scroll_target;
832 self.scroll_vel = 0.0;
833 self.scroll_offset_y = self.scroll_target_y;
834 self.scroll_vel_y = 0.0;
835 return;
836 }
837 };
838 self.last_scroll_tick = Some(now);
839
840 if dt > 0.0 {
842 let dx = self.scroll_target - self.scroll_offset;
843 let near_x = dx.abs() < 0.5 && self.scroll_vel.abs() < 0.5;
844 if near_x {
845 self.scroll_offset = self.scroll_target;
846 self.scroll_vel = 0.0;
847 } else {
848 let force_x = SCROLL_STIFFNESS * dx - SCROLL_DAMPING * self.scroll_vel;
849 self.scroll_vel += force_x * dt;
850 self.scroll_offset += self.scroll_vel * dt;
851 if (self.scroll_target - self.scroll_offset).signum() != dx.signum() && dx != 0.0 {
853 self.scroll_offset = self.scroll_target;
854 self.scroll_vel = 0.0;
855 }
856 }
857 }
858
859 if dt > 0.0 {
861 let dy = self.scroll_target_y - self.scroll_offset_y;
862 let near_y = dy.abs() < 0.5 && self.scroll_vel_y.abs() < 0.5;
863 if near_y {
864 self.scroll_offset_y = self.scroll_target_y;
865 self.scroll_vel_y = 0.0;
866 } else {
867 let force_y = SCROLL_STIFFNESS * dy - SCROLL_DAMPING * self.scroll_vel_y;
868 self.scroll_vel_y += force_y * dt;
869 self.scroll_offset_y += self.scroll_vel_y * dt;
870 if (self.scroll_target_y - self.scroll_offset_y).signum() != dy.signum()
871 && dy != 0.0
872 {
873 self.scroll_offset_y = self.scroll_target_y;
874 self.scroll_vel_y = 0.0;
875 }
876 }
877 }
878 }
879}
880
881pub fn TextField(
883 hint: impl Into<String>,
884 value: String,
885 modifier: repose_core::Modifier,
886 on_change: Option<impl Fn(String) + 'static>,
887 on_submit: Option<impl Fn(String) + 'static>,
888) -> repose_core::View {
889 TextFieldEx {
890 hint: hint.into(),
891 value,
892 modifier,
893 on_change: on_change.map(|f| Rc::new(f) as _),
894 on_submit: on_submit.map(|f| Rc::new(f) as _),
895 visual_transformation: None,
896 keyboard_options: None,
897 ime_action: None,
898 }
899 .build()
900}
901
902pub struct TextFieldEx {
904 pub hint: String,
905 pub value: String,
906 pub modifier: repose_core::Modifier,
907 pub on_change: Option<Rc<dyn Fn(String)>>,
908 pub on_submit: Option<Rc<dyn Fn(String)>>,
909 pub visual_transformation: Option<Rc<dyn repose_core::VisualTransformation>>,
911 pub keyboard_options: Option<KeyboardOptions>,
913 pub ime_action: Option<repose_core::ImeAction>,
915}
916
917impl TextFieldEx {
918 pub fn new(hint: impl Into<String>, value: String, modifier: repose_core::Modifier) -> Self {
919 Self {
920 hint: hint.into(),
921 value,
922 modifier,
923 on_change: None,
924 on_submit: None,
925 visual_transformation: None,
926 keyboard_options: None,
927 ime_action: None,
928 }
929 }
930
931 pub fn on_change(mut self, f: impl Fn(String) + 'static) -> Self {
932 self.on_change = Some(Rc::new(f));
933 self
934 }
935
936 pub fn on_submit(mut self, f: impl Fn(String) + 'static) -> Self {
937 self.on_submit = Some(Rc::new(f));
938 self
939 }
940
941 pub fn visual_transformation(mut self, vt: impl repose_core::VisualTransformation) -> Self {
942 self.visual_transformation = Some(Rc::new(vt));
943 self
944 }
945
946 pub fn keyboard_options(mut self, opts: KeyboardOptions) -> Self {
947 self.keyboard_options = Some(opts);
948 self
949 }
950
951 pub fn ime_action(mut self, action: repose_core::ImeAction) -> Self {
952 self.ime_action = Some(action);
953 self
954 }
955
956 pub fn password(mut self) -> Self {
957 self.visual_transformation =
958 Some(Rc::new(repose_core::PasswordVisualTransformation::default()));
959 self
960 }
961
962 pub fn build(self) -> repose_core::View {
963 text_field_view(
964 self.modifier,
965 self.hint,
966 self.value,
967 false,
968 self.on_change,
969 self.on_submit,
970 self.visual_transformation,
971 self.keyboard_options.map(|o| o.keyboard_type),
972 self.ime_action,
973 )
974 }
975}
976
977pub fn TextArea(
981 hint: impl Into<String>,
982 value: String,
983 modifier: repose_core::Modifier,
984 on_change: Option<impl Fn(String) + 'static>,
985 on_submit: Option<impl Fn(String) + 'static>,
986) -> repose_core::View {
987 TextAreaEx {
988 hint: hint.into(),
989 value,
990 modifier,
991 on_change: on_change.map(|f| Rc::new(f) as _),
992 on_submit: on_submit.map(|f| Rc::new(f) as _),
993 visual_transformation: None,
994 keyboard_options: None,
995 ime_action: None,
996 }
997 .build()
998}
999
1000pub struct TextAreaEx {
1002 pub hint: String,
1003 pub value: String,
1004 pub modifier: repose_core::Modifier,
1005 pub on_change: Option<Rc<dyn Fn(String)>>,
1006 pub on_submit: Option<Rc<dyn Fn(String)>>,
1007 pub visual_transformation: Option<Rc<dyn repose_core::VisualTransformation>>,
1008 pub keyboard_options: Option<KeyboardOptions>,
1009 pub ime_action: Option<repose_core::ImeAction>,
1010}
1011
1012impl TextAreaEx {
1013 pub fn new(hint: impl Into<String>, value: String, modifier: repose_core::Modifier) -> Self {
1014 Self {
1015 hint: hint.into(),
1016 value,
1017 modifier,
1018 on_change: None,
1019 on_submit: None,
1020 visual_transformation: None,
1021 keyboard_options: None,
1022 ime_action: None,
1023 }
1024 }
1025
1026 pub fn on_change(mut self, f: impl Fn(String) + 'static) -> Self {
1027 self.on_change = Some(Rc::new(f));
1028 self
1029 }
1030
1031 pub fn on_submit(mut self, f: impl Fn(String) + 'static) -> Self {
1032 self.on_submit = Some(Rc::new(f));
1033 self
1034 }
1035
1036 pub fn visual_transformation(mut self, vt: impl repose_core::VisualTransformation) -> Self {
1037 self.visual_transformation = Some(Rc::new(vt));
1038 self
1039 }
1040
1041 pub fn keyboard_options(mut self, opts: KeyboardOptions) -> Self {
1042 self.keyboard_options = Some(opts);
1043 self
1044 }
1045
1046 pub fn ime_action(mut self, action: repose_core::ImeAction) -> Self {
1047 self.ime_action = Some(action);
1048 self
1049 }
1050
1051 pub fn build(self) -> repose_core::View {
1052 text_field_view(
1053 self.modifier,
1054 self.hint,
1055 self.value,
1056 true,
1057 self.on_change,
1058 self.on_submit,
1059 self.visual_transformation,
1060 self.keyboard_options.map(|o| o.keyboard_type),
1061 self.ime_action,
1062 )
1063 }
1064}
1065
1066#[derive(Clone, Debug)]
1067pub struct TextAreaLayout {
1068 pub ranges: Vec<(usize, usize)>,
1069 pub line_h_px: f32,
1070}
1071
1072pub fn layout_text_area(text: &str, font_px: f32, wrap_w_px: f32) -> TextAreaLayout {
1073 let line_h = font_px * 1.3;
1074 let (ranges, _) = repose_text::wrap_line_ranges(text, font_px, wrap_w_px.max(1.0), None, true);
1075 TextAreaLayout {
1076 ranges,
1077 line_h_px: line_h,
1078 }
1079}
1080
1081fn locate_byte_in_ranges(ranges: &[(usize, usize)], b: usize) -> (usize, usize, usize) {
1083 if ranges.is_empty() {
1084 return (0, 0, b);
1085 }
1086 for (i, (s, e)) in ranges.iter().enumerate() {
1087 if b < *s {
1088 if i == 0 {
1089 return (0, 0, b);
1090 }
1091 let (ps, pe) = ranges[i - 1];
1092 let local = pe.saturating_sub(ps);
1093 return (i - 1, local, ps + local);
1094 }
1095 if b < *e {
1096 let local = b.saturating_sub(*s).min(e.saturating_sub(*s));
1097 return (i, local, *s + local);
1098 }
1099 if b == *e {
1100 if let Some((ns, _ne)) = ranges.get(i + 1)
1101 && *ns == b
1102 {
1103 return (i + 1, 0, b);
1104 }
1105 let local = e.saturating_sub(*s);
1106 return (i, local, *s + local);
1107 }
1108 }
1109 let (ls, le) = ranges[ranges.len() - 1];
1110 let local = le.saturating_sub(ls);
1111 (ranges.len() - 1, local, ls + local)
1112}
1113
1114pub fn caret_xy_for_byte(
1116 text: &str,
1117 font_px: f32,
1118 wrap_w_px: f32,
1119 byte: usize,
1120) -> (f32, f32, usize) {
1121 let layout = layout_text_area(text, font_px, wrap_w_px);
1122 let (ranges, line_h) = (&layout.ranges, layout.line_h_px);
1123 let (li, local, _) = locate_byte_in_ranges(ranges, byte);
1124 let (s, e) = ranges.get(li).copied().unwrap_or((0, 0));
1125 let line = &text[s..e];
1126 let m = measure_text(line, font_px, None);
1127 let ci = byte_to_char_index(&m, local);
1128 let x = m.positions.get(ci).copied().unwrap_or(0.0);
1129 let y = (li as f32) * line_h;
1130 (x, y, li)
1131}
1132
1133pub fn index_for_xy_bytes(text: &str, font_px: f32, wrap_w_px: f32, x_px: f32, y_px: f32) -> usize {
1135 let layout = layout_text_area(text, font_px, wrap_w_px);
1136 let li = ((y_px / layout.line_h_px).floor() as isize).max(0) as usize;
1137 let li = li.min(layout.ranges.len().saturating_sub(1));
1138 let (s, e) = layout.ranges.get(li).copied().unwrap_or((0, 0));
1139 let line = &text[s..e];
1140 let local = index_for_x_bytes(line, font_px, x_px.max(0.0));
1141 (s + local).min(text.len())
1142}
1143
1144pub fn move_caret_vertical(
1146 text: &str,
1147 font_px: f32,
1148 wrap_w_px: f32,
1149 cur_byte: usize,
1150 dir: i32, preferred_x: Option<f32>,
1152) -> (usize, f32) {
1153 let layout = layout_text_area(text, font_px, wrap_w_px);
1154 if layout.ranges.is_empty() {
1155 return (cur_byte, preferred_x.unwrap_or(0.0));
1156 }
1157 let (x, _y, li) = caret_xy_for_byte(text, font_px, wrap_w_px, cur_byte);
1158 let px = preferred_x.unwrap_or(x);
1159 let mut nli = li as i32 + dir;
1160 nli = nli.clamp(0, (layout.ranges.len().saturating_sub(1)) as i32);
1161 let nli = nli as usize;
1162 let (s, e) = layout.ranges[nli];
1163 let line = &text[s..e];
1164 let local = index_for_x_bytes(line, font_px, px.max(0.0));
1165 ((s + local).min(text.len()), px)
1166}
1167
1168pub fn line_home_end(
1170 text: &str,
1171 font_px: f32,
1172 wrap_w_px: f32,
1173 cur_byte: usize,
1174 to_end: bool,
1175) -> usize {
1176 let layout = layout_text_area(text, font_px, wrap_w_px);
1177 let (li, _local, _) = locate_byte_in_ranges(&layout.ranges, cur_byte);
1178 let (s, e) = layout.ranges.get(li).copied().unwrap_or((0, 0));
1179 if to_end { e } else { s }
1180}
1181
1182fn clamp_to_char_boundary(s: &str, i: usize) -> usize {
1183 if i >= s.len() {
1184 return s.len();
1185 }
1186 if s.is_char_boundary(i) {
1187 return i;
1188 }
1189 let mut j = i;
1190 while j > 0 && !s.is_char_boundary(j) {
1191 j -= 1;
1192 }
1193 j
1194}
1195
1196fn char_to_byte(s: &str, ci: usize) -> usize {
1197 if ci == 0 {
1198 0
1199 } else {
1200 s.char_indices().nth(ci).map(|(i, _)| i).unwrap_or(s.len())
1201 }
1202}
1203
1204pub(crate) fn paint_text_field(
1209 scene: &mut Scene,
1210 rect: repose_core::Rect,
1211 text_input: &TextInputConfig,
1212 state: Option<&Rc<RefCell<TextFieldState>>>,
1213 is_focused: bool,
1214 clip_rounded: Option<f32>,
1215) {
1216 let pad_x = dp_to_px(TF_PADDING_X_DP);
1217 let inner = repose_core::Rect {
1218 x: rect.x + pad_x,
1219 y: rect.y + dp_to_px(8.0),
1220 w: (rect.w - 2.0 * pad_x).max(0.0),
1221 h: (rect.h - dp_to_px(16.0)).max(0.0),
1222 };
1223
1224 let font_val = dp_to_px(TF_FONT_DP) * locals::text_scale().0;
1225 let line_h = font_val * 1.3;
1226 let text_off_y = (inner.h - line_h) / 2.0;
1227
1228 scene.nodes.push(SceneNode::PushClip {
1229 rect: inner,
1230 radius: 0.0,
1231 });
1232
1233 if is_focused {
1234 let radius = clip_rounded.unwrap_or(6.0);
1235 scene.nodes.push(SceneNode::Border {
1236 rect,
1237 color: locals::theme().focus,
1238 width: dp_to_px(2.0),
1239 radius: dp_to_px(radius),
1240 });
1241 }
1242
1243 let th = locals::theme();
1244 let rendered_by_vt = |original: &str| -> String {
1245 if let Some(ref vt) = text_input.visual_transformation {
1246 vt.filter(original).text
1247 } else {
1248 original.to_string()
1249 }
1250 };
1251
1252 if let Some(state_rc) = state {
1253 let st = state_rc.borrow();
1254
1255 if !text_input.multiline {
1256 let measure_for = if text_input.visual_transformation.is_some() && !st.text.is_empty() {
1258 rendered_by_vt(&st.text)
1259 } else {
1260 st.text.clone()
1261 };
1262 let has_vt = text_input.visual_transformation.is_some();
1263 let m = measure_text(&measure_for, font_val, None);
1264
1265 if st.selection.start != st.selection.end {
1267 let start_off = if has_vt {
1268 original_offset_to_display(&st.text, &measure_for, st.selection.start)
1269 } else {
1270 st.selection.start
1271 };
1272 let end_off = if has_vt {
1273 original_offset_to_display(&st.text, &measure_for, st.selection.end)
1274 } else {
1275 st.selection.end
1276 };
1277 let sx = m
1278 .positions
1279 .get(byte_to_char_index(&m, start_off))
1280 .copied()
1281 .unwrap_or(0.0)
1282 - st.scroll_offset;
1283 let ex = m
1284 .positions
1285 .get(byte_to_char_index(&m, end_off))
1286 .copied()
1287 .unwrap_or(sx)
1288 - st.scroll_offset;
1289 let selection = th.focus.with_alpha_f32(85.0 / 255.0);
1290 let vis_x = sx.max(0.0);
1291 let vis_ex = ex.max(0.0);
1292 scene.nodes.push(SceneNode::Rect {
1293 rect: repose_core::Rect {
1294 x: inner.x + vis_x,
1295 y: inner.y + text_off_y,
1296 w: (vis_ex - vis_x).max(0.0),
1297 h: line_h,
1298 },
1299 brush: Brush::Solid(selection),
1300 radius: 0.0,
1301 });
1302 }
1303
1304 let txt_col = if st.text.is_empty() {
1306 th.on_surface_variant
1307 } else {
1308 th.on_surface
1309 };
1310 let render_txt = if st.text.is_empty() {
1311 text_input.hint.clone()
1312 } else {
1313 rendered_by_vt(&st.text)
1314 };
1315 scene.nodes.push(SceneNode::Text {
1316 rect: repose_core::Rect {
1317 x: inner.x - st.scroll_offset,
1318 y: inner.y + text_off_y,
1319 w: inner.w,
1320 h: line_h,
1321 },
1322 text: Arc::from(render_txt),
1323 color: txt_col,
1324 size: font_val,
1325 font_family: None,
1326 });
1327
1328 if is_focused && st.selection.start == st.selection.end && st.caret_visible() {
1330 let caret_off = if has_vt {
1331 original_offset_to_display(&st.text, &measure_for, st.selection.end)
1332 } else {
1333 st.selection.end
1334 };
1335 let cx = m
1336 .positions
1337 .get(byte_to_char_index(&m, caret_off))
1338 .copied()
1339 .unwrap_or(0.0)
1340 - st.scroll_offset;
1341 scene.nodes.push(SceneNode::Rect {
1342 rect: repose_core::Rect {
1343 x: inner.x + cx.max(0.0),
1344 y: inner.y + text_off_y,
1345 w: dp_to_px(1.0),
1346 h: line_h,
1347 },
1348 brush: Brush::Solid(th.on_surface),
1349 radius: 0.0,
1350 });
1351 }
1352 } else {
1353 let layout = layout_text_area(&st.text, font_val, inner.w.max(1.0));
1355 let lh = layout.line_h_px;
1356
1357 if st.text.is_empty() {
1359 scene.nodes.push(SceneNode::Text {
1360 rect: repose_core::Rect {
1361 x: inner.x,
1362 y: inner.y,
1363 w: inner.w,
1364 h: inner.h,
1365 },
1366 text: Arc::from(text_input.hint.clone()),
1367 color: th.on_surface_variant,
1368 size: font_val,
1369 font_family: None,
1370 });
1371 } else {
1372 let display_full = rendered_by_vt(&st.text);
1373 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1374 let ln = display_full[s..e].to_string();
1375 let draw_y = inner.y + (i as f32) * lh - st.scroll_offset_y;
1376 if draw_y + lh < inner.y - 1.0 || draw_y > inner.y + inner.h + 1.0 {
1377 continue;
1378 }
1379 scene.nodes.push(SceneNode::Text {
1380 rect: repose_core::Rect {
1381 x: inner.x,
1382 y: draw_y,
1383 w: inner.w,
1384 h: lh,
1385 },
1386 text: Arc::<str>::from(ln),
1387 color: th.on_surface,
1388 size: font_val,
1389 font_family: None,
1390 });
1391 }
1392 }
1393
1394 if st.selection.start != st.selection.end {
1396 let sel_a: usize = st.selection.start.min(st.selection.end);
1397 let sel_b: usize = st.selection.start.max(st.selection.end);
1398 let selection = th.focus.with_alpha_f32(85.0 / 255.0);
1399 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1400 let os = sel_a.max(s);
1401 let oe = sel_b.min(e);
1402 if os >= oe {
1403 continue;
1404 }
1405 let ln = &st.text[s..e];
1406 let m = measure_text(ln, font_val, None);
1407 let ls = os - s;
1408 let le = oe - s;
1409 let sx = m
1410 .positions
1411 .get(byte_to_char_index(&m, ls))
1412 .copied()
1413 .unwrap_or(0.0);
1414 let ex = m
1415 .positions
1416 .get(byte_to_char_index(&m, le))
1417 .copied()
1418 .unwrap_or(sx);
1419 let draw_y = inner.y + (i as f32) * lh - st.scroll_offset_y;
1420 scene.nodes.push(SceneNode::Rect {
1421 rect: repose_core::Rect {
1422 x: inner.x + sx,
1423 y: draw_y,
1424 w: (ex - sx).max(0.0),
1425 h: lh,
1426 },
1427 brush: Brush::Solid(selection),
1428 radius: 0.0,
1429 });
1430 }
1431 }
1432
1433 if is_focused && st.selection.start == st.selection.end && st.caret_visible() {
1435 let caret = st.selection.end.min(st.text.len());
1436 let (cx, cy, _li) = caret_xy_for_byte(&st.text, font_val, inner.w.max(1.0), caret);
1437 let draw_x = inner.x + cx;
1438 let draw_y = inner.y + cy - st.scroll_offset_y;
1439 scene.nodes.push(SceneNode::Rect {
1440 rect: repose_core::Rect {
1441 x: draw_x,
1442 y: draw_y,
1443 w: dp_to_px(1.0),
1444 h: lh,
1445 },
1446 brush: Brush::Solid(th.on_surface),
1447 radius: 0.0,
1448 });
1449 }
1450 }
1451 } else {
1452 if text_input.value.is_empty() {
1454 scene.nodes.push(SceneNode::Text {
1455 rect: repose_core::Rect {
1456 x: inner.x,
1457 y: inner.y + text_off_y,
1458 w: inner.w,
1459 h: line_h,
1460 },
1461 text: Arc::from(text_input.hint.clone()),
1462 color: th.on_surface_variant,
1463 size: font_val,
1464 font_family: None,
1465 });
1466 } else if text_input.multiline {
1467 let layout = layout_text_area(&text_input.value, font_val, inner.w.max(1.0));
1468 let lh = layout.line_h_px;
1469 let display_full = rendered_by_vt(&text_input.value);
1470 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1471 let ln = display_full[s..e].to_string();
1472 let draw_y = inner.y + (i as f32) * lh;
1473 if draw_y + lh < inner.y - 1.0 || draw_y > inner.y + inner.h + 1.0 {
1474 continue;
1475 }
1476 scene.nodes.push(SceneNode::Text {
1477 rect: repose_core::Rect {
1478 x: inner.x,
1479 y: draw_y,
1480 w: inner.w,
1481 h: lh,
1482 },
1483 text: Arc::<str>::from(ln),
1484 color: th.on_surface,
1485 size: font_val,
1486 font_family: None,
1487 });
1488 }
1489 } else {
1490 scene.nodes.push(SceneNode::Text {
1491 rect: repose_core::Rect {
1492 x: inner.x,
1493 y: inner.y + text_off_y,
1494 w: inner.w,
1495 h: line_h,
1496 },
1497 text: Arc::from(rendered_by_vt(&text_input.value)),
1498 color: th.on_surface,
1499 size: font_val,
1500 font_family: None,
1501 });
1502 }
1503 }
1504
1505 scene.nodes.push(SceneNode::PopClip);
1506}
1507
1508fn text_field_view(
1512 modifier: Modifier,
1513 hint: String,
1514 value: String,
1515 multiline: bool,
1516 on_change: Option<Rc<dyn Fn(String)>>,
1517 on_submit: Option<Rc<dyn Fn(String)>>,
1518 visual_transformation: Option<Rc<dyn repose_core::VisualTransformation>>,
1519 keyboard_type: Option<KeyboardType>,
1520 ime_action: Option<ImeAction>,
1521) -> View {
1522 let modif = modifier.text_input(TextInputConfig {
1523 hint,
1524 multiline,
1525 on_change,
1526 on_submit,
1527 focus_tracker: None,
1528 value,
1529 visual_transformation,
1530 keyboard_type,
1531 ime_action,
1532 });
1533
1534 View::new(0, ViewKind::Box)
1535 .modifier(modif)
1536 .semantics(Semantics {
1537 role: Role::TextField,
1538 label: None,
1539 focused: false,
1540 enabled: true,
1541 })
1542}
1543
1544#[cfg(test)]
1545mod tests {
1546 use super::*;
1547
1548 #[test]
1549 fn test_index_for_x_bytes_grapheme() {
1550 let t = "A👍🏽B";
1551 let font_px = 16.0; let m = measure_text(t, font_px, None);
1553 for i in 0..m.byte_offsets.len() - 1 {
1554 let b = m.byte_offsets[i];
1555 let _ = &t[..b];
1556 }
1557 }
1558}