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