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 = repose_core::dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().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, repose_core::dp_to_px(2.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, repose_core::dp_to_px(2.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_DP: f32 = 16.0;
258
259#[derive(Clone, Copy, Debug)]
261pub struct KeyboardOptions {
262 pub keyboard_type: repose_core::KeyboardType,
263 pub autocorrect: bool,
264 pub capitalization: repose_core::KeyboardCapitalization,
265}
266
267impl Default for KeyboardOptions {
268 fn default() -> Self {
269 Self {
270 keyboard_type: repose_core::KeyboardType::default(),
271 autocorrect: true,
272 capitalization: repose_core::KeyboardCapitalization::default(),
273 }
274 }
275}
276pub const TF_PADDING_X_DP: f32 = 8.0;
278
279pub struct TextMetrics {
280 pub positions: Vec<f32>, pub byte_offsets: Vec<usize>,
284}
285
286pub struct TextMeasureConfig {
287 pub font_family: Option<&'static str>,
288 pub font_weight: u16,
289 pub font_style: u8,
290 pub letter_spacing: f32,
291 pub font_variation_settings: Option<String>,
292}
293
294impl Default for TextMeasureConfig {
295 fn default() -> Self {
296 Self {
297 font_family: None,
298 font_weight: 400,
299 font_style: 0,
300 letter_spacing: 0.0,
301 font_variation_settings: None,
302 }
303 }
304}
305
306pub fn measure_text(text: &str, font_px: f32, config: TextMeasureConfig) -> TextMetrics {
310 let m = repose_text::metrics_for_textfield(
311 text,
312 font_px,
313 config.font_family,
314 config.font_weight,
315 config.font_style,
316 config.letter_spacing,
317 config.font_variation_settings.as_deref(),
318 );
319 TextMetrics {
320 positions: m.positions,
321 byte_offsets: m.byte_offsets,
322 }
323}
324
325pub fn byte_to_char_index(m: &TextMetrics, byte: usize) -> usize {
326 match m.byte_offsets.binary_search(&byte) {
327 Ok(i) | Err(i) => i,
328 }
329}
330
331pub fn index_for_x_bytes(
333 text: &str,
334 font_px: f32,
335 x_px: f32,
336 font_weight: u16,
337 font_style: u8,
338) -> usize {
339 let m = measure_text(
340 text,
341 font_px,
342 TextMeasureConfig {
343 font_weight,
344 font_style,
345 ..Default::default()
346 },
347 );
348
349 let mut best_i = 0usize;
350 let mut best_d = f32::INFINITY;
351 for i in 0..m.positions.len() {
352 let d = (m.positions[i] - x_px).abs();
353 if d < best_d {
354 best_d = d;
355 best_i = i;
356 }
357 }
358 m.byte_offsets[best_i]
359}
360
361pub(crate) fn prev_grapheme_boundary(text: &str, byte: usize) -> usize {
363 let mut last = 0usize;
364 for (i, _) in text.grapheme_indices(true) {
365 if i >= byte {
366 break;
367 }
368 last = i;
369 }
370 last
371}
372
373pub(crate) fn next_grapheme_boundary(text: &str, byte: usize) -> usize {
374 for (i, _) in text.grapheme_indices(true) {
375 if i > byte {
376 return i;
377 }
378 }
379 text.len()
380}
381
382pub(crate) fn word_range(text: &str, byte: usize) -> (usize, usize) {
385 let byte = byte.min(text.len());
386 let is_word = |g: &str| g.chars().all(|c| c.is_alphanumeric() || c == '_');
387
388 let mut start = byte;
389 while start > 0 {
390 let p = prev_grapheme_boundary(text, start);
391 if is_word(&text[p..start]) {
392 start = p;
393 } else {
394 break;
395 }
396 }
397 let mut end = byte;
398 while end < text.len() {
399 let n = next_grapheme_boundary(text, end);
400 if is_word(&text[end..n]) {
401 end = n;
402 } else {
403 break;
404 }
405 }
406 if start == end {
407 let s = if byte == 0 {
408 0
409 } else {
410 prev_grapheme_boundary(text, byte)
411 };
412 let e = next_grapheme_boundary(text, byte);
413 (s, e.max(s))
414 } else {
415 (start, end)
416 }
417}
418
419pub struct TextFieldState {
420 pub text: String,
421 pub selection: Range<usize>,
422 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>,
429 pub(crate) last_tap_pos: Option<(f32, f32)>,
430 pub(crate) tap_count: u8,
431
432 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>>,
439 pub visual_transformation: Option<Rc<dyn VisualTransformation>>,
441 pub(crate) scroll_target: f32,
443 pub(crate) scroll_target_y: f32,
445 scroll_vel: f32,
447 scroll_vel_y: f32,
449 last_scroll_tick: Option<Instant>,
451
452 undo_stack: Vec<TextUndoOp>,
455 redo_stack: Vec<TextUndoOp>,
457 staging_undo: Option<TextUndoOp>,
459}
460
461impl std::fmt::Debug for TextFieldState {
462 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
463 f.debug_struct("TextFieldState")
464 .field("text", &self.text)
465 .field("selection", &self.selection)
466 .field("composition", &self.composition)
467 .field("scroll_offset", &self.scroll_offset)
468 .field("scroll_offset_y", &self.scroll_offset_y)
469 .field("drag_anchor", &self.drag_anchor)
470 .field("blink_start", &self.blink_start)
471 .field("inner_width", &self.inner_width)
472 .field("inner_height", &self.inner_height)
473 .field("preferred_x_px", &self.preferred_x_px)
474 .field(
475 "offset_map",
476 &self.offset_map.as_ref().map(|_| "<offset_mapping>"),
477 )
478 .field(
479 "visual_transformation",
480 &self.visual_transformation.as_ref().map(|_| "<vt>"),
481 )
482 .field("scroll_target", &self.scroll_target)
483 .field("scroll_target_y", &self.scroll_target_y)
484 .field("can_undo", &self.can_undo())
485 .field("can_redo", &self.can_redo())
486 .field("undo_count", &self.undo_stack.len())
487 .field("redo_count", &self.redo_stack.len())
488 .finish()
489 }
490}
491
492impl Default for TextFieldState {
493 fn default() -> Self {
494 Self::new()
495 }
496}
497
498impl Clone for TextFieldState {
499 fn clone(&self) -> Self {
500 Self {
501 text: self.text.clone(),
502 selection: self.selection.clone(),
503 composition: self.composition.clone(),
504 scroll_offset: self.scroll_offset,
505 scroll_offset_y: self.scroll_offset_y,
506 drag_anchor: self.drag_anchor,
507 last_tap_time: self.last_tap_time,
508 last_tap_pos: self.last_tap_pos,
509 tap_count: self.tap_count,
510 blink_start: self.blink_start,
511 inner_width: self.inner_width,
512 inner_height: self.inner_height,
513 preferred_x_px: self.preferred_x_px,
514 offset_map: self.offset_map.as_ref().map(|m| m.clone_box()),
515 visual_transformation: self.visual_transformation.clone(),
516 scroll_target: self.scroll_target,
517 scroll_target_y: self.scroll_target_y,
518 scroll_vel: self.scroll_vel,
519 scroll_vel_y: self.scroll_vel_y,
520 last_scroll_tick: self.last_scroll_tick,
521 undo_stack: self.undo_stack.clone(),
522 redo_stack: self.redo_stack.clone(),
523 staging_undo: self.staging_undo.clone(),
524 }
525 }
526}
527
528impl TextFieldState {
529 pub fn new() -> Self {
530 Self {
531 text: String::new(),
532 selection: 0..0,
533 composition: None,
534 scroll_offset: 0.0,
535 scroll_offset_y: 0.0,
536 drag_anchor: None,
537 last_tap_time: None,
538 last_tap_pos: None,
539 tap_count: 0,
540 blink_start: Instant::now(),
541 inner_width: 0.0,
542 inner_height: 0.0,
543 preferred_x_px: None,
544 offset_map: None,
545 visual_transformation: None,
546 scroll_target: 0.0,
547 scroll_target_y: 0.0,
548 scroll_vel: 0.0,
549 scroll_vel_y: 0.0,
550 last_scroll_tick: None,
551 undo_stack: Vec::new(),
552 redo_stack: Vec::new(),
553 staging_undo: None,
554 }
555 }
556
557 pub fn can_undo(&self) -> bool {
561 !self.undo_stack.is_empty() || self.staging_undo.is_some()
562 }
563
564 pub fn can_redo(&self) -> bool {
566 !self.redo_stack.is_empty()
567 }
568
569 pub fn undo(&mut self) -> bool {
571 self.flush_undo();
572 if let Some(op) = self.undo_stack.pop() {
573 let end = (op.index + op.post_text.len()).min(self.text.len());
574 self.text.replace_range(op.index..end, &op.pre_text);
575 self.selection = op.pre_selection.clone();
576 self.redo_stack.push(op);
577 self.preferred_x_px = None;
578 self.reset_caret_blink();
579 true
580 } else {
581 false
582 }
583 }
584
585 pub fn redo(&mut self) -> bool {
587 if let Some(op) = self.redo_stack.pop() {
588 let end = (op.index + op.pre_text.len()).min(self.text.len());
589 self.text.replace_range(op.index..end, &op.post_text);
590 self.selection = op.post_selection.clone();
591 self.undo_stack.push(op);
592 self.preferred_x_px = None;
593 self.reset_caret_blink();
594 true
595 } else {
596 false
597 }
598 }
599
600 pub fn clear_undo_history(&mut self) {
602 self.undo_stack.clear();
603 self.redo_stack.clear();
604 self.staging_undo = None;
605 }
606
607 fn record_edit(&mut self, op: TextUndoOp) {
611 if let Some(staging) = self.staging_undo.take() {
612 if let Some(merged) = staging.try_merge(&op) {
613 self.staging_undo = Some(merged);
614 return;
615 }
616 self.undo_stack.push(staging);
618 self.redo_stack.clear();
619 while self.undo_stack.len() + 1 > TEXT_UNDO_CAPACITY {
621 self.undo_stack.remove(0);
622 }
623 }
624 self.staging_undo = Some(op);
625 }
626
627 fn flush_undo(&mut self) {
629 if let Some(op) = self.staging_undo.take() {
630 self.undo_stack.push(op);
631 self.redo_stack.clear();
632 while self.undo_stack.len() > TEXT_UNDO_CAPACITY {
633 self.undo_stack.remove(0);
634 }
635 }
636 }
637
638 fn insert_text_impl(&mut self, text: &str, can_merge: bool) {
639 let start = self.selection.start.min(self.text.len());
640 let end = self.selection.end.min(self.text.len());
641 let pre_text = self.text[start..end].to_string();
642 let pre_selection = self.selection.clone();
643
644 self.text.replace_range(start..end, text);
645 let new_pos = start + text.len();
646 self.selection = new_pos..new_pos;
647 self.preferred_x_px = None;
648 self.reset_caret_blink();
649
650 if !pre_text.is_empty() || !text.is_empty() {
651 self.record_edit(TextUndoOp {
652 index: start,
653 pre_text,
654 post_text: text.to_string(),
655 pre_selection,
656 post_selection: self.selection.clone(),
657 time: Instant::now(),
658 can_merge,
659 });
660 }
661 }
662
663 pub fn insert_text(&mut self, text: &str) {
664 self.insert_text_impl(text, true);
665 }
666
667 pub fn insert_text_atomic(&mut self, text: &str) {
669 self.insert_text_impl(text, false);
670 }
671
672 pub fn delete_backward(&mut self) {
673 if self.selection.start == self.selection.end {
674 let pos = self.selection.start.min(self.text.len());
675 if pos > 0 {
676 let prev = prev_grapheme_boundary(&self.text, pos);
677 let pre_text = self.text[prev..pos].to_string();
678 let pre_selection = self.selection.clone();
679 self.text.replace_range(prev..pos, "");
680 self.selection = prev..prev;
681 self.preferred_x_px = None;
682 self.reset_caret_blink();
683 self.record_edit(TextUndoOp {
684 index: prev,
685 pre_text,
686 post_text: String::new(),
687 pre_selection,
688 post_selection: self.selection.clone(),
689 time: Instant::now(),
690 can_merge: true,
691 });
692 }
693 } else {
694 self.insert_text_impl("", true);
695 }
696 self.preferred_x_px = None;
697 self.reset_caret_blink();
698 }
699
700 pub fn delete_forward(&mut self) {
701 if self.selection.start == self.selection.end {
702 let pos = self.selection.start.min(self.text.len());
703 if pos < self.text.len() {
704 let next = next_grapheme_boundary(&self.text, pos);
705 let pre_text = self.text[pos..next].to_string();
706 let pre_selection = self.selection.clone();
707 self.text.replace_range(pos..next, "");
708 self.preferred_x_px = None;
709 self.reset_caret_blink();
710 self.record_edit(TextUndoOp {
711 index: pos,
712 pre_text,
713 post_text: String::new(),
714 pre_selection,
715 post_selection: self.selection.clone(),
716 time: Instant::now(),
717 can_merge: true,
718 });
719 }
720 } else {
721 self.insert_text_impl("", true);
722 }
723 self.preferred_x_px = None;
724 self.reset_caret_blink();
725 }
726
727 pub fn move_cursor(&mut self, delta: isize, extend_selection: bool) {
728 let mut pos = self.selection.end.min(self.text.len());
729 if delta < 0 {
730 for _ in 0..delta.unsigned_abs() {
731 pos = prev_grapheme_boundary(&self.text, pos);
732 }
733 } else if delta > 0 {
734 for _ in 0..(delta as usize) {
735 pos = next_grapheme_boundary(&self.text, pos);
736 }
737 }
738 if extend_selection {
739 self.selection.end = pos;
740 } else {
741 self.selection = pos..pos;
742 }
743 self.preferred_x_px = None;
744 self.reset_caret_blink();
745 }
746
747 pub fn selected_text(&self) -> String {
748 if self.selection.start == self.selection.end {
749 String::new()
750 } else {
751 self.text[self.selection.clone()].to_string()
752 }
753 }
754
755 pub fn set_composition(&mut self, text: String, cursor: Option<(usize, usize)>) {
756 if text.is_empty() {
757 if let Some(range) = self.composition.take() {
758 let s = clamp_to_char_boundary(&self.text, range.start.min(self.text.len()));
759 let e = clamp_to_char_boundary(&self.text, range.end.min(self.text.len()));
760 if s <= e {
761 self.text.replace_range(s..e, "");
762 self.selection = s..s;
763 }
764 }
765 self.preferred_x_px = None;
766 self.reset_caret_blink();
767 return;
768 }
769
770 let anchor_start;
771 if let Some(r) = self.composition.take() {
772 let mut s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
773 let mut e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
774 if e < s {
775 std::mem::swap(&mut s, &mut e);
776 }
777 self.text.replace_range(s..e, &text);
778 anchor_start = s;
779 } else {
780 let pos = clamp_to_char_boundary(&self.text, self.selection.start.min(self.text.len()));
781 self.text.insert_str(pos, &text);
782 anchor_start = pos;
783 }
784
785 self.composition = Some(anchor_start..(anchor_start + text.len()));
786
787 if let Some((c0, c1)) = cursor {
788 let b0 = char_to_byte(&text, c0);
789 let b1 = char_to_byte(&text, c1);
790 self.selection = (anchor_start + b0)..(anchor_start + b1);
791 } else {
792 let end = anchor_start + text.len();
793 self.selection = end..end;
794 }
795
796 self.preferred_x_px = None;
797 self.reset_caret_blink();
798 }
799
800 pub fn commit_composition(&mut self, text: String) {
801 let pre_selection = self.selection.clone();
802 if let Some(r) = self.composition.take() {
803 let s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
804 let e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
805 let pre_text = self.text[s..e].to_string();
806 self.text.replace_range(s..e, &text);
807 let new_pos = s + text.len();
808 self.selection = new_pos..new_pos;
809 self.preferred_x_px = None;
810 self.reset_caret_blink();
811 if !pre_text.is_empty() || !text.is_empty() {
812 self.record_edit(TextUndoOp {
813 index: s,
814 pre_text,
815 post_text: text,
816 pre_selection,
817 post_selection: self.selection.clone(),
818 time: Instant::now(),
819 can_merge: true,
820 });
821 }
822 } else {
823 let pos = clamp_to_char_boundary(&self.text, self.selection.end.min(self.text.len()));
824 self.text.insert_str(pos, &text);
825 let new_pos = pos + text.len();
826 self.selection = new_pos..new_pos;
827 self.preferred_x_px = None;
828 self.reset_caret_blink();
829 if !text.is_empty() {
830 self.record_edit(TextUndoOp {
831 index: pos,
832 pre_text: String::new(),
833 post_text: text,
834 pre_selection,
835 post_selection: self.selection.clone(),
836 time: Instant::now(),
837 can_merge: true,
838 });
839 }
840 }
841 }
842
843 pub fn cancel_composition(&mut self) {
844 if let Some(r) = self.composition.take() {
845 let s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
846 let e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
847 if s <= e {
848 self.text.replace_range(s..e, "");
849 self.selection = s..s;
850 }
851 }
852 self.preferred_x_px = None;
853 self.reset_caret_blink();
854 }
855
856 pub fn delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) {
857 if self.selection.start != self.selection.end {
858 let start = self.selection.start.min(self.text.len());
859 let end = self.selection.end.min(self.text.len());
860 self.text.replace_range(start..end, "");
861 self.selection = start..start;
862 self.preferred_x_px = None;
863 self.reset_caret_blink();
864 return;
865 }
866
867 let caret = self.selection.end.min(self.text.len());
868 let start_raw = caret.saturating_sub(before_bytes);
869 let end_raw = (caret + after_bytes).min(self.text.len());
870
871 let start = prev_grapheme_boundary(&self.text, start_raw);
872 let end = next_grapheme_boundary(&self.text, end_raw);
873 if start < end {
874 self.text.replace_range(start..end, "");
875 self.selection = start..start;
876 }
877 self.preferred_x_px = None;
878 self.reset_caret_blink();
879 }
880
881 pub fn begin_drag(&mut self, idx_byte: usize, extend: bool) {
882 let idx = idx_byte.min(self.text.len());
883 if extend {
884 let anchor = self.selection.start;
885 self.selection = anchor.min(idx)..anchor.max(idx);
886 self.drag_anchor = Some(anchor);
887 } else {
888 self.selection = idx..idx;
889 self.drag_anchor = Some(idx);
890 }
891 self.preferred_x_px = None;
892 self.reset_caret_blink();
893 }
894
895 pub fn drag_to(&mut self, idx_byte: usize) {
896 if let Some(anchor) = self.drag_anchor {
897 let i = idx_byte.min(self.text.len());
898 self.selection = anchor.min(i)..anchor.max(i);
899 }
900 self.preferred_x_px = None;
901 self.reset_caret_blink();
902 if self.selection.start < self.selection.end {
903 repose_core::clipboard::set_primary_selection(
904 &self.text[self.selection.start..self.selection.end],
905 );
906 }
907 }
908 pub fn end_drag(&mut self) {
909 self.drag_anchor = None;
910 if self.selection.start < self.selection.end {
911 repose_core::clipboard::set_primary_selection(
912 &self.text[self.selection.start..self.selection.end],
913 );
914 }
915 }
916
917 pub fn handle_pointer_down(
918 &mut self,
919 idx_byte: usize,
920 pos_px: (f32, f32),
921 shift: bool,
922 ) {
923 const DOUBLE_TAP_MS: u64 = 300;
924 const TAP_SLOP_PX: f32 = 12.0;
925
926 let now = Instant::now();
927 let mut count = self.tap_count;
928 if let (Some(t), Some(p)) = (self.last_tap_time, self.last_tap_pos) {
929 let dt = now.saturating_duration_since(t);
930 let dist = ((pos_px.0 - p.0).powi(2) + (pos_px.1 - p.1).powi(2)).sqrt();
931 if dt < Duration::from_millis(DOUBLE_TAP_MS) && dist < TAP_SLOP_PX {
932 count = count.saturating_add(1);
933 } else {
934 count = 1;
935 }
936 } else {
937 count = 1;
938 }
939 self.tap_count = count;
940 self.last_tap_time = Some(now);
941 self.last_tap_pos = Some(pos_px);
942
943 let idx = idx_byte.min(self.text.len());
944
945 if count >= 3 {
946 self.selection = 0..self.text.len();
948 self.drag_anchor = None;
949 self.preferred_x_px = None;
950 self.reset_caret_blink();
951 if self.selection.end > 0 {
952 repose_core::clipboard::set_primary_selection(&self.text);
953 }
954 return;
955 }
956
957 if count == 2 {
958 let (s, e) = word_range(&self.text, idx);
960 self.selection = s..e;
961 self.drag_anchor = Some(s);
962 self.preferred_x_px = None;
963 self.reset_caret_blink();
964 if e > s {
965 repose_core::clipboard::set_primary_selection(&self.text[s..e]);
966 }
967 return;
968 }
969
970 self.begin_drag(idx, shift);
972 }
973
974 pub fn select_word_at(&mut self, byte: usize) {
976 let (s, e) = word_range(&self.text, byte.min(self.text.len()));
977 self.selection = s..e;
978 self.drag_anchor = Some(s);
979 self.preferred_x_px = None;
980 self.reset_caret_blink();
981 }
982
983 pub fn select_all(&mut self) {
985 self.selection = 0..self.text.len();
986 self.drag_anchor = None;
987 self.preferred_x_px = None;
988 self.reset_caret_blink();
989 }
990
991 pub fn caret_index(&self) -> usize {
992 self.selection.end
993 }
994
995 pub fn ensure_caret_visible(&mut self, caret_x_px: f32, inner_width_px: f32, inset_px: f32) {
999 self.ensure_caret_visible_xy(caret_x_px, 0.0, inner_width_px, 1.0, inset_px);
1000 }
1001
1002 pub fn ensure_caret_visible_xy(
1005 &mut self,
1006 caret_x_px: f32,
1007 caret_y_px: f32,
1008 inner_w_px: f32,
1009 inner_h_px: f32,
1010 inset_px: f32,
1011 ) {
1012 let inset_px = inset_px.max(0.0);
1013
1014 let left_px = self.scroll_offset + inset_px;
1016 let right_px = self.scroll_offset + inner_w_px - inset_px;
1017 if caret_x_px < left_px {
1018 self.scroll_target = (caret_x_px - inset_px).max(0.0);
1019 } else if caret_x_px > right_px {
1020 self.scroll_target = (caret_x_px - inner_w_px + inset_px).max(0.0);
1021 }
1022
1023 let top_px = self.scroll_offset_y + inset_px;
1025 let bot_px = self.scroll_offset_y + inner_h_px - inset_px;
1026 if caret_y_px < top_px {
1027 self.scroll_target_y = (caret_y_px - inset_px).max(0.0);
1028 } else if caret_y_px > bot_px {
1029 self.scroll_target_y = (caret_y_px - inner_h_px + inset_px).max(0.0);
1030 }
1031 }
1032
1033 pub fn clamp_scroll(&mut self, content_h_px: f32) {
1034 let max_y = (content_h_px - self.inner_height).max(0.0);
1035 self.scroll_target_y = self.scroll_target_y.clamp(0.0, max_y);
1036 if self.scroll_target_y.is_nan() {
1037 self.scroll_target_y = 0.0;
1038 }
1039 }
1040
1041 pub fn reset_caret_blink(&mut self) {
1042 self.blink_start = Instant::now();
1043 }
1044 pub fn caret_visible(&self) -> bool {
1045 const PERIOD: Duration = Duration::from_millis(500);
1046 ((Instant::now() - self.blink_start).as_millis() / PERIOD.as_millis()).is_multiple_of(2)
1047 }
1048
1049 pub fn next_blink_deadline(&self) -> Option<Instant> {
1052 if self.selection.start != self.selection.end {
1053 return None;
1054 }
1055 const PERIOD_MS: u128 = 500;
1056 let now = Instant::now();
1057 let elapsed = now.saturating_duration_since(self.blink_start).as_millis();
1058 let next_tick = (elapsed / PERIOD_MS) + 1;
1059 Some(self.blink_start + Duration::from_millis((next_tick * PERIOD_MS) as u64))
1060 }
1061
1062 pub fn set_inner_width(&mut self, w_px: f32) {
1063 self.inner_width = w_px.max(0.0);
1064 if self.scroll_offset.is_nan() {
1065 self.scroll_offset = 0.0;
1066 }
1067 if self.scroll_target.is_nan() {
1068 self.scroll_target = 0.0;
1069 }
1070 }
1071 pub fn set_inner_height(&mut self, h_px: f32) {
1072 self.inner_height = h_px.max(0.0);
1073 if self.scroll_offset_y.is_nan() {
1074 self.scroll_offset_y = 0.0;
1075 }
1076 if self.scroll_target_y.is_nan() {
1077 self.scroll_target_y = 0.0;
1078 }
1079 }
1080
1081 pub fn tick_scroll_animation(&mut self) {
1085 let now = Instant::now();
1086 let dt = match self.last_scroll_tick {
1087 Some(prev) => {
1088 let d = now.saturating_duration_since(prev).as_secs_f32();
1089 d.min(0.05) }
1091 None => {
1092 self.last_scroll_tick = Some(now);
1095 self.scroll_offset = self.scroll_target;
1096 self.scroll_vel = 0.0;
1097 self.scroll_offset_y = self.scroll_target_y;
1098 self.scroll_vel_y = 0.0;
1099 return;
1100 }
1101 };
1102 self.last_scroll_tick = Some(now);
1103
1104 if dt > 0.0 {
1106 let dx = self.scroll_target - self.scroll_offset;
1107 let near_x = dx.abs() < 0.5 && self.scroll_vel.abs() < 0.5;
1108 if near_x {
1109 self.scroll_offset = self.scroll_target;
1110 self.scroll_vel = 0.0;
1111 } else {
1112 let force_x = SCROLL_STIFFNESS * dx - SCROLL_DAMPING * self.scroll_vel;
1113 self.scroll_vel += force_x * dt;
1114 self.scroll_offset += self.scroll_vel * dt;
1115 if (self.scroll_target - self.scroll_offset).signum() != dx.signum() && dx != 0.0 {
1117 self.scroll_offset = self.scroll_target;
1118 self.scroll_vel = 0.0;
1119 }
1120 }
1121 }
1122
1123 if dt > 0.0 {
1125 let dy = self.scroll_target_y - self.scroll_offset_y;
1126 let near_y = dy.abs() < 0.5 && self.scroll_vel_y.abs() < 0.5;
1127 if near_y {
1128 self.scroll_offset_y = self.scroll_target_y;
1129 self.scroll_vel_y = 0.0;
1130 } else {
1131 let force_y = SCROLL_STIFFNESS * dy - SCROLL_DAMPING * self.scroll_vel_y;
1132 self.scroll_vel_y += force_y * dt;
1133 self.scroll_offset_y += self.scroll_vel_y * dt;
1134 if (self.scroll_target_y - self.scroll_offset_y).signum() != dy.signum()
1135 && dy != 0.0
1136 {
1137 self.scroll_offset_y = self.scroll_target_y;
1138 self.scroll_vel_y = 0.0;
1139 }
1140 }
1141 }
1142 }
1143}
1144
1145#[derive(Clone)]
1155pub struct TextFieldConfig {
1156 pub enabled: bool,
1158 pub read_only: bool,
1160 pub input_transformation: Option<Rc<dyn repose_core::InputTransformation>>,
1162 pub text_style: repose_core::TextStyle,
1164 pub keyboard_options: repose_core::KeyboardOptions,
1166 pub on_keyboard_action: Option<Rc<dyn repose_core::KeyboardActionHandler>>,
1168 pub line_limits: repose_core::TextFieldLineLimits,
1170 pub on_text_layout: Option<Rc<dyn Fn(&repose_core::TextLayoutResult)>>,
1172 pub interaction_source: Option<repose_core::MutableInteractionSource>,
1174 pub focus_tracker: Option<Rc<Cell<bool>>>,
1177 pub cursor_brush: Option<repose_core::Brush>,
1179 pub output_transformation: Option<Rc<dyn repose_core::OutputTransformation>>,
1181 pub decorator: Option<Rc<dyn repose_core::TextFieldDecorator>>,
1183 pub codepoint_transformation: Option<repose_core::CodepointTransformation>,
1185 pub text_obfuscation_mode: repose_core::TextObfuscationMode,
1187 pub text_obfuscation_character: char,
1189
1190 pub on_change: Option<Rc<dyn Fn(String)>>,
1192 pub on_submit: Option<Rc<dyn Fn(String)>>,
1193 pub visual_transformation: Option<Rc<dyn repose_core::VisualTransformation>>,
1194 pub decoration_box: Option<Rc<dyn Fn(repose_core::View) -> repose_core::View>>,
1195}
1196
1197impl Default for TextFieldConfig {
1198 fn default() -> Self {
1199 Self {
1200 enabled: true,
1201 read_only: false,
1202 input_transformation: None,
1203 text_style: Default::default(),
1204 keyboard_options: repose_core::KeyboardOptions::DEFAULT.clone(),
1205 on_keyboard_action: None,
1206 line_limits: repose_core::TextFieldLineLimits::MultiLine {
1207 min_height_in_lines: 1,
1208 max_height_in_lines: usize::MAX,
1209 },
1210 on_text_layout: None,
1211 interaction_source: None,
1212 focus_tracker: None,
1213 cursor_brush: None,
1214 output_transformation: None,
1215 decorator: None,
1216 codepoint_transformation: None,
1217 text_obfuscation_mode: repose_core::TextObfuscationMode::System,
1218 text_obfuscation_character: '\u{2022}',
1219 on_change: None,
1220 on_submit: None,
1221 visual_transformation: None,
1222 decoration_box: None,
1223 }
1224 }
1225}
1226
1227pub fn BasicTextField(
1241 state: Rc<RefCell<TextFieldState>>,
1242 modifier: repose_core::Modifier,
1243 hint: impl Into<String>,
1244 config: TextFieldConfig,
1245) -> repose_core::View {
1246 let (single_line, max_lines, min_lines) = match config.line_limits {
1247 repose_core::TextFieldLineLimits::SingleLine => (true, 1, 1),
1248 repose_core::TextFieldLineLimits::MultiLine {
1249 min_height_in_lines,
1250 max_height_in_lines,
1251 } => (false, max_height_in_lines, min_height_in_lines),
1252 };
1253
1254 let ka = if let Some(ref handler) = config.on_keyboard_action {
1255 let handler = handler.clone();
1256 repose_core::KeyboardActions {
1257 on_done: Some({
1258 let h = handler.clone();
1259 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1260 h.on_keyboard_action(&|| {})
1261 })
1262 }),
1263 on_go: Some({
1264 let h = handler.clone();
1265 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1266 h.on_keyboard_action(&|| {})
1267 })
1268 }),
1269 on_next: Some({
1270 let h = handler.clone();
1271 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1272 h.on_keyboard_action(&|| {})
1273 })
1274 }),
1275 on_previous: Some({
1276 let h = handler.clone();
1277 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1278 h.on_keyboard_action(&|| {})
1279 })
1280 }),
1281 on_search: Some({
1282 let h = handler.clone();
1283 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1284 h.on_keyboard_action(&|| {})
1285 })
1286 }),
1287 on_send: Some({
1288 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1289 handler.on_keyboard_action(&|| {})
1290 })
1291 }),
1292 }
1293 } else {
1294 repose_core::KeyboardActions::default()
1295 };
1296
1297 let decoration_box = config
1298 .decorator
1299 .map(|d| Rc::new(move |inner: repose_core::View| d.decorate(inner)) as Rc<dyn Fn(_) -> _>);
1300
1301 let cursor_color = config.cursor_brush.and_then(|b| match b {
1302 repose_core::Brush::Solid(c) => Some(c),
1303 _ => None,
1304 });
1305
1306 let value = state.borrow().text.clone();
1307 let key = state.as_ptr() as u64;
1308 set_textfield_state(key, state.clone());
1309
1310 let state_on_change = {
1311 let s = state.clone();
1312 move |new_value: String| {
1313 s.borrow_mut().text = new_value;
1314 }
1315 };
1316
1317 let merged_on_change: Option<Rc<dyn Fn(String)>> =
1318 if let Some(ref cfg_on_change) = config.on_change {
1319 let a = Rc::new(state_on_change) as Rc<dyn Fn(String)>;
1320 let b = cfg_on_change.clone();
1321 Some(Rc::new(move |v: String| {
1322 a(v.clone());
1323 b(v);
1324 }) as Rc<dyn Fn(String)>)
1325 } else {
1326 Some(Rc::new(state_on_change) as Rc<dyn Fn(String)>)
1327 };
1328
1329 text_field_view(
1330 modifier,
1331 hint.into(),
1332 value,
1333 !single_line,
1334 merged_on_change,
1335 config.on_submit,
1336 config.visual_transformation,
1337 config.keyboard_options.keyboard_type,
1338 config.keyboard_options.capitalization,
1339 config.keyboard_options.ime_action,
1340 config.enabled,
1341 config.read_only,
1342 Some(max_lines),
1343 min_lines,
1344 cursor_color,
1345 config.on_text_layout,
1346 config.text_style,
1347 ka,
1348 config.interaction_source,
1349 config.focus_tracker,
1350 Some(config.line_limits),
1351 config.input_transformation,
1352 config.output_transformation,
1353 decoration_box,
1354 config.codepoint_transformation,
1355 )
1356}
1357
1358pub fn BasicSecureTextField(
1363 state: Rc<RefCell<TextFieldState>>,
1364 modifier: repose_core::Modifier,
1365 config: TextFieldConfig,
1366) -> repose_core::View {
1367 let mask = config.text_obfuscation_character;
1368 let secure_config = TextFieldConfig {
1369 line_limits: repose_core::TextFieldLineLimits::SingleLine,
1370 keyboard_options: repose_core::KeyboardOptions::SECURE_TEXT_FIELD,
1371 visual_transformation: match config.text_obfuscation_mode {
1372 repose_core::TextObfuscationMode::Visible => None,
1373 _ => Some(Rc::new(repose_core::PasswordVisualTransformation { mask })
1374 as Rc<dyn repose_core::VisualTransformation>),
1375 },
1376 ..config
1377 };
1378 BasicTextField(state, modifier, "", secure_config)
1379}
1380
1381#[derive(Clone, Debug)]
1382pub struct TextAreaLayout {
1383 pub ranges: Vec<(usize, usize)>,
1384 pub line_h_px: f32,
1385}
1386
1387pub fn layout_text_area(
1388 text: &str,
1389 font_px: f32,
1390 wrap_w_px: f32,
1391 font_weight: u16,
1392 font_style: u8,
1393 letter_spacing: f32,
1394 font_variation_settings: Option<&str>,
1395) -> TextAreaLayout {
1396 let line_h = font_px;
1397 let (ranges, _) = repose_text::wrap_line_ranges(
1398 text,
1399 font_px,
1400 wrap_w_px.max(1.0),
1401 None,
1402 true,
1403 font_weight,
1404 font_style,
1405 letter_spacing,
1406 font_variation_settings,
1407 );
1408 TextAreaLayout {
1409 ranges,
1410 line_h_px: line_h,
1411 }
1412}
1413
1414fn locate_byte_in_ranges(ranges: &[(usize, usize)], b: usize) -> (usize, usize, usize) {
1416 if ranges.is_empty() {
1417 return (0, 0, b);
1418 }
1419 for (i, (s, e)) in ranges.iter().enumerate() {
1420 if b < *s {
1421 if i == 0 {
1422 return (0, 0, b);
1423 }
1424 let (ps, pe) = ranges[i - 1];
1425 let local = pe.saturating_sub(ps);
1426 return (i - 1, local, ps + local);
1427 }
1428 if b < *e {
1429 let local = b.saturating_sub(*s).min(e.saturating_sub(*s));
1430 return (i, local, *s + local);
1431 }
1432 if b == *e {
1433 if let Some((ns, _ne)) = ranges.get(i + 1)
1434 && *ns == b
1435 {
1436 return (i + 1, 0, b);
1437 }
1438 let local = e.saturating_sub(*s);
1439 return (i, local, *s + local);
1440 }
1441 }
1442 let (ls, le) = ranges[ranges.len() - 1];
1443 let local = le.saturating_sub(ls);
1444 (ranges.len() - 1, local, ls + local)
1445}
1446
1447pub fn caret_xy_for_byte(
1449 text: &str,
1450 font_px: f32,
1451 wrap_w_px: f32,
1452 byte: usize,
1453) -> (f32, f32, usize) {
1454 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1455 let (ranges, line_h) = (&layout.ranges, layout.line_h_px);
1456 let (li, local, _) = locate_byte_in_ranges(ranges, byte);
1457 let (s, e) = ranges.get(li).copied().unwrap_or((0, 0));
1458 let line = &text[s..e];
1459 let m = measure_text(line, font_px, TextMeasureConfig::default());
1460 let ci = byte_to_char_index(&m, local);
1461 let x = m.positions.get(ci).copied().unwrap_or(0.0);
1462 let y = (li as f32) * line_h;
1463 (x, y, li)
1464}
1465
1466pub fn index_for_xy_bytes(text: &str, font_px: f32, wrap_w_px: f32, x_px: f32, y_px: f32) -> usize {
1468 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1469 let li = ((y_px / layout.line_h_px).floor() as isize).max(0) as usize;
1470 let li = li.min(layout.ranges.len().saturating_sub(1));
1471 let (s, e) = layout.ranges.get(li).copied().unwrap_or((0, 0));
1472 let line = &text[s..e];
1473 let local = index_for_x_bytes(line, font_px, x_px.max(0.0), 400, 0);
1474 (s + local).min(text.len())
1475}
1476
1477pub fn move_caret_vertical(
1479 text: &str,
1480 font_px: f32,
1481 wrap_w_px: f32,
1482 cur_byte: usize,
1483 dir: i32, preferred_x: Option<f32>,
1485) -> (usize, f32) {
1486 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1487 if layout.ranges.is_empty() {
1488 return (cur_byte, preferred_x.unwrap_or(0.0));
1489 }
1490 let (x, _y, li) = caret_xy_for_byte(text, font_px, wrap_w_px, cur_byte);
1491 let px = preferred_x.unwrap_or(x);
1492 let mut nli = li as i32 + dir;
1493 nli = nli.clamp(0, (layout.ranges.len().saturating_sub(1)) as i32);
1494 let nli = nli as usize;
1495 let (s, e) = layout.ranges[nli];
1496 let line = &text[s..e];
1497 let local = index_for_x_bytes(line, font_px, px.max(0.0), 400, 0);
1498 ((s + local).min(text.len()), px)
1499}
1500
1501pub fn line_home_end(
1503 text: &str,
1504 font_px: f32,
1505 wrap_w_px: f32,
1506 cur_byte: usize,
1507 to_end: bool,
1508) -> usize {
1509 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1510 let (li, _local, _) = locate_byte_in_ranges(&layout.ranges, cur_byte);
1511 let (s, e) = layout.ranges.get(li).copied().unwrap_or((0, 0));
1512 if to_end { e } else { s }
1513}
1514
1515fn clamp_to_char_boundary(s: &str, i: usize) -> usize {
1516 if i >= s.len() {
1517 return s.len();
1518 }
1519 if s.is_char_boundary(i) {
1520 return i;
1521 }
1522 let mut j = i;
1523 while j > 0 && !s.is_char_boundary(j) {
1524 j -= 1;
1525 }
1526 j
1527}
1528
1529fn char_to_byte(s: &str, ci: usize) -> usize {
1530 if ci == 0 {
1531 0
1532 } else {
1533 s.char_indices().nth(ci).map(|(i, _)| i).unwrap_or(s.len())
1534 }
1535}
1536
1537pub(crate) fn paint_text_field(
1549 scene: &mut Scene,
1550 rect: repose_core::Rect,
1551 text_input: &TextInputConfig,
1552 state: Option<&Rc<RefCell<TextFieldState>>>,
1553 is_focused: bool,
1554 clip_rounded: Option<[f32; 4]>,
1555 alpha_accum: f32,
1556) {
1557 let ts = text_input
1558 .text_style
1559 .as_ref()
1560 .map(|s| s.clone())
1561 .unwrap_or_default();
1562 let font_size_dp = if ts.font_size != 0.0 {
1563 ts.font_size
1564 } else {
1565 TF_FONT_DP
1566 };
1567 let font_val = dp_to_px(font_size_dp) * locals::text_scale().0;
1568 let line_h = if ts.line_height != 0.0 {
1569 dp_to_px(ts.line_height) * locals::text_scale().0
1570 } else if text_input.multiline {
1571 0.0 } else {
1573 font_val };
1575 let text_off_y = (rect.h - line_h.max(font_val)) / 2.0;
1576
1577 let clip_radius = clip_rounded.unwrap_or([0.0; 4]).map(dp_to_px);
1578 scene.nodes.push(SceneNode::PushClip {
1579 rect,
1580 radius: clip_radius,
1581 op: repose_core::ClipOp::Intersect,
1582 });
1583
1584 let th = locals::theme();
1585 let show_selection = text_input.enabled;
1586 let show_cursor = text_input.enabled && !text_input.read_only;
1587 let cursor_color = text_input.cursor_color.unwrap_or(th.on_surface);
1588 let rendered_by_vt = |original: &str| -> String {
1589 if let Some(ref vt) = text_input.visual_transformation {
1590 let annotated = repose_core::AnnotatedString::new(original.to_string(), vec![]);
1591 vt.filter(&annotated).text.text
1592 } else {
1593 original.to_string()
1594 }
1595 };
1596
1597 if let Some(state_rc) = state {
1598 let st = state_rc.borrow();
1599
1600 if !text_input.multiline {
1601 let measure_for = if text_input.visual_transformation.is_some() && !st.text.is_empty() {
1603 rendered_by_vt(&st.text)
1604 } else {
1605 st.text.clone()
1606 };
1607 let has_vt = text_input.visual_transformation.is_some();
1608 let m = measure_text(
1609 &measure_for,
1610 font_val,
1611 TextMeasureConfig {
1612 font_family: ts.font_family,
1613 font_weight: ts.font_weight.unwrap_or(400),
1614 font_style: ts.font_style.unwrap_or(0),
1615 letter_spacing: ts.letter_spacing,
1616 font_variation_settings: None,
1617 },
1618 );
1619
1620 if show_selection && st.selection.start != st.selection.end {
1622 let start_off = if has_vt {
1623 original_offset_to_display(&st.text, &measure_for, st.selection.start)
1624 } else {
1625 st.selection.start
1626 };
1627 let end_off = if has_vt {
1628 original_offset_to_display(&st.text, &measure_for, st.selection.end)
1629 } else {
1630 st.selection.end
1631 };
1632 let sx = m
1633 .positions
1634 .get(byte_to_char_index(&m, start_off))
1635 .copied()
1636 .unwrap_or(0.0)
1637 - st.scroll_offset;
1638 let ex = m
1639 .positions
1640 .get(byte_to_char_index(&m, end_off))
1641 .copied()
1642 .unwrap_or(sx)
1643 - st.scroll_offset;
1644 let selection = th.focus.with_alpha_f32(85.0 / 255.0);
1645 let vis_x = sx.max(0.0);
1646 let vis_ex = ex.max(0.0);
1647 scene.nodes.push(SceneNode::Rect {
1648 rect: repose_core::Rect {
1649 x: rect.x + vis_x,
1650 y: rect.y + text_off_y,
1651 w: (vis_ex - vis_x).max(0.0),
1652 h: line_h.max(font_val),
1653 },
1654 brush: Brush::Solid(selection),
1655 radius: [0.0; 4],
1656 });
1657 }
1658
1659 let txt_col = if st.text.is_empty() {
1661 ts.color.unwrap_or(th.on_surface_variant)
1662 } else {
1663 ts.color.unwrap_or(th.on_surface)
1664 };
1665 let render_txt = if st.text.is_empty() {
1666 text_input.hint.clone()
1667 } else {
1668 rendered_by_vt(&st.text)
1669 };
1670 scene.nodes.push(SceneNode::Text {
1671 rect: repose_core::Rect {
1672 x: rect.x - st.scroll_offset,
1673 y: rect.y + text_off_y,
1674 w: rect.w,
1675 h: line_h,
1676 },
1677 text: Arc::from(render_txt),
1678 color: mul_alpha_color(txt_col, alpha_accum),
1679 size: font_val,
1680 font_family: ts.font_family,
1681 text_align: ts.text_align,
1682 font_weight: FontWeight(ts.font_weight.unwrap_or(400)),
1683 font_style: match ts.font_style.unwrap_or(0) {
1684 1 => FontStyle::Italic,
1685 _ => FontStyle::Normal,
1686 },
1687 text_decoration: ts.text_decoration.unwrap_or_default(),
1688 letter_spacing: ts.letter_spacing,
1689 line_height: ts.line_height,
1690 extra_style: Default::default(),
1691 url: None,
1692 font_variation_settings: None,
1693 });
1694
1695 if show_cursor
1697 && is_focused
1698 && st.selection.start == st.selection.end
1699 && st.caret_visible()
1700 {
1701 let caret_off = if has_vt {
1702 original_offset_to_display(&st.text, &measure_for, st.selection.end)
1703 } else {
1704 st.selection.end
1705 };
1706 let cx = m
1707 .positions
1708 .get(byte_to_char_index(&m, caret_off))
1709 .copied()
1710 .unwrap_or(0.0)
1711 - st.scroll_offset;
1712 let cursor_y = rect.y + text_off_y + (line_h.max(font_val) - font_val) / 2.0;
1713 scene.nodes.push(SceneNode::Rect {
1714 rect: repose_core::Rect {
1715 x: rect.x + cx.max(0.0),
1716 y: cursor_y,
1717 w: dp_to_px(1.0),
1718 h: font_val,
1719 },
1720 brush: Brush::Solid(cursor_color),
1721 radius: [0.0; 4],
1722 });
1723 }
1724 } else {
1725 let render_text = if st.text.is_empty() {
1727 st.text.clone()
1728 } else if let Some(ref vt) = text_input.visual_transformation {
1729 let annotated = repose_core::AnnotatedString::new(st.text.clone(), vec![]);
1730 vt.filter(&annotated).text.text
1731 } else {
1732 st.text.clone()
1733 };
1734 let layout = layout_text_area(
1735 &render_text,
1736 font_val,
1737 rect.w.max(1.0),
1738 400,
1739 0,
1740 ts.letter_spacing,
1741 None,
1742 );
1743 let lh = layout.line_h_px;
1744 let max_line_count = text_input.max_lines.unwrap_or(usize::MAX);
1745
1746 if st.text.is_empty() {
1748 scene.nodes.push(SceneNode::Text {
1749 rect: repose_core::Rect {
1750 x: rect.x,
1751 y: rect.y,
1752 w: rect.w,
1753 h: line_h,
1754 },
1755 text: Arc::from(text_input.hint.clone()),
1756 color: mul_alpha_color(ts.color.unwrap_or(th.on_surface_variant), alpha_accum),
1757 size: font_val,
1758 font_family: ts.font_family,
1759 text_align: ts.text_align,
1760 font_weight: FontWeight(ts.font_weight.unwrap_or(400)),
1761 font_style: match ts.font_style.unwrap_or(0) {
1762 1 => FontStyle::Italic,
1763 _ => FontStyle::Normal,
1764 },
1765 text_decoration: ts.text_decoration.unwrap_or_default(),
1766 letter_spacing: ts.letter_spacing,
1767 line_height: ts.line_height,
1768 extra_style: Default::default(),
1769 url: None,
1770 font_variation_settings: None,
1771 });
1772 } else {
1773 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1774 if i >= max_line_count {
1775 break;
1776 }
1777 let ln = render_text[s..e].to_string();
1778 let draw_y = rect.y + (i as f32) * lh - st.scroll_offset_y;
1779 if draw_y + lh < rect.y - 1.0 || draw_y > rect.y + rect.h + 1.0 {
1780 continue;
1781 }
1782 scene.nodes.push(SceneNode::Text {
1783 rect: repose_core::Rect {
1784 x: rect.x,
1785 y: draw_y,
1786 w: rect.w,
1787 h: lh,
1788 },
1789 text: Arc::<str>::from(ln),
1790 color: mul_alpha_color(ts.color.unwrap_or(th.on_surface), alpha_accum),
1791 size: font_val,
1792 font_family: ts.font_family,
1793 text_align: ts.text_align,
1794 font_weight: FontWeight(ts.font_weight.unwrap_or(400)),
1795 font_style: match ts.font_style.unwrap_or(0) {
1796 1 => FontStyle::Italic,
1797 _ => FontStyle::Normal,
1798 },
1799 text_decoration: ts.text_decoration.unwrap_or_default(),
1800 letter_spacing: ts.letter_spacing,
1801 line_height: ts.line_height,
1802 extra_style: Default::default(),
1803 url: None,
1804 font_variation_settings: None,
1805 });
1806 }
1807 }
1808
1809 if show_selection && st.selection.start != st.selection.end {
1811 let sel_a_orig: usize = st.selection.start.min(st.selection.end);
1812 let sel_b_orig: usize = st.selection.start.max(st.selection.end);
1813 let has_vt = text_input.visual_transformation.is_some();
1814 let sel_a = if has_vt {
1815 original_offset_to_display(&st.text, &render_text, sel_a_orig)
1816 } else {
1817 sel_a_orig
1818 };
1819 let sel_b = if has_vt {
1820 original_offset_to_display(&st.text, &render_text, sel_b_orig)
1821 } else {
1822 sel_b_orig
1823 };
1824 let selection = th.focus.with_alpha_f32(85.0 / 255.0);
1825 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1826 if i >= max_line_count {
1827 break;
1828 }
1829 let os = sel_a.max(s);
1830 let oe = sel_b.min(e);
1831 if os >= oe {
1832 continue;
1833 }
1834 let ln = &render_text[s..e];
1835 let m = measure_text(
1836 ln,
1837 font_val,
1838 TextMeasureConfig {
1839 font_family: ts.font_family,
1840 font_weight: ts.font_weight.unwrap_or(400),
1841 font_style: ts.font_style.unwrap_or(0),
1842 letter_spacing: ts.letter_spacing,
1843 font_variation_settings: None,
1844 },
1845 );
1846 let ls = os - s;
1847 let le = oe - s;
1848 let sx = m
1849 .positions
1850 .get(byte_to_char_index(&m, ls))
1851 .copied()
1852 .unwrap_or(0.0);
1853 let ex = m
1854 .positions
1855 .get(byte_to_char_index(&m, le))
1856 .copied()
1857 .unwrap_or(sx);
1858 let draw_y = rect.y + (i as f32) * lh - st.scroll_offset_y;
1859 scene.nodes.push(SceneNode::Rect {
1860 rect: repose_core::Rect {
1861 x: rect.x + sx,
1862 y: draw_y,
1863 w: (ex - sx).max(0.0),
1864 h: lh,
1865 },
1866 brush: Brush::Solid(selection),
1867 radius: [0.0; 4],
1868 });
1869 }
1870 }
1871
1872 if show_cursor
1874 && is_focused
1875 && st.selection.start == st.selection.end
1876 && st.caret_visible()
1877 {
1878 let caret_orig = st.selection.end.min(st.text.len());
1879 let has_vt = text_input.visual_transformation.is_some();
1880 let caret = if has_vt {
1881 original_offset_to_display(&st.text, &render_text, caret_orig)
1882 } else {
1883 caret_orig
1884 };
1885 let (cx, cy, _li) =
1886 caret_xy_for_byte(&render_text, font_val, rect.w.max(1.0), caret);
1887 let draw_x = rect.x + cx;
1888 let draw_y = rect.y + cy - st.scroll_offset_y;
1889 scene.nodes.push(SceneNode::Rect {
1890 rect: repose_core::Rect {
1891 x: draw_x,
1892 y: draw_y + (lh - font_val) / 2.0,
1893 w: dp_to_px(1.0),
1894 h: font_val,
1895 },
1896 brush: Brush::Solid(cursor_color),
1897 radius: [0.0; 4],
1898 });
1899 }
1900 }
1901 } else {
1902 if text_input.value.is_empty() {
1904 let hint_y = if text_input.multiline {
1905 rect.y
1906 } else {
1907 rect.y + text_off_y
1908 };
1909 scene.nodes.push(SceneNode::Text {
1910 rect: repose_core::Rect {
1911 x: rect.x,
1912 y: hint_y,
1913 w: rect.w,
1914 h: line_h,
1915 },
1916 text: Arc::from(text_input.hint.clone()),
1917 color: mul_alpha_color(th.on_surface_variant, alpha_accum),
1918 size: font_val,
1919 font_family: None,
1920 text_align: TextAlign::Unspecified,
1921 font_weight: FontWeight::NORMAL,
1922 font_style: FontStyle::Normal,
1923 text_decoration: ts.text_decoration.unwrap_or_default(),
1924 letter_spacing: 0.0,
1925 line_height: 0.0,
1926 extra_style: Default::default(),
1927 url: None,
1928 font_variation_settings: None,
1929 });
1930 } else if text_input.multiline {
1931 let render_text = if text_input.value.is_empty() {
1932 text_input.value.clone()
1933 } else if let Some(ref vt) = text_input.visual_transformation {
1934 let annotated = repose_core::AnnotatedString::new(text_input.value.clone(), vec![]);
1935 vt.filter(&annotated).text.text
1936 } else {
1937 text_input.value.clone()
1938 };
1939 let layout = layout_text_area(
1940 &render_text,
1941 font_val,
1942 rect.w.max(1.0),
1943 400,
1944 0,
1945 ts.letter_spacing,
1946 None,
1947 );
1948 let lh = layout.line_h_px;
1949 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1950 let ln = render_text[s..e].to_string();
1951 let draw_y = rect.y + (i as f32) * lh;
1952 if draw_y + lh < rect.y - 1.0 || draw_y > rect.y + rect.h + 1.0 {
1953 continue;
1954 }
1955 scene.nodes.push(SceneNode::Text {
1956 rect: repose_core::Rect {
1957 x: rect.x,
1958 y: draw_y,
1959 w: rect.w,
1960 h: lh,
1961 },
1962 text: Arc::<str>::from(ln),
1963 color: mul_alpha_color(th.on_surface, alpha_accum),
1964 size: font_val,
1965 font_family: None,
1966 text_align: TextAlign::Unspecified,
1967 font_weight: FontWeight::NORMAL,
1968 font_style: FontStyle::Normal,
1969 text_decoration: ts.text_decoration.unwrap_or_default(),
1970 letter_spacing: 0.0,
1971 line_height: 0.0,
1972 extra_style: Default::default(),
1973 url: None,
1974 font_variation_settings: None,
1975 });
1976 }
1977 } else {
1978 scene.nodes.push(SceneNode::Text {
1979 rect: repose_core::Rect {
1980 x: rect.x,
1981 y: rect.y + text_off_y,
1982 w: rect.w,
1983 h: line_h,
1984 },
1985 text: Arc::from(rendered_by_vt(&text_input.value)),
1986 color: mul_alpha_color(th.on_surface, alpha_accum),
1987 size: font_val,
1988 font_family: None,
1989 text_align: TextAlign::Unspecified,
1990 font_weight: FontWeight::NORMAL,
1991 font_style: FontStyle::Normal,
1992 text_decoration: ts.text_decoration.unwrap_or_default(),
1993 letter_spacing: 0.0,
1994 line_height: 0.0,
1995 extra_style: Default::default(),
1996 url: None,
1997 font_variation_settings: None,
1998 });
1999 }
2000 }
2001
2002 if let Some(ref cb) = text_input.on_text_layout {
2004 let (
2005 line_count,
2006 content_w,
2007 content_h,
2008 first_baseline,
2009 last_baseline,
2010 did_overflow_w,
2011 did_overflow_h,
2012 lines,
2013 ) = if let Some(state_rc) = state {
2014 let st = state_rc.borrow();
2015 let display = if st.text.is_empty() {
2016 text_input.hint.clone()
2017 } else if let Some(ref vt) = text_input.visual_transformation {
2018 let annotated = repose_core::AnnotatedString::new(st.text.clone(), vec![]);
2019 vt.filter(&annotated).text.text
2020 } else {
2021 st.text.clone()
2022 };
2023 if text_input.multiline {
2024 let l = layout_text_area(
2025 &display,
2026 font_val,
2027 rect.w.max(1.0),
2028 400,
2029 0,
2030 ts.letter_spacing,
2031 None,
2032 );
2033 let lc = l.ranges.len();
2034 let cw = rect.w.max(0.0);
2035 let ch = (lc as f32 * l.line_h_px).max(0.0);
2036 let line_infos: Vec<_> = l
2037 .ranges
2038 .iter()
2039 .enumerate()
2040 .map(|(i, &(s, e))| {
2041 let top = i as f32 * l.line_h_px;
2042 let bottom = top + l.line_h_px;
2043 let line_text = &display[s..e];
2044 let m = measure_text(line_text, font_val, TextMeasureConfig::default());
2045 let line_w = m.positions.last().copied().unwrap_or(0.0);
2046 TextLineInfo {
2047 start: s,
2048 end: e,
2049 top,
2050 baseline: top + l.line_h_px * 0.8,
2051 bottom,
2052 left: 0.0,
2053 right: line_w,
2054 width: line_w,
2055 }
2056 })
2057 .collect();
2058 let fb = line_infos.first().map(|l| l.baseline).unwrap_or(0.0);
2059 let lb = line_infos.last().map(|l| l.baseline).unwrap_or(0.0);
2060 (lc, cw, ch, fb, lb, cw > rect.w, ch > rect.h, line_infos)
2061 } else {
2062 let m = measure_text(&display, font_val, TextMeasureConfig::default());
2063 let w = m.positions.last().copied().unwrap_or(0.0);
2064 let top = 0.0;
2065 let bottom = line_h.max(font_val);
2066 let baseline = bottom * 0.8;
2067 let line_info = TextLineInfo {
2068 start: 0,
2069 end: display.len(),
2070 top,
2071 baseline,
2072 bottom,
2073 left: 0.0,
2074 right: w,
2075 width: w,
2076 };
2077 (
2078 1,
2079 w.max(0.0),
2080 bottom,
2081 baseline,
2082 baseline,
2083 w > rect.w,
2084 bottom > rect.h,
2085 vec![line_info],
2086 )
2087 }
2088 } else {
2089 (0, 0.0, 0.0, 0.0, 0.0, false, false, vec![])
2090 };
2091 cb(&repose_core::TextLayoutResult {
2092 line_count,
2093 width_px: content_w,
2094 height_px: content_h,
2095 first_baseline,
2096 last_baseline,
2097 did_overflow_width: did_overflow_w,
2098 did_overflow_height: did_overflow_h,
2099 lines,
2100 });
2101 }
2102
2103 scene.nodes.push(SceneNode::PopClip);
2104}
2105
2106fn text_field_view(
2110 modifier: Modifier,
2111 hint: String,
2112 value: String,
2113 multiline: bool,
2114 on_change: Option<Rc<dyn Fn(String)>>,
2115 on_submit: Option<Rc<dyn Fn(String)>>,
2116 visual_transformation: Option<Rc<dyn repose_core::VisualTransformation>>,
2117 keyboard_type: repose_core::KeyboardType,
2118 capitalization: repose_core::KeyboardCapitalization,
2119 ime_action: repose_core::ImeAction,
2120 enabled: bool,
2121 read_only: bool,
2122 max_lines: Option<usize>,
2123 min_lines: usize,
2124 cursor_color: Option<Color>,
2125 on_text_layout: Option<Rc<dyn Fn(&repose_core::TextLayoutResult)>>,
2126 text_style: repose_core::TextStyle,
2127 keyboard_actions: repose_core::KeyboardActions,
2128 interaction_source: Option<repose_core::MutableInteractionSource>,
2129 focus_tracker: Option<Rc<Cell<bool>>>,
2130 line_limits: Option<repose_core::TextFieldLineLimits>,
2131 _input_transformation: Option<Rc<dyn repose_core::InputTransformation>>,
2132 _output_transformation: Option<Rc<dyn repose_core::OutputTransformation>>,
2133 _decoration_box: Option<Rc<dyn Fn(repose_core::View) -> repose_core::View>>,
2134 _codepoint_transformation: Option<repose_core::CodepointTransformation>,
2135) -> View {
2136 let modif = modifier.text_input(TextInputConfig {
2137 hint,
2138 multiline,
2139 on_change,
2140 on_submit,
2141 focus_tracker,
2142 value,
2143 visual_transformation,
2144 keyboard_type,
2145 capitalization,
2146 ime_action,
2147 enabled,
2148 read_only,
2149 max_lines,
2150 min_lines,
2151 cursor_color,
2152 on_text_layout,
2153 text_style: Some(text_style),
2154 keyboard_actions: Some(keyboard_actions),
2155 interaction_source: interaction_source.as_ref().map(|s| s.source()),
2156 line_limits,
2157 });
2158
2159 View::new(0, ViewKind::Box)
2160 .modifier(modif)
2161 .semantics(Semantics {
2162 role: Role::TextField,
2163 label: None,
2164 focused: false,
2165 enabled,
2166 selectable_group: false,
2167 })
2168}
2169
2170#[cfg(test)]
2171mod tests {
2172 use super::*;
2173
2174 #[test]
2175 fn test_index_for_x_bytes_grapheme() {
2176 let t = "A👍🏽B";
2177 let font_px = 16.0; let m = measure_text(t, font_px, TextMeasureConfig::default());
2179 for i in 0..m.byte_offsets.len() - 1 {
2180 let b = m.byte_offsets[i];
2181 let _ = &t[..b];
2182 }
2183 }
2184
2185 fn delete_op(
2186 index: usize,
2187 pre_text: &str,
2188 pre_selection: Range<usize>,
2189 post_selection: Range<usize>,
2190 ) -> TextUndoOp {
2191 TextUndoOp {
2192 index,
2193 pre_text: pre_text.to_string(),
2194 post_text: String::new(),
2195 pre_selection,
2196 post_selection,
2197 time: Instant::now(),
2198 can_merge: true,
2199 }
2200 }
2201
2202 #[test]
2203 fn deletion_type_collapsed_post_selection_is_backspace() {
2204 let op = delete_op(2, "c", 3..3, 2..2);
2206 assert_eq!(op.deletion_type(), TextDeleteType::Start);
2207 }
2208
2209 #[test]
2210 fn deletion_type_collapsed_post_selection_is_delete_forward() {
2211 let op = delete_op(3, "c", 3..3, 3..3);
2213 assert_eq!(op.deletion_type(), TextDeleteType::End);
2214 }
2215
2216 #[test]
2217 fn deletion_type_range_post_selection_is_not_by_user() {
2218 let op = delete_op(2, "de", 2..4, 3..5);
2222 assert_eq!(op.deletion_type(), TextDeleteType::NotByUser);
2223 }
2224
2225 #[test]
2226 fn backspace_ops_merge() {
2227 let a = delete_op(2, "c", 3..3, 2..2);
2229 let b = delete_op(1, "b", 2..2, 1..1);
2230 let merged = a.try_merge(&b).expect("consecutive backspaces should merge");
2231 assert_eq!(merged.index, 1);
2232 assert_eq!(merged.pre_text, "bc");
2233 }
2234
2235 #[test]
2236 fn selection_delete_does_not_merge_with_backspace() {
2237 let backspace = delete_op(2, "c", 3..3, 2..2);
2240 let selection = delete_op(2, "de", 2..4, 2..2);
2241 assert_eq!(selection.deletion_type(), TextDeleteType::Inner);
2242 assert!(backspace.try_merge(&selection).is_none());
2243 }
2244}