1use repose_core::*;
41use std::cell::{Cell, RefCell};
42use std::collections::HashMap;
43use std::ops::Range;
44use std::rc::Rc;
45use std::sync::Arc;
46use unicode_segmentation::UnicodeSegmentation;
47use web_time::Duration;
48use web_time::Instant;
49
50use crate::layout::mul_alpha_color;
51
52thread_local! {
53 static TEXTFIELD_STATES: RefCell<HashMap<u64, Rc<RefCell<TextFieldState>>>> = RefCell::new(HashMap::new());
54}
55
56pub fn set_textfield_state(key: u64, state: Rc<RefCell<TextFieldState>>) {
57 TEXTFIELD_STATES.with(|m| m.borrow_mut().insert(key, state));
58}
59
60pub fn get_textfield_state(key: u64) -> Option<Rc<RefCell<TextFieldState>>> {
61 TEXTFIELD_STATES.with(|m| m.borrow().get(&key).cloned())
62}
63
64pub fn ensure_caret_visible(state: &mut TextFieldState, multiline: bool) {
65 let font_px = TF_FONT_SP.to_px().0;
66 let wrap_width = state.inner_width;
67 if multiline {
68 let (cx, cy, _) = crate::textfield::caret_xy_for_byte(
69 &state.text,
70 font_px,
71 wrap_width,
72 state.caret_index(),
73 );
74 let iw = state.inner_width;
75 let ih = state.inner_height;
76 state.ensure_caret_visible_xy(cx, cy, iw, ih, Dp(2.0).to_px().0);
77 } else {
78 let caret_idx = state.caret_index();
79 let (display, caret_display_off) = if let Some(vt) = &state.visual_transformation {
80 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
81 let tfmd = vt.filter(&annotated);
82 let off =
83 repose_core::original_offset_to_display(&state.text, tfmd.text.as_str(), caret_idx);
84 (tfmd.text.text, off)
85 } else {
86 (state.text.clone(), caret_idx)
87 };
88 let m = crate::textfield::measure_text(&display, font_px, TextMeasureConfig::default());
89 let caret_x = m.positions.get(caret_display_off).copied().unwrap_or(0.0);
90 state.ensure_caret_visible(caret_x, wrap_width, Dp(2.0).to_px().0);
91 }
92}
93
94const TEXT_UNDO_CAPACITY: usize = 100;
96
97const SNAPSHOTS_INTERVAL_MILLIS: u128 = 5000;
99
100#[derive(Clone, Copy, Debug, PartialEq)]
102enum TextEditType {
103 Insert,
104 Delete,
105 Replace,
106}
107
108#[derive(Clone, Copy, Debug, PartialEq)]
110enum TextDeleteType {
111 Start, End, Inner, NotByUser,
115}
116
117#[derive(Clone, Debug)]
119pub struct TextUndoOp {
120 pub index: usize,
122 pub pre_text: String,
124 pub post_text: String,
126 pub pre_selection: Range<usize>,
128 pub post_selection: Range<usize>,
130 pub time: Instant,
132 pub can_merge: bool,
134}
135
136impl TextUndoOp {
137 fn edit_type(&self) -> TextEditType {
138 match (self.pre_text.is_empty(), self.post_text.is_empty()) {
139 (true, true) => unreachable!("Both pre and post text cannot be empty"),
140 (true, false) => TextEditType::Insert,
141 (false, true) => TextEditType::Delete,
142 (false, false) => TextEditType::Replace,
143 }
144 }
145
146 fn is_newline(&self) -> bool {
147 self.post_text == "\n" || self.post_text == "\r\n"
148 }
149
150 fn try_merge(&self, next: &TextUndoOp) -> Option<TextUndoOp> {
152 if !self.can_merge || !next.can_merge {
153 return None;
154 }
155
156 let elapsed = next.time.saturating_duration_since(self.time);
157 if elapsed.as_millis() >= SNAPSHOTS_INTERVAL_MILLIS {
158 return None;
159 }
160
161 if self.is_newline() || next.is_newline() {
162 return None;
163 }
164
165 let self_type = self.edit_type();
166 if self_type != next.edit_type() {
167 return None;
168 }
169
170 match self_type {
171 TextEditType::Insert => {
172 if self.index + self.post_text.len() == next.index {
174 Some(TextUndoOp {
175 index: self.index,
176 pre_text: String::new(),
177 post_text: format!("{}{}", self.post_text, next.post_text),
178 pre_selection: self.pre_selection.clone(),
179 post_selection: next.post_selection.clone(),
180 time: self.time,
181 can_merge: true,
182 })
183 } else {
184 None
185 }
186 }
187 TextEditType::Delete => {
188 let self_del = self.deletion_type();
189 let next_del = next.deletion_type();
190 if self_del == next_del
192 && (self_del == TextDeleteType::Start || self_del == TextDeleteType::End)
193 {
194 if self.index == next.index + next.pre_text.len() {
195 Some(TextUndoOp {
197 index: next.index,
198 pre_text: format!("{}{}", next.pre_text, self.pre_text),
199 post_text: String::new(),
200 pre_selection: self.pre_selection.clone(),
201 post_selection: next.post_selection.clone(),
202 time: self.time,
203 can_merge: true,
204 })
205 } else if self.index == next.index {
206 Some(TextUndoOp {
208 index: self.index,
209 pre_text: format!("{}{}", self.pre_text, next.pre_text),
210 post_text: String::new(),
211 pre_selection: self.pre_selection.clone(),
212 post_selection: next.post_selection.clone(),
213 time: self.time,
214 can_merge: true,
215 })
216 } else {
217 None
218 }
219 } else {
220 None
221 }
222 }
223 TextEditType::Replace => None,
224 }
225 }
226
227 fn deletion_type(&self) -> TextDeleteType {
229 if self.edit_type() != TextEditType::Delete {
230 return TextDeleteType::NotByUser;
231 }
232 if self.post_selection.start != self.post_selection.end {
233 return TextDeleteType::NotByUser;
234 }
235 if self.pre_selection.start == self.pre_selection.end {
236 if self.pre_selection.start > self.post_selection.start {
238 TextDeleteType::Start } else {
240 TextDeleteType::End }
242 } else if self.pre_selection.start == self.post_selection.start
243 && self.pre_selection.start == self.index
244 {
245 TextDeleteType::Inner
246 } else {
247 TextDeleteType::NotByUser
248 }
249 }
250}
251
252const SCROLL_STIFFNESS: f32 = 300.0;
254const SCROLL_DAMPING: f32 = 30.0;
255
256pub const TF_FONT_SP: Sp = Sp(16.0);
259
260#[derive(Clone, Copy, Debug)]
262pub struct KeyboardOptions {
263 pub keyboard_type: repose_core::KeyboardType,
264 pub autocorrect: bool,
265 pub capitalization: repose_core::KeyboardCapitalization,
266}
267
268impl Default for KeyboardOptions {
269 fn default() -> Self {
270 Self {
271 keyboard_type: repose_core::KeyboardType::default(),
272 autocorrect: true,
273 capitalization: repose_core::KeyboardCapitalization::default(),
274 }
275 }
276}
277pub const TF_PADDING_X: Dp = Dp(8.0);
279
280pub struct TextMetrics {
281 pub positions: Vec<f32>, pub byte_offsets: Vec<usize>,
285}
286
287pub struct TextMeasureConfig {
288 pub font_family: Option<&'static str>,
289 pub font_weight: u16,
290 pub font_style: u8,
291 pub letter_spacing: f32,
292 pub font_variation_settings: Option<String>,
293}
294
295impl Default for TextMeasureConfig {
296 fn default() -> Self {
297 Self {
298 font_family: None,
299 font_weight: 400,
300 font_style: 0,
301 letter_spacing: 0.0,
302 font_variation_settings: None,
303 }
304 }
305}
306
307pub fn measure_text(text: &str, font_px: f32, config: TextMeasureConfig) -> TextMetrics {
311 let m = repose_text::metrics_for_textfield(
312 text,
313 font_px,
314 config.font_family,
315 config.font_weight,
316 config.font_style,
317 config.letter_spacing,
318 config.font_variation_settings.as_deref(),
319 );
320 TextMetrics {
321 positions: m.positions,
322 byte_offsets: m.byte_offsets,
323 }
324}
325
326pub fn byte_to_char_index(m: &TextMetrics, byte: usize) -> usize {
327 match m.byte_offsets.binary_search(&byte) {
328 Ok(i) | Err(i) => i,
329 }
330}
331
332pub fn index_for_x_bytes(
334 text: &str,
335 font_px: f32,
336 x_px: f32,
337 font_weight: u16,
338 font_style: u8,
339) -> usize {
340 let m = measure_text(
341 text,
342 font_px,
343 TextMeasureConfig {
344 font_weight,
345 font_style,
346 ..Default::default()
347 },
348 );
349
350 let mut best_i = 0usize;
351 let mut best_d = f32::INFINITY;
352 for i in 0..m.positions.len() {
353 let d = (m.positions[i] - x_px).abs();
354 if d < best_d {
355 best_d = d;
356 best_i = i;
357 }
358 }
359 m.byte_offsets[best_i]
360}
361
362pub(crate) fn prev_grapheme_boundary(text: &str, byte: usize) -> usize {
364 let mut last = 0usize;
365 for (i, _) in text.grapheme_indices(true) {
366 if i >= byte {
367 break;
368 }
369 last = i;
370 }
371 last
372}
373
374pub(crate) fn next_grapheme_boundary(text: &str, byte: usize) -> usize {
375 for (i, _) in text.grapheme_indices(true) {
376 if i > byte {
377 return i;
378 }
379 }
380 text.len()
381}
382
383pub(crate) fn word_range(text: &str, byte: usize) -> (usize, usize) {
386 let byte = byte.min(text.len());
387 let is_word = |g: &str| g.chars().all(|c| c.is_alphanumeric() || c == '_');
388
389 let mut start = byte;
390 while start > 0 {
391 let p = prev_grapheme_boundary(text, start);
392 if is_word(&text[p..start]) {
393 start = p;
394 } else {
395 break;
396 }
397 }
398 let mut end = byte;
399 while end < text.len() {
400 let n = next_grapheme_boundary(text, end);
401 if is_word(&text[end..n]) {
402 end = n;
403 } else {
404 break;
405 }
406 }
407 if start == end {
408 let s = if byte == 0 {
409 0
410 } else {
411 prev_grapheme_boundary(text, byte)
412 };
413 let e = next_grapheme_boundary(text, byte);
414 (s, e.max(s))
415 } else {
416 (start, end)
417 }
418}
419
420pub struct TextFieldState {
421 pub text: String,
422 pub selection: Range<usize>,
423 pub composition: Option<Range<usize>>, pub scroll_offset: f32, pub scroll_offset_y: f32, pub drag_anchor: Option<usize>, pub(crate) last_tap_time: Option<Instant>,
430 pub(crate) last_tap_pos: Option<(f32, f32)>,
431 pub(crate) tap_count: u8,
432
433 pub blink_start: Instant, pub inner_width: f32, pub inner_height: f32, pub preferred_x_px: Option<f32>, pub offset_map: Option<Box<dyn OffsetMapping>>,
440 pub visual_transformation: Option<Rc<dyn VisualTransformation>>,
442 pub(crate) scroll_target: f32,
444 pub(crate) scroll_target_y: f32,
446 scroll_vel: f32,
448 scroll_vel_y: f32,
450 last_scroll_tick: Option<Instant>,
452
453 undo_stack: Vec<TextUndoOp>,
456 redo_stack: Vec<TextUndoOp>,
458 staging_undo: Option<TextUndoOp>,
460}
461
462impl std::fmt::Debug for TextFieldState {
463 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464 f.debug_struct("TextFieldState")
465 .field("text", &self.text)
466 .field("selection", &self.selection)
467 .field("composition", &self.composition)
468 .field("scroll_offset", &self.scroll_offset)
469 .field("scroll_offset_y", &self.scroll_offset_y)
470 .field("drag_anchor", &self.drag_anchor)
471 .field("blink_start", &self.blink_start)
472 .field("inner_width", &self.inner_width)
473 .field("inner_height", &self.inner_height)
474 .field("preferred_x_px", &self.preferred_x_px)
475 .field(
476 "offset_map",
477 &self.offset_map.as_ref().map(|_| "<offset_mapping>"),
478 )
479 .field(
480 "visual_transformation",
481 &self.visual_transformation.as_ref().map(|_| "<vt>"),
482 )
483 .field("scroll_target", &self.scroll_target)
484 .field("scroll_target_y", &self.scroll_target_y)
485 .field("can_undo", &self.can_undo())
486 .field("can_redo", &self.can_redo())
487 .field("undo_count", &self.undo_stack.len())
488 .field("redo_count", &self.redo_stack.len())
489 .finish()
490 }
491}
492
493impl Default for TextFieldState {
494 fn default() -> Self {
495 Self::new()
496 }
497}
498
499impl Clone for TextFieldState {
500 fn clone(&self) -> Self {
501 Self {
502 text: self.text.clone(),
503 selection: self.selection.clone(),
504 composition: self.composition.clone(),
505 scroll_offset: self.scroll_offset,
506 scroll_offset_y: self.scroll_offset_y,
507 drag_anchor: self.drag_anchor,
508 last_tap_time: self.last_tap_time,
509 last_tap_pos: self.last_tap_pos,
510 tap_count: self.tap_count,
511 blink_start: self.blink_start,
512 inner_width: self.inner_width,
513 inner_height: self.inner_height,
514 preferred_x_px: self.preferred_x_px,
515 offset_map: self.offset_map.as_ref().map(|m| m.clone_box()),
516 visual_transformation: self.visual_transformation.clone(),
517 scroll_target: self.scroll_target,
518 scroll_target_y: self.scroll_target_y,
519 scroll_vel: self.scroll_vel,
520 scroll_vel_y: self.scroll_vel_y,
521 last_scroll_tick: self.last_scroll_tick,
522 undo_stack: self.undo_stack.clone(),
523 redo_stack: self.redo_stack.clone(),
524 staging_undo: self.staging_undo.clone(),
525 }
526 }
527}
528
529impl TextFieldState {
530 pub fn new() -> Self {
531 Self {
532 text: String::new(),
533 selection: 0..0,
534 composition: None,
535 scroll_offset: 0.0,
536 scroll_offset_y: 0.0,
537 drag_anchor: None,
538 last_tap_time: None,
539 last_tap_pos: None,
540 tap_count: 0,
541 blink_start: Instant::now(),
542 inner_width: 0.0,
543 inner_height: 0.0,
544 preferred_x_px: None,
545 offset_map: None,
546 visual_transformation: None,
547 scroll_target: 0.0,
548 scroll_target_y: 0.0,
549 scroll_vel: 0.0,
550 scroll_vel_y: 0.0,
551 last_scroll_tick: None,
552 undo_stack: Vec::new(),
553 redo_stack: Vec::new(),
554 staging_undo: None,
555 }
556 }
557
558 pub fn can_undo(&self) -> bool {
562 !self.undo_stack.is_empty() || self.staging_undo.is_some()
563 }
564
565 pub fn can_redo(&self) -> bool {
567 !self.redo_stack.is_empty()
568 }
569
570 pub fn undo(&mut self) -> bool {
572 self.flush_undo();
573 if let Some(op) = self.undo_stack.pop() {
574 let end = (op.index + op.post_text.len()).min(self.text.len());
575 self.text.replace_range(op.index..end, &op.pre_text);
576 self.selection = op.pre_selection.clone();
577 self.redo_stack.push(op);
578 self.preferred_x_px = None;
579 self.reset_caret_blink();
580 true
581 } else {
582 false
583 }
584 }
585
586 pub fn redo(&mut self) -> bool {
588 if let Some(op) = self.redo_stack.pop() {
589 let end = (op.index + op.pre_text.len()).min(self.text.len());
590 self.text.replace_range(op.index..end, &op.post_text);
591 self.selection = op.post_selection.clone();
592 self.undo_stack.push(op);
593 self.preferred_x_px = None;
594 self.reset_caret_blink();
595 true
596 } else {
597 false
598 }
599 }
600
601 pub fn clear_undo_history(&mut self) {
603 self.undo_stack.clear();
604 self.redo_stack.clear();
605 self.staging_undo = None;
606 }
607
608 fn record_edit(&mut self, op: TextUndoOp) {
612 if let Some(staging) = self.staging_undo.take() {
613 if let Some(merged) = staging.try_merge(&op) {
614 self.staging_undo = Some(merged);
615 return;
616 }
617 self.undo_stack.push(staging);
619 self.redo_stack.clear();
620 while self.undo_stack.len() + 1 > TEXT_UNDO_CAPACITY {
622 self.undo_stack.remove(0);
623 }
624 }
625 self.staging_undo = Some(op);
626 }
627
628 fn flush_undo(&mut self) {
630 if let Some(op) = self.staging_undo.take() {
631 self.undo_stack.push(op);
632 self.redo_stack.clear();
633 while self.undo_stack.len() > TEXT_UNDO_CAPACITY {
634 self.undo_stack.remove(0);
635 }
636 }
637 }
638
639 fn insert_text_impl(&mut self, text: &str, can_merge: bool) {
640 let start = self.selection.start.min(self.text.len());
641 let end = self.selection.end.min(self.text.len());
642 let pre_text = self.text[start..end].to_string();
643 let pre_selection = self.selection.clone();
644
645 self.text.replace_range(start..end, text);
646 let new_pos = start + text.len();
647 self.selection = new_pos..new_pos;
648 self.preferred_x_px = None;
649 self.reset_caret_blink();
650
651 if !pre_text.is_empty() || !text.is_empty() {
652 self.record_edit(TextUndoOp {
653 index: start,
654 pre_text,
655 post_text: text.to_string(),
656 pre_selection,
657 post_selection: self.selection.clone(),
658 time: Instant::now(),
659 can_merge,
660 });
661 }
662 }
663
664 pub fn insert_text(&mut self, text: &str) {
665 self.insert_text_impl(text, true);
666 }
667
668 pub fn insert_text_atomic(&mut self, text: &str) {
670 self.insert_text_impl(text, false);
671 }
672
673 pub fn delete_backward(&mut self) {
674 if self.selection.start == self.selection.end {
675 let pos = self.selection.start.min(self.text.len());
676 if pos > 0 {
677 let prev = prev_grapheme_boundary(&self.text, pos);
678 let pre_text = self.text[prev..pos].to_string();
679 let pre_selection = self.selection.clone();
680 self.text.replace_range(prev..pos, "");
681 self.selection = prev..prev;
682 self.preferred_x_px = None;
683 self.reset_caret_blink();
684 self.record_edit(TextUndoOp {
685 index: prev,
686 pre_text,
687 post_text: String::new(),
688 pre_selection,
689 post_selection: self.selection.clone(),
690 time: Instant::now(),
691 can_merge: true,
692 });
693 }
694 } else {
695 self.insert_text_impl("", true);
696 }
697 self.preferred_x_px = None;
698 self.reset_caret_blink();
699 }
700
701 pub fn delete_forward(&mut self) {
702 if self.selection.start == self.selection.end {
703 let pos = self.selection.start.min(self.text.len());
704 if pos < self.text.len() {
705 let next = next_grapheme_boundary(&self.text, pos);
706 let pre_text = self.text[pos..next].to_string();
707 let pre_selection = self.selection.clone();
708 self.text.replace_range(pos..next, "");
709 self.preferred_x_px = None;
710 self.reset_caret_blink();
711 self.record_edit(TextUndoOp {
712 index: pos,
713 pre_text,
714 post_text: String::new(),
715 pre_selection,
716 post_selection: self.selection.clone(),
717 time: Instant::now(),
718 can_merge: true,
719 });
720 }
721 } else {
722 self.insert_text_impl("", true);
723 }
724 self.preferred_x_px = None;
725 self.reset_caret_blink();
726 }
727
728 pub fn move_cursor(&mut self, delta: isize, extend_selection: bool) {
729 let mut pos = self.selection.end.min(self.text.len());
730 if delta < 0 {
731 for _ in 0..delta.unsigned_abs() {
732 pos = prev_grapheme_boundary(&self.text, pos);
733 }
734 } else if delta > 0 {
735 for _ in 0..(delta as usize) {
736 pos = next_grapheme_boundary(&self.text, pos);
737 }
738 }
739 if extend_selection {
740 self.selection.end = pos;
741 } else {
742 self.selection = pos..pos;
743 }
744 self.preferred_x_px = None;
745 self.reset_caret_blink();
746 }
747
748 pub fn selected_text(&self) -> String {
749 if self.selection.start == self.selection.end {
750 String::new()
751 } else {
752 self.text[self.selection.clone()].to_string()
753 }
754 }
755
756 pub fn set_composition(&mut self, text: String, cursor: Option<(usize, usize)>) {
757 if text.is_empty() {
758 if let Some(range) = self.composition.take() {
759 let s = clamp_to_char_boundary(&self.text, range.start.min(self.text.len()));
760 let e = clamp_to_char_boundary(&self.text, range.end.min(self.text.len()));
761 if s <= e {
762 self.text.replace_range(s..e, "");
763 self.selection = s..s;
764 }
765 }
766 self.preferred_x_px = None;
767 self.reset_caret_blink();
768 return;
769 }
770
771 let anchor_start;
772 if let Some(r) = self.composition.take() {
773 let mut s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
774 let mut e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
775 if e < s {
776 std::mem::swap(&mut s, &mut e);
777 }
778 self.text.replace_range(s..e, &text);
779 anchor_start = s;
780 } else {
781 let pos = clamp_to_char_boundary(&self.text, self.selection.start.min(self.text.len()));
782 self.text.insert_str(pos, &text);
783 anchor_start = pos;
784 }
785
786 self.composition = Some(anchor_start..(anchor_start + text.len()));
787
788 if let Some((c0, c1)) = cursor {
789 let b0 = char_to_byte(&text, c0);
790 let b1 = char_to_byte(&text, c1);
791 self.selection = (anchor_start + b0)..(anchor_start + b1);
792 } else {
793 let end = anchor_start + text.len();
794 self.selection = end..end;
795 }
796
797 self.preferred_x_px = None;
798 self.reset_caret_blink();
799 }
800
801 pub fn commit_composition(&mut self, text: String) {
802 let pre_selection = self.selection.clone();
803 if let Some(r) = self.composition.take() {
804 let s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
805 let e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
806 let pre_text = self.text[s..e].to_string();
807 self.text.replace_range(s..e, &text);
808 let new_pos = s + text.len();
809 self.selection = new_pos..new_pos;
810 self.preferred_x_px = None;
811 self.reset_caret_blink();
812 if !pre_text.is_empty() || !text.is_empty() {
813 self.record_edit(TextUndoOp {
814 index: s,
815 pre_text,
816 post_text: text,
817 pre_selection,
818 post_selection: self.selection.clone(),
819 time: Instant::now(),
820 can_merge: true,
821 });
822 }
823 } else {
824 let pos = clamp_to_char_boundary(&self.text, self.selection.end.min(self.text.len()));
825 self.text.insert_str(pos, &text);
826 let new_pos = pos + text.len();
827 self.selection = new_pos..new_pos;
828 self.preferred_x_px = None;
829 self.reset_caret_blink();
830 if !text.is_empty() {
831 self.record_edit(TextUndoOp {
832 index: pos,
833 pre_text: String::new(),
834 post_text: text,
835 pre_selection,
836 post_selection: self.selection.clone(),
837 time: Instant::now(),
838 can_merge: true,
839 });
840 }
841 }
842 }
843
844 pub fn cancel_composition(&mut self) {
845 if let Some(r) = self.composition.take() {
846 let s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
847 let e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
848 if s <= e {
849 self.text.replace_range(s..e, "");
850 self.selection = s..s;
851 }
852 }
853 self.preferred_x_px = None;
854 self.reset_caret_blink();
855 }
856
857 pub fn delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) {
858 if self.selection.start != self.selection.end {
859 let start = self.selection.start.min(self.text.len());
860 let end = self.selection.end.min(self.text.len());
861 self.text.replace_range(start..end, "");
862 self.selection = start..start;
863 self.preferred_x_px = None;
864 self.reset_caret_blink();
865 return;
866 }
867
868 let caret = self.selection.end.min(self.text.len());
869 let start_raw = caret.saturating_sub(before_bytes);
870 let end_raw = (caret + after_bytes).min(self.text.len());
871
872 let start = prev_grapheme_boundary(&self.text, start_raw);
873 let end = next_grapheme_boundary(&self.text, end_raw);
874 if start < end {
875 self.text.replace_range(start..end, "");
876 self.selection = start..start;
877 }
878 self.preferred_x_px = None;
879 self.reset_caret_blink();
880 }
881
882 pub fn begin_drag(&mut self, idx_byte: usize, extend: bool) {
883 let idx = idx_byte.min(self.text.len());
884 if extend {
885 let anchor = self.selection.start;
886 self.selection = anchor.min(idx)..anchor.max(idx);
887 self.drag_anchor = Some(anchor);
888 } else {
889 self.selection = idx..idx;
890 self.drag_anchor = Some(idx);
891 }
892 self.preferred_x_px = None;
893 self.reset_caret_blink();
894 }
895
896 pub fn drag_to(&mut self, idx_byte: usize) {
897 if let Some(anchor) = self.drag_anchor {
898 let i = idx_byte.min(self.text.len());
899 self.selection = anchor.min(i)..anchor.max(i);
900 }
901 self.preferred_x_px = None;
902 self.reset_caret_blink();
903 if self.selection.start < self.selection.end {
904 repose_core::clipboard::set_primary_selection(
905 &self.text[self.selection.start..self.selection.end],
906 );
907 }
908 }
909 pub fn end_drag(&mut self) {
910 self.drag_anchor = None;
911 if self.selection.start < self.selection.end {
912 repose_core::clipboard::set_primary_selection(
913 &self.text[self.selection.start..self.selection.end],
914 );
915 }
916 }
917
918 pub fn handle_pointer_down(&mut self, idx_byte: usize, pos_px: (f32, f32), shift: bool) {
919 const DOUBLE_TAP_MS: u64 = 300;
920 const TAP_SLOP_PX: f32 = 12.0;
921
922 let now = Instant::now();
923 let mut count = self.tap_count;
924 if let (Some(t), Some(p)) = (self.last_tap_time, self.last_tap_pos) {
925 let dt = now.saturating_duration_since(t);
926 let dist = ((pos_px.0 - p.0).powi(2) + (pos_px.1 - p.1).powi(2)).sqrt();
927 if dt < Duration::from_millis(DOUBLE_TAP_MS) && dist < TAP_SLOP_PX {
928 count = count.saturating_add(1);
929 } else {
930 count = 1;
931 }
932 } else {
933 count = 1;
934 }
935 self.tap_count = count;
936 self.last_tap_time = Some(now);
937 self.last_tap_pos = Some(pos_px);
938
939 let idx = idx_byte.min(self.text.len());
940
941 if count >= 3 {
942 self.selection = 0..self.text.len();
944 self.drag_anchor = None;
945 self.preferred_x_px = None;
946 self.reset_caret_blink();
947 if self.selection.end > 0 {
948 repose_core::clipboard::set_primary_selection(&self.text);
949 }
950 return;
951 }
952
953 if count == 2 {
954 let (s, e) = word_range(&self.text, idx);
956 self.selection = s..e;
957 self.drag_anchor = Some(s);
958 self.preferred_x_px = None;
959 self.reset_caret_blink();
960 if e > s {
961 repose_core::clipboard::set_primary_selection(&self.text[s..e]);
962 }
963 return;
964 }
965
966 self.begin_drag(idx, shift);
968 }
969
970 pub fn select_word_at(&mut self, byte: usize) {
972 let (s, e) = word_range(&self.text, byte.min(self.text.len()));
973 self.selection = s..e;
974 self.drag_anchor = Some(s);
975 self.preferred_x_px = None;
976 self.reset_caret_blink();
977 }
978
979 pub fn select_all(&mut self) {
981 self.selection = 0..self.text.len();
982 self.drag_anchor = None;
983 self.preferred_x_px = None;
984 self.reset_caret_blink();
985 }
986
987 pub fn caret_index(&self) -> usize {
988 self.selection.end
989 }
990
991 pub fn ensure_caret_visible(&mut self, caret_x_px: f32, inner_width_px: f32, inset_px: f32) {
995 self.ensure_caret_visible_xy(caret_x_px, 0.0, inner_width_px, 1.0, inset_px);
996 }
997
998 pub fn ensure_caret_visible_xy(
1001 &mut self,
1002 caret_x_px: f32,
1003 caret_y_px: f32,
1004 inner_w_px: f32,
1005 inner_h_px: f32,
1006 inset_px: f32,
1007 ) {
1008 let inset_px = inset_px.max(0.0);
1009
1010 let left_px = self.scroll_offset + inset_px;
1012 let right_px = self.scroll_offset + inner_w_px - inset_px;
1013 if caret_x_px < left_px {
1014 self.scroll_target = (caret_x_px - inset_px).max(0.0);
1015 } else if caret_x_px > right_px {
1016 self.scroll_target = (caret_x_px - inner_w_px + inset_px).max(0.0);
1017 }
1018
1019 let top_px = self.scroll_offset_y + inset_px;
1021 let bot_px = self.scroll_offset_y + inner_h_px - inset_px;
1022 if caret_y_px < top_px {
1023 self.scroll_target_y = (caret_y_px - inset_px).max(0.0);
1024 } else if caret_y_px > bot_px {
1025 self.scroll_target_y = (caret_y_px - inner_h_px + inset_px).max(0.0);
1026 }
1027 }
1028
1029 pub fn clamp_scroll(&mut self, content_h_px: f32) {
1030 let max_y = (content_h_px - self.inner_height).max(0.0);
1031 self.scroll_target_y = self.scroll_target_y.clamp(0.0, max_y);
1032 if self.scroll_target_y.is_nan() {
1033 self.scroll_target_y = 0.0;
1034 }
1035 }
1036
1037 pub fn reset_caret_blink(&mut self) {
1038 self.blink_start = Instant::now();
1039 }
1040 pub fn caret_visible(&self) -> bool {
1041 const PERIOD: Duration = Duration::from_millis(500);
1042 ((Instant::now() - self.blink_start).as_millis() / PERIOD.as_millis()).is_multiple_of(2)
1043 }
1044
1045 pub fn next_blink_deadline(&self) -> Option<Instant> {
1048 if self.selection.start != self.selection.end {
1049 return None;
1050 }
1051 const PERIOD_MS: u128 = 500;
1052 let now = Instant::now();
1053 let elapsed = now.saturating_duration_since(self.blink_start).as_millis();
1054 let next_tick = (elapsed / PERIOD_MS) + 1;
1055 Some(self.blink_start + Duration::from_millis((next_tick * PERIOD_MS) as u64))
1056 }
1057
1058 pub fn with_text(text: impl Into<String>) -> Self {
1060 let mut s = Self::new();
1061 s.text = text.into();
1062 s.selection = 0..0;
1063 s
1064 }
1065
1066 pub fn place_cursor_at_end(&mut self) {
1068 let len = self.text.len();
1069 self.selection = len..len;
1070 self.drag_anchor = None;
1071 self.preferred_x_px = None;
1072 self.reset_caret_blink();
1073 }
1074
1075 pub fn apply_controlled_value(&mut self, value: &str) {
1079 if self.text.as_str() == value {
1080 return;
1081 }
1082 if self.composition.is_some() {
1083 return;
1084 }
1085 self.text = value.to_string();
1086 let len = self.text.len();
1087 let ns = clamp_to_char_boundary(&self.text, self.selection.start.min(len));
1088 let ne = clamp_to_char_boundary(&self.text, self.selection.end.min(len));
1089 self.selection = ns..ne;
1090 self.drag_anchor = None;
1091 }
1092
1093 pub fn set_inner_width(&mut self, w_px: f32) {
1094 self.inner_width = w_px.max(0.0);
1095 if self.scroll_offset.is_nan() {
1096 self.scroll_offset = 0.0;
1097 }
1098 if self.scroll_target.is_nan() {
1099 self.scroll_target = 0.0;
1100 }
1101 }
1102 pub fn set_inner_height(&mut self, h_px: f32) {
1103 self.inner_height = h_px.max(0.0);
1104 if self.scroll_offset_y.is_nan() {
1105 self.scroll_offset_y = 0.0;
1106 }
1107 if self.scroll_target_y.is_nan() {
1108 self.scroll_target_y = 0.0;
1109 }
1110 }
1111
1112 pub fn tick_scroll_animation(&mut self) {
1116 let now = Instant::now();
1117 let dt = match self.last_scroll_tick {
1118 Some(prev) => {
1119 let d = now.saturating_duration_since(prev).as_secs_f32();
1120 d.min(0.05) }
1122 None => {
1123 self.last_scroll_tick = Some(now);
1126 self.scroll_offset = self.scroll_target;
1127 self.scroll_vel = 0.0;
1128 self.scroll_offset_y = self.scroll_target_y;
1129 self.scroll_vel_y = 0.0;
1130 return;
1131 }
1132 };
1133 self.last_scroll_tick = Some(now);
1134
1135 if dt > 0.0 {
1137 let dx = self.scroll_target - self.scroll_offset;
1138 let near_x = dx.abs() < 0.5 && self.scroll_vel.abs() < 0.5;
1139 if near_x {
1140 self.scroll_offset = self.scroll_target;
1141 self.scroll_vel = 0.0;
1142 } else {
1143 let force_x = SCROLL_STIFFNESS * dx - SCROLL_DAMPING * self.scroll_vel;
1144 self.scroll_vel += force_x * dt;
1145 self.scroll_offset += self.scroll_vel * dt;
1146 if (self.scroll_target - self.scroll_offset).signum() != dx.signum() && dx != 0.0 {
1148 self.scroll_offset = self.scroll_target;
1149 self.scroll_vel = 0.0;
1150 }
1151 }
1152 }
1153
1154 if dt > 0.0 {
1156 let dy = self.scroll_target_y - self.scroll_offset_y;
1157 let near_y = dy.abs() < 0.5 && self.scroll_vel_y.abs() < 0.5;
1158 if near_y {
1159 self.scroll_offset_y = self.scroll_target_y;
1160 self.scroll_vel_y = 0.0;
1161 } else {
1162 let force_y = SCROLL_STIFFNESS * dy - SCROLL_DAMPING * self.scroll_vel_y;
1163 self.scroll_vel_y += force_y * dt;
1164 self.scroll_offset_y += self.scroll_vel_y * dt;
1165 if (self.scroll_target_y - self.scroll_offset_y).signum() != dy.signum()
1166 && dy != 0.0
1167 {
1168 self.scroll_offset_y = self.scroll_target_y;
1169 self.scroll_vel_y = 0.0;
1170 }
1171 }
1172 }
1173 }
1174}
1175
1176#[derive(Clone)]
1186pub struct TextFieldConfig {
1187 pub enabled: bool,
1189 pub read_only: bool,
1191 pub input_transformation: Option<Rc<dyn repose_core::InputTransformation>>,
1193 pub text_style: repose_core::TextStyle,
1195 pub keyboard_options: repose_core::KeyboardOptions,
1197 pub on_keyboard_action: Option<Rc<dyn repose_core::KeyboardActionHandler>>,
1199 pub line_limits: repose_core::TextFieldLineLimits,
1201 pub on_text_layout: Option<Rc<dyn Fn(&repose_core::TextLayoutResult)>>,
1203 pub interaction_source: Option<repose_core::MutableInteractionSource>,
1205 pub focus_tracker: Option<Rc<Cell<bool>>>,
1208 pub cursor_brush: Option<repose_core::Brush>,
1210 pub output_transformation: Option<Rc<dyn repose_core::OutputTransformation>>,
1212 pub decorator: Option<Rc<dyn repose_core::TextFieldDecorator>>,
1214 pub codepoint_transformation: Option<repose_core::CodepointTransformation>,
1216 pub text_obfuscation_mode: repose_core::TextObfuscationMode,
1218 pub text_obfuscation_character: char,
1220
1221 pub on_change: Option<Rc<dyn Fn(String)>>,
1223 pub on_submit: Option<Rc<dyn Fn(String)>>,
1224 pub visual_transformation: Option<Rc<dyn repose_core::VisualTransformation>>,
1225 pub decoration_box: Option<Rc<dyn Fn(repose_core::View) -> repose_core::View>>,
1226}
1227
1228impl Default for TextFieldConfig {
1229 fn default() -> Self {
1230 Self {
1231 enabled: true,
1232 read_only: false,
1233 input_transformation: None,
1234 text_style: Default::default(),
1235 keyboard_options: repose_core::KeyboardOptions::DEFAULT,
1236 on_keyboard_action: None,
1237 line_limits: repose_core::TextFieldLineLimits::MultiLine {
1238 min_height_in_lines: 1,
1239 max_height_in_lines: usize::MAX,
1240 },
1241 on_text_layout: None,
1242 interaction_source: None,
1243 focus_tracker: None,
1244 cursor_brush: None,
1245 output_transformation: None,
1246 decorator: None,
1247 codepoint_transformation: None,
1248 text_obfuscation_mode: repose_core::TextObfuscationMode::System,
1249 text_obfuscation_character: '\u{2022}',
1250 on_change: None,
1251 on_submit: None,
1252 visual_transformation: None,
1253 decoration_box: None,
1254 }
1255 }
1256}
1257
1258pub fn BasicTextField(
1272 state: Rc<RefCell<TextFieldState>>,
1273 modifier: repose_core::Modifier,
1274 hint: impl Into<String>,
1275 config: TextFieldConfig,
1276) -> repose_core::View {
1277 let (single_line, max_lines, min_lines) = match config.line_limits {
1278 repose_core::TextFieldLineLimits::SingleLine => (true, 1, 1),
1279 repose_core::TextFieldLineLimits::MultiLine {
1280 min_height_in_lines,
1281 max_height_in_lines,
1282 } => (false, max_height_in_lines, min_height_in_lines),
1283 };
1284
1285 let ka = if let Some(ref handler) = config.on_keyboard_action {
1286 let handler = handler.clone();
1287 repose_core::KeyboardActions {
1288 on_done: Some({
1289 let h = handler.clone();
1290 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1291 h.on_keyboard_action(&|| {})
1292 })
1293 }),
1294 on_go: Some({
1295 let h = handler.clone();
1296 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1297 h.on_keyboard_action(&|| {})
1298 })
1299 }),
1300 on_next: Some({
1301 let h = handler.clone();
1302 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1303 h.on_keyboard_action(&|| {})
1304 })
1305 }),
1306 on_previous: Some({
1307 let h = handler.clone();
1308 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1309 h.on_keyboard_action(&|| {})
1310 })
1311 }),
1312 on_search: Some({
1313 let h = handler.clone();
1314 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1315 h.on_keyboard_action(&|| {})
1316 })
1317 }),
1318 on_send: Some({
1319 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1320 handler.on_keyboard_action(&|| {})
1321 })
1322 }),
1323 }
1324 } else {
1325 repose_core::KeyboardActions::default()
1326 };
1327
1328 let decoration_box = config
1329 .decorator
1330 .map(|d| Rc::new(move |inner: repose_core::View| d.decorate(inner)) as Rc<dyn Fn(_) -> _>);
1331
1332 let cursor_color = config.cursor_brush.and_then(|b| match b {
1333 repose_core::Brush::Solid(c) => Some(c),
1334 _ => None,
1335 });
1336
1337 let value = state.borrow().text.clone();
1338 let key = state.as_ptr() as u64;
1339 set_textfield_state(key, state.clone());
1340
1341 let state_on_change = {
1342 let s = state.clone();
1343 move |new_value: String| {
1344 s.borrow_mut().text = new_value;
1345 }
1346 };
1347
1348 let merged_on_change: Option<Rc<dyn Fn(String)>> =
1349 if let Some(ref cfg_on_change) = config.on_change {
1350 let a = Rc::new(state_on_change) as Rc<dyn Fn(String)>;
1351 let b = cfg_on_change.clone();
1352 Some(Rc::new(move |v: String| {
1353 a(v.clone());
1354 b(v);
1355 }) as Rc<dyn Fn(String)>)
1356 } else {
1357 Some(Rc::new(state_on_change) as Rc<dyn Fn(String)>)
1358 };
1359
1360 text_field_view(
1361 modifier,
1362 hint.into(),
1363 value,
1364 !single_line,
1365 merged_on_change,
1366 config.on_submit,
1367 config.visual_transformation,
1368 config.keyboard_options.keyboard_type,
1369 config.keyboard_options.capitalization,
1370 config.keyboard_options.ime_action,
1371 config.keyboard_options.auto_correct_enabled,
1372 config.enabled,
1373 config.read_only,
1374 Some(max_lines),
1375 min_lines,
1376 cursor_color,
1377 config.on_text_layout,
1378 config.text_style,
1379 ka,
1380 config.interaction_source,
1381 config.focus_tracker,
1382 Some(config.line_limits),
1383 config.input_transformation,
1384 config.output_transformation,
1385 decoration_box,
1386 config.codepoint_transformation,
1387 )
1388}
1389
1390pub fn BasicSecureTextField(
1395 state: Rc<RefCell<TextFieldState>>,
1396 modifier: repose_core::Modifier,
1397 config: TextFieldConfig,
1398) -> repose_core::View {
1399 let mask = config.text_obfuscation_character;
1400 let secure_config = TextFieldConfig {
1401 line_limits: repose_core::TextFieldLineLimits::SingleLine,
1402 keyboard_options: repose_core::KeyboardOptions::SECURE_TEXT_FIELD,
1403 visual_transformation: match config.text_obfuscation_mode {
1404 repose_core::TextObfuscationMode::Visible => None,
1405 _ => Some(Rc::new(repose_core::PasswordVisualTransformation { mask })
1406 as Rc<dyn repose_core::VisualTransformation>),
1407 },
1408 ..config
1409 };
1410 BasicTextField(state, modifier, "", secure_config)
1411}
1412
1413#[derive(Clone, Debug)]
1414pub struct TextAreaLayout {
1415 pub ranges: Vec<(usize, usize)>,
1416 pub line_h_px: f32,
1417}
1418
1419pub fn layout_text_area(
1420 text: &str,
1421 font_px: f32,
1422 wrap_w_px: f32,
1423 font_weight: u16,
1424 font_style: u8,
1425 letter_spacing: f32,
1426 font_variation_settings: Option<&str>,
1427) -> TextAreaLayout {
1428 let line_h = font_px;
1429 let (ranges, _) = repose_text::wrap_line_ranges(
1430 text,
1431 font_px,
1432 wrap_w_px.max(1.0),
1433 None,
1434 true,
1435 font_weight,
1436 font_style,
1437 letter_spacing,
1438 font_variation_settings,
1439 );
1440 TextAreaLayout {
1441 ranges,
1442 line_h_px: line_h,
1443 }
1444}
1445
1446fn locate_byte_in_ranges(ranges: &[(usize, usize)], b: usize) -> (usize, usize, usize) {
1448 if ranges.is_empty() {
1449 return (0, 0, b);
1450 }
1451 for (i, (s, e)) in ranges.iter().enumerate() {
1452 if b < *s {
1453 if i == 0 {
1454 return (0, 0, b);
1455 }
1456 let (ps, pe) = ranges[i - 1];
1457 let local = pe.saturating_sub(ps);
1458 return (i - 1, local, ps + local);
1459 }
1460 if b < *e {
1461 let local = b.saturating_sub(*s).min(e.saturating_sub(*s));
1462 return (i, local, *s + local);
1463 }
1464 if b == *e {
1465 if let Some((ns, _ne)) = ranges.get(i + 1)
1466 && *ns == b
1467 {
1468 return (i + 1, 0, b);
1469 }
1470 let local = e.saturating_sub(*s);
1471 return (i, local, *s + local);
1472 }
1473 }
1474 let (ls, le) = ranges[ranges.len() - 1];
1475 let local = le.saturating_sub(ls);
1476 (ranges.len() - 1, local, ls + local)
1477}
1478
1479pub fn caret_xy_for_byte(
1481 text: &str,
1482 font_px: f32,
1483 wrap_w_px: f32,
1484 byte: usize,
1485) -> (f32, f32, usize) {
1486 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1487 let (ranges, line_h) = (&layout.ranges, layout.line_h_px);
1488 let (li, local, _) = locate_byte_in_ranges(ranges, byte);
1489 let (s, e) = ranges.get(li).copied().unwrap_or((0, 0));
1490 let line = &text[s..e];
1491 let m = measure_text(line, font_px, TextMeasureConfig::default());
1492 let ci = byte_to_char_index(&m, local);
1493 let x = m.positions.get(ci).copied().unwrap_or(0.0);
1494 let y = (li as f32) * line_h;
1495 (x, y, li)
1496}
1497
1498pub fn index_for_xy_bytes(text: &str, font_px: f32, wrap_w_px: f32, x_px: f32, y_px: f32) -> usize {
1500 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1501 let li = ((y_px / layout.line_h_px).floor() as isize).max(0) as usize;
1502 let li = li.min(layout.ranges.len().saturating_sub(1));
1503 let (s, e) = layout.ranges.get(li).copied().unwrap_or((0, 0));
1504 let line = &text[s..e];
1505 let local = index_for_x_bytes(line, font_px, x_px.max(0.0), 400, 0);
1506 (s + local).min(text.len())
1507}
1508
1509pub fn move_caret_vertical(
1511 text: &str,
1512 font_px: f32,
1513 wrap_w_px: f32,
1514 cur_byte: usize,
1515 dir: i32, preferred_x: Option<f32>,
1517) -> (usize, f32) {
1518 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1519 if layout.ranges.is_empty() {
1520 return (cur_byte, preferred_x.unwrap_or(0.0));
1521 }
1522 let (x, _y, li) = caret_xy_for_byte(text, font_px, wrap_w_px, cur_byte);
1523 let px = preferred_x.unwrap_or(x);
1524 let mut nli = li as i32 + dir;
1525 nli = nli.clamp(0, (layout.ranges.len().saturating_sub(1)) as i32);
1526 let nli = nli as usize;
1527 let (s, e) = layout.ranges[nli];
1528 let line = &text[s..e];
1529 let local = index_for_x_bytes(line, font_px, px.max(0.0), 400, 0);
1530 ((s + local).min(text.len()), px)
1531}
1532
1533pub fn line_home_end(
1535 text: &str,
1536 font_px: f32,
1537 wrap_w_px: f32,
1538 cur_byte: usize,
1539 to_end: bool,
1540) -> usize {
1541 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1542 let (li, _local, _) = locate_byte_in_ranges(&layout.ranges, cur_byte);
1543 let (s, e) = layout.ranges.get(li).copied().unwrap_or((0, 0));
1544 if to_end { e } else { s }
1545}
1546
1547fn clamp_to_char_boundary(s: &str, i: usize) -> usize {
1548 if i >= s.len() {
1549 return s.len();
1550 }
1551 if s.is_char_boundary(i) {
1552 return i;
1553 }
1554 let mut j = i;
1555 while j > 0 && !s.is_char_boundary(j) {
1556 j -= 1;
1557 }
1558 j
1559}
1560
1561fn char_to_byte(s: &str, ci: usize) -> usize {
1562 if ci == 0 {
1563 0
1564 } else {
1565 s.char_indices().nth(ci).map(|(i, _)| i).unwrap_or(s.len())
1566 }
1567}
1568
1569pub(crate) fn paint_text_field(
1581 scene: &mut Scene,
1582 rect: repose_core::Rect,
1583 text_input: &TextInputConfig,
1584 state: Option<&Rc<RefCell<TextFieldState>>>,
1585 is_focused: bool,
1586 clip_rounded: Option<[Dp; 4]>,
1587 alpha_accum: f32,
1588) {
1589 let ts = text_input.text_style.clone().unwrap_or_default();
1590 let font_size_sp = if ts.font_size != Sp::ZERO {
1591 ts.font_size
1592 } else {
1593 TF_FONT_SP
1594 };
1595 let font_val = font_size_sp.to_px().0;
1596 let line_h = if ts.line_height != Sp::ZERO {
1597 ts.line_height.to_px().0
1598 } else if text_input.multiline {
1599 0.0 } else {
1601 font_val };
1603 let text_off_y = (rect.h - line_h.max(font_val)) / 2.0;
1604
1605 let clip_radius = clip_rounded.unwrap_or([Dp::ZERO; 4]).map(|v| v.to_px());
1606 scene.nodes.push(SceneNode::PushClip {
1607 rect,
1608 radius: clip_radius,
1609 op: repose_core::ClipOp::Intersect,
1610 });
1611
1612 let th = locals::theme();
1613 let show_selection = text_input.enabled;
1614 let show_cursor = text_input.enabled && !text_input.read_only;
1615 let cursor_color = text_input.cursor_color.unwrap_or(th.on_surface);
1616 let rendered_by_vt = |original: &str| -> String {
1617 if let Some(ref vt) = text_input.visual_transformation {
1618 let annotated = repose_core::AnnotatedString::new(original.to_string(), vec![]);
1619 vt.filter(&annotated).text.text
1620 } else {
1621 original.to_string()
1622 }
1623 };
1624
1625 if let Some(state_rc) = state {
1626 let st = state_rc.borrow();
1627
1628 if !text_input.multiline {
1629 let measure_for = if text_input.visual_transformation.is_some() && !st.text.is_empty() {
1631 rendered_by_vt(&st.text)
1632 } else {
1633 st.text.clone()
1634 };
1635 let has_vt = text_input.visual_transformation.is_some();
1636 let m = measure_text(
1637 &measure_for,
1638 font_val,
1639 TextMeasureConfig {
1640 font_family: ts.font_family,
1641 font_weight: ts.font_weight.unwrap_or(400),
1642 font_style: ts.font_style.unwrap_or(0),
1643 letter_spacing: ts.letter_spacing.to_px().0,
1644 font_variation_settings: None,
1645 },
1646 );
1647
1648 if show_selection && st.selection.start != st.selection.end {
1650 let start_off = if has_vt {
1651 original_offset_to_display(&st.text, &measure_for, st.selection.start)
1652 } else {
1653 st.selection.start
1654 };
1655 let end_off = if has_vt {
1656 original_offset_to_display(&st.text, &measure_for, st.selection.end)
1657 } else {
1658 st.selection.end
1659 };
1660 let sx = m
1661 .positions
1662 .get(byte_to_char_index(&m, start_off))
1663 .copied()
1664 .unwrap_or(0.0)
1665 - st.scroll_offset;
1666 let ex = m
1667 .positions
1668 .get(byte_to_char_index(&m, end_off))
1669 .copied()
1670 .unwrap_or(sx)
1671 - st.scroll_offset;
1672 let selection = th.focus.with_alpha_f32(85.0 / 255.0);
1673 let vis_x = sx.max(0.0);
1674 let vis_ex = ex.max(0.0);
1675 scene.nodes.push(SceneNode::Rect {
1676 rect: repose_core::Rect {
1677 x: rect.x + vis_x,
1678 y: rect.y + text_off_y,
1679 w: (vis_ex - vis_x).max(0.0),
1680 h: line_h.max(font_val),
1681 },
1682 brush: Brush::Solid(selection),
1683 radius: [Px::ZERO; 4],
1684 });
1685 }
1686
1687 if let Some(comp) = st.composition.clone() {
1689 let cs = if has_vt {
1690 original_offset_to_display(&st.text, &measure_for, comp.start)
1691 } else {
1692 comp.start
1693 };
1694 let ce = if has_vt {
1695 original_offset_to_display(&st.text, &measure_for, comp.end)
1696 } else {
1697 comp.end
1698 };
1699 let sx = m
1700 .positions
1701 .get(byte_to_char_index(&m, cs))
1702 .copied()
1703 .unwrap_or(0.0)
1704 - st.scroll_offset;
1705 let ex = m
1706 .positions
1707 .get(byte_to_char_index(&m, ce))
1708 .copied()
1709 .unwrap_or(sx)
1710 - st.scroll_offset;
1711 let y = rect.y + text_off_y + line_h.max(font_val) - Dp(2.0).to_px().0;
1712 scene.nodes.push(SceneNode::Rect {
1713 rect: repose_core::Rect {
1714 x: rect.x + sx.max(0.0),
1715 y,
1716 w: (ex - sx).max(Dp(2.0).to_px().0),
1717 h: Dp(2.0).to_px().0,
1718 },
1719 brush: Brush::Solid(th.focus),
1720 radius: [Px::ZERO; 4],
1721 });
1722 }
1723
1724 let display_src = if st.text.is_empty() && !text_input.value.is_empty() {
1726 text_input.value.as_str()
1727 } else {
1728 st.text.as_str()
1729 };
1730 let txt_col = if display_src.is_empty() {
1731 ts.color.unwrap_or(th.on_surface_variant)
1732 } else {
1733 ts.color.unwrap_or(th.on_surface)
1734 };
1735 let render_txt = if display_src.is_empty() {
1736 text_input.hint.clone()
1737 } else {
1738 rendered_by_vt(display_src)
1739 };
1740 scene.nodes.push(SceneNode::Text {
1741 rect: repose_core::Rect {
1742 x: rect.x - st.scroll_offset,
1743 y: rect.y + text_off_y,
1744 w: rect.w,
1745 h: line_h,
1746 },
1747 text: Arc::from(render_txt),
1748 color: mul_alpha_color(txt_col, alpha_accum),
1749 size: Px(font_val),
1750 font_family: ts.font_family,
1751 text_align: ts.text_align,
1752 font_weight: FontWeight(ts.font_weight.unwrap_or(400)),
1753 font_style: match ts.font_style.unwrap_or(0) {
1754 1 => FontStyle::Italic,
1755 _ => FontStyle::Normal,
1756 },
1757 text_decoration: ts.text_decoration.unwrap_or_default(),
1758 letter_spacing: ts.letter_spacing.to_px(),
1759 line_height: ts.line_height.to_px(),
1760 extra_style: Default::default(),
1761 url: None,
1762 font_variation_settings: None,
1763 });
1764
1765 if show_cursor
1767 && is_focused
1768 && st.selection.start == st.selection.end
1769 && st.caret_visible()
1770 {
1771 let caret_off = if has_vt {
1772 original_offset_to_display(&st.text, &measure_for, st.selection.end)
1773 } else {
1774 st.selection.end
1775 };
1776 let cx = m
1777 .positions
1778 .get(byte_to_char_index(&m, caret_off))
1779 .copied()
1780 .unwrap_or(0.0)
1781 - st.scroll_offset;
1782 let cursor_y = rect.y + text_off_y + (line_h.max(font_val) - font_val) / 2.0;
1783 scene.nodes.push(SceneNode::Rect {
1784 rect: repose_core::Rect {
1785 x: rect.x + cx.max(0.0),
1786 y: cursor_y,
1787 w: Dp(1.0).to_px().0,
1788 h: font_val,
1789 },
1790 brush: Brush::Solid(cursor_color),
1791 radius: [Px::ZERO; 4],
1792 });
1793 }
1794 } else {
1795 let render_text = if st.text.is_empty() {
1797 st.text.clone()
1798 } else if let Some(ref vt) = text_input.visual_transformation {
1799 let annotated = repose_core::AnnotatedString::new(st.text.clone(), vec![]);
1800 vt.filter(&annotated).text.text
1801 } else {
1802 st.text.clone()
1803 };
1804 let layout = layout_text_area(
1805 &render_text,
1806 font_val,
1807 rect.w.max(1.0),
1808 400,
1809 0,
1810 ts.letter_spacing.to_px().0,
1811 None,
1812 );
1813 let lh = layout.line_h_px;
1814 let max_line_count = text_input.max_lines.unwrap_or(usize::MAX);
1815
1816 if st.text.is_empty() {
1818 scene.nodes.push(SceneNode::Text {
1819 rect: repose_core::Rect {
1820 x: rect.x,
1821 y: rect.y,
1822 w: rect.w,
1823 h: line_h,
1824 },
1825 text: Arc::from(text_input.hint.clone()),
1826 color: mul_alpha_color(ts.color.unwrap_or(th.on_surface_variant), alpha_accum),
1827 size: Px(font_val),
1828 font_family: ts.font_family,
1829 text_align: ts.text_align,
1830 font_weight: FontWeight(ts.font_weight.unwrap_or(400)),
1831 font_style: match ts.font_style.unwrap_or(0) {
1832 1 => FontStyle::Italic,
1833 _ => FontStyle::Normal,
1834 },
1835 text_decoration: ts.text_decoration.unwrap_or_default(),
1836 letter_spacing: ts.letter_spacing.to_px(),
1837 line_height: ts.line_height.to_px(),
1838 extra_style: Default::default(),
1839 url: None,
1840 font_variation_settings: None,
1841 });
1842 } else {
1843 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1844 if i >= max_line_count {
1845 break;
1846 }
1847 let ln = render_text[s..e].to_string();
1848 let draw_y = rect.y + (i as f32) * lh - st.scroll_offset_y;
1849 if draw_y + lh < rect.y - 1.0 || draw_y > rect.y + rect.h + 1.0 {
1850 continue;
1851 }
1852 scene.nodes.push(SceneNode::Text {
1853 rect: repose_core::Rect {
1854 x: rect.x,
1855 y: draw_y,
1856 w: rect.w,
1857 h: lh,
1858 },
1859 text: Arc::<str>::from(ln),
1860 color: mul_alpha_color(ts.color.unwrap_or(th.on_surface), alpha_accum),
1861 size: Px(font_val),
1862 font_family: ts.font_family,
1863 text_align: ts.text_align,
1864 font_weight: FontWeight(ts.font_weight.unwrap_or(400)),
1865 font_style: match ts.font_style.unwrap_or(0) {
1866 1 => FontStyle::Italic,
1867 _ => FontStyle::Normal,
1868 },
1869 text_decoration: ts.text_decoration.unwrap_or_default(),
1870 letter_spacing: ts.letter_spacing.to_px(),
1871 line_height: ts.line_height.to_px(),
1872 extra_style: Default::default(),
1873 url: None,
1874 font_variation_settings: None,
1875 });
1876 }
1877 }
1878
1879 if show_selection && st.selection.start != st.selection.end {
1881 let sel_a_orig: usize = st.selection.start.min(st.selection.end);
1882 let sel_b_orig: usize = st.selection.start.max(st.selection.end);
1883 let has_vt = text_input.visual_transformation.is_some();
1884 let sel_a = if has_vt {
1885 original_offset_to_display(&st.text, &render_text, sel_a_orig)
1886 } else {
1887 sel_a_orig
1888 };
1889 let sel_b = if has_vt {
1890 original_offset_to_display(&st.text, &render_text, sel_b_orig)
1891 } else {
1892 sel_b_orig
1893 };
1894 let selection = th.focus.with_alpha_f32(85.0 / 255.0);
1895 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1896 if i >= max_line_count {
1897 break;
1898 }
1899 let os = sel_a.max(s);
1900 let oe = sel_b.min(e);
1901 if os >= oe {
1902 continue;
1903 }
1904 let ln = &render_text[s..e];
1905 let m = measure_text(
1906 ln,
1907 font_val,
1908 TextMeasureConfig {
1909 font_family: ts.font_family,
1910 font_weight: ts.font_weight.unwrap_or(400),
1911 font_style: ts.font_style.unwrap_or(0),
1912 letter_spacing: ts.letter_spacing.to_px().0,
1913 font_variation_settings: None,
1914 },
1915 );
1916 let ls = os - s;
1917 let le = oe - s;
1918 let sx = m
1919 .positions
1920 .get(byte_to_char_index(&m, ls))
1921 .copied()
1922 .unwrap_or(0.0);
1923 let ex = m
1924 .positions
1925 .get(byte_to_char_index(&m, le))
1926 .copied()
1927 .unwrap_or(sx);
1928 let draw_y = rect.y + (i as f32) * lh - st.scroll_offset_y;
1929 scene.nodes.push(SceneNode::Rect {
1930 rect: repose_core::Rect {
1931 x: rect.x + sx,
1932 y: draw_y,
1933 w: (ex - sx).max(0.0),
1934 h: lh,
1935 },
1936 brush: Brush::Solid(selection),
1937 radius: [Px::ZERO; 4],
1938 });
1939 }
1940 }
1941
1942 if let Some(comp) = st.composition.clone() {
1945 let has_vt = text_input.visual_transformation.is_some();
1946 let comp_a = if has_vt {
1947 original_offset_to_display(&st.text, &render_text, comp.start)
1948 } else {
1949 comp.start
1950 };
1951 let comp_b = if has_vt {
1952 original_offset_to_display(&st.text, &render_text, comp.end)
1953 } else {
1954 comp.end
1955 };
1956 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1957 if i >= max_line_count {
1958 break;
1959 }
1960 let os = comp_a.max(s);
1961 let oe = comp_b.min(e);
1962 if os >= oe {
1963 continue;
1964 }
1965 let ln = &render_text[s..e];
1966 let m = measure_text(
1967 ln,
1968 font_val,
1969 TextMeasureConfig {
1970 font_family: ts.font_family,
1971 font_weight: ts.font_weight.unwrap_or(400),
1972 font_style: ts.font_style.unwrap_or(0),
1973 letter_spacing: ts.letter_spacing.to_px().0,
1974 font_variation_settings: None,
1975 },
1976 );
1977 let ls = os - s;
1978 let le = oe - s;
1979 let sx = m
1980 .positions
1981 .get(byte_to_char_index(&m, ls))
1982 .copied()
1983 .unwrap_or(0.0);
1984 let ex = m
1985 .positions
1986 .get(byte_to_char_index(&m, le))
1987 .copied()
1988 .unwrap_or(sx);
1989 let draw_y = rect.y + (i as f32) * lh - st.scroll_offset_y;
1990 scene.nodes.push(SceneNode::Rect {
1991 rect: repose_core::Rect {
1992 x: rect.x + sx,
1993 y: draw_y + lh - Dp(2.0).to_px().0,
1994 w: (ex - sx).max(Dp(2.0).to_px().0),
1995 h: Dp(2.0).to_px().0,
1996 },
1997 brush: Brush::Solid(th.focus),
1998 radius: [Px::ZERO; 4],
1999 });
2000 }
2001 }
2002
2003 if show_cursor
2005 && is_focused
2006 && st.selection.start == st.selection.end
2007 && st.caret_visible()
2008 {
2009 let caret_orig = st.selection.end.min(st.text.len());
2010 let has_vt = text_input.visual_transformation.is_some();
2011 let caret = if has_vt {
2012 original_offset_to_display(&st.text, &render_text, caret_orig)
2013 } else {
2014 caret_orig
2015 };
2016 let (cx, cy, _li) =
2017 caret_xy_for_byte(&render_text, font_val, rect.w.max(1.0), caret);
2018 let draw_x = rect.x + cx;
2019 let draw_y = rect.y + cy - st.scroll_offset_y;
2020 scene.nodes.push(SceneNode::Rect {
2021 rect: repose_core::Rect {
2022 x: draw_x,
2023 y: draw_y + (lh - font_val) / 2.0,
2024 w: Dp(1.0).to_px().0,
2025 h: font_val,
2026 },
2027 brush: Brush::Solid(cursor_color),
2028 radius: [Px::ZERO; 4],
2029 });
2030 }
2031 }
2032 } else {
2033 if text_input.value.is_empty() {
2035 let hint_y = if text_input.multiline {
2036 rect.y
2037 } else {
2038 rect.y + text_off_y
2039 };
2040 scene.nodes.push(SceneNode::Text {
2041 rect: repose_core::Rect {
2042 x: rect.x,
2043 y: hint_y,
2044 w: rect.w,
2045 h: line_h,
2046 },
2047 text: Arc::from(text_input.hint.clone()),
2048 color: mul_alpha_color(th.on_surface_variant, alpha_accum),
2049 size: Px(font_val),
2050 font_family: None,
2051 text_align: TextAlign::Unspecified,
2052 font_weight: FontWeight::NORMAL,
2053 font_style: FontStyle::Normal,
2054 text_decoration: ts.text_decoration.unwrap_or_default(),
2055 letter_spacing: Px::ZERO,
2056 line_height: Px::ZERO,
2057 extra_style: Default::default(),
2058 url: None,
2059 font_variation_settings: None,
2060 });
2061 } else if text_input.multiline {
2062 let render_text = if text_input.value.is_empty() {
2063 text_input.value.clone()
2064 } else if let Some(ref vt) = text_input.visual_transformation {
2065 let annotated = repose_core::AnnotatedString::new(text_input.value.clone(), vec![]);
2066 vt.filter(&annotated).text.text
2067 } else {
2068 text_input.value.clone()
2069 };
2070 let layout = layout_text_area(
2071 &render_text,
2072 font_val,
2073 rect.w.max(1.0),
2074 400,
2075 0,
2076 ts.letter_spacing.to_px().0,
2077 None,
2078 );
2079 let lh = layout.line_h_px;
2080 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
2081 let ln = render_text[s..e].to_string();
2082 let draw_y = rect.y + (i as f32) * lh;
2083 if draw_y + lh < rect.y - 1.0 || draw_y > rect.y + rect.h + 1.0 {
2084 continue;
2085 }
2086 scene.nodes.push(SceneNode::Text {
2087 rect: repose_core::Rect {
2088 x: rect.x,
2089 y: draw_y,
2090 w: rect.w,
2091 h: lh,
2092 },
2093 text: Arc::<str>::from(ln),
2094 color: mul_alpha_color(th.on_surface, alpha_accum),
2095 size: Px(font_val),
2096 font_family: None,
2097 text_align: TextAlign::Unspecified,
2098 font_weight: FontWeight::NORMAL,
2099 font_style: FontStyle::Normal,
2100 text_decoration: ts.text_decoration.unwrap_or_default(),
2101 letter_spacing: Px::ZERO,
2102 line_height: Px::ZERO,
2103 extra_style: Default::default(),
2104 url: None,
2105 font_variation_settings: None,
2106 });
2107 }
2108 } else {
2109 scene.nodes.push(SceneNode::Text {
2110 rect: repose_core::Rect {
2111 x: rect.x,
2112 y: rect.y + text_off_y,
2113 w: rect.w,
2114 h: line_h,
2115 },
2116 text: Arc::from(rendered_by_vt(&text_input.value)),
2117 color: mul_alpha_color(th.on_surface, alpha_accum),
2118 size: Px(font_val),
2119 font_family: None,
2120 text_align: TextAlign::Unspecified,
2121 font_weight: FontWeight::NORMAL,
2122 font_style: FontStyle::Normal,
2123 text_decoration: ts.text_decoration.unwrap_or_default(),
2124 letter_spacing: Px::ZERO,
2125 line_height: Px::ZERO,
2126 extra_style: Default::default(),
2127 url: None,
2128 font_variation_settings: None,
2129 });
2130 }
2131 }
2132
2133 if let Some(ref cb) = text_input.on_text_layout {
2135 let (
2136 line_count,
2137 content_w,
2138 content_h,
2139 first_baseline,
2140 last_baseline,
2141 did_overflow_w,
2142 did_overflow_h,
2143 lines,
2144 ) = if let Some(state_rc) = state {
2145 let st = state_rc.borrow();
2146 let display = if st.text.is_empty() {
2147 text_input.hint.clone()
2148 } else if let Some(ref vt) = text_input.visual_transformation {
2149 let annotated = repose_core::AnnotatedString::new(st.text.clone(), vec![]);
2150 vt.filter(&annotated).text.text
2151 } else {
2152 st.text.clone()
2153 };
2154 if text_input.multiline {
2155 let l = layout_text_area(
2156 &display,
2157 font_val,
2158 rect.w.max(1.0),
2159 400,
2160 0,
2161 ts.letter_spacing.to_px().0,
2162 None,
2163 );
2164 let lc = l.ranges.len();
2165 let cw = rect.w.max(0.0);
2166 let ch = (lc as f32 * l.line_h_px).max(0.0);
2167 let line_infos: Vec<_> = l
2168 .ranges
2169 .iter()
2170 .enumerate()
2171 .map(|(i, &(s, e))| {
2172 let top = i as f32 * l.line_h_px;
2173 let bottom = top + l.line_h_px;
2174 let line_text = &display[s..e];
2175 let m = measure_text(line_text, font_val, TextMeasureConfig::default());
2176 let line_w = m.positions.last().copied().unwrap_or(0.0);
2177 TextLineInfo {
2178 start: s,
2179 end: e,
2180 top,
2181 baseline: top + l.line_h_px * 0.8,
2182 bottom,
2183 left: 0.0,
2184 right: line_w,
2185 width: line_w,
2186 }
2187 })
2188 .collect();
2189 let fb = line_infos.first().map(|l| l.baseline).unwrap_or(0.0);
2190 let lb = line_infos.last().map(|l| l.baseline).unwrap_or(0.0);
2191 (lc, cw, ch, fb, lb, cw > rect.w, ch > rect.h, line_infos)
2192 } else {
2193 let m = measure_text(&display, font_val, TextMeasureConfig::default());
2194 let w = m.positions.last().copied().unwrap_or(0.0);
2195 let top = 0.0;
2196 let bottom = line_h.max(font_val);
2197 let baseline = bottom * 0.8;
2198 let line_info = TextLineInfo {
2199 start: 0,
2200 end: display.len(),
2201 top,
2202 baseline,
2203 bottom,
2204 left: 0.0,
2205 right: w,
2206 width: w,
2207 };
2208 (
2209 1,
2210 w.max(0.0),
2211 bottom,
2212 baseline,
2213 baseline,
2214 w > rect.w,
2215 bottom > rect.h,
2216 vec![line_info],
2217 )
2218 }
2219 } else {
2220 (0, 0.0, 0.0, 0.0, 0.0, false, false, vec![])
2221 };
2222 cb(&repose_core::TextLayoutResult {
2223 line_count,
2224 width_px: content_w,
2225 height_px: content_h,
2226 first_baseline,
2227 last_baseline,
2228 did_overflow_width: did_overflow_w,
2229 did_overflow_height: did_overflow_h,
2230 lines,
2231 });
2232 }
2233
2234 scene.nodes.push(SceneNode::PopClip);
2235}
2236
2237fn text_field_view(
2241 modifier: Modifier,
2242 hint: String,
2243 value: String,
2244 multiline: bool,
2245 on_change: Option<Rc<dyn Fn(String)>>,
2246 on_submit: Option<Rc<dyn Fn(String)>>,
2247 visual_transformation: Option<Rc<dyn repose_core::VisualTransformation>>,
2248 keyboard_type: repose_core::KeyboardType,
2249 capitalization: repose_core::KeyboardCapitalization,
2250 ime_action: repose_core::ImeAction,
2251 auto_correct_enabled: Option<bool>,
2252 enabled: bool,
2253 read_only: bool,
2254 max_lines: Option<usize>,
2255 min_lines: usize,
2256 cursor_color: Option<Color>,
2257 on_text_layout: Option<Rc<dyn Fn(&repose_core::TextLayoutResult)>>,
2258 text_style: repose_core::TextStyle,
2259 keyboard_actions: repose_core::KeyboardActions,
2260 interaction_source: Option<repose_core::MutableInteractionSource>,
2261 focus_tracker: Option<Rc<Cell<bool>>>,
2262 line_limits: Option<repose_core::TextFieldLineLimits>,
2263 _input_transformation: Option<Rc<dyn repose_core::InputTransformation>>,
2264 _output_transformation: Option<Rc<dyn repose_core::OutputTransformation>>,
2265 decoration_box: Option<Rc<dyn Fn(repose_core::View) -> repose_core::View>>,
2266 _codepoint_transformation: Option<repose_core::CodepointTransformation>,
2267) -> View {
2268 let mut modif = modifier.text_input(TextInputConfig {
2269 hint,
2270 multiline,
2271 on_change,
2272 on_submit,
2273 focus_tracker,
2274 value,
2275 visual_transformation,
2276 keyboard_type,
2277 capitalization,
2278 ime_action,
2279 auto_correct_enabled,
2280 enabled,
2281 read_only,
2282 max_lines,
2283 min_lines,
2284 cursor_color,
2285 on_text_layout,
2286 text_style: Some(text_style),
2287 keyboard_actions: Some(keyboard_actions),
2288 interaction_source: interaction_source.as_ref().map(|s| s.source()),
2289 line_limits,
2290 });
2291
2292 if !enabled {
2295 modif = modif.focusable(false).enabled(false);
2296 } else if read_only {
2297 modif = modif.focusable(true);
2298 }
2299
2300 let inner = View::new(0, ViewKind::Box)
2301 .modifier(modif)
2302 .semantics(Semantics {
2303 role: Role::TextField,
2304 enabled,
2305 ..Default::default()
2306 });
2307
2308 if let Some(decorate) = decoration_box {
2310 decorate(inner)
2311 } else {
2312 inner
2313 }
2314}
2315
2316pub fn tf_ensure_visible_in_rect(state: &mut TextFieldState, inner_rect: repose_core::Rect) {
2319 use crate::textfield::{TF_FONT_SP, TF_PADDING_X, TextMeasureConfig, measure_text};
2320 let font_px = TF_FONT_SP.to_px().0;
2321 let m = measure_text(&state.text, font_px, TextMeasureConfig::default());
2322 let caret_x_px = m.positions.get(state.caret_index()).copied().unwrap_or(0.0);
2323 state.ensure_caret_visible(
2324 caret_x_px,
2325 inner_rect.w - 2.0 * TF_PADDING_X.to_px().0,
2326 Dp(2.0).to_px().0,
2327 );
2328}
2329
2330#[cfg(test)]
2331mod tests {
2332 use super::*;
2333
2334 #[test]
2335 fn test_index_for_x_bytes_grapheme() {
2336 let t = "A👍🏽B";
2337 let font_px = 16.0; let m = measure_text(t, font_px, TextMeasureConfig::default());
2339 for i in 0..m.byte_offsets.len() - 1 {
2340 let b = m.byte_offsets[i];
2341 let _ = &t[..b];
2342 }
2343 }
2344
2345 fn delete_op(
2346 index: usize,
2347 pre_text: &str,
2348 pre_selection: Range<usize>,
2349 post_selection: Range<usize>,
2350 ) -> TextUndoOp {
2351 TextUndoOp {
2352 index,
2353 pre_text: pre_text.to_string(),
2354 post_text: String::new(),
2355 pre_selection,
2356 post_selection,
2357 time: Instant::now(),
2358 can_merge: true,
2359 }
2360 }
2361
2362 #[test]
2363 fn deletion_type_collapsed_post_selection_is_backspace() {
2364 let op = delete_op(2, "c", 3..3, 2..2);
2366 assert_eq!(op.deletion_type(), TextDeleteType::Start);
2367 }
2368
2369 #[test]
2370 fn deletion_type_collapsed_post_selection_is_delete_forward() {
2371 let op = delete_op(3, "c", 3..3, 3..3);
2373 assert_eq!(op.deletion_type(), TextDeleteType::End);
2374 }
2375
2376 #[test]
2377 fn deletion_type_range_post_selection_is_not_by_user() {
2378 let op = delete_op(2, "de", 2..4, 3..5);
2382 assert_eq!(op.deletion_type(), TextDeleteType::NotByUser);
2383 }
2384
2385 #[test]
2386 fn backspace_ops_merge() {
2387 let a = delete_op(2, "c", 3..3, 2..2);
2389 let b = delete_op(1, "b", 2..2, 1..1);
2390 let merged = a
2391 .try_merge(&b)
2392 .expect("consecutive backspaces should merge");
2393 assert_eq!(merged.index, 1);
2394 assert_eq!(merged.pre_text, "bc");
2395 }
2396
2397 #[test]
2398 fn selection_delete_does_not_merge_with_backspace() {
2399 let backspace = delete_op(2, "c", 3..3, 2..2);
2402 let selection = delete_op(2, "de", 2..4, 2..2);
2403 assert_eq!(selection.deletion_type(), TextDeleteType::Inner);
2404 assert!(backspace.try_merge(&selection).is_none());
2405 }
2406}