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 set_inner_width(&mut self, w_px: f32) {
1058 self.inner_width = w_px.max(0.0);
1059 if self.scroll_offset.is_nan() {
1060 self.scroll_offset = 0.0;
1061 }
1062 if self.scroll_target.is_nan() {
1063 self.scroll_target = 0.0;
1064 }
1065 }
1066 pub fn set_inner_height(&mut self, h_px: f32) {
1067 self.inner_height = h_px.max(0.0);
1068 if self.scroll_offset_y.is_nan() {
1069 self.scroll_offset_y = 0.0;
1070 }
1071 if self.scroll_target_y.is_nan() {
1072 self.scroll_target_y = 0.0;
1073 }
1074 }
1075
1076 pub fn tick_scroll_animation(&mut self) {
1080 let now = Instant::now();
1081 let dt = match self.last_scroll_tick {
1082 Some(prev) => {
1083 let d = now.saturating_duration_since(prev).as_secs_f32();
1084 d.min(0.05) }
1086 None => {
1087 self.last_scroll_tick = Some(now);
1090 self.scroll_offset = self.scroll_target;
1091 self.scroll_vel = 0.0;
1092 self.scroll_offset_y = self.scroll_target_y;
1093 self.scroll_vel_y = 0.0;
1094 return;
1095 }
1096 };
1097 self.last_scroll_tick = Some(now);
1098
1099 if dt > 0.0 {
1101 let dx = self.scroll_target - self.scroll_offset;
1102 let near_x = dx.abs() < 0.5 && self.scroll_vel.abs() < 0.5;
1103 if near_x {
1104 self.scroll_offset = self.scroll_target;
1105 self.scroll_vel = 0.0;
1106 } else {
1107 let force_x = SCROLL_STIFFNESS * dx - SCROLL_DAMPING * self.scroll_vel;
1108 self.scroll_vel += force_x * dt;
1109 self.scroll_offset += self.scroll_vel * dt;
1110 if (self.scroll_target - self.scroll_offset).signum() != dx.signum() && dx != 0.0 {
1112 self.scroll_offset = self.scroll_target;
1113 self.scroll_vel = 0.0;
1114 }
1115 }
1116 }
1117
1118 if dt > 0.0 {
1120 let dy = self.scroll_target_y - self.scroll_offset_y;
1121 let near_y = dy.abs() < 0.5 && self.scroll_vel_y.abs() < 0.5;
1122 if near_y {
1123 self.scroll_offset_y = self.scroll_target_y;
1124 self.scroll_vel_y = 0.0;
1125 } else {
1126 let force_y = SCROLL_STIFFNESS * dy - SCROLL_DAMPING * self.scroll_vel_y;
1127 self.scroll_vel_y += force_y * dt;
1128 self.scroll_offset_y += self.scroll_vel_y * dt;
1129 if (self.scroll_target_y - self.scroll_offset_y).signum() != dy.signum()
1130 && dy != 0.0
1131 {
1132 self.scroll_offset_y = self.scroll_target_y;
1133 self.scroll_vel_y = 0.0;
1134 }
1135 }
1136 }
1137 }
1138}
1139
1140#[derive(Clone)]
1150pub struct TextFieldConfig {
1151 pub enabled: bool,
1153 pub read_only: bool,
1155 pub input_transformation: Option<Rc<dyn repose_core::InputTransformation>>,
1157 pub text_style: repose_core::TextStyle,
1159 pub keyboard_options: repose_core::KeyboardOptions,
1161 pub on_keyboard_action: Option<Rc<dyn repose_core::KeyboardActionHandler>>,
1163 pub line_limits: repose_core::TextFieldLineLimits,
1165 pub on_text_layout: Option<Rc<dyn Fn(&repose_core::TextLayoutResult)>>,
1167 pub interaction_source: Option<repose_core::MutableInteractionSource>,
1169 pub focus_tracker: Option<Rc<Cell<bool>>>,
1172 pub cursor_brush: Option<repose_core::Brush>,
1174 pub output_transformation: Option<Rc<dyn repose_core::OutputTransformation>>,
1176 pub decorator: Option<Rc<dyn repose_core::TextFieldDecorator>>,
1178 pub codepoint_transformation: Option<repose_core::CodepointTransformation>,
1180 pub text_obfuscation_mode: repose_core::TextObfuscationMode,
1182 pub text_obfuscation_character: char,
1184
1185 pub on_change: Option<Rc<dyn Fn(String)>>,
1187 pub on_submit: Option<Rc<dyn Fn(String)>>,
1188 pub visual_transformation: Option<Rc<dyn repose_core::VisualTransformation>>,
1189 pub decoration_box: Option<Rc<dyn Fn(repose_core::View) -> repose_core::View>>,
1190}
1191
1192impl Default for TextFieldConfig {
1193 fn default() -> Self {
1194 Self {
1195 enabled: true,
1196 read_only: false,
1197 input_transformation: None,
1198 text_style: Default::default(),
1199 keyboard_options: repose_core::KeyboardOptions::DEFAULT,
1200 on_keyboard_action: None,
1201 line_limits: repose_core::TextFieldLineLimits::MultiLine {
1202 min_height_in_lines: 1,
1203 max_height_in_lines: usize::MAX,
1204 },
1205 on_text_layout: None,
1206 interaction_source: None,
1207 focus_tracker: None,
1208 cursor_brush: None,
1209 output_transformation: None,
1210 decorator: None,
1211 codepoint_transformation: None,
1212 text_obfuscation_mode: repose_core::TextObfuscationMode::System,
1213 text_obfuscation_character: '\u{2022}',
1214 on_change: None,
1215 on_submit: None,
1216 visual_transformation: None,
1217 decoration_box: None,
1218 }
1219 }
1220}
1221
1222pub fn BasicTextField(
1236 state: Rc<RefCell<TextFieldState>>,
1237 modifier: repose_core::Modifier,
1238 hint: impl Into<String>,
1239 config: TextFieldConfig,
1240) -> repose_core::View {
1241 let (single_line, max_lines, min_lines) = match config.line_limits {
1242 repose_core::TextFieldLineLimits::SingleLine => (true, 1, 1),
1243 repose_core::TextFieldLineLimits::MultiLine {
1244 min_height_in_lines,
1245 max_height_in_lines,
1246 } => (false, max_height_in_lines, min_height_in_lines),
1247 };
1248
1249 let ka = if let Some(ref handler) = config.on_keyboard_action {
1250 let handler = handler.clone();
1251 repose_core::KeyboardActions {
1252 on_done: Some({
1253 let h = handler.clone();
1254 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1255 h.on_keyboard_action(&|| {})
1256 })
1257 }),
1258 on_go: Some({
1259 let h = handler.clone();
1260 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1261 h.on_keyboard_action(&|| {})
1262 })
1263 }),
1264 on_next: Some({
1265 let h = handler.clone();
1266 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1267 h.on_keyboard_action(&|| {})
1268 })
1269 }),
1270 on_previous: Some({
1271 let h = handler.clone();
1272 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1273 h.on_keyboard_action(&|| {})
1274 })
1275 }),
1276 on_search: Some({
1277 let h = handler.clone();
1278 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1279 h.on_keyboard_action(&|| {})
1280 })
1281 }),
1282 on_send: Some({
1283 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1284 handler.on_keyboard_action(&|| {})
1285 })
1286 }),
1287 }
1288 } else {
1289 repose_core::KeyboardActions::default()
1290 };
1291
1292 let decoration_box = config
1293 .decorator
1294 .map(|d| Rc::new(move |inner: repose_core::View| d.decorate(inner)) as Rc<dyn Fn(_) -> _>);
1295
1296 let cursor_color = config.cursor_brush.and_then(|b| match b {
1297 repose_core::Brush::Solid(c) => Some(c),
1298 _ => None,
1299 });
1300
1301 let value = state.borrow().text.clone();
1302 let key = state.as_ptr() as u64;
1303 set_textfield_state(key, state.clone());
1304
1305 let state_on_change = {
1306 let s = state.clone();
1307 move |new_value: String| {
1308 s.borrow_mut().text = new_value;
1309 }
1310 };
1311
1312 let merged_on_change: Option<Rc<dyn Fn(String)>> =
1313 if let Some(ref cfg_on_change) = config.on_change {
1314 let a = Rc::new(state_on_change) as Rc<dyn Fn(String)>;
1315 let b = cfg_on_change.clone();
1316 Some(Rc::new(move |v: String| {
1317 a(v.clone());
1318 b(v);
1319 }) as Rc<dyn Fn(String)>)
1320 } else {
1321 Some(Rc::new(state_on_change) as Rc<dyn Fn(String)>)
1322 };
1323
1324 text_field_view(
1325 modifier,
1326 hint.into(),
1327 value,
1328 !single_line,
1329 merged_on_change,
1330 config.on_submit,
1331 config.visual_transformation,
1332 config.keyboard_options.keyboard_type,
1333 config.keyboard_options.capitalization,
1334 config.keyboard_options.ime_action,
1335 config.keyboard_options.auto_correct_enabled,
1336 config.enabled,
1337 config.read_only,
1338 Some(max_lines),
1339 min_lines,
1340 cursor_color,
1341 config.on_text_layout,
1342 config.text_style,
1343 ka,
1344 config.interaction_source,
1345 config.focus_tracker,
1346 Some(config.line_limits),
1347 config.input_transformation,
1348 config.output_transformation,
1349 decoration_box,
1350 config.codepoint_transformation,
1351 )
1352}
1353
1354pub fn BasicSecureTextField(
1359 state: Rc<RefCell<TextFieldState>>,
1360 modifier: repose_core::Modifier,
1361 config: TextFieldConfig,
1362) -> repose_core::View {
1363 let mask = config.text_obfuscation_character;
1364 let secure_config = TextFieldConfig {
1365 line_limits: repose_core::TextFieldLineLimits::SingleLine,
1366 keyboard_options: repose_core::KeyboardOptions::SECURE_TEXT_FIELD,
1367 visual_transformation: match config.text_obfuscation_mode {
1368 repose_core::TextObfuscationMode::Visible => None,
1369 _ => Some(Rc::new(repose_core::PasswordVisualTransformation { mask })
1370 as Rc<dyn repose_core::VisualTransformation>),
1371 },
1372 ..config
1373 };
1374 BasicTextField(state, modifier, "", secure_config)
1375}
1376
1377#[derive(Clone, Debug)]
1378pub struct TextAreaLayout {
1379 pub ranges: Vec<(usize, usize)>,
1380 pub line_h_px: f32,
1381}
1382
1383pub fn layout_text_area(
1384 text: &str,
1385 font_px: f32,
1386 wrap_w_px: f32,
1387 font_weight: u16,
1388 font_style: u8,
1389 letter_spacing: f32,
1390 font_variation_settings: Option<&str>,
1391) -> TextAreaLayout {
1392 let line_h = font_px;
1393 let (ranges, _) = repose_text::wrap_line_ranges(
1394 text,
1395 font_px,
1396 wrap_w_px.max(1.0),
1397 None,
1398 true,
1399 font_weight,
1400 font_style,
1401 letter_spacing,
1402 font_variation_settings,
1403 );
1404 TextAreaLayout {
1405 ranges,
1406 line_h_px: line_h,
1407 }
1408}
1409
1410fn locate_byte_in_ranges(ranges: &[(usize, usize)], b: usize) -> (usize, usize, usize) {
1412 if ranges.is_empty() {
1413 return (0, 0, b);
1414 }
1415 for (i, (s, e)) in ranges.iter().enumerate() {
1416 if b < *s {
1417 if i == 0 {
1418 return (0, 0, b);
1419 }
1420 let (ps, pe) = ranges[i - 1];
1421 let local = pe.saturating_sub(ps);
1422 return (i - 1, local, ps + local);
1423 }
1424 if b < *e {
1425 let local = b.saturating_sub(*s).min(e.saturating_sub(*s));
1426 return (i, local, *s + local);
1427 }
1428 if b == *e {
1429 if let Some((ns, _ne)) = ranges.get(i + 1)
1430 && *ns == b
1431 {
1432 return (i + 1, 0, b);
1433 }
1434 let local = e.saturating_sub(*s);
1435 return (i, local, *s + local);
1436 }
1437 }
1438 let (ls, le) = ranges[ranges.len() - 1];
1439 let local = le.saturating_sub(ls);
1440 (ranges.len() - 1, local, ls + local)
1441}
1442
1443pub fn caret_xy_for_byte(
1445 text: &str,
1446 font_px: f32,
1447 wrap_w_px: f32,
1448 byte: usize,
1449) -> (f32, f32, usize) {
1450 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1451 let (ranges, line_h) = (&layout.ranges, layout.line_h_px);
1452 let (li, local, _) = locate_byte_in_ranges(ranges, byte);
1453 let (s, e) = ranges.get(li).copied().unwrap_or((0, 0));
1454 let line = &text[s..e];
1455 let m = measure_text(line, font_px, TextMeasureConfig::default());
1456 let ci = byte_to_char_index(&m, local);
1457 let x = m.positions.get(ci).copied().unwrap_or(0.0);
1458 let y = (li as f32) * line_h;
1459 (x, y, li)
1460}
1461
1462pub fn index_for_xy_bytes(text: &str, font_px: f32, wrap_w_px: f32, x_px: f32, y_px: f32) -> usize {
1464 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1465 let li = ((y_px / layout.line_h_px).floor() as isize).max(0) as usize;
1466 let li = li.min(layout.ranges.len().saturating_sub(1));
1467 let (s, e) = layout.ranges.get(li).copied().unwrap_or((0, 0));
1468 let line = &text[s..e];
1469 let local = index_for_x_bytes(line, font_px, x_px.max(0.0), 400, 0);
1470 (s + local).min(text.len())
1471}
1472
1473pub fn move_caret_vertical(
1475 text: &str,
1476 font_px: f32,
1477 wrap_w_px: f32,
1478 cur_byte: usize,
1479 dir: i32, preferred_x: Option<f32>,
1481) -> (usize, f32) {
1482 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1483 if layout.ranges.is_empty() {
1484 return (cur_byte, preferred_x.unwrap_or(0.0));
1485 }
1486 let (x, _y, li) = caret_xy_for_byte(text, font_px, wrap_w_px, cur_byte);
1487 let px = preferred_x.unwrap_or(x);
1488 let mut nli = li as i32 + dir;
1489 nli = nli.clamp(0, (layout.ranges.len().saturating_sub(1)) as i32);
1490 let nli = nli as usize;
1491 let (s, e) = layout.ranges[nli];
1492 let line = &text[s..e];
1493 let local = index_for_x_bytes(line, font_px, px.max(0.0), 400, 0);
1494 ((s + local).min(text.len()), px)
1495}
1496
1497pub fn line_home_end(
1499 text: &str,
1500 font_px: f32,
1501 wrap_w_px: f32,
1502 cur_byte: usize,
1503 to_end: bool,
1504) -> usize {
1505 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1506 let (li, _local, _) = locate_byte_in_ranges(&layout.ranges, cur_byte);
1507 let (s, e) = layout.ranges.get(li).copied().unwrap_or((0, 0));
1508 if to_end { e } else { s }
1509}
1510
1511fn clamp_to_char_boundary(s: &str, i: usize) -> usize {
1512 if i >= s.len() {
1513 return s.len();
1514 }
1515 if s.is_char_boundary(i) {
1516 return i;
1517 }
1518 let mut j = i;
1519 while j > 0 && !s.is_char_boundary(j) {
1520 j -= 1;
1521 }
1522 j
1523}
1524
1525fn char_to_byte(s: &str, ci: usize) -> usize {
1526 if ci == 0 {
1527 0
1528 } else {
1529 s.char_indices().nth(ci).map(|(i, _)| i).unwrap_or(s.len())
1530 }
1531}
1532
1533pub(crate) fn paint_text_field(
1545 scene: &mut Scene,
1546 rect: repose_core::Rect,
1547 text_input: &TextInputConfig,
1548 state: Option<&Rc<RefCell<TextFieldState>>>,
1549 is_focused: bool,
1550 clip_rounded: Option<[f32; 4]>,
1551 alpha_accum: f32,
1552) {
1553 let ts = text_input.text_style.clone().unwrap_or_default();
1554 let font_size_dp = if ts.font_size != 0.0 {
1555 ts.font_size
1556 } else {
1557 TF_FONT_DP
1558 };
1559 let font_val = dp_to_px(font_size_dp) * locals::text_scale().0;
1560 let line_h = if ts.line_height != 0.0 {
1561 dp_to_px(ts.line_height) * locals::text_scale().0
1562 } else if text_input.multiline {
1563 0.0 } else {
1565 font_val };
1567 let text_off_y = (rect.h - line_h.max(font_val)) / 2.0;
1568
1569 let clip_radius = clip_rounded.unwrap_or([0.0; 4]).map(dp_to_px);
1570 scene.nodes.push(SceneNode::PushClip {
1571 rect,
1572 radius: clip_radius,
1573 op: repose_core::ClipOp::Intersect,
1574 });
1575
1576 let th = locals::theme();
1577 let show_selection = text_input.enabled;
1578 let show_cursor = text_input.enabled && !text_input.read_only;
1579 let cursor_color = text_input.cursor_color.unwrap_or(th.on_surface);
1580 let rendered_by_vt = |original: &str| -> String {
1581 if let Some(ref vt) = text_input.visual_transformation {
1582 let annotated = repose_core::AnnotatedString::new(original.to_string(), vec![]);
1583 vt.filter(&annotated).text.text
1584 } else {
1585 original.to_string()
1586 }
1587 };
1588
1589 if let Some(state_rc) = state {
1590 let st = state_rc.borrow();
1591
1592 if !text_input.multiline {
1593 let measure_for = if text_input.visual_transformation.is_some() && !st.text.is_empty() {
1595 rendered_by_vt(&st.text)
1596 } else {
1597 st.text.clone()
1598 };
1599 let has_vt = text_input.visual_transformation.is_some();
1600 let m = measure_text(
1601 &measure_for,
1602 font_val,
1603 TextMeasureConfig {
1604 font_family: ts.font_family,
1605 font_weight: ts.font_weight.unwrap_or(400),
1606 font_style: ts.font_style.unwrap_or(0),
1607 letter_spacing: ts.letter_spacing,
1608 font_variation_settings: None,
1609 },
1610 );
1611
1612 if show_selection && st.selection.start != st.selection.end {
1614 let start_off = if has_vt {
1615 original_offset_to_display(&st.text, &measure_for, st.selection.start)
1616 } else {
1617 st.selection.start
1618 };
1619 let end_off = if has_vt {
1620 original_offset_to_display(&st.text, &measure_for, st.selection.end)
1621 } else {
1622 st.selection.end
1623 };
1624 let sx = m
1625 .positions
1626 .get(byte_to_char_index(&m, start_off))
1627 .copied()
1628 .unwrap_or(0.0)
1629 - st.scroll_offset;
1630 let ex = m
1631 .positions
1632 .get(byte_to_char_index(&m, end_off))
1633 .copied()
1634 .unwrap_or(sx)
1635 - st.scroll_offset;
1636 let selection = th.focus.with_alpha_f32(85.0 / 255.0);
1637 let vis_x = sx.max(0.0);
1638 let vis_ex = ex.max(0.0);
1639 scene.nodes.push(SceneNode::Rect {
1640 rect: repose_core::Rect {
1641 x: rect.x + vis_x,
1642 y: rect.y + text_off_y,
1643 w: (vis_ex - vis_x).max(0.0),
1644 h: line_h.max(font_val),
1645 },
1646 brush: Brush::Solid(selection),
1647 radius: [0.0; 4],
1648 });
1649 }
1650
1651 if let Some(comp) = st.composition.clone() {
1653 let cs = if has_vt {
1654 original_offset_to_display(&st.text, &measure_for, comp.start)
1655 } else {
1656 comp.start
1657 };
1658 let ce = if has_vt {
1659 original_offset_to_display(&st.text, &measure_for, comp.end)
1660 } else {
1661 comp.end
1662 };
1663 let sx = m
1664 .positions
1665 .get(byte_to_char_index(&m, cs))
1666 .copied()
1667 .unwrap_or(0.0)
1668 - st.scroll_offset;
1669 let ex = m
1670 .positions
1671 .get(byte_to_char_index(&m, ce))
1672 .copied()
1673 .unwrap_or(sx)
1674 - st.scroll_offset;
1675 let y = rect.y + text_off_y + line_h.max(font_val) - dp_to_px(2.0);
1676 scene.nodes.push(SceneNode::Rect {
1677 rect: repose_core::Rect {
1678 x: rect.x + sx.max(0.0),
1679 y,
1680 w: (ex - sx).max(dp_to_px(2.0)),
1681 h: dp_to_px(2.0),
1682 },
1683 brush: Brush::Solid(th.focus),
1684 radius: [0.0; 4],
1685 });
1686 }
1687
1688 let txt_col = if st.text.is_empty() {
1690 ts.color.unwrap_or(th.on_surface_variant)
1691 } else {
1692 ts.color.unwrap_or(th.on_surface)
1693 };
1694 let render_txt = if st.text.is_empty() {
1695 text_input.hint.clone()
1696 } else {
1697 rendered_by_vt(&st.text)
1698 };
1699 scene.nodes.push(SceneNode::Text {
1700 rect: repose_core::Rect {
1701 x: rect.x - st.scroll_offset,
1702 y: rect.y + text_off_y,
1703 w: rect.w,
1704 h: line_h,
1705 },
1706 text: Arc::from(render_txt),
1707 color: mul_alpha_color(txt_col, alpha_accum),
1708 size: font_val,
1709 font_family: ts.font_family,
1710 text_align: ts.text_align,
1711 font_weight: FontWeight(ts.font_weight.unwrap_or(400)),
1712 font_style: match ts.font_style.unwrap_or(0) {
1713 1 => FontStyle::Italic,
1714 _ => FontStyle::Normal,
1715 },
1716 text_decoration: ts.text_decoration.unwrap_or_default(),
1717 letter_spacing: ts.letter_spacing,
1718 line_height: ts.line_height,
1719 extra_style: Default::default(),
1720 url: None,
1721 font_variation_settings: None,
1722 });
1723
1724 if show_cursor
1726 && is_focused
1727 && st.selection.start == st.selection.end
1728 && st.caret_visible()
1729 {
1730 let caret_off = if has_vt {
1731 original_offset_to_display(&st.text, &measure_for, st.selection.end)
1732 } else {
1733 st.selection.end
1734 };
1735 let cx = m
1736 .positions
1737 .get(byte_to_char_index(&m, caret_off))
1738 .copied()
1739 .unwrap_or(0.0)
1740 - st.scroll_offset;
1741 let cursor_y = rect.y + text_off_y + (line_h.max(font_val) - font_val) / 2.0;
1742 scene.nodes.push(SceneNode::Rect {
1743 rect: repose_core::Rect {
1744 x: rect.x + cx.max(0.0),
1745 y: cursor_y,
1746 w: dp_to_px(1.0),
1747 h: font_val,
1748 },
1749 brush: Brush::Solid(cursor_color),
1750 radius: [0.0; 4],
1751 });
1752 }
1753 } else {
1754 let render_text = if st.text.is_empty() {
1756 st.text.clone()
1757 } else if let Some(ref vt) = text_input.visual_transformation {
1758 let annotated = repose_core::AnnotatedString::new(st.text.clone(), vec![]);
1759 vt.filter(&annotated).text.text
1760 } else {
1761 st.text.clone()
1762 };
1763 let layout = layout_text_area(
1764 &render_text,
1765 font_val,
1766 rect.w.max(1.0),
1767 400,
1768 0,
1769 ts.letter_spacing,
1770 None,
1771 );
1772 let lh = layout.line_h_px;
1773 let max_line_count = text_input.max_lines.unwrap_or(usize::MAX);
1774
1775 if st.text.is_empty() {
1777 scene.nodes.push(SceneNode::Text {
1778 rect: repose_core::Rect {
1779 x: rect.x,
1780 y: rect.y,
1781 w: rect.w,
1782 h: line_h,
1783 },
1784 text: Arc::from(text_input.hint.clone()),
1785 color: mul_alpha_color(ts.color.unwrap_or(th.on_surface_variant), alpha_accum),
1786 size: font_val,
1787 font_family: ts.font_family,
1788 text_align: ts.text_align,
1789 font_weight: FontWeight(ts.font_weight.unwrap_or(400)),
1790 font_style: match ts.font_style.unwrap_or(0) {
1791 1 => FontStyle::Italic,
1792 _ => FontStyle::Normal,
1793 },
1794 text_decoration: ts.text_decoration.unwrap_or_default(),
1795 letter_spacing: ts.letter_spacing,
1796 line_height: ts.line_height,
1797 extra_style: Default::default(),
1798 url: None,
1799 font_variation_settings: None,
1800 });
1801 } else {
1802 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1803 if i >= max_line_count {
1804 break;
1805 }
1806 let ln = render_text[s..e].to_string();
1807 let draw_y = rect.y + (i as f32) * lh - st.scroll_offset_y;
1808 if draw_y + lh < rect.y - 1.0 || draw_y > rect.y + rect.h + 1.0 {
1809 continue;
1810 }
1811 scene.nodes.push(SceneNode::Text {
1812 rect: repose_core::Rect {
1813 x: rect.x,
1814 y: draw_y,
1815 w: rect.w,
1816 h: lh,
1817 },
1818 text: Arc::<str>::from(ln),
1819 color: mul_alpha_color(ts.color.unwrap_or(th.on_surface), alpha_accum),
1820 size: font_val,
1821 font_family: ts.font_family,
1822 text_align: ts.text_align,
1823 font_weight: FontWeight(ts.font_weight.unwrap_or(400)),
1824 font_style: match ts.font_style.unwrap_or(0) {
1825 1 => FontStyle::Italic,
1826 _ => FontStyle::Normal,
1827 },
1828 text_decoration: ts.text_decoration.unwrap_or_default(),
1829 letter_spacing: ts.letter_spacing,
1830 line_height: ts.line_height,
1831 extra_style: Default::default(),
1832 url: None,
1833 font_variation_settings: None,
1834 });
1835 }
1836 }
1837
1838 if show_selection && st.selection.start != st.selection.end {
1840 let sel_a_orig: usize = st.selection.start.min(st.selection.end);
1841 let sel_b_orig: usize = st.selection.start.max(st.selection.end);
1842 let has_vt = text_input.visual_transformation.is_some();
1843 let sel_a = if has_vt {
1844 original_offset_to_display(&st.text, &render_text, sel_a_orig)
1845 } else {
1846 sel_a_orig
1847 };
1848 let sel_b = if has_vt {
1849 original_offset_to_display(&st.text, &render_text, sel_b_orig)
1850 } else {
1851 sel_b_orig
1852 };
1853 let selection = th.focus.with_alpha_f32(85.0 / 255.0);
1854 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1855 if i >= max_line_count {
1856 break;
1857 }
1858 let os = sel_a.max(s);
1859 let oe = sel_b.min(e);
1860 if os >= oe {
1861 continue;
1862 }
1863 let ln = &render_text[s..e];
1864 let m = measure_text(
1865 ln,
1866 font_val,
1867 TextMeasureConfig {
1868 font_family: ts.font_family,
1869 font_weight: ts.font_weight.unwrap_or(400),
1870 font_style: ts.font_style.unwrap_or(0),
1871 letter_spacing: ts.letter_spacing,
1872 font_variation_settings: None,
1873 },
1874 );
1875 let ls = os - s;
1876 let le = oe - s;
1877 let sx = m
1878 .positions
1879 .get(byte_to_char_index(&m, ls))
1880 .copied()
1881 .unwrap_or(0.0);
1882 let ex = m
1883 .positions
1884 .get(byte_to_char_index(&m, le))
1885 .copied()
1886 .unwrap_or(sx);
1887 let draw_y = rect.y + (i as f32) * lh - st.scroll_offset_y;
1888 scene.nodes.push(SceneNode::Rect {
1889 rect: repose_core::Rect {
1890 x: rect.x + sx,
1891 y: draw_y,
1892 w: (ex - sx).max(0.0),
1893 h: lh,
1894 },
1895 brush: Brush::Solid(selection),
1896 radius: [0.0; 4],
1897 });
1898 }
1899 }
1900
1901 if let Some(comp) = st.composition.clone() {
1904 let has_vt = text_input.visual_transformation.is_some();
1905 let comp_a = if has_vt {
1906 original_offset_to_display(&st.text, &render_text, comp.start)
1907 } else {
1908 comp.start
1909 };
1910 let comp_b = if has_vt {
1911 original_offset_to_display(&st.text, &render_text, comp.end)
1912 } else {
1913 comp.end
1914 };
1915 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1916 if i >= max_line_count {
1917 break;
1918 }
1919 let os = comp_a.max(s);
1920 let oe = comp_b.min(e);
1921 if os >= oe {
1922 continue;
1923 }
1924 let ln = &render_text[s..e];
1925 let m = measure_text(
1926 ln,
1927 font_val,
1928 TextMeasureConfig {
1929 font_family: ts.font_family,
1930 font_weight: ts.font_weight.unwrap_or(400),
1931 font_style: ts.font_style.unwrap_or(0),
1932 letter_spacing: ts.letter_spacing,
1933 font_variation_settings: None,
1934 },
1935 );
1936 let ls = os - s;
1937 let le = oe - s;
1938 let sx = m
1939 .positions
1940 .get(byte_to_char_index(&m, ls))
1941 .copied()
1942 .unwrap_or(0.0);
1943 let ex = m
1944 .positions
1945 .get(byte_to_char_index(&m, le))
1946 .copied()
1947 .unwrap_or(sx);
1948 let draw_y = rect.y + (i as f32) * lh - st.scroll_offset_y;
1949 scene.nodes.push(SceneNode::Rect {
1950 rect: repose_core::Rect {
1951 x: rect.x + sx,
1952 y: draw_y + lh - dp_to_px(2.0),
1953 w: (ex - sx).max(dp_to_px(2.0)),
1954 h: dp_to_px(2.0),
1955 },
1956 brush: Brush::Solid(th.focus),
1957 radius: [0.0; 4],
1958 });
1959 }
1960 }
1961
1962 if show_cursor
1964 && is_focused
1965 && st.selection.start == st.selection.end
1966 && st.caret_visible()
1967 {
1968 let caret_orig = st.selection.end.min(st.text.len());
1969 let has_vt = text_input.visual_transformation.is_some();
1970 let caret = if has_vt {
1971 original_offset_to_display(&st.text, &render_text, caret_orig)
1972 } else {
1973 caret_orig
1974 };
1975 let (cx, cy, _li) =
1976 caret_xy_for_byte(&render_text, font_val, rect.w.max(1.0), caret);
1977 let draw_x = rect.x + cx;
1978 let draw_y = rect.y + cy - st.scroll_offset_y;
1979 scene.nodes.push(SceneNode::Rect {
1980 rect: repose_core::Rect {
1981 x: draw_x,
1982 y: draw_y + (lh - font_val) / 2.0,
1983 w: dp_to_px(1.0),
1984 h: font_val,
1985 },
1986 brush: Brush::Solid(cursor_color),
1987 radius: [0.0; 4],
1988 });
1989 }
1990 }
1991 } else {
1992 if text_input.value.is_empty() {
1994 let hint_y = if text_input.multiline {
1995 rect.y
1996 } else {
1997 rect.y + text_off_y
1998 };
1999 scene.nodes.push(SceneNode::Text {
2000 rect: repose_core::Rect {
2001 x: rect.x,
2002 y: hint_y,
2003 w: rect.w,
2004 h: line_h,
2005 },
2006 text: Arc::from(text_input.hint.clone()),
2007 color: mul_alpha_color(th.on_surface_variant, alpha_accum),
2008 size: font_val,
2009 font_family: None,
2010 text_align: TextAlign::Unspecified,
2011 font_weight: FontWeight::NORMAL,
2012 font_style: FontStyle::Normal,
2013 text_decoration: ts.text_decoration.unwrap_or_default(),
2014 letter_spacing: 0.0,
2015 line_height: 0.0,
2016 extra_style: Default::default(),
2017 url: None,
2018 font_variation_settings: None,
2019 });
2020 } else if text_input.multiline {
2021 let render_text = if text_input.value.is_empty() {
2022 text_input.value.clone()
2023 } else if let Some(ref vt) = text_input.visual_transformation {
2024 let annotated = repose_core::AnnotatedString::new(text_input.value.clone(), vec![]);
2025 vt.filter(&annotated).text.text
2026 } else {
2027 text_input.value.clone()
2028 };
2029 let layout = layout_text_area(
2030 &render_text,
2031 font_val,
2032 rect.w.max(1.0),
2033 400,
2034 0,
2035 ts.letter_spacing,
2036 None,
2037 );
2038 let lh = layout.line_h_px;
2039 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
2040 let ln = render_text[s..e].to_string();
2041 let draw_y = rect.y + (i as f32) * lh;
2042 if draw_y + lh < rect.y - 1.0 || draw_y > rect.y + rect.h + 1.0 {
2043 continue;
2044 }
2045 scene.nodes.push(SceneNode::Text {
2046 rect: repose_core::Rect {
2047 x: rect.x,
2048 y: draw_y,
2049 w: rect.w,
2050 h: lh,
2051 },
2052 text: Arc::<str>::from(ln),
2053 color: mul_alpha_color(th.on_surface, alpha_accum),
2054 size: font_val,
2055 font_family: None,
2056 text_align: TextAlign::Unspecified,
2057 font_weight: FontWeight::NORMAL,
2058 font_style: FontStyle::Normal,
2059 text_decoration: ts.text_decoration.unwrap_or_default(),
2060 letter_spacing: 0.0,
2061 line_height: 0.0,
2062 extra_style: Default::default(),
2063 url: None,
2064 font_variation_settings: None,
2065 });
2066 }
2067 } else {
2068 scene.nodes.push(SceneNode::Text {
2069 rect: repose_core::Rect {
2070 x: rect.x,
2071 y: rect.y + text_off_y,
2072 w: rect.w,
2073 h: line_h,
2074 },
2075 text: Arc::from(rendered_by_vt(&text_input.value)),
2076 color: mul_alpha_color(th.on_surface, alpha_accum),
2077 size: font_val,
2078 font_family: None,
2079 text_align: TextAlign::Unspecified,
2080 font_weight: FontWeight::NORMAL,
2081 font_style: FontStyle::Normal,
2082 text_decoration: ts.text_decoration.unwrap_or_default(),
2083 letter_spacing: 0.0,
2084 line_height: 0.0,
2085 extra_style: Default::default(),
2086 url: None,
2087 font_variation_settings: None,
2088 });
2089 }
2090 }
2091
2092 if let Some(ref cb) = text_input.on_text_layout {
2094 let (
2095 line_count,
2096 content_w,
2097 content_h,
2098 first_baseline,
2099 last_baseline,
2100 did_overflow_w,
2101 did_overflow_h,
2102 lines,
2103 ) = if let Some(state_rc) = state {
2104 let st = state_rc.borrow();
2105 let display = if st.text.is_empty() {
2106 text_input.hint.clone()
2107 } else if let Some(ref vt) = text_input.visual_transformation {
2108 let annotated = repose_core::AnnotatedString::new(st.text.clone(), vec![]);
2109 vt.filter(&annotated).text.text
2110 } else {
2111 st.text.clone()
2112 };
2113 if text_input.multiline {
2114 let l = layout_text_area(
2115 &display,
2116 font_val,
2117 rect.w.max(1.0),
2118 400,
2119 0,
2120 ts.letter_spacing,
2121 None,
2122 );
2123 let lc = l.ranges.len();
2124 let cw = rect.w.max(0.0);
2125 let ch = (lc as f32 * l.line_h_px).max(0.0);
2126 let line_infos: Vec<_> = l
2127 .ranges
2128 .iter()
2129 .enumerate()
2130 .map(|(i, &(s, e))| {
2131 let top = i as f32 * l.line_h_px;
2132 let bottom = top + l.line_h_px;
2133 let line_text = &display[s..e];
2134 let m = measure_text(line_text, font_val, TextMeasureConfig::default());
2135 let line_w = m.positions.last().copied().unwrap_or(0.0);
2136 TextLineInfo {
2137 start: s,
2138 end: e,
2139 top,
2140 baseline: top + l.line_h_px * 0.8,
2141 bottom,
2142 left: 0.0,
2143 right: line_w,
2144 width: line_w,
2145 }
2146 })
2147 .collect();
2148 let fb = line_infos.first().map(|l| l.baseline).unwrap_or(0.0);
2149 let lb = line_infos.last().map(|l| l.baseline).unwrap_or(0.0);
2150 (lc, cw, ch, fb, lb, cw > rect.w, ch > rect.h, line_infos)
2151 } else {
2152 let m = measure_text(&display, font_val, TextMeasureConfig::default());
2153 let w = m.positions.last().copied().unwrap_or(0.0);
2154 let top = 0.0;
2155 let bottom = line_h.max(font_val);
2156 let baseline = bottom * 0.8;
2157 let line_info = TextLineInfo {
2158 start: 0,
2159 end: display.len(),
2160 top,
2161 baseline,
2162 bottom,
2163 left: 0.0,
2164 right: w,
2165 width: w,
2166 };
2167 (
2168 1,
2169 w.max(0.0),
2170 bottom,
2171 baseline,
2172 baseline,
2173 w > rect.w,
2174 bottom > rect.h,
2175 vec![line_info],
2176 )
2177 }
2178 } else {
2179 (0, 0.0, 0.0, 0.0, 0.0, false, false, vec![])
2180 };
2181 cb(&repose_core::TextLayoutResult {
2182 line_count,
2183 width_px: content_w,
2184 height_px: content_h,
2185 first_baseline,
2186 last_baseline,
2187 did_overflow_width: did_overflow_w,
2188 did_overflow_height: did_overflow_h,
2189 lines,
2190 });
2191 }
2192
2193 scene.nodes.push(SceneNode::PopClip);
2194}
2195
2196fn text_field_view(
2200 modifier: Modifier,
2201 hint: String,
2202 value: String,
2203 multiline: bool,
2204 on_change: Option<Rc<dyn Fn(String)>>,
2205 on_submit: Option<Rc<dyn Fn(String)>>,
2206 visual_transformation: Option<Rc<dyn repose_core::VisualTransformation>>,
2207 keyboard_type: repose_core::KeyboardType,
2208 capitalization: repose_core::KeyboardCapitalization,
2209 ime_action: repose_core::ImeAction,
2210 auto_correct_enabled: Option<bool>,
2211 enabled: bool,
2212 read_only: bool,
2213 max_lines: Option<usize>,
2214 min_lines: usize,
2215 cursor_color: Option<Color>,
2216 on_text_layout: Option<Rc<dyn Fn(&repose_core::TextLayoutResult)>>,
2217 text_style: repose_core::TextStyle,
2218 keyboard_actions: repose_core::KeyboardActions,
2219 interaction_source: Option<repose_core::MutableInteractionSource>,
2220 focus_tracker: Option<Rc<Cell<bool>>>,
2221 line_limits: Option<repose_core::TextFieldLineLimits>,
2222 _input_transformation: Option<Rc<dyn repose_core::InputTransformation>>,
2223 _output_transformation: Option<Rc<dyn repose_core::OutputTransformation>>,
2224 _decoration_box: Option<Rc<dyn Fn(repose_core::View) -> repose_core::View>>,
2225 _codepoint_transformation: Option<repose_core::CodepointTransformation>,
2226) -> View {
2227 let modif = modifier.text_input(TextInputConfig {
2228 hint,
2229 multiline,
2230 on_change,
2231 on_submit,
2232 focus_tracker,
2233 value,
2234 visual_transformation,
2235 keyboard_type,
2236 capitalization,
2237 ime_action,
2238 auto_correct_enabled,
2239 enabled,
2240 read_only,
2241 max_lines,
2242 min_lines,
2243 cursor_color,
2244 on_text_layout,
2245 text_style: Some(text_style),
2246 keyboard_actions: Some(keyboard_actions),
2247 interaction_source: interaction_source.as_ref().map(|s| s.source()),
2248 line_limits,
2249 });
2250
2251 View::new(0, ViewKind::Box)
2252 .modifier(modif)
2253 .semantics(Semantics {
2254 role: Role::TextField,
2255 label: None,
2256 focused: false,
2257 enabled,
2258 selectable_group: false,
2259 })
2260}
2261
2262#[cfg(test)]
2263mod tests {
2264 use super::*;
2265
2266 #[test]
2267 fn test_index_for_x_bytes_grapheme() {
2268 let t = "A👍🏽B";
2269 let font_px = 16.0; let m = measure_text(t, font_px, TextMeasureConfig::default());
2271 for i in 0..m.byte_offsets.len() - 1 {
2272 let b = m.byte_offsets[i];
2273 let _ = &t[..b];
2274 }
2275 }
2276
2277 fn delete_op(
2278 index: usize,
2279 pre_text: &str,
2280 pre_selection: Range<usize>,
2281 post_selection: Range<usize>,
2282 ) -> TextUndoOp {
2283 TextUndoOp {
2284 index,
2285 pre_text: pre_text.to_string(),
2286 post_text: String::new(),
2287 pre_selection,
2288 post_selection,
2289 time: Instant::now(),
2290 can_merge: true,
2291 }
2292 }
2293
2294 #[test]
2295 fn deletion_type_collapsed_post_selection_is_backspace() {
2296 let op = delete_op(2, "c", 3..3, 2..2);
2298 assert_eq!(op.deletion_type(), TextDeleteType::Start);
2299 }
2300
2301 #[test]
2302 fn deletion_type_collapsed_post_selection_is_delete_forward() {
2303 let op = delete_op(3, "c", 3..3, 3..3);
2305 assert_eq!(op.deletion_type(), TextDeleteType::End);
2306 }
2307
2308 #[test]
2309 fn deletion_type_range_post_selection_is_not_by_user() {
2310 let op = delete_op(2, "de", 2..4, 3..5);
2314 assert_eq!(op.deletion_type(), TextDeleteType::NotByUser);
2315 }
2316
2317 #[test]
2318 fn backspace_ops_merge() {
2319 let a = delete_op(2, "c", 3..3, 2..2);
2321 let b = delete_op(1, "b", 2..2, 1..1);
2322 let merged = a
2323 .try_merge(&b)
2324 .expect("consecutive backspaces should merge");
2325 assert_eq!(merged.index, 1);
2326 assert_eq!(merged.pre_text, "bc");
2327 }
2328
2329 #[test]
2330 fn selection_delete_does_not_merge_with_backspace() {
2331 let backspace = delete_op(2, "c", 3..3, 2..2);
2334 let selection = delete_op(2, "de", 2..4, 2..2);
2335 assert_eq!(selection.deletion_type(), TextDeleteType::Inner);
2336 assert!(backspace.try_merge(&selection).is_none());
2337 }
2338}