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