1use repose_core::*;
41use std::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 cursor_brush: Option<repose_core::Brush>,
1176 pub output_transformation: Option<Rc<dyn repose_core::OutputTransformation>>,
1178 pub decorator: Option<Rc<dyn repose_core::TextFieldDecorator>>,
1180 pub codepoint_transformation: Option<repose_core::CodepointTransformation>,
1182 pub text_obfuscation_mode: repose_core::TextObfuscationMode,
1184 pub text_obfuscation_character: char,
1186
1187 pub on_change: Option<Rc<dyn Fn(String)>>,
1189 pub on_submit: Option<Rc<dyn Fn(String)>>,
1190 pub visual_transformation: Option<Rc<dyn repose_core::VisualTransformation>>,
1191 pub decoration_box: Option<Rc<dyn Fn(repose_core::View) -> repose_core::View>>,
1192}
1193
1194impl Default for TextFieldConfig {
1195 fn default() -> Self {
1196 Self {
1197 enabled: true,
1198 read_only: false,
1199 input_transformation: None,
1200 text_style: Default::default(),
1201 keyboard_options: repose_core::KeyboardOptions::DEFAULT.clone(),
1202 on_keyboard_action: None,
1203 line_limits: repose_core::TextFieldLineLimits::MultiLine {
1204 min_height_in_lines: 1,
1205 max_height_in_lines: usize::MAX,
1206 },
1207 on_text_layout: None,
1208 interaction_source: None,
1209 cursor_brush: None,
1210 output_transformation: None,
1211 decorator: None,
1212 codepoint_transformation: None,
1213 text_obfuscation_mode: repose_core::TextObfuscationMode::System,
1214 text_obfuscation_character: '\u{2022}',
1215 on_change: None,
1216 on_submit: None,
1217 visual_transformation: None,
1218 decoration_box: None,
1219 }
1220 }
1221}
1222
1223pub fn BasicTextField(
1237 state: Rc<RefCell<TextFieldState>>,
1238 modifier: repose_core::Modifier,
1239 hint: impl Into<String>,
1240 config: TextFieldConfig,
1241) -> repose_core::View {
1242 let (single_line, max_lines, min_lines) = match config.line_limits {
1243 repose_core::TextFieldLineLimits::SingleLine => (true, 1, 1),
1244 repose_core::TextFieldLineLimits::MultiLine {
1245 min_height_in_lines,
1246 max_height_in_lines,
1247 } => (false, max_height_in_lines, min_height_in_lines),
1248 };
1249
1250 let ka = if let Some(ref handler) = config.on_keyboard_action {
1251 let handler = handler.clone();
1252 repose_core::KeyboardActions {
1253 on_done: Some({
1254 let h = handler.clone();
1255 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1256 h.on_keyboard_action(&|| {})
1257 })
1258 }),
1259 on_go: Some({
1260 let h = handler.clone();
1261 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1262 h.on_keyboard_action(&|| {})
1263 })
1264 }),
1265 on_next: Some({
1266 let h = handler.clone();
1267 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1268 h.on_keyboard_action(&|| {})
1269 })
1270 }),
1271 on_previous: Some({
1272 let h = handler.clone();
1273 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1274 h.on_keyboard_action(&|| {})
1275 })
1276 }),
1277 on_search: Some({
1278 let h = handler.clone();
1279 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1280 h.on_keyboard_action(&|| {})
1281 })
1282 }),
1283 on_send: Some({
1284 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1285 handler.on_keyboard_action(&|| {})
1286 })
1287 }),
1288 }
1289 } else {
1290 repose_core::KeyboardActions::default()
1291 };
1292
1293 let decoration_box = config
1294 .decorator
1295 .map(|d| Rc::new(move |inner: repose_core::View| d.decorate(inner)) as Rc<dyn Fn(_) -> _>);
1296
1297 let cursor_color = config.cursor_brush.and_then(|b| match b {
1298 repose_core::Brush::Solid(c) => Some(c),
1299 _ => None,
1300 });
1301
1302 let value = state.borrow().text.clone();
1303 let key = state.as_ptr() as u64;
1304 set_textfield_state(key, state.clone());
1305
1306 let state_on_change = {
1307 let s = state.clone();
1308 move |new_value: String| {
1309 s.borrow_mut().text = new_value;
1310 }
1311 };
1312
1313 let merged_on_change: Option<Rc<dyn Fn(String)>> =
1314 if let Some(ref cfg_on_change) = config.on_change {
1315 let a = Rc::new(state_on_change) as Rc<dyn Fn(String)>;
1316 let b = cfg_on_change.clone();
1317 Some(Rc::new(move |v: String| {
1318 a(v.clone());
1319 b(v);
1320 }) as Rc<dyn Fn(String)>)
1321 } else {
1322 Some(Rc::new(state_on_change) as Rc<dyn Fn(String)>)
1323 };
1324
1325 text_field_view(
1326 modifier,
1327 hint.into(),
1328 value,
1329 !single_line,
1330 merged_on_change,
1331 config.on_submit,
1332 config.visual_transformation,
1333 config.keyboard_options.keyboard_type,
1334 config.keyboard_options.capitalization,
1335 config.keyboard_options.ime_action,
1336 config.enabled,
1337 config.read_only,
1338 Some(max_lines),
1339 min_lines,
1340 cursor_color,
1341 config.on_text_layout,
1342 config.text_style,
1343 ka,
1344 config.interaction_source,
1345 Some(config.line_limits),
1346 config.input_transformation,
1347 config.output_transformation,
1348 decoration_box,
1349 config.codepoint_transformation,
1350 )
1351}
1352
1353pub fn BasicSecureTextField(
1358 state: Rc<RefCell<TextFieldState>>,
1359 modifier: repose_core::Modifier,
1360 config: TextFieldConfig,
1361) -> repose_core::View {
1362 let mask = config.text_obfuscation_character;
1363 let secure_config = TextFieldConfig {
1364 line_limits: repose_core::TextFieldLineLimits::SingleLine,
1365 keyboard_options: repose_core::KeyboardOptions::SECURE_TEXT_FIELD,
1366 visual_transformation: match config.text_obfuscation_mode {
1367 repose_core::TextObfuscationMode::Visible => None,
1368 _ => Some(Rc::new(repose_core::PasswordVisualTransformation { mask })
1369 as Rc<dyn repose_core::VisualTransformation>),
1370 },
1371 ..config
1372 };
1373 BasicTextField(state, modifier, "", secure_config)
1374}
1375
1376#[derive(Clone, Debug)]
1377pub struct TextAreaLayout {
1378 pub ranges: Vec<(usize, usize)>,
1379 pub line_h_px: f32,
1380}
1381
1382pub fn layout_text_area(
1383 text: &str,
1384 font_px: f32,
1385 wrap_w_px: f32,
1386 font_weight: u16,
1387 font_style: u8,
1388 letter_spacing: f32,
1389 font_variation_settings: Option<&str>,
1390) -> TextAreaLayout {
1391 let line_h = font_px;
1392 let (ranges, _) = repose_text::wrap_line_ranges(
1393 text,
1394 font_px,
1395 wrap_w_px.max(1.0),
1396 None,
1397 true,
1398 font_weight,
1399 font_style,
1400 letter_spacing,
1401 font_variation_settings,
1402 );
1403 TextAreaLayout {
1404 ranges,
1405 line_h_px: line_h,
1406 }
1407}
1408
1409fn locate_byte_in_ranges(ranges: &[(usize, usize)], b: usize) -> (usize, usize, usize) {
1411 if ranges.is_empty() {
1412 return (0, 0, b);
1413 }
1414 for (i, (s, e)) in ranges.iter().enumerate() {
1415 if b < *s {
1416 if i == 0 {
1417 return (0, 0, b);
1418 }
1419 let (ps, pe) = ranges[i - 1];
1420 let local = pe.saturating_sub(ps);
1421 return (i - 1, local, ps + local);
1422 }
1423 if b < *e {
1424 let local = b.saturating_sub(*s).min(e.saturating_sub(*s));
1425 return (i, local, *s + local);
1426 }
1427 if b == *e {
1428 if let Some((ns, _ne)) = ranges.get(i + 1)
1429 && *ns == b
1430 {
1431 return (i + 1, 0, b);
1432 }
1433 let local = e.saturating_sub(*s);
1434 return (i, local, *s + local);
1435 }
1436 }
1437 let (ls, le) = ranges[ranges.len() - 1];
1438 let local = le.saturating_sub(ls);
1439 (ranges.len() - 1, local, ls + local)
1440}
1441
1442pub fn caret_xy_for_byte(
1444 text: &str,
1445 font_px: f32,
1446 wrap_w_px: f32,
1447 byte: usize,
1448) -> (f32, f32, usize) {
1449 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1450 let (ranges, line_h) = (&layout.ranges, layout.line_h_px);
1451 let (li, local, _) = locate_byte_in_ranges(ranges, byte);
1452 let (s, e) = ranges.get(li).copied().unwrap_or((0, 0));
1453 let line = &text[s..e];
1454 let m = measure_text(line, font_px, TextMeasureConfig::default());
1455 let ci = byte_to_char_index(&m, local);
1456 let x = m.positions.get(ci).copied().unwrap_or(0.0);
1457 let y = (li as f32) * line_h;
1458 (x, y, li)
1459}
1460
1461pub fn index_for_xy_bytes(text: &str, font_px: f32, wrap_w_px: f32, x_px: f32, y_px: f32) -> usize {
1463 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1464 let li = ((y_px / layout.line_h_px).floor() as isize).max(0) as usize;
1465 let li = li.min(layout.ranges.len().saturating_sub(1));
1466 let (s, e) = layout.ranges.get(li).copied().unwrap_or((0, 0));
1467 let line = &text[s..e];
1468 let local = index_for_x_bytes(line, font_px, x_px.max(0.0), 400, 0);
1469 (s + local).min(text.len())
1470}
1471
1472pub fn move_caret_vertical(
1474 text: &str,
1475 font_px: f32,
1476 wrap_w_px: f32,
1477 cur_byte: usize,
1478 dir: i32, preferred_x: Option<f32>,
1480) -> (usize, f32) {
1481 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1482 if layout.ranges.is_empty() {
1483 return (cur_byte, preferred_x.unwrap_or(0.0));
1484 }
1485 let (x, _y, li) = caret_xy_for_byte(text, font_px, wrap_w_px, cur_byte);
1486 let px = preferred_x.unwrap_or(x);
1487 let mut nli = li as i32 + dir;
1488 nli = nli.clamp(0, (layout.ranges.len().saturating_sub(1)) as i32);
1489 let nli = nli as usize;
1490 let (s, e) = layout.ranges[nli];
1491 let line = &text[s..e];
1492 let local = index_for_x_bytes(line, font_px, px.max(0.0), 400, 0);
1493 ((s + local).min(text.len()), px)
1494}
1495
1496pub fn line_home_end(
1498 text: &str,
1499 font_px: f32,
1500 wrap_w_px: f32,
1501 cur_byte: usize,
1502 to_end: bool,
1503) -> usize {
1504 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1505 let (li, _local, _) = locate_byte_in_ranges(&layout.ranges, cur_byte);
1506 let (s, e) = layout.ranges.get(li).copied().unwrap_or((0, 0));
1507 if to_end { e } else { s }
1508}
1509
1510fn clamp_to_char_boundary(s: &str, i: usize) -> usize {
1511 if i >= s.len() {
1512 return s.len();
1513 }
1514 if s.is_char_boundary(i) {
1515 return i;
1516 }
1517 let mut j = i;
1518 while j > 0 && !s.is_char_boundary(j) {
1519 j -= 1;
1520 }
1521 j
1522}
1523
1524fn char_to_byte(s: &str, ci: usize) -> usize {
1525 if ci == 0 {
1526 0
1527 } else {
1528 s.char_indices().nth(ci).map(|(i, _)| i).unwrap_or(s.len())
1529 }
1530}
1531
1532pub(crate) fn paint_text_field(
1544 scene: &mut Scene,
1545 rect: repose_core::Rect,
1546 text_input: &TextInputConfig,
1547 state: Option<&Rc<RefCell<TextFieldState>>>,
1548 is_focused: bool,
1549 clip_rounded: Option<[f32; 4]>,
1550 alpha_accum: f32,
1551) {
1552 let ts = text_input
1553 .text_style
1554 .as_ref()
1555 .map(|s| s.clone())
1556 .unwrap_or_default();
1557 let font_size_dp = if ts.font_size != 0.0 {
1558 ts.font_size
1559 } else {
1560 TF_FONT_DP
1561 };
1562 let font_val = dp_to_px(font_size_dp) * locals::text_scale().0;
1563 let line_h = if ts.line_height != 0.0 {
1564 dp_to_px(ts.line_height) * locals::text_scale().0
1565 } else if text_input.multiline {
1566 0.0 } else {
1568 font_val };
1570 let text_off_y = (rect.h - line_h.max(font_val)) / 2.0;
1571
1572 let clip_radius = clip_rounded.unwrap_or([0.0; 4]).map(dp_to_px);
1573 scene.nodes.push(SceneNode::PushClip {
1574 rect,
1575 radius: clip_radius,
1576 op: repose_core::ClipOp::Intersect,
1577 });
1578
1579 let th = locals::theme();
1580 let show_selection = text_input.enabled;
1581 let show_cursor = text_input.enabled && !text_input.read_only;
1582 let cursor_color = text_input.cursor_color.unwrap_or(th.on_surface);
1583 let rendered_by_vt = |original: &str| -> String {
1584 if let Some(ref vt) = text_input.visual_transformation {
1585 let annotated = repose_core::AnnotatedString::new(original.to_string(), vec![]);
1586 vt.filter(&annotated).text.text
1587 } else {
1588 original.to_string()
1589 }
1590 };
1591
1592 if let Some(state_rc) = state {
1593 let st = state_rc.borrow();
1594
1595 if !text_input.multiline {
1596 let measure_for = if text_input.visual_transformation.is_some() && !st.text.is_empty() {
1598 rendered_by_vt(&st.text)
1599 } else {
1600 st.text.clone()
1601 };
1602 let has_vt = text_input.visual_transformation.is_some();
1603 let m = measure_text(
1604 &measure_for,
1605 font_val,
1606 TextMeasureConfig {
1607 font_family: ts.font_family,
1608 font_weight: ts.font_weight.unwrap_or(400),
1609 font_style: ts.font_style.unwrap_or(0),
1610 letter_spacing: ts.letter_spacing,
1611 font_variation_settings: None,
1612 },
1613 );
1614
1615 if show_selection && st.selection.start != st.selection.end {
1617 let start_off = if has_vt {
1618 original_offset_to_display(&st.text, &measure_for, st.selection.start)
1619 } else {
1620 st.selection.start
1621 };
1622 let end_off = if has_vt {
1623 original_offset_to_display(&st.text, &measure_for, st.selection.end)
1624 } else {
1625 st.selection.end
1626 };
1627 let sx = m
1628 .positions
1629 .get(byte_to_char_index(&m, start_off))
1630 .copied()
1631 .unwrap_or(0.0)
1632 - st.scroll_offset;
1633 let ex = m
1634 .positions
1635 .get(byte_to_char_index(&m, end_off))
1636 .copied()
1637 .unwrap_or(sx)
1638 - st.scroll_offset;
1639 let selection = th.focus.with_alpha_f32(85.0 / 255.0);
1640 let vis_x = sx.max(0.0);
1641 let vis_ex = ex.max(0.0);
1642 scene.nodes.push(SceneNode::Rect {
1643 rect: repose_core::Rect {
1644 x: rect.x + vis_x,
1645 y: rect.y + text_off_y,
1646 w: (vis_ex - vis_x).max(0.0),
1647 h: line_h.max(font_val),
1648 },
1649 brush: Brush::Solid(selection),
1650 radius: [0.0; 4],
1651 });
1652 }
1653
1654 let txt_col = if st.text.is_empty() {
1656 ts.color.unwrap_or(th.on_surface_variant)
1657 } else {
1658 ts.color.unwrap_or(th.on_surface)
1659 };
1660 let render_txt = if st.text.is_empty() {
1661 text_input.hint.clone()
1662 } else {
1663 rendered_by_vt(&st.text)
1664 };
1665 scene.nodes.push(SceneNode::Text {
1666 rect: repose_core::Rect {
1667 x: rect.x - st.scroll_offset,
1668 y: rect.y + text_off_y,
1669 w: rect.w,
1670 h: line_h,
1671 },
1672 text: Arc::from(render_txt),
1673 color: mul_alpha_color(txt_col, alpha_accum),
1674 size: font_val,
1675 font_family: ts.font_family,
1676 text_align: ts.text_align,
1677 font_weight: FontWeight(ts.font_weight.unwrap_or(400)),
1678 font_style: match ts.font_style.unwrap_or(0) {
1679 1 => FontStyle::Italic,
1680 _ => FontStyle::Normal,
1681 },
1682 text_decoration: ts.text_decoration.unwrap_or_default(),
1683 letter_spacing: ts.letter_spacing,
1684 line_height: ts.line_height,
1685 extra_style: Default::default(),
1686 url: None,
1687 font_variation_settings: None,
1688 });
1689
1690 if show_cursor
1692 && is_focused
1693 && st.selection.start == st.selection.end
1694 && st.caret_visible()
1695 {
1696 let caret_off = if has_vt {
1697 original_offset_to_display(&st.text, &measure_for, st.selection.end)
1698 } else {
1699 st.selection.end
1700 };
1701 let cx = m
1702 .positions
1703 .get(byte_to_char_index(&m, caret_off))
1704 .copied()
1705 .unwrap_or(0.0)
1706 - st.scroll_offset;
1707 let cursor_y = rect.y + text_off_y + (line_h.max(font_val) - font_val) / 2.0;
1708 scene.nodes.push(SceneNode::Rect {
1709 rect: repose_core::Rect {
1710 x: rect.x + cx.max(0.0),
1711 y: cursor_y,
1712 w: dp_to_px(1.0),
1713 h: font_val,
1714 },
1715 brush: Brush::Solid(cursor_color),
1716 radius: [0.0; 4],
1717 });
1718 }
1719 } else {
1720 let render_text = if st.text.is_empty() {
1722 st.text.clone()
1723 } else if let Some(ref vt) = text_input.visual_transformation {
1724 let annotated = repose_core::AnnotatedString::new(st.text.clone(), vec![]);
1725 vt.filter(&annotated).text.text
1726 } else {
1727 st.text.clone()
1728 };
1729 let layout = layout_text_area(
1730 &render_text,
1731 font_val,
1732 rect.w.max(1.0),
1733 400,
1734 0,
1735 ts.letter_spacing,
1736 None,
1737 );
1738 let lh = layout.line_h_px;
1739 let max_line_count = text_input.max_lines.unwrap_or(usize::MAX);
1740
1741 if st.text.is_empty() {
1743 scene.nodes.push(SceneNode::Text {
1744 rect: repose_core::Rect {
1745 x: rect.x,
1746 y: rect.y,
1747 w: rect.w,
1748 h: line_h,
1749 },
1750 text: Arc::from(text_input.hint.clone()),
1751 color: mul_alpha_color(ts.color.unwrap_or(th.on_surface_variant), alpha_accum),
1752 size: font_val,
1753 font_family: ts.font_family,
1754 text_align: ts.text_align,
1755 font_weight: FontWeight(ts.font_weight.unwrap_or(400)),
1756 font_style: match ts.font_style.unwrap_or(0) {
1757 1 => FontStyle::Italic,
1758 _ => FontStyle::Normal,
1759 },
1760 text_decoration: ts.text_decoration.unwrap_or_default(),
1761 letter_spacing: ts.letter_spacing,
1762 line_height: ts.line_height,
1763 extra_style: Default::default(),
1764 url: None,
1765 font_variation_settings: None,
1766 });
1767 } else {
1768 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1769 if i >= max_line_count {
1770 break;
1771 }
1772 let ln = render_text[s..e].to_string();
1773 let draw_y = rect.y + (i as f32) * lh - st.scroll_offset_y;
1774 if draw_y + lh < rect.y - 1.0 || draw_y > rect.y + rect.h + 1.0 {
1775 continue;
1776 }
1777 scene.nodes.push(SceneNode::Text {
1778 rect: repose_core::Rect {
1779 x: rect.x,
1780 y: draw_y,
1781 w: rect.w,
1782 h: lh,
1783 },
1784 text: Arc::<str>::from(ln),
1785 color: mul_alpha_color(ts.color.unwrap_or(th.on_surface), alpha_accum),
1786 size: font_val,
1787 font_family: ts.font_family,
1788 text_align: ts.text_align,
1789 font_weight: FontWeight(ts.font_weight.unwrap_or(400)),
1790 font_style: match ts.font_style.unwrap_or(0) {
1791 1 => FontStyle::Italic,
1792 _ => FontStyle::Normal,
1793 },
1794 text_decoration: ts.text_decoration.unwrap_or_default(),
1795 letter_spacing: ts.letter_spacing,
1796 line_height: ts.line_height,
1797 extra_style: Default::default(),
1798 url: None,
1799 font_variation_settings: None,
1800 });
1801 }
1802 }
1803
1804 if show_selection && st.selection.start != st.selection.end {
1806 let sel_a_orig: usize = st.selection.start.min(st.selection.end);
1807 let sel_b_orig: usize = st.selection.start.max(st.selection.end);
1808 let has_vt = text_input.visual_transformation.is_some();
1809 let sel_a = if has_vt {
1810 original_offset_to_display(&st.text, &render_text, sel_a_orig)
1811 } else {
1812 sel_a_orig
1813 };
1814 let sel_b = if has_vt {
1815 original_offset_to_display(&st.text, &render_text, sel_b_orig)
1816 } else {
1817 sel_b_orig
1818 };
1819 let selection = th.focus.with_alpha_f32(85.0 / 255.0);
1820 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1821 if i >= max_line_count {
1822 break;
1823 }
1824 let os = sel_a.max(s);
1825 let oe = sel_b.min(e);
1826 if os >= oe {
1827 continue;
1828 }
1829 let ln = &render_text[s..e];
1830 let m = measure_text(
1831 ln,
1832 font_val,
1833 TextMeasureConfig {
1834 font_family: ts.font_family,
1835 font_weight: ts.font_weight.unwrap_or(400),
1836 font_style: ts.font_style.unwrap_or(0),
1837 letter_spacing: ts.letter_spacing,
1838 font_variation_settings: None,
1839 },
1840 );
1841 let ls = os - s;
1842 let le = oe - s;
1843 let sx = m
1844 .positions
1845 .get(byte_to_char_index(&m, ls))
1846 .copied()
1847 .unwrap_or(0.0);
1848 let ex = m
1849 .positions
1850 .get(byte_to_char_index(&m, le))
1851 .copied()
1852 .unwrap_or(sx);
1853 let draw_y = rect.y + (i as f32) * lh - st.scroll_offset_y;
1854 scene.nodes.push(SceneNode::Rect {
1855 rect: repose_core::Rect {
1856 x: rect.x + sx,
1857 y: draw_y,
1858 w: (ex - sx).max(0.0),
1859 h: lh,
1860 },
1861 brush: Brush::Solid(selection),
1862 radius: [0.0; 4],
1863 });
1864 }
1865 }
1866
1867 if show_cursor
1869 && is_focused
1870 && st.selection.start == st.selection.end
1871 && st.caret_visible()
1872 {
1873 let caret_orig = st.selection.end.min(st.text.len());
1874 let has_vt = text_input.visual_transformation.is_some();
1875 let caret = if has_vt {
1876 original_offset_to_display(&st.text, &render_text, caret_orig)
1877 } else {
1878 caret_orig
1879 };
1880 let (cx, cy, _li) =
1881 caret_xy_for_byte(&render_text, font_val, rect.w.max(1.0), caret);
1882 let draw_x = rect.x + cx;
1883 let draw_y = rect.y + cy - st.scroll_offset_y;
1884 scene.nodes.push(SceneNode::Rect {
1885 rect: repose_core::Rect {
1886 x: draw_x,
1887 y: draw_y + (lh - font_val) / 2.0,
1888 w: dp_to_px(1.0),
1889 h: font_val,
1890 },
1891 brush: Brush::Solid(cursor_color),
1892 radius: [0.0; 4],
1893 });
1894 }
1895 }
1896 } else {
1897 if text_input.value.is_empty() {
1899 let hint_y = if text_input.multiline {
1900 rect.y
1901 } else {
1902 rect.y + text_off_y
1903 };
1904 scene.nodes.push(SceneNode::Text {
1905 rect: repose_core::Rect {
1906 x: rect.x,
1907 y: hint_y,
1908 w: rect.w,
1909 h: line_h,
1910 },
1911 text: Arc::from(text_input.hint.clone()),
1912 color: mul_alpha_color(th.on_surface_variant, alpha_accum),
1913 size: font_val,
1914 font_family: None,
1915 text_align: TextAlign::Unspecified,
1916 font_weight: FontWeight::NORMAL,
1917 font_style: FontStyle::Normal,
1918 text_decoration: ts.text_decoration.unwrap_or_default(),
1919 letter_spacing: 0.0,
1920 line_height: 0.0,
1921 extra_style: Default::default(),
1922 url: None,
1923 font_variation_settings: None,
1924 });
1925 } else if text_input.multiline {
1926 let render_text = if text_input.value.is_empty() {
1927 text_input.value.clone()
1928 } else if let Some(ref vt) = text_input.visual_transformation {
1929 let annotated = repose_core::AnnotatedString::new(text_input.value.clone(), vec![]);
1930 vt.filter(&annotated).text.text
1931 } else {
1932 text_input.value.clone()
1933 };
1934 let layout = layout_text_area(
1935 &render_text,
1936 font_val,
1937 rect.w.max(1.0),
1938 400,
1939 0,
1940 ts.letter_spacing,
1941 None,
1942 );
1943 let lh = layout.line_h_px;
1944 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1945 let ln = render_text[s..e].to_string();
1946 let draw_y = rect.y + (i as f32) * lh;
1947 if draw_y + lh < rect.y - 1.0 || draw_y > rect.y + rect.h + 1.0 {
1948 continue;
1949 }
1950 scene.nodes.push(SceneNode::Text {
1951 rect: repose_core::Rect {
1952 x: rect.x,
1953 y: draw_y,
1954 w: rect.w,
1955 h: lh,
1956 },
1957 text: Arc::<str>::from(ln),
1958 color: mul_alpha_color(th.on_surface, alpha_accum),
1959 size: font_val,
1960 font_family: None,
1961 text_align: TextAlign::Unspecified,
1962 font_weight: FontWeight::NORMAL,
1963 font_style: FontStyle::Normal,
1964 text_decoration: ts.text_decoration.unwrap_or_default(),
1965 letter_spacing: 0.0,
1966 line_height: 0.0,
1967 extra_style: Default::default(),
1968 url: None,
1969 font_variation_settings: None,
1970 });
1971 }
1972 } else {
1973 scene.nodes.push(SceneNode::Text {
1974 rect: repose_core::Rect {
1975 x: rect.x,
1976 y: rect.y + text_off_y,
1977 w: rect.w,
1978 h: line_h,
1979 },
1980 text: Arc::from(rendered_by_vt(&text_input.value)),
1981 color: mul_alpha_color(th.on_surface, alpha_accum),
1982 size: font_val,
1983 font_family: None,
1984 text_align: TextAlign::Unspecified,
1985 font_weight: FontWeight::NORMAL,
1986 font_style: FontStyle::Normal,
1987 text_decoration: ts.text_decoration.unwrap_or_default(),
1988 letter_spacing: 0.0,
1989 line_height: 0.0,
1990 extra_style: Default::default(),
1991 url: None,
1992 font_variation_settings: None,
1993 });
1994 }
1995 }
1996
1997 if let Some(ref cb) = text_input.on_text_layout {
1999 let (
2000 line_count,
2001 content_w,
2002 content_h,
2003 first_baseline,
2004 last_baseline,
2005 did_overflow_w,
2006 did_overflow_h,
2007 lines,
2008 ) = if let Some(state_rc) = state {
2009 let st = state_rc.borrow();
2010 let display = if st.text.is_empty() {
2011 text_input.hint.clone()
2012 } else if let Some(ref vt) = text_input.visual_transformation {
2013 let annotated = repose_core::AnnotatedString::new(st.text.clone(), vec![]);
2014 vt.filter(&annotated).text.text
2015 } else {
2016 st.text.clone()
2017 };
2018 if text_input.multiline {
2019 let l = layout_text_area(
2020 &display,
2021 font_val,
2022 rect.w.max(1.0),
2023 400,
2024 0,
2025 ts.letter_spacing,
2026 None,
2027 );
2028 let lc = l.ranges.len();
2029 let cw = rect.w.max(0.0);
2030 let ch = (lc as f32 * l.line_h_px).max(0.0);
2031 let line_infos: Vec<_> = l
2032 .ranges
2033 .iter()
2034 .enumerate()
2035 .map(|(i, &(s, e))| {
2036 let top = i as f32 * l.line_h_px;
2037 let bottom = top + l.line_h_px;
2038 let line_text = &display[s..e];
2039 let m = measure_text(line_text, font_val, TextMeasureConfig::default());
2040 let line_w = m.positions.last().copied().unwrap_or(0.0);
2041 TextLineInfo {
2042 start: s,
2043 end: e,
2044 top,
2045 baseline: top + l.line_h_px * 0.8,
2046 bottom,
2047 left: 0.0,
2048 right: line_w,
2049 width: line_w,
2050 }
2051 })
2052 .collect();
2053 let fb = line_infos.first().map(|l| l.baseline).unwrap_or(0.0);
2054 let lb = line_infos.last().map(|l| l.baseline).unwrap_or(0.0);
2055 (lc, cw, ch, fb, lb, cw > rect.w, ch > rect.h, line_infos)
2056 } else {
2057 let m = measure_text(&display, font_val, TextMeasureConfig::default());
2058 let w = m.positions.last().copied().unwrap_or(0.0);
2059 let top = 0.0;
2060 let bottom = line_h.max(font_val);
2061 let baseline = bottom * 0.8;
2062 let line_info = TextLineInfo {
2063 start: 0,
2064 end: display.len(),
2065 top,
2066 baseline,
2067 bottom,
2068 left: 0.0,
2069 right: w,
2070 width: w,
2071 };
2072 (
2073 1,
2074 w.max(0.0),
2075 bottom,
2076 baseline,
2077 baseline,
2078 w > rect.w,
2079 bottom > rect.h,
2080 vec![line_info],
2081 )
2082 }
2083 } else {
2084 (0, 0.0, 0.0, 0.0, 0.0, false, false, vec![])
2085 };
2086 cb(&repose_core::TextLayoutResult {
2087 line_count,
2088 width_px: content_w,
2089 height_px: content_h,
2090 first_baseline,
2091 last_baseline,
2092 did_overflow_width: did_overflow_w,
2093 did_overflow_height: did_overflow_h,
2094 lines,
2095 });
2096 }
2097
2098 scene.nodes.push(SceneNode::PopClip);
2099}
2100
2101fn text_field_view(
2105 modifier: Modifier,
2106 hint: String,
2107 value: String,
2108 multiline: bool,
2109 on_change: Option<Rc<dyn Fn(String)>>,
2110 on_submit: Option<Rc<dyn Fn(String)>>,
2111 visual_transformation: Option<Rc<dyn repose_core::VisualTransformation>>,
2112 keyboard_type: repose_core::KeyboardType,
2113 capitalization: repose_core::KeyboardCapitalization,
2114 ime_action: repose_core::ImeAction,
2115 enabled: bool,
2116 read_only: bool,
2117 max_lines: Option<usize>,
2118 min_lines: usize,
2119 cursor_color: Option<Color>,
2120 on_text_layout: Option<Rc<dyn Fn(&repose_core::TextLayoutResult)>>,
2121 text_style: repose_core::TextStyle,
2122 keyboard_actions: repose_core::KeyboardActions,
2123 interaction_source: Option<repose_core::MutableInteractionSource>,
2124 line_limits: Option<repose_core::TextFieldLineLimits>,
2125 _input_transformation: Option<Rc<dyn repose_core::InputTransformation>>,
2126 _output_transformation: Option<Rc<dyn repose_core::OutputTransformation>>,
2127 _decoration_box: Option<Rc<dyn Fn(repose_core::View) -> repose_core::View>>,
2128 _codepoint_transformation: Option<repose_core::CodepointTransformation>,
2129) -> View {
2130 let modif = modifier.text_input(TextInputConfig {
2131 hint,
2132 multiline,
2133 on_change,
2134 on_submit,
2135 focus_tracker: None,
2136 value,
2137 visual_transformation,
2138 keyboard_type,
2139 capitalization,
2140 ime_action,
2141 enabled,
2142 read_only,
2143 max_lines,
2144 min_lines,
2145 cursor_color,
2146 on_text_layout,
2147 text_style: Some(text_style),
2148 keyboard_actions: Some(keyboard_actions),
2149 interaction_source: interaction_source.as_ref().map(|s| s.source()),
2150 line_limits,
2151 });
2152
2153 View::new(0, ViewKind::Box)
2154 .modifier(modif)
2155 .semantics(Semantics {
2156 role: Role::TextField,
2157 label: None,
2158 focused: false,
2159 enabled,
2160 selectable_group: false,
2161 })
2162}
2163
2164#[cfg(test)]
2165mod tests {
2166 use super::*;
2167
2168 #[test]
2169 fn test_index_for_x_bytes_grapheme() {
2170 let t = "A👍🏽B";
2171 let font_px = 16.0; let m = measure_text(t, font_px, TextMeasureConfig::default());
2173 for i in 0..m.byte_offsets.len() - 1 {
2174 let b = m.byte_offsets[i];
2175 let _ = &t[..b];
2176 }
2177 }
2178}