1use repose_core::*;
41use std::cell::{Cell, RefCell};
42use std::collections::HashMap;
43use std::ops::Range;
44use std::rc::{Rc, Weak};
45use std::sync::Arc;
46use std::sync::atomic::{AtomicU64, Ordering};
47use unicode_segmentation::UnicodeSegmentation;
48use web_time::Duration;
49use web_time::Instant;
50
51use crate::layout::mul_alpha_color;
52use crate::text_selection::{
53 SELECTION_BACKGROUND_ALPHA, SelectionLine, push_selection, selection_brush, selection_rects,
54};
55
56static NEXT_TEXTFIELD_TRANSFORM_ID: AtomicU64 = AtomicU64::new(1);
57
58thread_local! {
59 static TEXTFIELD_STATES: RefCell<HashMap<u64, Weak<RefCell<TextFieldState>>>> =
60 RefCell::new(HashMap::new());
61 static TEXTFIELD_TRANSFORMS: RefCell<HashMap<u64, TextFieldTransformEntry>> =
62 RefCell::new(HashMap::new());
63 static TEXTFIELD_TRANSFORM_MARKERS: RefCell<HashMap<u64, u64>> =
64 RefCell::new(HashMap::new());
65}
66
67#[derive(Clone)]
68struct TextFieldTransforms {
69 visual: Option<Rc<dyn VisualTransformation>>,
70 input: Option<Rc<dyn InputTransformation>>,
71 output: Option<Rc<dyn OutputTransformation>>,
72 codepoint: Option<Rc<CodepointTransformation>>,
73}
74
75struct TextFieldTransformEntry {
76 transforms: TextFieldTransforms,
77 marker: Weak<TextFieldTransformMarker>,
78}
79
80#[derive(Debug)]
81struct TextFieldTransformMarker(u64);
82
83impl VisualTransformation for TextFieldTransformMarker {
84 fn filter(&self, text: &AnnotatedString) -> TransformedText {
85 let transforms = TEXTFIELD_TRANSFORMS.with(|all| {
86 all.borrow()
87 .get(&self.0)
88 .map(|entry| entry.transforms.clone())
89 });
90 let Some(transforms) = transforms else {
91 return TransformedText::new(text.clone(), Box::new(IdentityOffsetMapping));
92 };
93 let mut transformed = transforms.visual.as_ref().map_or_else(
94 || TransformedText::new(text.clone(), Box::new(IdentityOffsetMapping)),
95 |visual| visual.filter(text),
96 );
97 if let Some(output) = &transforms.output {
98 let before = transformed.text.text.clone();
99 let mut buffer = TransformationBuffer::new(before.clone(), 0..before.len());
100 output.transform_output(&mut buffer);
101 let after = buffer.text;
102 let stage = CodepointOffsetMapping::new(&before, &after);
103 transformed.offset_mapping = Box::new(ComposedOffsetMapping {
104 first: transformed.offset_mapping,
105 second: Box::new(stage),
106 });
107 transformed.text.text = after;
108 }
109 if let Some(codepoint) = &transforms.codepoint {
110 let before = transformed.text.text.clone();
111 let after = before
112 .chars()
113 .enumerate()
114 .map(|(index, ch)| codepoint.transform(index, ch))
115 .collect::<String>();
116 let stage = CodepointOffsetMapping::new(&before, &after);
117 transformed.offset_mapping = Box::new(ComposedOffsetMapping {
118 first: transformed.offset_mapping,
119 second: Box::new(stage),
120 });
121 transformed.text.text = after;
122 }
123 transformed
124 }
125}
126
127pub fn set_textfield_state(key: u64, state: Rc<RefCell<TextFieldState>>) {
128 TEXTFIELD_STATES.with(|states| {
129 states.borrow_mut().insert(key, Rc::downgrade(&state));
130 });
131}
132
133pub fn get_textfield_state(key: u64) -> Option<Rc<RefCell<TextFieldState>>> {
134 TEXTFIELD_STATES.with(|states| states.borrow().get(&key).and_then(Weak::upgrade))
135}
136
137pub(crate) fn prune_textfield_registries() {
138 TEXTFIELD_STATES.with(|states| {
139 states
140 .borrow_mut()
141 .retain(|_, state| state.strong_count() != 0);
142 });
143 let mut live = std::collections::HashSet::new();
144 TEXTFIELD_TRANSFORMS.with(|transforms| {
145 transforms.borrow_mut().retain(|id, entry| {
146 let keep = entry.marker.strong_count() != 0;
147 if keep {
148 live.insert(*id);
149 }
150 keep
151 });
152 });
153 TEXTFIELD_TRANSFORM_MARKERS.with(|markers| {
154 markers.borrow_mut().retain(|_, id| live.contains(id));
155 });
156}
157
158pub fn ensure_caret_visible(state: &mut TextFieldState, multiline: bool) {
159 let metrics = TextFieldMetrics::default();
160 ensure_caret_visible_with_metrics(state, multiline, &metrics);
161}
162
163pub fn ensure_caret_visible_with_metrics(
164 state: &mut TextFieldState,
165 multiline: bool,
166 metrics: &TextFieldMetrics,
167) {
168 let caret = state.caret_index();
169 let (display, display_caret) = if let Some(transformation) = &state.visual_transformation {
170 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
171 let transformed = transformation.filter(&annotated);
172 let offset = transformed.offset_mapping.original_to_transformed(caret);
173 (transformed.text.text, offset)
174 } else {
175 (state.text.clone(), caret)
176 };
177 let wrap_width = state.inner_width.max(1.0);
178 if multiline {
179 let (cx, cy, _) =
180 caret_xy_for_byte_with_metrics(&display, wrap_width, display_caret, metrics);
181 state.ensure_caret_visible_xy(
182 cx,
183 cy,
184 state.inner_width,
185 state.inner_height,
186 Dp(2.0).to_px().0,
187 );
188 } else {
189 let measured = measure_text(&display, metrics.font_px, metrics.measure_config());
190 let caret_x = measured
191 .positions
192 .get(byte_to_char_index(&measured, display_caret))
193 .copied()
194 .unwrap_or(0.0);
195 state.ensure_caret_visible(caret_x, wrap_width, Dp(2.0).to_px().0);
196 }
197}
198
199const TEXT_UNDO_CAPACITY: usize = 100;
201
202const SNAPSHOTS_INTERVAL_MILLIS: u128 = 5000;
204
205#[derive(Clone, Copy, Debug, PartialEq)]
207enum TextEditType {
208 Insert,
209 Delete,
210 Replace,
211}
212
213#[derive(Clone, Copy, Debug, PartialEq)]
215enum TextDeleteType {
216 Start, End, Inner, NotByUser,
220}
221
222#[derive(Clone, Debug)]
224pub struct TextUndoOp {
225 pub index: usize,
227 pub pre_text: String,
229 pub post_text: String,
231 pub pre_selection: Range<usize>,
233 pub post_selection: Range<usize>,
235 pub time: Instant,
237 pub can_merge: bool,
239}
240
241impl TextUndoOp {
242 fn edit_type(&self) -> TextEditType {
243 match (self.pre_text.is_empty(), self.post_text.is_empty()) {
244 (true, true) => unreachable!("Both pre and post text cannot be empty"),
245 (true, false) => TextEditType::Insert,
246 (false, true) => TextEditType::Delete,
247 (false, false) => TextEditType::Replace,
248 }
249 }
250
251 fn is_newline(&self) -> bool {
252 self.post_text == "\n" || self.post_text == "\r\n"
253 }
254
255 fn try_merge(&self, next: &TextUndoOp) -> Option<TextUndoOp> {
257 if !self.can_merge || !next.can_merge {
258 return None;
259 }
260
261 let elapsed = next.time.saturating_duration_since(self.time);
262 if elapsed.as_millis() >= SNAPSHOTS_INTERVAL_MILLIS {
263 return None;
264 }
265
266 if self.is_newline() || next.is_newline() {
267 return None;
268 }
269
270 let self_type = self.edit_type();
271 if self_type != next.edit_type() {
272 return None;
273 }
274
275 match self_type {
276 TextEditType::Insert => {
277 if self.index + self.post_text.len() == next.index {
279 Some(TextUndoOp {
280 index: self.index,
281 pre_text: String::new(),
282 post_text: format!("{}{}", self.post_text, next.post_text),
283 pre_selection: self.pre_selection.clone(),
284 post_selection: next.post_selection.clone(),
285 time: self.time,
286 can_merge: true,
287 })
288 } else {
289 None
290 }
291 }
292 TextEditType::Delete => {
293 let self_del = self.deletion_type();
294 let next_del = next.deletion_type();
295 if self_del == next_del
297 && (self_del == TextDeleteType::Start || self_del == TextDeleteType::End)
298 {
299 if self.index == next.index + next.pre_text.len() {
300 Some(TextUndoOp {
302 index: next.index,
303 pre_text: format!("{}{}", next.pre_text, self.pre_text),
304 post_text: String::new(),
305 pre_selection: self.pre_selection.clone(),
306 post_selection: next.post_selection.clone(),
307 time: self.time,
308 can_merge: true,
309 })
310 } else if self.index == next.index {
311 Some(TextUndoOp {
313 index: self.index,
314 pre_text: format!("{}{}", self.pre_text, next.pre_text),
315 post_text: String::new(),
316 pre_selection: self.pre_selection.clone(),
317 post_selection: next.post_selection.clone(),
318 time: self.time,
319 can_merge: true,
320 })
321 } else {
322 None
323 }
324 } else {
325 None
326 }
327 }
328 TextEditType::Replace => None,
329 }
330 }
331
332 fn deletion_type(&self) -> TextDeleteType {
334 if self.edit_type() != TextEditType::Delete {
335 return TextDeleteType::NotByUser;
336 }
337 if self.post_selection.start != self.post_selection.end {
338 return TextDeleteType::NotByUser;
339 }
340 if self.pre_selection.start == self.pre_selection.end {
341 if self.pre_selection.start > self.post_selection.start {
343 TextDeleteType::Start } else {
345 TextDeleteType::End }
347 } else if self.pre_selection.start == self.post_selection.start
348 && self.pre_selection.start == self.index
349 {
350 TextDeleteType::Inner
351 } else {
352 TextDeleteType::NotByUser
353 }
354 }
355}
356
357const SCROLL_STIFFNESS: f32 = 300.0;
359const SCROLL_DAMPING: f32 = 30.0;
360
361pub const TF_FONT_SP: Sp = Sp(16.0);
364
365#[derive(Clone, Copy, Debug)]
367pub struct KeyboardOptions {
368 pub keyboard_type: repose_core::KeyboardType,
369 pub autocorrect: bool,
370 pub capitalization: repose_core::KeyboardCapitalization,
371}
372
373impl Default for KeyboardOptions {
374 fn default() -> Self {
375 Self {
376 keyboard_type: repose_core::KeyboardType::default(),
377 autocorrect: true,
378 capitalization: repose_core::KeyboardCapitalization::default(),
379 }
380 }
381}
382pub const TF_PADDING_X: Dp = Dp(8.0);
384
385pub struct TextMetrics {
386 pub positions: Vec<f32>, pub byte_offsets: Vec<usize>,
390}
391
392#[derive(Clone)]
393pub struct TextMeasureConfig {
394 pub font_family: Option<&'static str>,
395 pub font_weight: u16,
396 pub font_style: u8,
397 pub letter_spacing: f32,
398 pub font_variation_settings: Option<String>,
399}
400
401impl Default for TextMeasureConfig {
402 fn default() -> Self {
403 Self {
404 font_family: None,
405 font_weight: 400,
406 font_style: 0,
407 letter_spacing: 0.0,
408 font_variation_settings: None,
409 }
410 }
411}
412
413#[derive(Clone, Debug)]
414pub struct TextFieldMetrics {
415 pub font_px: f32,
416 pub font_family: Option<&'static str>,
417 pub font_weight: u16,
418 pub font_style: u8,
419 pub letter_spacing_px: f32,
420 pub font_variation_settings: Option<String>,
421 pub line_height_px: f32,
422}
423
424impl Default for TextFieldMetrics {
425 fn default() -> Self {
426 let font_px = TF_FONT_SP.to_px().0;
427 Self {
428 font_px,
429 font_family: None,
430 font_weight: 400,
431 font_style: 0,
432 letter_spacing_px: 0.0,
433 font_variation_settings: None,
434 line_height_px: font_px,
435 }
436 }
437}
438
439impl TextFieldMetrics {
440 pub(crate) fn from_config(config: &TextInputConfig) -> Self {
441 let style = config.text_style.clone().unwrap_or_default();
442 let font_px = if style.font_size == Sp::ZERO {
443 TF_FONT_SP.to_px().0
444 } else {
445 style.font_size.to_px().0
446 };
447 Self {
448 font_px,
449 font_family: style.font_family,
450 font_weight: style.font_weight.unwrap_or(400),
451 font_style: style.font_style.unwrap_or(0),
452 letter_spacing_px: style.letter_spacing.to_px().0,
453 font_variation_settings: style.font_variation_settings.clone(),
454 line_height_px: if style.line_height == Sp::ZERO {
455 font_px
456 } else {
457 style.line_height.to_px().0
458 },
459 }
460 }
461
462 pub fn measure_config(&self) -> TextMeasureConfig {
463 TextMeasureConfig {
464 font_family: self.font_family,
465 font_weight: self.font_weight,
466 font_style: self.font_style,
467 letter_spacing: self.letter_spacing_px,
468 font_variation_settings: self.font_variation_settings.clone(),
469 }
470 }
471}
472
473struct TransformationBuffer {
474 text: String,
475 selection: Range<usize>,
476 original_text: String,
477 original_selection: Range<usize>,
478}
479
480impl TransformationBuffer {
481 fn new(text: String, selection: Range<usize>) -> Self {
482 let selection = clamp_range(&text, selection);
483 Self {
484 original_text: text.clone(),
485 original_selection: selection.clone(),
486 text,
487 selection,
488 }
489 }
490
491 fn normalized(&mut self) {
492 self.selection = clamp_range(&self.text, self.selection.clone());
493 }
494}
495
496impl TextFieldBuffer for TransformationBuffer {
497 fn text(&self) -> &str {
498 &self.text
499 }
500
501 fn set_text(&mut self, text: &str) {
502 self.text = text.to_string();
503 self.normalized();
504 }
505
506 fn selection(&self) -> TextRange {
507 TextRange::new(self.selection.start, self.selection.end)
508 }
509
510 fn set_selection(&mut self, selection: TextRange) {
511 self.selection = clamp_range(
512 &self.text,
513 selection.start.min(selection.end)..selection.start.max(selection.end),
514 );
515 }
516
517 fn length(&self) -> usize {
518 self.text.chars().count()
519 }
520
521 fn replace(&mut self, start: usize, end: usize, text: &str) {
522 let start = byte_for_char(&self.text, start);
523 let end = byte_for_char(&self.text, end);
524 self.text.replace_range(start..end.max(start), text);
525 self.selection = clamp_range(&self.text, start..start + text.len());
526 }
527
528 fn insert(&mut self, index: usize, text: &str) {
529 let start = byte_for_char(&self.text, index);
530 self.text.insert_str(start, text);
531 self.selection = clamp_range(&self.text, start..start + text.len());
532 }
533
534 fn delete(&mut self, start: usize, end: usize) {
535 let start = byte_for_char(&self.text, start);
536 let end = byte_for_char(&self.text, end);
537 self.text.replace_range(start..end.max(start), "");
538 self.selection = clamp_range(&self.text, start..start);
539 }
540
541 fn place_cursor_before_char_at(&mut self, index: usize) {
542 let start = byte_for_char(&self.text, index);
543 self.selection = clamp_range(&self.text, start..start);
544 }
545
546 fn place_cursor_at_end(&mut self) {
547 let end = self.text.len();
548 self.selection = end..end;
549 }
550
551 fn select_all(&mut self) {
552 self.selection = 0..self.text.len();
553 }
554
555 fn revert_all_changes(&mut self) {
556 self.text = self.original_text.clone();
557 self.selection = self.original_selection.clone();
558 }
559
560 fn original_text(&self) -> &str {
561 &self.original_text
562 }
563
564 fn original_selection(&self) -> TextRange {
565 TextRange::new(self.original_selection.start, self.original_selection.end)
566 }
567
568 fn has_selection(&self) -> bool {
569 self.selection.start != self.selection.end
570 }
571}
572
573fn byte_for_char(text: &str, index: usize) -> usize {
574 text.char_indices().nth(index).map_or(text.len(), |v| v.0)
575}
576
577fn clamp_range(text: &str, range: Range<usize>) -> Range<usize> {
578 let mut start = range.start.min(text.len());
579 let mut end = range.end.min(text.len());
580 while start > 0 && !text.is_char_boundary(start) {
581 start -= 1;
582 }
583 while end > 0 && !text.is_char_boundary(end) {
584 end -= 1;
585 }
586 start..end
587}
588
589#[derive(Debug)]
590struct CodepointOffsetMapping {
591 original: Vec<usize>,
592 transformed: Vec<usize>,
593}
594
595impl CodepointOffsetMapping {
596 fn new(original: &str, transformed: &str) -> Self {
597 let original_offsets: Vec<usize> = original
598 .char_indices()
599 .map(|(index, _)| index)
600 .chain(std::iter::once(original.len()))
601 .collect();
602 let transformed_offsets: Vec<usize> = transformed
603 .char_indices()
604 .map(|(index, _)| index)
605 .chain(std::iter::once(transformed.len()))
606 .collect();
607 if original_offsets.len() == transformed_offsets.len() {
608 Self {
609 original: original_offsets,
610 transformed: transformed_offsets,
611 }
612 } else {
613 Self {
614 original: vec![0, original.len()],
615 transformed: vec![0, transformed.len()],
616 }
617 }
618 }
619
620 fn map(offsets: &[usize], value: usize) -> usize {
621 let index = match offsets.binary_search(&value) {
622 Ok(index) => index,
623 Err(index) => index.saturating_sub(1),
624 };
625 offsets
626 .get(index)
627 .copied()
628 .or_else(|| offsets.last().copied())
629 .unwrap_or(0)
630 }
631}
632
633impl OffsetMapping for CodepointOffsetMapping {
634 fn original_to_transformed(&self, offset: usize) -> usize {
635 Self::map(&self.original, offset)
636 }
637
638 fn transformed_to_original(&self, offset: usize) -> usize {
639 Self::map(&self.transformed, offset)
640 }
641
642 fn clone_box(&self) -> Box<dyn OffsetMapping> {
643 Box::new(Self {
644 original: self.original.clone(),
645 transformed: self.transformed.clone(),
646 })
647 }
648}
649
650#[derive(Debug)]
651struct ComposedOffsetMapping {
652 first: Box<dyn OffsetMapping>,
653 second: Box<dyn OffsetMapping>,
654}
655
656impl OffsetMapping for ComposedOffsetMapping {
657 fn original_to_transformed(&self, offset: usize) -> usize {
658 self.second
659 .original_to_transformed(self.first.original_to_transformed(offset))
660 }
661
662 fn transformed_to_original(&self, offset: usize) -> usize {
663 self.first
664 .transformed_to_original(self.second.transformed_to_original(offset))
665 }
666
667 fn clone_box(&self) -> Box<dyn OffsetMapping> {
668 Box::new(Self {
669 first: self.first.clone_box(),
670 second: self.second.clone_box(),
671 })
672 }
673}
674
675fn install_textfield_transforms(
676 visual: Option<Rc<dyn VisualTransformation>>,
677 input: Option<Rc<dyn InputTransformation>>,
678 output: Option<Rc<dyn OutputTransformation>>,
679 codepoint: Option<CodepointTransformation>,
680) -> Option<Rc<dyn VisualTransformation>> {
681 if visual.is_none() && input.is_none() && output.is_none() && codepoint.is_none() {
682 return None;
683 }
684 let id = NEXT_TEXTFIELD_TRANSFORM_ID.fetch_add(1, Ordering::Relaxed);
685 let marker = Rc::new(TextFieldTransformMarker(id));
686 let pointer = Rc::as_ptr(&marker) as u64;
687 let visual_marker: Rc<dyn VisualTransformation> = marker.clone();
688 TEXTFIELD_TRANSFORMS.with(|all| {
689 all.borrow_mut().insert(
690 id,
691 TextFieldTransformEntry {
692 transforms: TextFieldTransforms {
693 visual,
694 input,
695 output,
696 codepoint: codepoint.map(Rc::new),
697 },
698 marker: Rc::downgrade(&marker),
699 },
700 );
701 });
702 TEXTFIELD_TRANSFORM_MARKERS.with(|markers| {
703 markers.borrow_mut().insert(pointer, id);
704 });
705 Some(visual_marker)
706}
707
708pub(crate) fn textfield_transform_id(transformation: &Rc<dyn VisualTransformation>) -> Option<u64> {
709 let pointer = Rc::as_ptr(transformation) as *const () as u64;
710 TEXTFIELD_TRANSFORM_MARKERS.with(|markers| markers.borrow().get(&pointer).copied())
711}
712
713fn apply_textfield_input_transformation_id(id: Option<u64>, state: &mut TextFieldState) {
714 let Some(id) = id else {
715 return;
716 };
717 let input = TEXTFIELD_TRANSFORMS.with(|all| {
718 all.borrow()
719 .get(&id)
720 .and_then(|entry| entry.transforms.input.clone())
721 });
722 let Some(input) = input else {
723 return;
724 };
725 let before = state.text.clone();
726 let composition = state.composition.clone();
727 let mut buffer = TransformationBuffer::new(state.text.clone(), state.selection.clone());
728 input.transform_input(&mut buffer);
729 state.text = buffer.text;
730 state.selection = buffer.selection;
731 if let Some(composition) = composition {
732 state.composition = Some(remap_text_range(&before, &state.text, composition));
733 }
734}
735
736fn remap_text_range(before: &str, after: &str, range: Range<usize>) -> Range<usize> {
737 if before == after {
738 return range;
739 }
740 let mut prefix = before
741 .as_bytes()
742 .iter()
743 .zip(after.as_bytes())
744 .take_while(|(left, right)| left == right)
745 .count();
746 while prefix > 0 && (!before.is_char_boundary(prefix) || !after.is_char_boundary(prefix)) {
747 prefix -= 1;
748 }
749 let max_suffix = (before.len() - prefix).min(after.len() - prefix);
750 let mut suffix = before[before.len() - max_suffix..]
751 .bytes()
752 .rev()
753 .zip(after[after.len() - max_suffix..].bytes().rev())
754 .take_while(|(left, right)| left == right)
755 .count();
756 while suffix > 0
757 && (!before.is_char_boundary(before.len() - suffix)
758 || !after.is_char_boundary(after.len() - suffix))
759 {
760 suffix -= 1;
761 }
762 let new_end = after.len() - suffix;
763 let old_start = prefix.min(range.start.min(before.len()));
764 let old_range_end = range.end.min(before.len()).max(old_start);
765 let map = |offset: usize| {
766 let offset = offset.clamp(old_start, old_range_end);
767 if offset == old_start {
768 prefix
769 } else if offset == old_range_end {
770 new_end
771 } else {
772 let relative = offset - old_start;
773 let changed_len = new_end.saturating_sub(prefix);
774 prefix + relative.min(changed_len)
775 }
776 };
777 let start = map(old_start);
778 let end = map(old_range_end);
779 clamp_range(after, start.min(end)..start.max(end))
780}
781
782pub fn apply_textfield_input_transformation(hit: &HitRegion, state: &mut TextFieldState) {
783 apply_textfield_input_transformation_id(crate::hit_testing::textfield_transform_id(hit), state);
784}
785
786pub fn dispatch_textfield_keyboard_action(
787 hit: &HitRegion,
788 action: repose_core::ImeAction,
789 default: &dyn Fn(),
790) -> bool {
791 crate::hit_testing::dispatch_keyboard_action(hit, action, default)
792}
793
794pub fn textfield_metrics(hit: &HitRegion) -> TextFieldMetrics {
795 crate::hit_testing::textfield_metrics(hit)
796}
797
798pub fn insert_text_with_input_transformation(
799 hit: &HitRegion,
800 state: &mut TextFieldState,
801 text: &str,
802 atomic: bool,
803) -> bool {
804 perform_input_edit(
805 crate::hit_testing::textfield_transform_id(hit),
806 state,
807 |state| {
808 if atomic {
809 state.insert_text_atomic(text);
810 } else {
811 state.insert_text(text);
812 }
813 },
814 )
815 .1
816}
817
818pub fn delete_backward_with_input_transformation(
819 hit: &HitRegion,
820 state: &mut TextFieldState,
821) -> bool {
822 perform_input_edit(
823 crate::hit_testing::textfield_transform_id(hit),
824 state,
825 TextFieldState::delete_backward,
826 )
827 .1
828}
829
830pub fn delete_forward_with_input_transformation(
831 hit: &HitRegion,
832 state: &mut TextFieldState,
833) -> bool {
834 perform_input_edit(
835 crate::hit_testing::textfield_transform_id(hit),
836 state,
837 TextFieldState::delete_forward,
838 )
839 .1
840}
841
842pub fn delete_surrounding_with_input_transformation(
846 hit: &HitRegion,
847 state: &mut TextFieldState,
848 before_bytes: usize,
849 after_bytes: usize,
850) -> bool {
851 perform_input_edit(
852 crate::hit_testing::textfield_transform_id(hit),
853 state,
854 |state| {
855 state.delete_surrounding(before_bytes, after_bytes);
856 },
857 )
858 .1
859}
860
861pub fn cut_with_input_transformation(
862 hit: &HitRegion,
863 state: &mut TextFieldState,
864) -> Option<String> {
865 let selected = state.selected_text();
866 if selected.is_empty() {
867 return None;
868 }
869 let changed = perform_input_edit(
870 crate::hit_testing::textfield_transform_id(hit),
871 state,
872 |state| state.insert_text_atomic(""),
873 )
874 .1;
875 changed.then_some(selected)
876}
877
878pub fn commit_composition_with_input_transformation(
879 hit: &HitRegion,
880 state: &mut TextFieldState,
881 text: String,
882) {
883 perform_input_edit(
884 crate::hit_testing::textfield_transform_id(hit),
885 state,
886 |state| state.commit_composition(text),
887 );
888}
889
890pub fn undo_with_input_transformation(_hit: &HitRegion, state: &mut TextFieldState) -> bool {
891 state.undo()
892}
893
894pub fn redo_with_input_transformation(_hit: &HitRegion, state: &mut TextFieldState) -> bool {
895 state.redo()
896}
897
898pub(crate) fn insert_text_with_input_transformation_config(
899 config: &TextInputConfig,
900 state: &mut TextFieldState,
901 text: &str,
902 atomic: bool,
903) -> bool {
904 let transform_id = config
905 .visual_transformation
906 .as_ref()
907 .and_then(textfield_transform_id);
908 perform_input_edit(transform_id, state, |state| {
909 if atomic {
910 state.insert_text_atomic(text);
911 } else {
912 state.insert_text(text);
913 }
914 })
915 .1
916}
917
918pub(crate) fn cut_with_input_transformation_config(
919 config: &TextInputConfig,
920 state: &mut TextFieldState,
921) -> Option<String> {
922 let selected = state.selected_text();
923 if selected.is_empty() {
924 return None;
925 }
926 let transform_id = config
927 .visual_transformation
928 .as_ref()
929 .and_then(textfield_transform_id);
930 perform_input_edit(transform_id, state, |state| state.insert_text_atomic(""))
931 .1
932 .then_some(selected)
933}
934
935pub(crate) fn undo_with_input_transformation_config(
936 _config: &TextInputConfig,
937 state: &mut TextFieldState,
938) -> bool {
939 state.undo()
940}
941
942pub(crate) fn redo_with_input_transformation_config(
943 _config: &TextInputConfig,
944 state: &mut TextFieldState,
945) -> bool {
946 state.redo()
947}
948
949fn perform_input_edit<T>(
950 transform_id: Option<u64>,
951 state: &mut TextFieldState,
952 edit: impl FnOnce(&mut TextFieldState) -> T,
953) -> (T, bool) {
954 let before = state.text.clone();
955 let before_selection = state.selection.clone();
956 let previous = state.staging_undo.take();
957 let redo = std::mem::take(&mut state.redo_stack);
958 let output = edit(state);
959 let Some(latest) = state.staging_undo.take() else {
960 state.staging_undo = previous;
961 state.redo_stack = redo;
962 return (output, false);
963 };
964 apply_textfield_input_transformation_id(transform_id, state);
965 let canonical = canonical_edit(
966 &before,
967 before_selection,
968 state,
969 latest.time,
970 latest.can_merge,
971 );
972 state.staging_undo = previous;
973 let Some(canonical) = canonical else {
974 state.redo_stack = redo;
975 return (output, false);
976 };
977 state.record_edit(canonical);
978 (output, true)
979}
980
981fn canonical_edit(
982 before: &str,
983 before_selection: Range<usize>,
984 state: &TextFieldState,
985 time: Instant,
986 can_merge: bool,
987) -> Option<TextUndoOp> {
988 canonical_edit_between(
989 before,
990 before_selection,
991 &state.text,
992 state.selection.clone(),
993 time,
994 can_merge,
995 )
996}
997
998fn canonical_edit_between(
999 before: &str,
1000 before_selection: Range<usize>,
1001 after: &str,
1002 after_selection: Range<usize>,
1003 time: Instant,
1004 can_merge: bool,
1005) -> Option<TextUndoOp> {
1006 if before == after {
1007 return None;
1008 }
1009 let mut prefix = before
1010 .as_bytes()
1011 .iter()
1012 .zip(after.as_bytes())
1013 .take_while(|(left, right)| left == right)
1014 .count();
1015 while prefix > 0 && (!before.is_char_boundary(prefix) || !after.is_char_boundary(prefix)) {
1016 prefix -= 1;
1017 }
1018 let max_suffix = (before.len() - prefix).min(after.len() - prefix);
1019 let mut suffix = before[before.len() - max_suffix..]
1020 .bytes()
1021 .rev()
1022 .zip(after[after.len() - max_suffix..].bytes().rev())
1023 .take_while(|(left, right)| left == right)
1024 .count();
1025 while suffix > 0
1026 && (!before.is_char_boundary(before.len() - suffix)
1027 || !after.is_char_boundary(after.len() - suffix))
1028 {
1029 suffix -= 1;
1030 }
1031 let pre_end = before.len() - suffix;
1032 let post_end = after.len() - suffix;
1033 Some(TextUndoOp {
1034 index: prefix,
1035 pre_text: before[prefix..pre_end].to_string(),
1036 post_text: after[prefix..post_end].to_string(),
1037 pre_selection: before_selection,
1038 post_selection: after_selection,
1039 time,
1040 can_merge,
1041 })
1042}
1043
1044pub fn measure_text(text: &str, font_px: f32, config: TextMeasureConfig) -> TextMetrics {
1048 let m = repose_text::metrics_for_textfield(
1049 text,
1050 font_px,
1051 config.font_family,
1052 config.font_weight,
1053 config.font_style,
1054 config.letter_spacing,
1055 config.font_variation_settings.as_deref(),
1056 );
1057 TextMetrics {
1058 positions: m.positions,
1059 byte_offsets: m.byte_offsets,
1060 }
1061}
1062
1063pub fn byte_to_char_index(m: &TextMetrics, byte: usize) -> usize {
1064 match m.byte_offsets.binary_search(&byte) {
1065 Ok(i) | Err(i) => i,
1066 }
1067}
1068
1069pub fn index_for_x_bytes(
1071 text: &str,
1072 font_px: f32,
1073 x_px: f32,
1074 font_weight: u16,
1075 font_style: u8,
1076) -> usize {
1077 let m = measure_text(
1078 text,
1079 font_px,
1080 TextMeasureConfig {
1081 font_weight,
1082 font_style,
1083 ..Default::default()
1084 },
1085 );
1086
1087 if m.positions.is_empty() {
1088 return 0;
1089 }
1090 let mut best_i = 0usize;
1091 let mut best_d = f32::INFINITY;
1092 for (index, position) in m.positions.iter().enumerate() {
1093 let distance = (position - x_px).abs();
1094 if distance < best_d {
1095 best_d = distance;
1096 best_i = index;
1097 }
1098 }
1099 m.byte_offsets[best_i]
1100}
1101
1102pub(crate) fn prev_grapheme_boundary(text: &str, byte: usize) -> usize {
1104 let mut last = 0usize;
1105 for (i, _) in text.grapheme_indices(true) {
1106 if i >= byte {
1107 break;
1108 }
1109 last = i;
1110 }
1111 last
1112}
1113
1114pub(crate) fn next_grapheme_boundary(text: &str, byte: usize) -> usize {
1115 for (i, _) in text.grapheme_indices(true) {
1116 if i > byte {
1117 return i;
1118 }
1119 }
1120 text.len()
1121}
1122
1123pub(crate) fn word_range(text: &str, byte: usize) -> (usize, usize) {
1126 let byte = byte.min(text.len());
1127
1128 let mut start = byte;
1129 while start > 0 {
1130 let p = prev_grapheme_boundary(text, start);
1131 if is_word_grapheme(&text[p..start]) {
1132 start = p;
1133 } else {
1134 break;
1135 }
1136 }
1137 let mut end = byte;
1138 while end < text.len() {
1139 let n = next_grapheme_boundary(text, end);
1140 if is_word_grapheme(&text[end..n]) {
1141 end = n;
1142 } else {
1143 break;
1144 }
1145 }
1146 if start == end {
1147 let s = if byte == 0 {
1148 0
1149 } else {
1150 prev_grapheme_boundary(text, byte)
1151 };
1152 let e = next_grapheme_boundary(text, byte);
1153 (s, e.max(s))
1154 } else {
1155 (start, end)
1156 }
1157}
1158
1159fn is_word_char(c: char) -> bool {
1160 c.is_alphanumeric() || c == '_'
1161}
1162
1163fn is_word_grapheme(grapheme: &str) -> bool {
1164 !grapheme.is_empty() && grapheme.chars().all(is_word_char)
1165}
1166
1167fn step_over(pos: &mut usize, text: &str, want_word: bool) {
1168 while *pos < text.len() {
1169 let next = next_grapheme_boundary(text, *pos);
1170 if is_word_grapheme(&text[*pos..next]) != want_word {
1171 return;
1172 }
1173 *pos = next;
1174 }
1175}
1176
1177fn step_back(pos: &mut usize, text: &str, want_word: bool) {
1178 while *pos > 0 {
1179 let prev = prev_grapheme_boundary(text, *pos);
1180 if is_word_grapheme(&text[prev..*pos]) != want_word {
1181 return;
1182 }
1183 *pos = prev;
1184 }
1185}
1186
1187pub fn prev_word_boundary(text: &str, byte: usize) -> usize {
1190 let origin = byte.min(text.len());
1191 let mut pos = origin;
1192 step_back(&mut pos, text, true);
1193 if pos == origin {
1194 step_back(&mut pos, text, false);
1195 step_back(&mut pos, text, true);
1196 }
1197 pos
1198}
1199
1200pub fn next_word_boundary(text: &str, byte: usize) -> usize {
1203 let origin = byte.min(text.len());
1204 let mut pos = origin;
1205 step_over(&mut pos, text, true);
1206 if pos == origin {
1207 step_over(&mut pos, text, false);
1208 step_over(&mut pos, text, true);
1209 }
1210 pos
1211}
1212
1213pub struct TextFieldState {
1214 pub text: String,
1215 pub selection: Range<usize>,
1216 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>,
1223 pub(crate) last_tap_pos: Option<(f32, f32)>,
1224 pub(crate) tap_count: u8,
1225
1226 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>>,
1233 pub visual_transformation: Option<Rc<dyn VisualTransformation>>,
1235 pub(crate) scroll_target: f32,
1237 pub(crate) scroll_target_y: f32,
1239 scroll_vel: f32,
1241 scroll_vel_y: f32,
1243 last_scroll_tick: Option<Instant>,
1245
1246 undo_stack: Vec<TextUndoOp>,
1249 redo_stack: Vec<TextUndoOp>,
1251 staging_undo: Option<TextUndoOp>,
1253}
1254
1255impl std::fmt::Debug for TextFieldState {
1256 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1257 f.debug_struct("TextFieldState")
1258 .field("text", &self.text)
1259 .field("selection", &self.selection)
1260 .field("composition", &self.composition)
1261 .field("scroll_offset", &self.scroll_offset)
1262 .field("scroll_offset_y", &self.scroll_offset_y)
1263 .field("drag_anchor", &self.drag_anchor)
1264 .field("blink_start", &self.blink_start)
1265 .field("inner_width", &self.inner_width)
1266 .field("inner_height", &self.inner_height)
1267 .field("preferred_x_px", &self.preferred_x_px)
1268 .field(
1269 "offset_map",
1270 &self.offset_map.as_ref().map(|_| "<offset_mapping>"),
1271 )
1272 .field(
1273 "visual_transformation",
1274 &self.visual_transformation.as_ref().map(|_| "<vt>"),
1275 )
1276 .field("scroll_target", &self.scroll_target)
1277 .field("scroll_target_y", &self.scroll_target_y)
1278 .field("can_undo", &self.can_undo())
1279 .field("can_redo", &self.can_redo())
1280 .field("undo_count", &self.undo_stack.len())
1281 .field("redo_count", &self.redo_stack.len())
1282 .finish()
1283 }
1284}
1285
1286impl Default for TextFieldState {
1287 fn default() -> Self {
1288 Self::new()
1289 }
1290}
1291
1292impl Clone for TextFieldState {
1293 fn clone(&self) -> Self {
1294 Self {
1295 text: self.text.clone(),
1296 selection: self.selection.clone(),
1297 composition: self.composition.clone(),
1298 scroll_offset: self.scroll_offset,
1299 scroll_offset_y: self.scroll_offset_y,
1300 drag_anchor: self.drag_anchor,
1301 last_tap_time: self.last_tap_time,
1302 last_tap_pos: self.last_tap_pos,
1303 tap_count: self.tap_count,
1304 blink_start: self.blink_start,
1305 inner_width: self.inner_width,
1306 inner_height: self.inner_height,
1307 preferred_x_px: self.preferred_x_px,
1308 offset_map: self.offset_map.as_ref().map(|m| m.clone_box()),
1309 visual_transformation: self.visual_transformation.clone(),
1310 scroll_target: self.scroll_target,
1311 scroll_target_y: self.scroll_target_y,
1312 scroll_vel: self.scroll_vel,
1313 scroll_vel_y: self.scroll_vel_y,
1314 last_scroll_tick: self.last_scroll_tick,
1315 undo_stack: self.undo_stack.clone(),
1316 redo_stack: self.redo_stack.clone(),
1317 staging_undo: self.staging_undo.clone(),
1318 }
1319 }
1320}
1321
1322impl TextFieldState {
1323 pub fn new() -> Self {
1324 Self {
1325 text: String::new(),
1326 selection: 0..0,
1327 composition: None,
1328 scroll_offset: 0.0,
1329 scroll_offset_y: 0.0,
1330 drag_anchor: None,
1331 last_tap_time: None,
1332 last_tap_pos: None,
1333 tap_count: 0,
1334 blink_start: Instant::now(),
1335 inner_width: 0.0,
1336 inner_height: 0.0,
1337 preferred_x_px: None,
1338 offset_map: None,
1339 visual_transformation: None,
1340 scroll_target: 0.0,
1341 scroll_target_y: 0.0,
1342 scroll_vel: 0.0,
1343 scroll_vel_y: 0.0,
1344 last_scroll_tick: None,
1345 undo_stack: Vec::new(),
1346 redo_stack: Vec::new(),
1347 staging_undo: None,
1348 }
1349 }
1350
1351 pub fn can_undo(&self) -> bool {
1355 !self.undo_stack.is_empty() || self.staging_undo.is_some()
1356 }
1357
1358 pub fn can_redo(&self) -> bool {
1360 !self.redo_stack.is_empty()
1361 }
1362
1363 pub fn undo(&mut self) -> bool {
1365 self.flush_undo();
1366 if let Some(op) = self.undo_stack.pop() {
1367 let idx = clamp_to_char_boundary(&self.text, op.index.min(self.text.len()));
1368 let end = clamp_to_char_boundary(
1369 &self.text,
1370 (op.index + op.post_text.len()).min(self.text.len()),
1371 );
1372 let end = end.max(idx);
1373 self.text.replace_range(idx..end, &op.pre_text);
1374 self.selection = op.pre_selection.clone();
1375 self.redo_stack.push(op);
1376 self.preferred_x_px = None;
1377 self.reset_caret_blink();
1378 true
1379 } else {
1380 false
1381 }
1382 }
1383
1384 pub fn redo(&mut self) -> bool {
1386 if let Some(op) = self.redo_stack.pop() {
1387 let idx = clamp_to_char_boundary(&self.text, op.index.min(self.text.len()));
1388 let end = clamp_to_char_boundary(
1389 &self.text,
1390 (op.index + op.pre_text.len()).min(self.text.len()),
1391 );
1392 let end = end.max(idx);
1393 self.text.replace_range(idx..end, &op.post_text);
1394 self.selection = op.post_selection.clone();
1395 self.undo_stack.push(op);
1396 self.preferred_x_px = None;
1397 self.reset_caret_blink();
1398 true
1399 } else {
1400 false
1401 }
1402 }
1403
1404 pub fn clear_undo_history(&mut self) {
1406 self.undo_stack.clear();
1407 self.redo_stack.clear();
1408 self.staging_undo = None;
1409 }
1410
1411 fn record_edit(&mut self, op: TextUndoOp) {
1415 self.redo_stack.clear();
1416 if let Some(staging) = self.staging_undo.take() {
1417 if let Some(merged) = staging.try_merge(&op) {
1418 self.staging_undo = Some(merged);
1419 return;
1420 }
1421 self.undo_stack.push(staging);
1423 while self.undo_stack.len() + 1 > TEXT_UNDO_CAPACITY {
1425 self.undo_stack.remove(0);
1426 }
1427 }
1428 self.staging_undo = Some(op);
1429 }
1430
1431 fn flush_undo(&mut self) {
1433 if let Some(op) = self.staging_undo.take() {
1434 self.undo_stack.push(op);
1435 self.redo_stack.clear();
1436 while self.undo_stack.len() > TEXT_UNDO_CAPACITY {
1437 self.undo_stack.remove(0);
1438 }
1439 }
1440 }
1441
1442 fn insert_text_impl(&mut self, text: &str, can_merge: bool) {
1443 let a = self.selection.start.min(self.text.len());
1444 let b = self.selection.end.min(self.text.len());
1445 let start = clamp_to_char_boundary(&self.text, a.min(b));
1446 let end = clamp_to_char_boundary(&self.text, a.max(b));
1447 let pre_text = self.text[start..end].to_string();
1448 let pre_selection = self.selection.clone();
1449
1450 self.text.replace_range(start..end, text);
1451 let new_pos = start + text.len();
1452 self.selection = new_pos..new_pos;
1453 self.preferred_x_px = None;
1454 self.reset_caret_blink();
1455
1456 if !pre_text.is_empty() || !text.is_empty() {
1457 self.record_edit(TextUndoOp {
1458 index: start,
1459 pre_text,
1460 post_text: text.to_string(),
1461 pre_selection,
1462 post_selection: self.selection.clone(),
1463 time: Instant::now(),
1464 can_merge,
1465 });
1466 }
1467 }
1468
1469 pub fn insert_text(&mut self, text: &str) {
1470 self.insert_text_impl(text, true);
1471 }
1472
1473 pub fn insert_text_atomic(&mut self, text: &str) {
1475 self.insert_text_impl(text, false);
1476 }
1477
1478 pub fn delete_backward(&mut self) {
1479 if self.selection.start == self.selection.end {
1480 let pos = self.selection.start.min(self.text.len());
1481 if pos > 0 {
1482 let prev = prev_grapheme_boundary(&self.text, pos);
1483 let pre_text = self.text[prev..pos].to_string();
1484 let pre_selection = self.selection.clone();
1485 self.text.replace_range(prev..pos, "");
1486 self.selection = prev..prev;
1487 self.preferred_x_px = None;
1488 self.reset_caret_blink();
1489 self.record_edit(TextUndoOp {
1490 index: prev,
1491 pre_text,
1492 post_text: String::new(),
1493 pre_selection,
1494 post_selection: self.selection.clone(),
1495 time: Instant::now(),
1496 can_merge: true,
1497 });
1498 }
1499 } else {
1500 self.insert_text_impl("", true);
1501 }
1502 self.preferred_x_px = None;
1503 self.reset_caret_blink();
1504 }
1505
1506 pub fn delete_forward(&mut self) {
1507 if self.selection.start == self.selection.end {
1508 let pos = self.selection.start.min(self.text.len());
1509 if pos < self.text.len() {
1510 let next = next_grapheme_boundary(&self.text, pos);
1511 let pre_text = self.text[pos..next].to_string();
1512 let pre_selection = self.selection.clone();
1513 self.text.replace_range(pos..next, "");
1514 self.preferred_x_px = None;
1515 self.reset_caret_blink();
1516 self.record_edit(TextUndoOp {
1517 index: pos,
1518 pre_text,
1519 post_text: String::new(),
1520 pre_selection,
1521 post_selection: self.selection.clone(),
1522 time: Instant::now(),
1523 can_merge: true,
1524 });
1525 }
1526 } else {
1527 self.insert_text_impl("", true);
1528 }
1529 self.preferred_x_px = None;
1530 self.reset_caret_blink();
1531 }
1532
1533 pub fn move_cursor(&mut self, delta: isize, extend_selection: bool, by_word: bool) {
1538 let origin = self.selection.end.min(self.text.len());
1539 let pos = if !by_word {
1540 let mut pos = origin;
1541 if delta < 0 {
1542 for _ in 0..delta.unsigned_abs() {
1543 pos = prev_grapheme_boundary(&self.text, pos);
1544 }
1545 } else if delta > 0 {
1546 for _ in 0..(delta as usize) {
1547 pos = next_grapheme_boundary(&self.text, pos);
1548 }
1549 }
1550 pos
1551 } else if delta < 0 {
1552 prev_word_boundary(&self.text, origin)
1553 } else {
1554 next_word_boundary(&self.text, origin)
1555 };
1556 if extend_selection {
1557 self.selection.end = pos;
1558 } else {
1559 self.selection = pos..pos;
1560 }
1561 self.preferred_x_px = None;
1562 self.reset_caret_blink();
1563 }
1564
1565 pub fn selected_text(&self) -> String {
1566 let a = self.selection.start.min(self.text.len());
1567 let b = self.selection.end.min(self.text.len());
1568 let (lo, hi) = (a.min(b), a.max(b));
1569 if lo == hi {
1570 String::new()
1571 } else {
1572 let lo = clamp_to_char_boundary(&self.text, lo);
1573 let hi = clamp_to_char_boundary(&self.text, hi);
1574 self.text[lo..hi].to_string()
1575 }
1576 }
1577
1578 pub fn selection_range(&self) -> std::ops::Range<usize> {
1580 let a = self.selection.start.min(self.text.len());
1581 let b = self.selection.end.min(self.text.len());
1582 let (mut lo, mut hi) = (a.min(b), a.max(b));
1583 lo = clamp_to_char_boundary(&self.text, lo);
1584 hi = clamp_to_char_boundary(&self.text, hi);
1585 lo..hi
1586 }
1587
1588 pub fn set_composition(&mut self, text: String, cursor: Option<(usize, usize)>) {
1589 if text.is_empty() {
1590 if let Some(range) = self.composition.take() {
1591 let s = clamp_to_char_boundary(&self.text, range.start.min(self.text.len()));
1592 let e = clamp_to_char_boundary(&self.text, range.end.min(self.text.len()));
1593 if s <= e {
1594 self.text.replace_range(s..e, "");
1595 self.selection = s..s;
1596 }
1597 }
1598 self.preferred_x_px = None;
1599 self.reset_caret_blink();
1600 return;
1601 }
1602
1603 let anchor_start;
1604 if let Some(r) = self.composition.take() {
1605 let mut s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
1606 let mut e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
1607 if e < s {
1608 std::mem::swap(&mut s, &mut e);
1609 }
1610 self.text.replace_range(s..e, &text);
1611 anchor_start = s;
1612 } else {
1613 let pos = clamp_to_char_boundary(&self.text, self.selection.start.min(self.text.len()));
1614 self.text.insert_str(pos, &text);
1615 anchor_start = pos;
1616 }
1617
1618 self.composition = Some(anchor_start..(anchor_start + text.len()));
1619
1620 if let Some((c0, c1)) = cursor {
1621 let b0 = clamp_cursor_in_str(&text, c0);
1622 let b1 = clamp_cursor_in_str(&text, c1);
1623 self.selection = (anchor_start + b0)..(anchor_start + b1);
1624 } else {
1625 let end = anchor_start + text.len();
1626 self.selection = end..end;
1627 }
1628
1629 self.preferred_x_px = None;
1630 self.reset_caret_blink();
1631 }
1632
1633 pub fn commit_composition(&mut self, text: String) {
1634 let pre_selection = self.selection.clone();
1635 if let Some(r) = self.composition.take() {
1636 let s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
1637 let e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
1638 let pre_text = self.text[s..e].to_string();
1639 self.text.replace_range(s..e, &text);
1640 let new_pos = s + text.len();
1641 self.selection = new_pos..new_pos;
1642 self.preferred_x_px = None;
1643 self.reset_caret_blink();
1644 if !pre_text.is_empty() || !text.is_empty() {
1645 self.record_edit(TextUndoOp {
1646 index: s,
1647 pre_text,
1648 post_text: text,
1649 pre_selection,
1650 post_selection: self.selection.clone(),
1651 time: Instant::now(),
1652 can_merge: true,
1653 });
1654 }
1655 } else {
1656 let pos = clamp_to_char_boundary(&self.text, self.selection.end.min(self.text.len()));
1657 self.text.insert_str(pos, &text);
1658 let new_pos = pos + text.len();
1659 self.selection = new_pos..new_pos;
1660 self.preferred_x_px = None;
1661 self.reset_caret_blink();
1662 if !text.is_empty() {
1663 self.record_edit(TextUndoOp {
1664 index: pos,
1665 pre_text: String::new(),
1666 post_text: text,
1667 pre_selection,
1668 post_selection: self.selection.clone(),
1669 time: Instant::now(),
1670 can_merge: true,
1671 });
1672 }
1673 }
1674 }
1675
1676 pub fn finish_composition(&mut self) {
1677 self.composition = None;
1678 self.preferred_x_px = None;
1679 self.reset_caret_blink();
1680 }
1681
1682 pub fn cancel_composition(&mut self) {
1683 if let Some(r) = self.composition.take() {
1684 let s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
1685 let e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
1686 if s <= e {
1687 self.text.replace_range(s..e, "");
1688 self.selection = s..s;
1689 }
1690 }
1691 self.preferred_x_px = None;
1692 self.reset_caret_blink();
1693 }
1694}
1695
1696fn clamp_cursor_in_str(s: &str, i: usize) -> usize {
1697 let mut j = i.min(s.len());
1698 while j > 0 && !s.is_char_boundary(j) {
1699 j -= 1;
1700 }
1701 j
1702}
1703
1704fn is_grapheme_boundary(text: &str, byte: usize) -> bool {
1705 if byte == 0 || byte == text.len() {
1706 return true;
1707 }
1708 if !text.is_char_boundary(byte) {
1709 return false;
1710 }
1711 for (i, _) in text.grapheme_indices(true) {
1712 if i == byte {
1713 return true;
1714 }
1715 if i > byte {
1716 return false;
1717 }
1718 }
1719 false
1720}
1721
1722fn snap_inward_start(text: &str, pos: usize) -> usize {
1724 let pos = pos.min(text.len());
1725 if is_grapheme_boundary(text, pos) {
1726 return pos;
1727 }
1728 next_grapheme_boundary(text, pos)
1729}
1730
1731fn snap_inward_end(text: &str, pos: usize) -> usize {
1733 let pos = pos.min(text.len());
1734 if is_grapheme_boundary(text, pos) {
1735 return pos;
1736 }
1737 let mut b = pos;
1738 while b > 0 && !text.is_char_boundary(b) {
1739 b -= 1;
1740 }
1741 if is_grapheme_boundary(text, b) {
1742 return b;
1743 }
1744 prev_grapheme_boundary(text, pos)
1745}
1746
1747impl TextFieldState {
1748 pub fn delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) {
1752 let range = if self.selection.start != self.selection.end {
1753 self.selection_range()
1754 } else {
1755 let caret = clamp_to_char_boundary(&self.text, self.selection.end.min(self.text.len()));
1756 let start =
1757 snap_inward_start(&self.text, caret.saturating_sub(before_bytes)).min(caret);
1758 let end =
1759 snap_inward_end(&self.text, (caret + after_bytes).min(self.text.len())).max(caret);
1760 if start >= end {
1761 self.preferred_x_px = None;
1762 self.reset_caret_blink();
1763 return;
1764 }
1765 start..end
1766 };
1767
1768 let pre_text = self.text[range.clone()].to_string();
1769 let pre_selection = self.selection.clone();
1770 self.text.replace_range(range.clone(), "");
1771 self.selection = range.start..range.start;
1772 self.preferred_x_px = None;
1773 self.reset_caret_blink();
1774 self.record_edit(TextUndoOp {
1775 index: range.start,
1776 pre_text,
1777 post_text: String::new(),
1778 pre_selection,
1779 post_selection: self.selection.clone(),
1780 time: Instant::now(),
1781 can_merge: true,
1782 });
1783 }
1784
1785 pub fn begin_drag(&mut self, idx_byte: usize, extend: bool) {
1786 let idx = idx_byte.min(self.text.len());
1787 if extend {
1788 let anchor = self.selection.start;
1789 self.selection = anchor.min(idx)..anchor.max(idx);
1790 self.drag_anchor = Some(anchor);
1791 } else {
1792 self.selection = idx..idx;
1793 self.drag_anchor = Some(idx);
1794 }
1795 self.preferred_x_px = None;
1796 self.reset_caret_blink();
1797 }
1798
1799 pub fn drag_to(&mut self, idx_byte: usize) {
1800 if let Some(anchor) = self.drag_anchor {
1801 let i = idx_byte.min(self.text.len());
1802 self.selection = anchor.min(i)..anchor.max(i);
1803 }
1804 self.preferred_x_px = None;
1805 self.reset_caret_blink();
1806 if self.selection.start < self.selection.end {
1807 repose_core::clipboard::set_primary_selection(
1808 &self.text[self.selection.start..self.selection.end],
1809 );
1810 }
1811 }
1812 pub fn end_drag(&mut self) {
1813 self.drag_anchor = None;
1814 if self.selection.start < self.selection.end {
1815 repose_core::clipboard::set_primary_selection(
1816 &self.text[self.selection.start..self.selection.end],
1817 );
1818 }
1819 }
1820
1821 pub fn handle_pointer_down(&mut self, idx_byte: usize, pos_px: (f32, f32), shift: bool) {
1822 const DOUBLE_TAP_MS: u64 = 300;
1823 const TAP_SLOP_PX: f32 = 12.0;
1824
1825 let now = Instant::now();
1826 let mut count = self.tap_count;
1827 if let (Some(t), Some(p)) = (self.last_tap_time, self.last_tap_pos) {
1828 let dt = now.saturating_duration_since(t);
1829 let dist = ((pos_px.0 - p.0).powi(2) + (pos_px.1 - p.1).powi(2)).sqrt();
1830 if dt < Duration::from_millis(DOUBLE_TAP_MS) && dist < TAP_SLOP_PX {
1831 count = count.saturating_add(1);
1832 } else {
1833 count = 1;
1834 }
1835 } else {
1836 count = 1;
1837 }
1838 self.tap_count = count;
1839 self.last_tap_time = Some(now);
1840 self.last_tap_pos = Some(pos_px);
1841
1842 let idx = idx_byte.min(self.text.len());
1843
1844 if count > 3 {
1845 count = (count - 1) % 3 + 1;
1846 self.tap_count = count;
1847 }
1848 if count >= 3 {
1849 self.selection = 0..self.text.len();
1850 self.drag_anchor = None;
1851 self.preferred_x_px = None;
1852 self.reset_caret_blink();
1853 if self.selection.end > 0 {
1854 repose_core::clipboard::set_primary_selection(&self.text);
1855 }
1856 return;
1857 }
1858
1859 if count == 2 {
1860 let (s, e) = word_range(&self.text, idx);
1862 self.selection = s..e;
1863 self.drag_anchor = Some(s);
1864 self.preferred_x_px = None;
1865 self.reset_caret_blink();
1866 if e > s {
1867 repose_core::clipboard::set_primary_selection(&self.text[s..e]);
1868 }
1869 return;
1870 }
1871
1872 self.begin_drag(idx, shift);
1874 }
1875
1876 pub fn select_word_at(&mut self, byte: usize) {
1878 let (s, e) = word_range(&self.text, byte.min(self.text.len()));
1879 self.selection = s..e;
1880 self.drag_anchor = Some(s);
1881 self.preferred_x_px = None;
1882 self.reset_caret_blink();
1883 }
1884
1885 pub fn select_all(&mut self) {
1887 self.selection = 0..self.text.len();
1888 self.drag_anchor = None;
1889 self.preferred_x_px = None;
1890 self.reset_caret_blink();
1891 }
1892
1893 pub fn caret_index(&self) -> usize {
1894 self.selection.end
1895 }
1896
1897 pub fn ensure_caret_visible(&mut self, caret_x_px: f32, inner_width_px: f32, inset_px: f32) {
1901 self.ensure_caret_visible_xy(caret_x_px, 0.0, inner_width_px, 1.0, inset_px);
1902 }
1903
1904 pub fn ensure_caret_visible_xy(
1907 &mut self,
1908 caret_x_px: f32,
1909 caret_y_px: f32,
1910 inner_w_px: f32,
1911 inner_h_px: f32,
1912 inset_px: f32,
1913 ) {
1914 let inset_px = inset_px.max(0.0);
1915
1916 let left_px = self.scroll_offset + inset_px;
1918 let right_px = self.scroll_offset + inner_w_px - inset_px;
1919 if caret_x_px < left_px {
1920 self.scroll_target = (caret_x_px - inset_px).max(0.0);
1921 } else if caret_x_px > right_px {
1922 self.scroll_target = (caret_x_px - inner_w_px + inset_px).max(0.0);
1923 }
1924
1925 let top_px = self.scroll_offset_y + inset_px;
1927 let bot_px = self.scroll_offset_y + inner_h_px - inset_px;
1928 if caret_y_px < top_px {
1929 self.scroll_target_y = (caret_y_px - inset_px).max(0.0);
1930 } else if caret_y_px > bot_px {
1931 self.scroll_target_y = (caret_y_px - inner_h_px + inset_px).max(0.0);
1932 }
1933 }
1934
1935 pub fn clamp_scroll(&mut self, content_h_px: f32) {
1936 let max_y = (content_h_px - self.inner_height).max(0.0);
1937 self.scroll_target_y = self.scroll_target_y.clamp(0.0, max_y);
1938 if self.scroll_target_y.is_nan() {
1939 self.scroll_target_y = 0.0;
1940 }
1941 }
1942
1943 pub fn reset_caret_blink(&mut self) {
1944 self.blink_start = Instant::now();
1945 }
1946 pub fn caret_visible(&self) -> bool {
1947 const PERIOD: Duration = Duration::from_millis(500);
1948 ((Instant::now() - self.blink_start).as_millis() / PERIOD.as_millis()).is_multiple_of(2)
1949 }
1950
1951 pub fn next_blink_deadline(&self) -> Option<Instant> {
1954 if self.selection.start != self.selection.end {
1955 return None;
1956 }
1957 const PERIOD_MS: u128 = 500;
1958 let now = Instant::now();
1959 let elapsed = now.saturating_duration_since(self.blink_start).as_millis();
1960 let next_tick = (elapsed / PERIOD_MS) + 1;
1961 Some(self.blink_start + Duration::from_millis((next_tick * PERIOD_MS) as u64))
1962 }
1963
1964 pub fn with_text(text: impl Into<String>) -> Self {
1966 let mut s = Self::new();
1967 s.text = text.into();
1968 s.selection = 0..0;
1969 s
1970 }
1971
1972 pub fn place_cursor_at_end(&mut self) {
1974 let len = self.text.len();
1975 self.selection = len..len;
1976 self.drag_anchor = None;
1977 self.preferred_x_px = None;
1978 self.reset_caret_blink();
1979 }
1980
1981 pub fn apply_controlled_value(&mut self, value: &str) {
1985 if self.text.as_str() == value {
1986 return;
1987 }
1988 if self.composition.is_some() {
1989 return;
1990 }
1991 self.text = value.to_string();
1992 let len = self.text.len();
1993 let ns = clamp_to_char_boundary(&self.text, self.selection.start.min(len));
1994 let ne = clamp_to_char_boundary(&self.text, self.selection.end.min(len));
1995 self.selection = ns..ne;
1996 self.drag_anchor = None;
1997 }
1998
1999 pub fn set_inner_width(&mut self, w_px: f32) {
2000 self.inner_width = w_px.max(0.0);
2001 if self.scroll_offset.is_nan() {
2002 self.scroll_offset = 0.0;
2003 }
2004 if self.scroll_target.is_nan() {
2005 self.scroll_target = 0.0;
2006 }
2007 }
2008 pub fn set_inner_height(&mut self, h_px: f32) {
2009 self.inner_height = h_px.max(0.0);
2010 if self.scroll_offset_y.is_nan() {
2011 self.scroll_offset_y = 0.0;
2012 }
2013 if self.scroll_target_y.is_nan() {
2014 self.scroll_target_y = 0.0;
2015 }
2016 }
2017
2018 pub fn tick_scroll_animation(&mut self) {
2022 let now = Instant::now();
2023 let dt = match self.last_scroll_tick {
2024 Some(prev) => {
2025 let d = now.saturating_duration_since(prev).as_secs_f32();
2026 d.min(0.05) }
2028 None => {
2029 self.last_scroll_tick = Some(now);
2032 self.scroll_offset = self.scroll_target;
2033 self.scroll_vel = 0.0;
2034 self.scroll_offset_y = self.scroll_target_y;
2035 self.scroll_vel_y = 0.0;
2036 return;
2037 }
2038 };
2039 self.last_scroll_tick = Some(now);
2040
2041 if dt > 0.0 {
2043 let dx = self.scroll_target - self.scroll_offset;
2044 let near_x = dx.abs() < 0.5 && self.scroll_vel.abs() < 0.5;
2045 if near_x {
2046 self.scroll_offset = self.scroll_target;
2047 self.scroll_vel = 0.0;
2048 } else {
2049 let force_x = SCROLL_STIFFNESS * dx - SCROLL_DAMPING * self.scroll_vel;
2050 self.scroll_vel += force_x * dt;
2051 self.scroll_offset += self.scroll_vel * dt;
2052 if (self.scroll_target - self.scroll_offset).signum() != dx.signum() && dx != 0.0 {
2054 self.scroll_offset = self.scroll_target;
2055 self.scroll_vel = 0.0;
2056 }
2057 }
2058 }
2059
2060 if dt > 0.0 {
2062 let dy = self.scroll_target_y - self.scroll_offset_y;
2063 let near_y = dy.abs() < 0.5 && self.scroll_vel_y.abs() < 0.5;
2064 if near_y {
2065 self.scroll_offset_y = self.scroll_target_y;
2066 self.scroll_vel_y = 0.0;
2067 } else {
2068 let force_y = SCROLL_STIFFNESS * dy - SCROLL_DAMPING * self.scroll_vel_y;
2069 self.scroll_vel_y += force_y * dt;
2070 self.scroll_offset_y += self.scroll_vel_y * dt;
2071 if (self.scroll_target_y - self.scroll_offset_y).signum() != dy.signum()
2072 && dy != 0.0
2073 {
2074 self.scroll_offset_y = self.scroll_target_y;
2075 self.scroll_vel_y = 0.0;
2076 }
2077 }
2078 }
2079 }
2080}
2081
2082#[derive(Clone)]
2092pub struct TextFieldConfig {
2093 pub enabled: bool,
2095 pub read_only: bool,
2097 pub input_transformation: Option<Rc<dyn repose_core::InputTransformation>>,
2099 pub text_style: repose_core::TextStyle,
2101 pub keyboard_options: repose_core::KeyboardOptions,
2103 pub on_keyboard_action: Option<Rc<dyn repose_core::KeyboardActionHandler>>,
2105 pub line_limits: repose_core::TextFieldLineLimits,
2107 pub on_text_layout: Option<Rc<dyn Fn(&repose_core::TextLayoutResult)>>,
2109 pub interaction_source: Option<repose_core::MutableInteractionSource>,
2111 pub focus_tracker: Option<Rc<Cell<bool>>>,
2114 pub cursor_brush: Option<repose_core::Brush>,
2116 pub output_transformation: Option<Rc<dyn repose_core::OutputTransformation>>,
2118 pub decorator: Option<Rc<dyn repose_core::TextFieldDecorator>>,
2120 pub codepoint_transformation: Option<repose_core::CodepointTransformation>,
2122 pub text_obfuscation_mode: repose_core::TextObfuscationMode,
2124 pub text_obfuscation_character: char,
2126
2127 pub on_change: Option<Rc<dyn Fn(String)>>,
2129 pub on_submit: Option<Rc<dyn Fn(String)>>,
2130 pub visual_transformation: Option<Rc<dyn repose_core::VisualTransformation>>,
2131 pub decoration_box: Option<Rc<dyn Fn(repose_core::View) -> repose_core::View>>,
2132}
2133
2134impl Default for TextFieldConfig {
2135 fn default() -> Self {
2136 Self {
2137 enabled: true,
2138 read_only: false,
2139 input_transformation: None,
2140 text_style: Default::default(),
2141 keyboard_options: repose_core::KeyboardOptions::DEFAULT,
2142 on_keyboard_action: None,
2143 line_limits: repose_core::TextFieldLineLimits::MultiLine {
2144 min_height_in_lines: 1,
2145 max_height_in_lines: usize::MAX,
2146 },
2147 on_text_layout: None,
2148 interaction_source: None,
2149 focus_tracker: None,
2150 cursor_brush: None,
2151 output_transformation: None,
2152 decorator: None,
2153 codepoint_transformation: None,
2154 text_obfuscation_mode: repose_core::TextObfuscationMode::System,
2155 text_obfuscation_character: '\u{2022}',
2156 on_change: None,
2157 on_submit: None,
2158 visual_transformation: None,
2159 decoration_box: None,
2160 }
2161 }
2162}
2163
2164pub fn BasicTextField(
2178 state: Rc<RefCell<TextFieldState>>,
2179 modifier: repose_core::Modifier,
2180 hint: impl Into<String>,
2181 config: TextFieldConfig,
2182) -> repose_core::View {
2183 let (single_line, max_lines, min_lines) = match config.line_limits {
2184 repose_core::TextFieldLineLimits::SingleLine => (true, 1, 1),
2185 repose_core::TextFieldLineLimits::MultiLine {
2186 min_height_in_lines,
2187 max_height_in_lines,
2188 } => (false, max_height_in_lines, min_height_in_lines),
2189 };
2190
2191 let ka = if let Some(ref handler) = config.on_keyboard_action {
2192 let default_submit = config.on_submit.clone();
2193 let default_state = state.clone();
2194 let make_action = move |handler: Rc<dyn repose_core::KeyboardActionHandler>| {
2195 let default_submit = default_submit.clone();
2196 let default_state = default_state.clone();
2197 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
2198 handler.on_keyboard_action(&|| {
2199 if let Some(callback) = &default_submit {
2200 let text = default_state.borrow().text.clone();
2201 callback(text);
2202 }
2203 });
2204 }) as Rc<dyn Fn(&dyn repose_core::KeyboardActionScope)>
2205 };
2206 repose_core::KeyboardActions {
2207 on_done: Some(make_action(handler.clone())),
2208 on_go: Some(make_action(handler.clone())),
2209 on_next: Some(make_action(handler.clone())),
2210 on_previous: Some(make_action(handler.clone())),
2211 on_search: Some(make_action(handler.clone())),
2212 on_send: Some(make_action(handler.clone())),
2213 }
2214 } else {
2215 repose_core::KeyboardActions::default()
2216 };
2217
2218 let transform_cache_key = format!(
2219 "textfield-transforms:{:x}:{:?}:{:x}:{:x}:{:x}:{}",
2220 Rc::as_ptr(&state) as usize,
2221 modifier.key,
2222 config
2223 .visual_transformation
2224 .as_ref()
2225 .map(|value| Rc::as_ptr(value) as *const () as usize)
2226 .unwrap_or(0),
2227 config
2228 .input_transformation
2229 .as_ref()
2230 .map(|value| Rc::as_ptr(value) as *const () as usize)
2231 .unwrap_or(0),
2232 config
2233 .output_transformation
2234 .as_ref()
2235 .map(|value| Rc::as_ptr(value) as *const () as usize)
2236 .unwrap_or(0),
2237 config.codepoint_transformation.is_some(),
2238 );
2239 let visual_transformation = remember_with_key(transform_cache_key, || {
2240 install_textfield_transforms(
2241 config.visual_transformation,
2242 config.input_transformation,
2243 config.output_transformation,
2244 config.codepoint_transformation,
2245 )
2246 });
2247 let visual_transformation = visual_transformation.as_ref().clone();
2248
2249 let decoration_box = config
2250 .decorator
2251 .map(|d| Rc::new(move |inner: repose_core::View| d.decorate(inner)) as Rc<dyn Fn(_) -> _>);
2252
2253 let cursor_color = config.cursor_brush.and_then(|b| match b {
2254 repose_core::Brush::Solid(c) => Some(c),
2255 _ => None,
2256 });
2257
2258 let value = state.borrow().text.clone();
2259 let key = state.as_ptr() as u64;
2260 set_textfield_state(key, state.clone());
2261
2262 let state_on_change = {
2263 let s = state.clone();
2264 move |new_value: String| {
2265 s.borrow_mut().text = new_value;
2266 }
2267 };
2268
2269 let merged_on_change: Option<Rc<dyn Fn(String)>> =
2270 if let Some(ref cfg_on_change) = config.on_change {
2271 let a = Rc::new(state_on_change) as Rc<dyn Fn(String)>;
2272 let b = cfg_on_change.clone();
2273 Some(Rc::new(move |v: String| {
2274 a(v.clone());
2275 b(v);
2276 }) as Rc<dyn Fn(String)>)
2277 } else {
2278 Some(Rc::new(state_on_change) as Rc<dyn Fn(String)>)
2279 };
2280
2281 text_field_view(
2282 modifier,
2283 hint.into(),
2284 value,
2285 !single_line,
2286 merged_on_change,
2287 config.on_submit,
2288 visual_transformation,
2289 config.keyboard_options.keyboard_type,
2290 config.keyboard_options.capitalization,
2291 config.keyboard_options.ime_action,
2292 config.keyboard_options.auto_correct_enabled,
2293 config.enabled,
2294 config.read_only,
2295 Some(max_lines),
2296 min_lines,
2297 cursor_color,
2298 config.on_text_layout,
2299 config.text_style,
2300 ka,
2301 config.interaction_source,
2302 config.focus_tracker,
2303 Some(config.line_limits),
2304 decoration_box,
2305 )
2306}
2307
2308pub fn BasicSecureTextField(
2316 state: Rc<RefCell<TextFieldState>>,
2317 modifier: repose_core::Modifier,
2318 hint: impl Into<String>,
2319 config: TextFieldConfig,
2320) -> repose_core::View {
2321 let mask = config.text_obfuscation_character;
2322 let secure_config = TextFieldConfig {
2323 line_limits: repose_core::TextFieldLineLimits::SingleLine,
2324 keyboard_options: repose_core::KeyboardOptions::SECURE_TEXT_FIELD,
2325 visual_transformation: match config.text_obfuscation_mode {
2326 repose_core::TextObfuscationMode::Visible => None,
2327 _ => Some(Rc::new(repose_core::PasswordVisualTransformation { mask })
2328 as Rc<dyn repose_core::VisualTransformation>),
2329 },
2330 ..config
2331 };
2332 BasicTextField(state, modifier, hint, secure_config)
2333}
2334
2335#[derive(Clone, Debug)]
2336pub struct TextAreaLayout {
2337 pub ranges: Vec<(usize, usize)>,
2338 pub line_h_px: f32,
2339}
2340
2341pub fn layout_text_area(
2342 text: &str,
2343 font_px: f32,
2344 wrap_w_px: f32,
2345 font_weight: u16,
2346 font_style: u8,
2347 letter_spacing: f32,
2348 font_variation_settings: Option<&str>,
2349) -> TextAreaLayout {
2350 layout_text_area_with_metrics(
2351 text,
2352 wrap_w_px,
2353 &TextFieldMetrics {
2354 font_px,
2355 font_weight,
2356 font_style,
2357 letter_spacing_px: letter_spacing,
2358 font_variation_settings: font_variation_settings.map(str::to_string),
2359 line_height_px: font_px,
2360 ..TextFieldMetrics::default()
2361 },
2362 )
2363}
2364
2365pub fn layout_text_area_with_metrics(
2366 text: &str,
2367 wrap_w_px: f32,
2368 metrics: &TextFieldMetrics,
2369) -> TextAreaLayout {
2370 let (ranges, _) = repose_text::wrap_line_ranges(
2371 text,
2372 metrics.font_px,
2373 wrap_w_px.max(1.0),
2374 None,
2375 true,
2376 metrics.font_weight,
2377 metrics.font_style,
2378 metrics.letter_spacing_px,
2379 metrics.font_variation_settings.as_deref(),
2380 );
2381 TextAreaLayout {
2382 ranges,
2383 line_h_px: metrics.line_height_px,
2384 }
2385}
2386
2387fn locate_byte_in_ranges(ranges: &[(usize, usize)], b: usize) -> (usize, usize, usize) {
2388 if ranges.is_empty() {
2389 return (0, 0, b);
2390 }
2391 let next = ranges.partition_point(|(start, _)| *start <= b);
2392 if next == 0 {
2393 return (0, 0, b);
2394 }
2395 let index = next - 1;
2396 let (start, end) = ranges[index];
2397 let local = b.saturating_sub(start).min(end.saturating_sub(start));
2398 (index, local, start + local)
2399}
2400
2401pub fn caret_xy_for_byte(
2402 text: &str,
2403 font_px: f32,
2404 wrap_w_px: f32,
2405 byte: usize,
2406) -> (f32, f32, usize) {
2407 caret_xy_for_byte_with_metrics(
2408 text,
2409 wrap_w_px,
2410 byte,
2411 &TextFieldMetrics {
2412 font_px,
2413 line_height_px: font_px,
2414 ..TextFieldMetrics::default()
2415 },
2416 )
2417}
2418
2419pub fn caret_xy_for_byte_with_metrics(
2420 text: &str,
2421 wrap_w_px: f32,
2422 byte: usize,
2423 metrics: &TextFieldMetrics,
2424) -> (f32, f32, usize) {
2425 let layout = layout_text_area_with_metrics(text, wrap_w_px, metrics);
2426 let (ranges, line_h) = (&layout.ranges, layout.line_h_px);
2427 let (li, local, _) = locate_byte_in_ranges(ranges, byte);
2428 let (s, e) = ranges.get(li).copied().unwrap_or((0, 0));
2429 let line = &text[s..e];
2430 let measured = measure_text(line, metrics.font_px, metrics.measure_config());
2431 let ci = byte_to_char_index(&measured, local);
2432 let x = measured.positions.get(ci).copied().unwrap_or(0.0);
2433 (x, li as f32 * line_h, li)
2434}
2435
2436pub fn index_for_x_bytes_with_config(
2437 text: &str,
2438 font_px: f32,
2439 x_px: f32,
2440 config: TextMeasureConfig,
2441) -> usize {
2442 let measured = measure_text(text, font_px, config);
2443 if measured.positions.is_empty() {
2444 return 0;
2445 }
2446 let mut best_i = 0usize;
2447 let mut best_d = f32::INFINITY;
2448 for (index, position) in measured.positions.iter().enumerate() {
2449 let distance = (position - x_px).abs();
2450 if distance < best_d {
2451 best_d = distance;
2452 best_i = index;
2453 }
2454 }
2455 measured.byte_offsets[best_i]
2456}
2457
2458pub fn index_for_xy_bytes(text: &str, font_px: f32, wrap_w_px: f32, x_px: f32, y_px: f32) -> usize {
2459 index_for_xy_bytes_with_metrics(
2460 text,
2461 wrap_w_px,
2462 x_px,
2463 y_px,
2464 &TextFieldMetrics {
2465 font_px,
2466 line_height_px: font_px,
2467 ..TextFieldMetrics::default()
2468 },
2469 )
2470}
2471
2472pub fn index_for_xy_bytes_with_metrics(
2473 text: &str,
2474 wrap_w_px: f32,
2475 x_px: f32,
2476 y_px: f32,
2477 metrics: &TextFieldMetrics,
2478) -> usize {
2479 let layout = layout_text_area_with_metrics(text, wrap_w_px, metrics);
2480 let line = ((y_px / layout.line_h_px).floor() as isize).max(0) as usize;
2481 let line = line.min(layout.ranges.len().saturating_sub(1));
2482 let (start, end) = layout.ranges.get(line).copied().unwrap_or((0, 0));
2483 let local = index_for_x_bytes_with_config(
2484 &text[start..end],
2485 metrics.font_px,
2486 x_px.max(0.0),
2487 metrics.measure_config(),
2488 );
2489 (start + local).min(text.len())
2490}
2491
2492pub fn move_caret_vertical(
2493 text: &str,
2494 font_px: f32,
2495 wrap_w_px: f32,
2496 cur_byte: usize,
2497 dir: i32,
2498 preferred_x: Option<f32>,
2499) -> (usize, f32) {
2500 move_caret_vertical_with_metrics(
2501 text,
2502 wrap_w_px,
2503 cur_byte,
2504 dir,
2505 preferred_x,
2506 &TextFieldMetrics {
2507 font_px,
2508 line_height_px: font_px,
2509 ..TextFieldMetrics::default()
2510 },
2511 )
2512}
2513
2514pub fn move_caret_vertical_with_metrics(
2515 text: &str,
2516 wrap_w_px: f32,
2517 cur_byte: usize,
2518 dir: i32,
2519 preferred_x: Option<f32>,
2520 metrics: &TextFieldMetrics,
2521) -> (usize, f32) {
2522 let layout = layout_text_area_with_metrics(text, wrap_w_px, metrics);
2523 if layout.ranges.is_empty() {
2524 return (cur_byte, preferred_x.unwrap_or(0.0));
2525 }
2526 let (x, _, line) = caret_xy_for_byte_with_metrics(text, wrap_w_px, cur_byte, metrics);
2527 let x = preferred_x.unwrap_or(x);
2528 let line = (line as i32 + dir).clamp(0, layout.ranges.len().saturating_sub(1) as i32) as usize;
2529 let (start, end) = layout.ranges[line];
2530 let local = index_for_x_bytes_with_config(
2531 &text[start..end],
2532 metrics.font_px,
2533 x.max(0.0),
2534 metrics.measure_config(),
2535 );
2536 ((start + local).min(text.len()), x)
2537}
2538
2539pub fn line_home_end(
2540 text: &str,
2541 font_px: f32,
2542 wrap_w_px: f32,
2543 cur_byte: usize,
2544 to_end: bool,
2545) -> usize {
2546 line_home_end_with_metrics(
2547 text,
2548 wrap_w_px,
2549 cur_byte,
2550 to_end,
2551 &TextFieldMetrics {
2552 font_px,
2553 line_height_px: font_px,
2554 ..TextFieldMetrics::default()
2555 },
2556 )
2557}
2558
2559pub fn line_home_end_with_metrics(
2560 text: &str,
2561 wrap_w_px: f32,
2562 cur_byte: usize,
2563 to_end: bool,
2564 metrics: &TextFieldMetrics,
2565) -> usize {
2566 let layout = layout_text_area_with_metrics(text, wrap_w_px, metrics);
2567 let (line, _, _) = locate_byte_in_ranges(&layout.ranges, cur_byte);
2568 let (start, end) = layout.ranges.get(line).copied().unwrap_or((0, 0));
2569 if cur_byte < start || cur_byte > end {
2573 return cur_byte;
2574 }
2575 if to_end { end } else { start }
2576}
2577
2578fn clamp_to_char_boundary(s: &str, i: usize) -> usize {
2582 if i >= s.len() {
2583 return s.len();
2584 }
2585 let mut j = i;
2586 while j > 0 && !s.is_char_boundary(j) {
2587 j -= 1;
2588 }
2589 if j == 0 || j == s.len() || is_grapheme_boundary(s, j) {
2590 return j;
2591 }
2592 prev_grapheme_boundary(s, j)
2593}
2594
2595fn hint_line(
2598 text_input: &TextInputConfig,
2599 metrics: &TextFieldMetrics,
2600 font_px: f32,
2601 max_width: f32,
2602) -> String {
2603 repose_text::ellipsize_line_with_family(
2604 &text_input.hint,
2605 font_px,
2606 max_width,
2607 metrics.font_family,
2608 metrics.font_weight,
2609 metrics.font_style,
2610 metrics.letter_spacing_px,
2611 metrics.font_variation_settings.as_deref(),
2612 )
2613}
2614
2615pub(crate) fn paint_text_field(
2627 scene: &mut Scene,
2628 rect: repose_core::Rect,
2629 text_input: &TextInputConfig,
2630 state: Option<&Rc<RefCell<TextFieldState>>>,
2631 is_focused: bool,
2632 alpha_accum: f32,
2633) {
2634 let ts = text_input.text_style.clone().unwrap_or_default();
2635 let metrics = TextFieldMetrics::from_config(text_input);
2636 let font_val = metrics.font_px;
2637 let line_h = if text_input.multiline {
2638 metrics.line_height_px
2639 } else {
2640 font_val
2641 };
2642 let text_off_y = (rect.h - line_h.max(font_val)) / 2.0;
2643
2644 scene.nodes.push(SceneNode::PushClip {
2648 rect,
2649 radius: [Px::ZERO; 4],
2650 op: repose_core::ClipOp::Intersect,
2651 });
2652
2653 let th = locals::theme();
2654 let show_selection = text_input.enabled;
2655 let show_cursor = text_input.enabled && !text_input.read_only;
2656 let cursor_color = text_input.cursor_color.unwrap_or(th.on_surface);
2657 let rendered_by_vt = |original: &str| -> String {
2658 if let Some(ref vt) = text_input.visual_transformation {
2659 let annotated = repose_core::AnnotatedString::new(original.to_string(), vec![]);
2660 vt.filter(&annotated).text.text
2661 } else {
2662 original.to_string()
2663 }
2664 };
2665
2666 if let Some(state_rc) = state {
2667 let st = state_rc.borrow();
2668
2669 if !text_input.multiline {
2670 let measure_for = if text_input.visual_transformation.is_some() && !st.text.is_empty() {
2672 rendered_by_vt(&st.text)
2673 } else {
2674 st.text.clone()
2675 };
2676 let has_vt = text_input.visual_transformation.is_some();
2677 let m = measure_text(&measure_for, font_val, metrics.measure_config());
2678
2679 if show_selection && st.selection.start != st.selection.end {
2681 let start_off = if has_vt {
2682 original_offset_to_display_with_mapping(
2683 &st.text,
2684 &measure_for,
2685 st.selection.start,
2686 st.offset_map.as_deref(),
2687 )
2688 } else {
2689 st.selection.start
2690 };
2691 let end_off = if has_vt {
2692 original_offset_to_display_with_mapping(
2693 &st.text,
2694 &measure_for,
2695 st.selection.end,
2696 st.offset_map.as_deref(),
2697 )
2698 } else {
2699 st.selection.end
2700 };
2701 let sx = m
2702 .positions
2703 .get(byte_to_char_index(&m, start_off))
2704 .copied()
2705 .unwrap_or(0.0)
2706 - st.scroll_offset;
2707 let ex = m
2708 .positions
2709 .get(byte_to_char_index(&m, end_off))
2710 .copied()
2711 .unwrap_or(sx)
2712 - st.scroll_offset;
2713 let selection = th.primary.with_alpha(SELECTION_BACKGROUND_ALPHA);
2714 let vis_x = sx.max(0.0);
2715 let vis_ex = ex.max(0.0);
2716 scene.nodes.push(SceneNode::Rect {
2717 rect: repose_core::Rect {
2718 x: rect.x + vis_x,
2719 y: rect.y + text_off_y,
2720 w: (vis_ex - vis_x).max(0.0),
2721 h: line_h.max(font_val),
2722 },
2723 brush: Brush::Solid(selection),
2724 radius: [Px::ZERO; 4],
2725 });
2726 }
2727
2728 if let Some(comp) = st.composition.clone() {
2730 let cs = if has_vt {
2731 original_offset_to_display_with_mapping(
2732 &st.text,
2733 &measure_for,
2734 comp.start,
2735 st.offset_map.as_deref(),
2736 )
2737 } else {
2738 comp.start
2739 };
2740 let ce = if has_vt {
2741 original_offset_to_display_with_mapping(
2742 &st.text,
2743 &measure_for,
2744 comp.end,
2745 st.offset_map.as_deref(),
2746 )
2747 } else {
2748 comp.end
2749 };
2750 let sx = m
2751 .positions
2752 .get(byte_to_char_index(&m, cs))
2753 .copied()
2754 .unwrap_or(0.0)
2755 - st.scroll_offset;
2756 let ex = m
2757 .positions
2758 .get(byte_to_char_index(&m, ce))
2759 .copied()
2760 .unwrap_or(sx)
2761 - st.scroll_offset;
2762 let y = rect.y + text_off_y + line_h.max(font_val) - Dp(2.0).to_px().0;
2763 scene.nodes.push(SceneNode::Rect {
2764 rect: repose_core::Rect {
2765 x: rect.x + sx.max(0.0),
2766 y,
2767 w: (ex - sx).max(Dp(2.0).to_px().0),
2768 h: Dp(2.0).to_px().0,
2769 },
2770 brush: Brush::Solid(th.focus),
2771 radius: [Px::ZERO; 4],
2772 });
2773 }
2774
2775 let display_src = if st.text.is_empty() && !text_input.value.is_empty() {
2777 text_input.value.as_str()
2778 } else {
2779 st.text.as_str()
2780 };
2781 let txt_col = if display_src.is_empty() {
2782 ts.color.unwrap_or(th.on_surface_variant)
2783 } else {
2784 ts.color.unwrap_or(th.on_surface)
2785 };
2786 let render_txt = if display_src.is_empty() {
2787 hint_line(text_input, &metrics, font_val, rect.w)
2788 } else {
2789 rendered_by_vt(display_src)
2790 };
2791 scene.nodes.push(SceneNode::Text {
2792 rect: repose_core::Rect {
2793 x: rect.x - st.scroll_offset,
2794 y: rect.y + text_off_y,
2795 w: rect.w,
2796 h: line_h,
2797 },
2798 text: Arc::from(render_txt),
2799 color: mul_alpha_color(txt_col, alpha_accum),
2800 size: Px(font_val),
2801 style: TextPaintStyle {
2802 font_family: metrics.font_family,
2803 text_align: ts.text_align,
2804 font_weight: FontWeight(metrics.font_weight),
2805 font_style: match metrics.font_style {
2806 1 => FontStyle::Italic,
2807 _ => FontStyle::Normal,
2808 },
2809 text_decoration: ts.text_decoration.unwrap_or_default(),
2810 letter_spacing: Px(metrics.letter_spacing_px),
2811 line_height: Px(metrics.line_height_px),
2812 font_variation_settings: metrics.font_variation_settings.clone().map(Arc::from),
2813 ..Default::default()
2814 },
2815 });
2816
2817 if show_cursor
2819 && is_focused
2820 && st.selection.start == st.selection.end
2821 && st.caret_visible()
2822 {
2823 let caret_off = if has_vt {
2824 original_offset_to_display_with_mapping(
2825 &st.text,
2826 &measure_for,
2827 st.selection.end,
2828 st.offset_map.as_deref(),
2829 )
2830 } else {
2831 st.selection.end
2832 };
2833 let cx = m
2834 .positions
2835 .get(byte_to_char_index(&m, caret_off))
2836 .copied()
2837 .unwrap_or(0.0)
2838 - st.scroll_offset;
2839 let cursor_y = rect.y + text_off_y + (line_h.max(font_val) - font_val) / 2.0;
2840 scene.nodes.push(SceneNode::Rect {
2841 rect: repose_core::Rect {
2842 x: rect.x + cx.max(0.0),
2843 y: cursor_y,
2844 w: Dp(1.0).to_px().0,
2845 h: font_val,
2846 },
2847 brush: Brush::Solid(cursor_color),
2848 radius: [Px::ZERO; 4],
2849 });
2850 }
2851 } else {
2852 let render_text = if st.text.is_empty() {
2854 st.text.clone()
2855 } else if let Some(ref vt) = text_input.visual_transformation {
2856 let annotated = repose_core::AnnotatedString::new(st.text.clone(), vec![]);
2857 vt.filter(&annotated).text.text
2858 } else {
2859 st.text.clone()
2860 };
2861 let layout = layout_text_area_with_metrics(&render_text, rect.w.max(1.0), &metrics);
2862 let lh = layout.line_h_px;
2863 let max_line_count = text_input.max_lines.unwrap_or(usize::MAX);
2864 let visible_line_count = layout.ranges.len().min(max_line_count);
2865 let line_step = if lh.is_finite() && lh > 0.0 { lh } else { 1.0 };
2866 let first_visible_line = ((((st.scroll_offset_y - 1.0) / line_step).floor().max(0.0)
2867 as usize)
2868 .saturating_sub(1))
2869 .min(visible_line_count);
2870 let last_visible_line = (((st.scroll_offset_y + rect.h + 1.0) / line_step)
2871 .ceil()
2872 .max(0.0) as usize)
2873 .min(visible_line_count)
2874 .max(first_visible_line);
2875
2876 if st.text.is_empty() {
2878 scene.nodes.push(SceneNode::Text {
2879 rect: repose_core::Rect {
2880 x: rect.x,
2881 y: rect.y,
2882 w: rect.w,
2883 h: line_h,
2884 },
2885 text: Arc::from(hint_line(text_input, &metrics, font_val, rect.w)),
2886 color: mul_alpha_color(ts.color.unwrap_or(th.on_surface_variant), alpha_accum),
2887 size: Px(font_val),
2888 style: TextPaintStyle {
2889 font_family: metrics.font_family,
2890 text_align: ts.text_align,
2891 font_weight: FontWeight(metrics.font_weight),
2892 font_style: match metrics.font_style {
2893 1 => FontStyle::Italic,
2894 _ => FontStyle::Normal,
2895 },
2896 text_decoration: ts.text_decoration.unwrap_or_default(),
2897 letter_spacing: Px(metrics.letter_spacing_px),
2898 line_height: Px(metrics.line_height_px),
2899 font_variation_settings: metrics
2900 .font_variation_settings
2901 .clone()
2902 .map(Arc::from),
2903 ..Default::default()
2904 },
2905 });
2906 } else {
2907 for i in first_visible_line..last_visible_line {
2908 let (s, e) = layout.ranges[i];
2909 let ln = render_text[s..e].to_string();
2910 let draw_y = rect.y + (i as f32) * lh - st.scroll_offset_y;
2911 if draw_y + lh < rect.y - 1.0 || draw_y > rect.y + rect.h + 1.0 {
2912 continue;
2913 }
2914 scene.nodes.push(SceneNode::Text {
2915 rect: repose_core::Rect {
2916 x: rect.x,
2917 y: draw_y,
2918 w: rect.w,
2919 h: lh,
2920 },
2921 text: Arc::<str>::from(ln),
2922 color: mul_alpha_color(ts.color.unwrap_or(th.on_surface), alpha_accum),
2923 size: Px(font_val),
2924 style: TextPaintStyle {
2925 font_family: metrics.font_family,
2926 text_align: ts.text_align,
2927 font_weight: FontWeight(metrics.font_weight),
2928 font_style: match metrics.font_style {
2929 1 => FontStyle::Italic,
2930 _ => FontStyle::Normal,
2931 },
2932 text_decoration: ts.text_decoration.unwrap_or_default(),
2933 letter_spacing: Px(metrics.letter_spacing_px),
2934 line_height: Px(metrics.line_height_px),
2935 font_variation_settings: metrics
2936 .font_variation_settings
2937 .clone()
2938 .map(Arc::from),
2939 ..Default::default()
2940 },
2941 });
2942 }
2943 }
2944
2945 if show_selection && st.selection.start != st.selection.end {
2947 let sel_a_orig: usize = st.selection.start.min(st.selection.end);
2948 let sel_b_orig: usize = st.selection.start.max(st.selection.end);
2949 let has_vt = text_input.visual_transformation.is_some();
2950 let sel_a = if has_vt {
2951 original_offset_to_display_with_mapping(
2952 &st.text,
2953 &render_text,
2954 sel_a_orig,
2955 st.offset_map.as_deref(),
2956 )
2957 } else {
2958 sel_a_orig
2959 };
2960 let sel_b = if has_vt {
2961 original_offset_to_display_with_mapping(
2962 &st.text,
2963 &render_text,
2964 sel_b_orig,
2965 st.offset_map.as_deref(),
2966 )
2967 } else {
2968 sel_b_orig
2969 };
2970 let measure_cfg = metrics.measure_config();
2971 let x_for = |line: &SelectionLine, byte: usize| -> f32 {
2972 let m = measure_text(
2973 &render_text[line.start..line.end],
2974 font_val,
2975 measure_cfg.clone(),
2976 );
2977 m.positions
2978 .get(byte_to_char_index(&m, byte.saturating_sub(line.start)))
2979 .copied()
2980 .unwrap_or(0.0)
2981 };
2982 let sel_lines: Vec<SelectionLine> = (first_visible_line..last_visible_line)
2983 .filter_map(|i| {
2984 let &(start, end) = layout.ranges.get(i)?;
2985 let line_w =
2986 measure_text(&render_text[start..end], font_val, measure_cfg.clone())
2987 .positions
2988 .last()
2989 .copied()
2990 .unwrap_or(0.0);
2991 Some(SelectionLine {
2992 start,
2993 end,
2994 left: 0.0,
2995 right: line_w,
2996 top: (i as f32) * lh - st.scroll_offset_y,
2997 height: lh,
2998 })
2999 })
3000 .collect();
3001 let rects = selection_rects(&sel_lines, &(sel_a..sel_b), &x_for, (rect.x, rect.y));
3002 push_selection(scene, rects, selection_brush(th.primary));
3003 }
3004
3005 if let Some(comp) = st.composition.clone() {
3008 let has_vt = text_input.visual_transformation.is_some();
3009 let comp_a = if has_vt {
3010 original_offset_to_display_with_mapping(
3011 &st.text,
3012 &render_text,
3013 comp.start,
3014 st.offset_map.as_deref(),
3015 )
3016 } else {
3017 comp.start
3018 };
3019 let comp_b = if has_vt {
3020 original_offset_to_display_with_mapping(
3021 &st.text,
3022 &render_text,
3023 comp.end,
3024 st.offset_map.as_deref(),
3025 )
3026 } else {
3027 comp.end
3028 };
3029 for i in first_visible_line..last_visible_line {
3030 let (s, e) = layout.ranges[i];
3031 let os = comp_a.max(s);
3032 let oe = comp_b.min(e);
3033 if os >= oe {
3034 continue;
3035 }
3036 let ln = &render_text[s..e];
3037 let m = measure_text(ln, font_val, metrics.measure_config());
3038 let ls = os - s;
3039 let le = oe - s;
3040 let sx = m
3041 .positions
3042 .get(byte_to_char_index(&m, ls))
3043 .copied()
3044 .unwrap_or(0.0);
3045 let ex = m
3046 .positions
3047 .get(byte_to_char_index(&m, le))
3048 .copied()
3049 .unwrap_or(sx);
3050 let draw_y = rect.y + (i as f32) * lh - st.scroll_offset_y;
3051 scene.nodes.push(SceneNode::Rect {
3052 rect: repose_core::Rect {
3053 x: rect.x + sx,
3054 y: draw_y + lh - Dp(2.0).to_px().0,
3055 w: (ex - sx).max(Dp(2.0).to_px().0),
3056 h: Dp(2.0).to_px().0,
3057 },
3058 brush: Brush::Solid(th.focus),
3059 radius: [Px::ZERO; 4],
3060 });
3061 }
3062 }
3063
3064 if show_cursor
3066 && is_focused
3067 && st.selection.start == st.selection.end
3068 && st.caret_visible()
3069 {
3070 let caret_orig = st.selection.end.min(st.text.len());
3071 let has_vt = text_input.visual_transformation.is_some();
3072 let caret = if has_vt {
3073 original_offset_to_display_with_mapping(
3074 &st.text,
3075 &render_text,
3076 caret_orig,
3077 st.offset_map.as_deref(),
3078 )
3079 } else {
3080 caret_orig
3081 };
3082 let (cx, cy, _li) =
3083 caret_xy_for_byte_with_metrics(&render_text, rect.w.max(1.0), caret, &metrics);
3084 let draw_x = rect.x + cx;
3085 let draw_y = rect.y + cy - st.scroll_offset_y;
3086 scene.nodes.push(SceneNode::Rect {
3087 rect: repose_core::Rect {
3088 x: draw_x,
3089 y: draw_y + (lh - font_val) / 2.0,
3090 w: Dp(1.0).to_px().0,
3091 h: font_val,
3092 },
3093 brush: Brush::Solid(cursor_color),
3094 radius: [Px::ZERO; 4],
3095 });
3096 }
3097 }
3098 } else {
3099 if text_input.value.is_empty() {
3101 let hint_y = if text_input.multiline {
3102 rect.y
3103 } else {
3104 rect.y + text_off_y
3105 };
3106 scene.nodes.push(SceneNode::Text {
3107 rect: repose_core::Rect {
3108 x: rect.x,
3109 y: hint_y,
3110 w: rect.w,
3111 h: line_h,
3112 },
3113 text: Arc::from(hint_line(text_input, &metrics, font_val, rect.w)),
3114 color: mul_alpha_color(th.on_surface_variant, alpha_accum),
3115 size: Px(font_val),
3116 style: TextPaintStyle {
3117 font_family: metrics.font_family,
3118 font_weight: FontWeight(metrics.font_weight),
3119 font_style: if metrics.font_style == 1 {
3120 FontStyle::Italic
3121 } else {
3122 FontStyle::Normal
3123 },
3124 text_decoration: ts.text_decoration.unwrap_or_default(),
3125 letter_spacing: Px(metrics.letter_spacing_px),
3126 line_height: Px(metrics.line_height_px),
3127 font_variation_settings: metrics.font_variation_settings.clone().map(Arc::from),
3128 ..Default::default()
3129 },
3130 });
3131 } else if text_input.multiline {
3132 let render_text = if text_input.value.is_empty() {
3133 text_input.value.clone()
3134 } else if let Some(ref vt) = text_input.visual_transformation {
3135 let annotated = repose_core::AnnotatedString::new(text_input.value.clone(), vec![]);
3136 vt.filter(&annotated).text.text
3137 } else {
3138 text_input.value.clone()
3139 };
3140 let layout = layout_text_area_with_metrics(&render_text, rect.w.max(1.0), &metrics);
3141 let lh = layout.line_h_px;
3142 let line_step = if lh.is_finite() && lh > 0.0 { lh } else { 1.0 };
3143 let first_visible_line =
3144 ((-1.0 / line_step).floor().max(0.0) as usize).min(layout.ranges.len());
3145 let last_visible_line =
3146 ((rect.h + 1.0) / line_step).ceil().max(0.0) as usize + first_visible_line;
3147 for i in first_visible_line..last_visible_line.min(layout.ranges.len()) {
3148 let (s, e) = layout.ranges[i];
3149 let ln = render_text[s..e].to_string();
3150 let draw_y = rect.y + (i as f32) * lh;
3151 if draw_y + lh < rect.y - 1.0 || draw_y > rect.y + rect.h + 1.0 {
3152 continue;
3153 }
3154 scene.nodes.push(SceneNode::Text {
3155 rect: repose_core::Rect {
3156 x: rect.x,
3157 y: draw_y,
3158 w: rect.w,
3159 h: lh,
3160 },
3161 text: Arc::<str>::from(ln),
3162 color: mul_alpha_color(th.on_surface, alpha_accum),
3163 size: Px(font_val),
3164 style: TextPaintStyle {
3165 font_family: metrics.font_family,
3166 font_weight: FontWeight(metrics.font_weight),
3167 font_style: if metrics.font_style == 1 {
3168 FontStyle::Italic
3169 } else {
3170 FontStyle::Normal
3171 },
3172 text_decoration: ts.text_decoration.unwrap_or_default(),
3173 letter_spacing: Px(metrics.letter_spacing_px),
3174 line_height: Px(metrics.line_height_px),
3175 font_variation_settings: metrics
3176 .font_variation_settings
3177 .clone()
3178 .map(Arc::from),
3179 ..Default::default()
3180 },
3181 });
3182 }
3183 } else {
3184 scene.nodes.push(SceneNode::Text {
3185 rect: repose_core::Rect {
3186 x: rect.x,
3187 y: rect.y + text_off_y,
3188 w: rect.w,
3189 h: line_h,
3190 },
3191 text: Arc::from(rendered_by_vt(&text_input.value)),
3192 color: mul_alpha_color(th.on_surface, alpha_accum),
3193 size: Px(font_val),
3194 style: TextPaintStyle {
3195 font_family: metrics.font_family,
3196 font_weight: FontWeight(metrics.font_weight),
3197 font_style: if metrics.font_style == 1 {
3198 FontStyle::Italic
3199 } else {
3200 FontStyle::Normal
3201 },
3202 text_decoration: ts.text_decoration.unwrap_or_default(),
3203 letter_spacing: Px(metrics.letter_spacing_px),
3204 line_height: Px(metrics.line_height_px),
3205 font_variation_settings: metrics.font_variation_settings.clone().map(Arc::from),
3206 ..Default::default()
3207 },
3208 });
3209 }
3210 }
3211
3212 if let Some(ref cb) = text_input.on_text_layout {
3214 let (
3215 line_count,
3216 content_w,
3217 content_h,
3218 first_baseline,
3219 last_baseline,
3220 did_overflow_w,
3221 did_overflow_h,
3222 lines,
3223 ) = if let Some(state_rc) = state {
3224 let st = state_rc.borrow();
3225 let display = if st.text.is_empty() {
3226 hint_line(text_input, &metrics, font_val, rect.w)
3227 } else if let Some(ref vt) = text_input.visual_transformation {
3228 let annotated = repose_core::AnnotatedString::new(st.text.clone(), vec![]);
3229 vt.filter(&annotated).text.text
3230 } else {
3231 st.text.clone()
3232 };
3233 if text_input.multiline {
3234 let l = layout_text_area_with_metrics(&display, rect.w.max(1.0), &metrics);
3235 let lc = l.ranges.len();
3236 let cw = rect.w.max(0.0);
3237 let ch = (lc as f32 * l.line_h_px).max(0.0);
3238 let line_infos: Vec<_> = l
3239 .ranges
3240 .iter()
3241 .enumerate()
3242 .map(|(i, &(s, e))| {
3243 let top = i as f32 * l.line_h_px;
3244 let bottom = top + l.line_h_px;
3245 let line_text = &display[s..e];
3246 let m = measure_text(line_text, font_val, metrics.measure_config());
3247 let line_w = m.positions.last().copied().unwrap_or(0.0);
3248 TextLineInfo {
3249 start: s,
3250 end: e,
3251 top,
3252 baseline: top + l.line_h_px * 0.8,
3253 bottom,
3254 left: 0.0,
3255 right: line_w,
3256 width: line_w,
3257 }
3258 })
3259 .collect();
3260 let fb = line_infos.first().map(|l| l.baseline).unwrap_or(0.0);
3261 let lb = line_infos.last().map(|l| l.baseline).unwrap_or(0.0);
3262 (lc, cw, ch, fb, lb, cw > rect.w, ch > rect.h, line_infos)
3263 } else {
3264 let m = measure_text(&display, font_val, metrics.measure_config());
3265 let w = m.positions.last().copied().unwrap_or(0.0);
3266 let top = 0.0;
3267 let bottom = line_h.max(font_val);
3268 let baseline = bottom * 0.8;
3269 let line_info = TextLineInfo {
3270 start: 0,
3271 end: display.len(),
3272 top,
3273 baseline,
3274 bottom,
3275 left: 0.0,
3276 right: w,
3277 width: w,
3278 };
3279 (
3280 1,
3281 w.max(0.0),
3282 bottom,
3283 baseline,
3284 baseline,
3285 w > rect.w,
3286 bottom > rect.h,
3287 vec![line_info],
3288 )
3289 }
3290 } else {
3291 (0, 0.0, 0.0, 0.0, 0.0, false, false, vec![])
3292 };
3293 cb(&repose_core::TextLayoutResult {
3294 line_count,
3295 width_px: content_w,
3296 height_px: content_h,
3297 first_baseline,
3298 last_baseline,
3299 did_overflow_width: did_overflow_w,
3300 did_overflow_height: did_overflow_h,
3301 lines,
3302 });
3303 }
3304
3305 scene.nodes.push(SceneNode::PopClip);
3306}
3307
3308fn text_field_view(
3312 modifier: Modifier,
3313 hint: String,
3314 value: String,
3315 multiline: bool,
3316 on_change: Option<Rc<dyn Fn(String)>>,
3317 on_submit: Option<Rc<dyn Fn(String)>>,
3318 visual_transformation: Option<Rc<dyn repose_core::VisualTransformation>>,
3319 keyboard_type: repose_core::KeyboardType,
3320 capitalization: repose_core::KeyboardCapitalization,
3321 ime_action: repose_core::ImeAction,
3322 auto_correct_enabled: Option<bool>,
3323 enabled: bool,
3324 read_only: bool,
3325 max_lines: Option<usize>,
3326 min_lines: usize,
3327 cursor_color: Option<Color>,
3328 on_text_layout: Option<Rc<dyn Fn(&repose_core::TextLayoutResult)>>,
3329 text_style: repose_core::TextStyle,
3330 keyboard_actions: repose_core::KeyboardActions,
3331 interaction_source: Option<repose_core::MutableInteractionSource>,
3332 focus_tracker: Option<Rc<Cell<bool>>>,
3333 line_limits: Option<repose_core::TextFieldLineLimits>,
3334 decoration_box: Option<Rc<dyn Fn(repose_core::View) -> repose_core::View>>,
3335) -> View {
3336 let mut modif = modifier.text_input(TextInputConfig {
3337 hint,
3338 multiline,
3339 on_change,
3340 on_submit,
3341 focus_tracker,
3342 value,
3343 visual_transformation,
3344 keyboard_type,
3345 capitalization,
3346 ime_action,
3347 auto_correct_enabled,
3348 enabled,
3349 read_only,
3350 sensitive: matches!(
3351 keyboard_type,
3352 repose_core::KeyboardType::Password
3353 | repose_core::KeyboardType::NumberPassword
3354 | repose_core::KeyboardType::DecimalPassword
3355 | repose_core::KeyboardType::NumberPasswordSigned
3356 | repose_core::KeyboardType::DecimalPasswordSigned
3357 ),
3358 max_lines,
3359 min_lines,
3360 cursor_color,
3361 on_text_layout,
3362 text_style: Some(text_style),
3363 keyboard_actions: Some(keyboard_actions),
3364 interaction_source: interaction_source.as_ref().map(|s| s.source()),
3365 line_limits,
3366 });
3367
3368 if !enabled {
3371 modif = modif.focusable(false).enabled(false);
3372 } else if read_only {
3373 modif = modif.focusable(true);
3374 }
3375
3376 let inner = View::new(0, ViewKind::Box)
3377 .modifier(modif)
3378 .semantics(Semantics {
3379 role: Role::TextField,
3380 enabled,
3381 ..Default::default()
3382 });
3383
3384 if let Some(decorate) = decoration_box {
3386 decorate(inner)
3387 } else {
3388 inner
3389 }
3390}
3391
3392pub fn tf_ensure_visible_in_rect(state: &mut TextFieldState, inner_rect: repose_core::Rect) {
3395 use crate::textfield::{TF_PADDING_X, TextFieldMetrics};
3396 let metrics = TextFieldMetrics::default();
3397 let caret = state.caret_index();
3398 let (display, display_caret) = if let Some(transformation) = &state.visual_transformation {
3399 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
3400 let transformed = transformation.filter(&annotated);
3401 (
3402 transformed.text.text,
3403 transformed.offset_mapping.original_to_transformed(caret),
3404 )
3405 } else {
3406 (state.text.clone(), caret)
3407 };
3408 let measured = measure_text(&display, metrics.font_px, metrics.measure_config());
3409 let caret_x_px = measured
3410 .positions
3411 .get(byte_to_char_index(&measured, display_caret))
3412 .copied()
3413 .unwrap_or(0.0);
3414 state.ensure_caret_visible(
3415 caret_x_px,
3416 inner_rect.w - 2.0 * TF_PADDING_X.to_px().0,
3417 Dp(2.0).to_px().0,
3418 );
3419}
3420
3421#[cfg(test)]
3422mod tests {
3423 use super::*;
3424
3425 #[test]
3426 fn test_index_for_x_bytes_grapheme() {
3427 let t = "A👍🏽B";
3428 let font_px = 16.0; let m = measure_text(t, font_px, TextMeasureConfig::default());
3430 for i in 0..m.byte_offsets.len() - 1 {
3431 let b = m.byte_offsets[i];
3432 let _ = &t[..b];
3433 }
3434 }
3435
3436 fn delete_op(
3437 index: usize,
3438 pre_text: &str,
3439 pre_selection: Range<usize>,
3440 post_selection: Range<usize>,
3441 ) -> TextUndoOp {
3442 TextUndoOp {
3443 index,
3444 pre_text: pre_text.to_string(),
3445 post_text: String::new(),
3446 pre_selection,
3447 post_selection,
3448 time: Instant::now(),
3449 can_merge: true,
3450 }
3451 }
3452
3453 #[test]
3454 fn deletion_type_collapsed_post_selection_is_backspace() {
3455 let op = delete_op(2, "c", 3..3, 2..2);
3457 assert_eq!(op.deletion_type(), TextDeleteType::Start);
3458 }
3459
3460 #[test]
3461 fn deletion_type_collapsed_post_selection_is_delete_forward() {
3462 let op = delete_op(3, "c", 3..3, 3..3);
3464 assert_eq!(op.deletion_type(), TextDeleteType::End);
3465 }
3466
3467 #[test]
3468 fn deletion_type_range_post_selection_is_not_by_user() {
3469 let op = delete_op(2, "de", 2..4, 3..5);
3473 assert_eq!(op.deletion_type(), TextDeleteType::NotByUser);
3474 }
3475
3476 #[test]
3477 fn backspace_ops_merge() {
3478 let a = delete_op(2, "c", 3..3, 2..2);
3480 let b = delete_op(1, "b", 2..2, 1..1);
3481 let merged = a
3482 .try_merge(&b)
3483 .expect("consecutive backspaces should merge");
3484 assert_eq!(merged.index, 1);
3485 assert_eq!(merged.pre_text, "bc");
3486 }
3487
3488 #[test]
3489 fn selection_delete_does_not_merge_with_backspace() {
3490 let backspace = delete_op(2, "c", 3..3, 2..2);
3493 let selection = delete_op(2, "de", 2..4, 2..2);
3494 assert_eq!(selection.deletion_type(), TextDeleteType::Inner);
3495 assert!(backspace.try_merge(&selection).is_none());
3496 }
3497
3498 #[test]
3507 fn multiline_tap_and_caret_round_trip() {
3508 let m = TextFieldMetrics::default();
3509 let lh = m.line_height_px;
3510 for (text, wrap) in [
3511 ("hello world\nsecond line here\nthird", 1000.0),
3512 ("hello world\nsecond line here\nthird", 60.0),
3513 ("a\n\nb", 1000.0),
3514 ("one\n", 1000.0),
3515 ("日本語テスト二行目", 30.0),
3516 ("héllo wörld\nünicode", 1000.0),
3517 ] {
3518 let layout = layout_text_area_with_metrics(text, wrap, &m);
3519 for b in 0..=text.len() {
3520 if !text.is_char_boundary(b) {
3521 continue;
3522 }
3523 let (x, y, line) = caret_xy_for_byte_with_metrics(text, wrap, b, &m);
3524 let Some(&(line_start, line_end)) = layout.ranges.get(line) else {
3525 panic!("byte {b} of {text:?} reported absent line {line}");
3526 };
3527 assert!(
3528 line_start <= b && b <= line_end,
3529 "byte {b} of {text:?} reported line {line} [{line_start}..{line_end}]"
3530 );
3531 let got = index_for_xy_bytes_with_metrics(text, wrap, x + 0.5, y + lh * 0.5, &m);
3532 assert_eq!(got, b, "byte {b} of {text:?} at ({x}, {y}) tapped to {got}");
3533 }
3534 }
3535 }
3536
3537 #[test]
3540 fn home_end_never_move_caret_against_the_user() {
3541 let m = TextFieldMetrics::default();
3542 for text in ["ab \ncd", "ab \ncd", " indented line\nnext", "one\n"] {
3543 for wrap in [1000.0f32, 30.0] {
3544 let ranges = layout_text_area_with_metrics(text, wrap, &m).ranges;
3545 for b in 0..=text.len() {
3546 let (line, _, _) = locate_byte_in_ranges(&ranges, b);
3547 let (start, end) = ranges.get(line).copied().unwrap_or((b, b));
3548 let to_end = line_home_end_with_metrics(text, wrap, b, true, &m);
3549 let to_home = line_home_end_with_metrics(text, wrap, b, false, &m);
3550 if start <= b && b <= end {
3551 assert_eq!(to_end, end, "End from {b} in {text:?} wrap {wrap}");
3552 assert_eq!(to_home, start, "Home from {b} in {text:?} wrap {wrap}");
3553 } else {
3554 assert_eq!(to_end, b, "End from gap byte {b} in {text:?} wrap {wrap}");
3555 assert_eq!(to_home, b, "Home from gap byte {b} in {text:?} wrap {wrap}");
3556 }
3557 }
3558 }
3559 }
3560 }
3561}