1use repose_core::*;
41use std::cell::RefCell;
42use std::collections::HashMap;
43use std::ops::Range;
44use std::rc::Rc;
45use std::sync::Arc;
46use unicode_segmentation::UnicodeSegmentation;
47use web_time::Duration;
48use web_time::Instant;
49
50use crate::layout::mul_alpha_color;
51
52thread_local! {
53 static TEXTFIELD_STATES: RefCell<HashMap<u64, Rc<RefCell<TextFieldState>>>> = RefCell::new(HashMap::new());
54}
55
56pub fn set_textfield_state(key: u64, state: Rc<RefCell<TextFieldState>>) {
57 TEXTFIELD_STATES.with(|m| m.borrow_mut().insert(key, state));
58}
59
60pub fn get_textfield_state(key: u64) -> Option<Rc<RefCell<TextFieldState>>> {
61 TEXTFIELD_STATES.with(|m| m.borrow().get(&key).cloned())
62}
63
64pub fn ensure_caret_visible(state: &mut TextFieldState, multiline: bool) {
65 let font_px = repose_core::dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
66 let wrap_width = state.inner_width;
67 if multiline {
68 let (cx, cy, _) = crate::textfield::caret_xy_for_byte(
69 &state.text,
70 font_px,
71 wrap_width,
72 state.caret_index(),
73 );
74 let iw = state.inner_width;
75 let ih = state.inner_height;
76 state.ensure_caret_visible_xy(cx, cy, iw, ih, repose_core::dp_to_px(2.0));
77 } else {
78 let caret_idx = state.caret_index();
79 let (display, caret_display_off) = if let Some(vt) = &state.visual_transformation {
80 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
81 let tfmd = vt.filter(&annotated);
82 let off =
83 repose_core::original_offset_to_display(&state.text, tfmd.text.as_str(), caret_idx);
84 (tfmd.text.text, off)
85 } else {
86 (state.text.clone(), caret_idx)
87 };
88 let m = crate::textfield::measure_text(&display, font_px, TextMeasureConfig::default());
89 let caret_x = m.positions.get(caret_display_off).copied().unwrap_or(0.0);
90 state.ensure_caret_visible(caret_x, wrap_width, repose_core::dp_to_px(2.0));
91 }
92}
93
94const TEXT_UNDO_CAPACITY: usize = 100;
96
97const SNAPSHOTS_INTERVAL_MILLIS: u128 = 5000;
99
100#[derive(Clone, Copy, Debug, PartialEq)]
102enum TextEditType {
103 Insert,
104 Delete,
105 Replace,
106}
107
108#[derive(Clone, Copy, Debug, PartialEq)]
110enum TextDeleteType {
111 Start, End, Inner, NotByUser,
115}
116
117#[derive(Clone, Debug)]
119pub struct TextUndoOp {
120 pub index: usize,
122 pub pre_text: String,
124 pub post_text: String,
126 pub pre_selection: Range<usize>,
128 pub post_selection: Range<usize>,
130 pub time: Instant,
132 pub can_merge: bool,
134}
135
136impl TextUndoOp {
137 fn edit_type(&self) -> TextEditType {
138 match (self.pre_text.is_empty(), self.post_text.is_empty()) {
139 (true, true) => unreachable!("Both pre and post text cannot be empty"),
140 (true, false) => TextEditType::Insert,
141 (false, true) => TextEditType::Delete,
142 (false, false) => TextEditType::Replace,
143 }
144 }
145
146 fn is_newline(&self) -> bool {
147 self.post_text == "\n" || self.post_text == "\r\n"
148 }
149
150 fn try_merge(&self, next: &TextUndoOp) -> Option<TextUndoOp> {
152 if !self.can_merge || !next.can_merge {
153 return None;
154 }
155
156 let elapsed = next.time.saturating_duration_since(self.time);
157 if elapsed.as_millis() >= SNAPSHOTS_INTERVAL_MILLIS {
158 return None;
159 }
160
161 if self.is_newline() || next.is_newline() {
162 return None;
163 }
164
165 let self_type = self.edit_type();
166 if self_type != next.edit_type() {
167 return None;
168 }
169
170 match self_type {
171 TextEditType::Insert => {
172 if self.index + self.post_text.len() == next.index {
174 Some(TextUndoOp {
175 index: self.index,
176 pre_text: String::new(),
177 post_text: format!("{}{}", self.post_text, next.post_text),
178 pre_selection: self.pre_selection.clone(),
179 post_selection: next.post_selection.clone(),
180 time: self.time,
181 can_merge: true,
182 })
183 } else {
184 None
185 }
186 }
187 TextEditType::Delete => {
188 let self_del = self.deletion_type();
189 let next_del = next.deletion_type();
190 if self_del == next_del
192 && (self_del == TextDeleteType::Start || self_del == TextDeleteType::End)
193 {
194 if self.index == next.index + next.pre_text.len() {
195 Some(TextUndoOp {
197 index: next.index,
198 pre_text: format!("{}{}", next.pre_text, self.pre_text),
199 post_text: String::new(),
200 pre_selection: self.pre_selection.clone(),
201 post_selection: next.post_selection.clone(),
202 time: self.time,
203 can_merge: true,
204 })
205 } else if self.index == next.index {
206 Some(TextUndoOp {
208 index: self.index,
209 pre_text: format!("{}{}", self.pre_text, next.pre_text),
210 post_text: String::new(),
211 pre_selection: self.pre_selection.clone(),
212 post_selection: next.post_selection.clone(),
213 time: self.time,
214 can_merge: true,
215 })
216 } else {
217 None
218 }
219 } else {
220 None
221 }
222 }
223 TextEditType::Replace => None,
224 }
225 }
226
227 fn deletion_type(&self) -> TextDeleteType {
229 if self.edit_type() != TextEditType::Delete {
230 return TextDeleteType::NotByUser;
231 }
232 if !self.post_selection.start == self.post_selection.end {
233 return TextDeleteType::NotByUser;
234 }
235 if self.pre_selection.start == self.pre_selection.end {
236 if self.pre_selection.start > self.post_selection.start {
238 TextDeleteType::Start } else {
240 TextDeleteType::End }
242 } else if self.pre_selection.start == self.post_selection.start
243 && self.pre_selection.start == self.index
244 {
245 TextDeleteType::Inner
246 } else {
247 TextDeleteType::NotByUser
248 }
249 }
250}
251
252const SCROLL_STIFFNESS: f32 = 300.0;
254const SCROLL_DAMPING: f32 = 30.0;
255
256pub const TF_FONT_DP: f32 = 16.0;
258
259#[derive(Clone, Copy, Debug)]
261pub struct KeyboardOptions {
262 pub keyboard_type: repose_core::KeyboardType,
263 pub autocorrect: bool,
264 pub capitalization: repose_core::KeyboardCapitalization,
265}
266
267impl Default for KeyboardOptions {
268 fn default() -> Self {
269 Self {
270 keyboard_type: repose_core::KeyboardType::default(),
271 autocorrect: true,
272 capitalization: repose_core::KeyboardCapitalization::default(),
273 }
274 }
275}
276pub const TF_PADDING_X_DP: f32 = 8.0;
278
279pub struct TextMetrics {
280 pub positions: Vec<f32>, pub byte_offsets: Vec<usize>,
284}
285
286pub struct TextMeasureConfig {
287 pub font_family: Option<&'static str>,
288 pub font_weight: u16,
289 pub font_style: u8,
290 pub letter_spacing: f32,
291 pub font_variation_settings: Option<String>,
292}
293
294impl Default for TextMeasureConfig {
295 fn default() -> Self {
296 Self {
297 font_family: None,
298 font_weight: 400,
299 font_style: 0,
300 letter_spacing: 0.0,
301 font_variation_settings: None,
302 }
303 }
304}
305
306pub fn measure_text(text: &str, font_px: f32, config: TextMeasureConfig) -> TextMetrics {
310 let m = repose_text::metrics_for_textfield(
311 text,
312 font_px,
313 config.font_family,
314 config.font_weight,
315 config.font_style,
316 config.letter_spacing,
317 config.font_variation_settings.as_deref(),
318 );
319 TextMetrics {
320 positions: m.positions,
321 byte_offsets: m.byte_offsets,
322 }
323}
324
325pub fn byte_to_char_index(m: &TextMetrics, byte: usize) -> usize {
326 match m.byte_offsets.binary_search(&byte) {
327 Ok(i) | Err(i) => i,
328 }
329}
330
331pub fn index_for_x_bytes(
333 text: &str,
334 font_px: f32,
335 x_px: f32,
336 font_weight: u16,
337 font_style: u8,
338) -> usize {
339 let m = measure_text(
340 text,
341 font_px,
342 TextMeasureConfig {
343 font_weight,
344 font_style,
345 ..Default::default()
346 },
347 );
348
349 let mut best_i = 0usize;
350 let mut best_d = f32::INFINITY;
351 for i in 0..m.positions.len() {
352 let d = (m.positions[i] - x_px).abs();
353 if d < best_d {
354 best_d = d;
355 best_i = i;
356 }
357 }
358 m.byte_offsets[best_i]
359}
360
361fn prev_grapheme_boundary(text: &str, byte: usize) -> usize {
363 let mut last = 0usize;
364 for (i, _) in text.grapheme_indices(true) {
365 if i >= byte {
366 break;
367 }
368 last = i;
369 }
370 last
371}
372
373fn next_grapheme_boundary(text: &str, byte: usize) -> usize {
374 for (i, _) in text.grapheme_indices(true) {
375 if i > byte {
376 return i;
377 }
378 }
379 text.len()
380}
381
382pub struct TextFieldState {
383 pub text: String,
384 pub selection: Range<usize>,
385 pub composition: Option<Range<usize>>, pub scroll_offset: f32, pub scroll_offset_y: f32, pub drag_anchor: Option<usize>, 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>>,
396 pub visual_transformation: Option<Rc<dyn VisualTransformation>>,
398 pub(crate) scroll_target: f32,
400 pub(crate) scroll_target_y: f32,
402 scroll_vel: f32,
404 scroll_vel_y: f32,
406 last_scroll_tick: Option<Instant>,
408
409 undo_stack: Vec<TextUndoOp>,
412 redo_stack: Vec<TextUndoOp>,
414 staging_undo: Option<TextUndoOp>,
416}
417
418impl std::fmt::Debug for TextFieldState {
419 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420 f.debug_struct("TextFieldState")
421 .field("text", &self.text)
422 .field("selection", &self.selection)
423 .field("composition", &self.composition)
424 .field("scroll_offset", &self.scroll_offset)
425 .field("scroll_offset_y", &self.scroll_offset_y)
426 .field("drag_anchor", &self.drag_anchor)
427 .field("blink_start", &self.blink_start)
428 .field("inner_width", &self.inner_width)
429 .field("inner_height", &self.inner_height)
430 .field("preferred_x_px", &self.preferred_x_px)
431 .field(
432 "offset_map",
433 &self.offset_map.as_ref().map(|_| "<offset_mapping>"),
434 )
435 .field(
436 "visual_transformation",
437 &self.visual_transformation.as_ref().map(|_| "<vt>"),
438 )
439 .field("scroll_target", &self.scroll_target)
440 .field("scroll_target_y", &self.scroll_target_y)
441 .field("can_undo", &self.can_undo())
442 .field("can_redo", &self.can_redo())
443 .field("undo_count", &self.undo_stack.len())
444 .field("redo_count", &self.redo_stack.len())
445 .finish()
446 }
447}
448
449impl Default for TextFieldState {
450 fn default() -> Self {
451 Self::new()
452 }
453}
454
455impl Clone for TextFieldState {
456 fn clone(&self) -> Self {
457 Self {
458 text: self.text.clone(),
459 selection: self.selection.clone(),
460 composition: self.composition.clone(),
461 scroll_offset: self.scroll_offset,
462 scroll_offset_y: self.scroll_offset_y,
463 drag_anchor: self.drag_anchor,
464 blink_start: self.blink_start,
465 inner_width: self.inner_width,
466 inner_height: self.inner_height,
467 preferred_x_px: self.preferred_x_px,
468 offset_map: self.offset_map.as_ref().map(|m| m.clone_box()),
469 visual_transformation: self.visual_transformation.clone(),
470 scroll_target: self.scroll_target,
471 scroll_target_y: self.scroll_target_y,
472 scroll_vel: self.scroll_vel,
473 scroll_vel_y: self.scroll_vel_y,
474 last_scroll_tick: self.last_scroll_tick,
475 undo_stack: self.undo_stack.clone(),
476 redo_stack: self.redo_stack.clone(),
477 staging_undo: self.staging_undo.clone(),
478 }
479 }
480}
481
482impl TextFieldState {
483 pub fn new() -> Self {
484 Self {
485 text: String::new(),
486 selection: 0..0,
487 composition: None,
488 scroll_offset: 0.0,
489 scroll_offset_y: 0.0,
490 drag_anchor: None,
491 blink_start: Instant::now(),
492 inner_width: 0.0,
493 inner_height: 0.0,
494 preferred_x_px: None,
495 offset_map: None,
496 visual_transformation: None,
497 scroll_target: 0.0,
498 scroll_target_y: 0.0,
499 scroll_vel: 0.0,
500 scroll_vel_y: 0.0,
501 last_scroll_tick: None,
502 undo_stack: Vec::new(),
503 redo_stack: Vec::new(),
504 staging_undo: None,
505 }
506 }
507
508 pub fn can_undo(&self) -> bool {
512 !self.undo_stack.is_empty() || self.staging_undo.is_some()
513 }
514
515 pub fn can_redo(&self) -> bool {
517 !self.redo_stack.is_empty()
518 }
519
520 pub fn undo(&mut self) -> bool {
522 self.flush_undo();
523 if let Some(op) = self.undo_stack.pop() {
524 let end = (op.index + op.post_text.len()).min(self.text.len());
525 self.text.replace_range(op.index..end, &op.pre_text);
526 self.selection = op.pre_selection.clone();
527 self.redo_stack.push(op);
528 self.preferred_x_px = None;
529 self.reset_caret_blink();
530 true
531 } else {
532 false
533 }
534 }
535
536 pub fn redo(&mut self) -> bool {
538 if let Some(op) = self.redo_stack.pop() {
539 let end = (op.index + op.pre_text.len()).min(self.text.len());
540 self.text.replace_range(op.index..end, &op.post_text);
541 self.selection = op.post_selection.clone();
542 self.undo_stack.push(op);
543 self.preferred_x_px = None;
544 self.reset_caret_blink();
545 true
546 } else {
547 false
548 }
549 }
550
551 pub fn clear_undo_history(&mut self) {
553 self.undo_stack.clear();
554 self.redo_stack.clear();
555 self.staging_undo = None;
556 }
557
558 fn record_edit(&mut self, op: TextUndoOp) {
562 if let Some(staging) = self.staging_undo.take() {
563 if let Some(merged) = staging.try_merge(&op) {
564 self.staging_undo = Some(merged);
565 return;
566 }
567 self.undo_stack.push(staging);
569 self.redo_stack.clear();
570 while self.undo_stack.len() + 1 > TEXT_UNDO_CAPACITY {
572 self.undo_stack.remove(0);
573 }
574 }
575 self.staging_undo = Some(op);
576 }
577
578 fn flush_undo(&mut self) {
580 if let Some(op) = self.staging_undo.take() {
581 self.undo_stack.push(op);
582 self.redo_stack.clear();
583 while self.undo_stack.len() > TEXT_UNDO_CAPACITY {
584 self.undo_stack.remove(0);
585 }
586 }
587 }
588
589 fn insert_text_impl(&mut self, text: &str, can_merge: bool) {
590 let start = self.selection.start.min(self.text.len());
591 let end = self.selection.end.min(self.text.len());
592 let pre_text = self.text[start..end].to_string();
593 let pre_selection = self.selection.clone();
594
595 self.text.replace_range(start..end, text);
596 let new_pos = start + text.len();
597 self.selection = new_pos..new_pos;
598 self.preferred_x_px = None;
599 self.reset_caret_blink();
600
601 if !pre_text.is_empty() || !text.is_empty() {
602 self.record_edit(TextUndoOp {
603 index: start,
604 pre_text,
605 post_text: text.to_string(),
606 pre_selection,
607 post_selection: self.selection.clone(),
608 time: Instant::now(),
609 can_merge,
610 });
611 }
612 }
613
614 pub fn insert_text(&mut self, text: &str) {
615 self.insert_text_impl(text, true);
616 }
617
618 pub fn insert_text_atomic(&mut self, text: &str) {
620 self.insert_text_impl(text, false);
621 }
622
623 pub fn delete_backward(&mut self) {
624 if self.selection.start == self.selection.end {
625 let pos = self.selection.start.min(self.text.len());
626 if pos > 0 {
627 let prev = prev_grapheme_boundary(&self.text, pos);
628 let pre_text = self.text[prev..pos].to_string();
629 let pre_selection = self.selection.clone();
630 self.text.replace_range(prev..pos, "");
631 self.selection = prev..prev;
632 self.preferred_x_px = None;
633 self.reset_caret_blink();
634 self.record_edit(TextUndoOp {
635 index: prev,
636 pre_text,
637 post_text: String::new(),
638 pre_selection,
639 post_selection: self.selection.clone(),
640 time: Instant::now(),
641 can_merge: true,
642 });
643 }
644 } else {
645 self.insert_text_impl("", true);
646 }
647 self.preferred_x_px = None;
648 self.reset_caret_blink();
649 }
650
651 pub fn delete_forward(&mut self) {
652 if self.selection.start == self.selection.end {
653 let pos = self.selection.start.min(self.text.len());
654 if pos < self.text.len() {
655 let next = next_grapheme_boundary(&self.text, pos);
656 let pre_text = self.text[pos..next].to_string();
657 let pre_selection = self.selection.clone();
658 self.text.replace_range(pos..next, "");
659 self.preferred_x_px = None;
660 self.reset_caret_blink();
661 self.record_edit(TextUndoOp {
662 index: pos,
663 pre_text,
664 post_text: String::new(),
665 pre_selection,
666 post_selection: self.selection.clone(),
667 time: Instant::now(),
668 can_merge: true,
669 });
670 }
671 } else {
672 self.insert_text_impl("", true);
673 }
674 self.preferred_x_px = None;
675 self.reset_caret_blink();
676 }
677
678 pub fn move_cursor(&mut self, delta: isize, extend_selection: bool) {
679 let mut pos = self.selection.end.min(self.text.len());
680 if delta < 0 {
681 for _ in 0..delta.unsigned_abs() {
682 pos = prev_grapheme_boundary(&self.text, pos);
683 }
684 } else if delta > 0 {
685 for _ in 0..(delta as usize) {
686 pos = next_grapheme_boundary(&self.text, pos);
687 }
688 }
689 if extend_selection {
690 self.selection.end = pos;
691 } else {
692 self.selection = pos..pos;
693 }
694 self.preferred_x_px = None;
695 self.reset_caret_blink();
696 }
697
698 pub fn selected_text(&self) -> String {
699 if self.selection.start == self.selection.end {
700 String::new()
701 } else {
702 self.text[self.selection.clone()].to_string()
703 }
704 }
705
706 pub fn set_composition(&mut self, text: String, cursor: Option<(usize, usize)>) {
707 if text.is_empty() {
708 if let Some(range) = self.composition.take() {
709 let s = clamp_to_char_boundary(&self.text, range.start.min(self.text.len()));
710 let e = clamp_to_char_boundary(&self.text, range.end.min(self.text.len()));
711 if s <= e {
712 self.text.replace_range(s..e, "");
713 self.selection = s..s;
714 }
715 }
716 self.preferred_x_px = None;
717 self.reset_caret_blink();
718 return;
719 }
720
721 let anchor_start;
722 if let Some(r) = self.composition.take() {
723 let mut s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
724 let mut e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
725 if e < s {
726 std::mem::swap(&mut s, &mut e);
727 }
728 self.text.replace_range(s..e, &text);
729 anchor_start = s;
730 } else {
731 let pos = clamp_to_char_boundary(&self.text, self.selection.start.min(self.text.len()));
732 self.text.insert_str(pos, &text);
733 anchor_start = pos;
734 }
735
736 self.composition = Some(anchor_start..(anchor_start + text.len()));
737
738 if let Some((c0, c1)) = cursor {
739 let b0 = char_to_byte(&text, c0);
740 let b1 = char_to_byte(&text, c1);
741 self.selection = (anchor_start + b0)..(anchor_start + b1);
742 } else {
743 let end = anchor_start + text.len();
744 self.selection = end..end;
745 }
746
747 self.preferred_x_px = None;
748 self.reset_caret_blink();
749 }
750
751 pub fn commit_composition(&mut self, text: String) {
752 let pre_selection = self.selection.clone();
753 if let Some(r) = self.composition.take() {
754 let s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
755 let e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
756 let pre_text = self.text[s..e].to_string();
757 self.text.replace_range(s..e, &text);
758 let new_pos = s + text.len();
759 self.selection = new_pos..new_pos;
760 self.preferred_x_px = None;
761 self.reset_caret_blink();
762 if !pre_text.is_empty() || !text.is_empty() {
763 self.record_edit(TextUndoOp {
764 index: s,
765 pre_text,
766 post_text: text,
767 pre_selection,
768 post_selection: self.selection.clone(),
769 time: Instant::now(),
770 can_merge: true,
771 });
772 }
773 } else {
774 let pos = clamp_to_char_boundary(&self.text, self.selection.end.min(self.text.len()));
775 self.text.insert_str(pos, &text);
776 let new_pos = pos + text.len();
777 self.selection = new_pos..new_pos;
778 self.preferred_x_px = None;
779 self.reset_caret_blink();
780 if !text.is_empty() {
781 self.record_edit(TextUndoOp {
782 index: pos,
783 pre_text: String::new(),
784 post_text: text,
785 pre_selection,
786 post_selection: self.selection.clone(),
787 time: Instant::now(),
788 can_merge: true,
789 });
790 }
791 }
792 }
793
794 pub fn cancel_composition(&mut self) {
795 if let Some(r) = self.composition.take() {
796 let s = clamp_to_char_boundary(&self.text, r.start.min(self.text.len()));
797 let e = clamp_to_char_boundary(&self.text, r.end.min(self.text.len()));
798 if s <= e {
799 self.text.replace_range(s..e, "");
800 self.selection = s..s;
801 }
802 }
803 self.preferred_x_px = None;
804 self.reset_caret_blink();
805 }
806
807 pub fn delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) {
808 if self.selection.start != self.selection.end {
809 let start = self.selection.start.min(self.text.len());
810 let end = self.selection.end.min(self.text.len());
811 self.text.replace_range(start..end, "");
812 self.selection = start..start;
813 self.preferred_x_px = None;
814 self.reset_caret_blink();
815 return;
816 }
817
818 let caret = self.selection.end.min(self.text.len());
819 let start_raw = caret.saturating_sub(before_bytes);
820 let end_raw = (caret + after_bytes).min(self.text.len());
821
822 let start = prev_grapheme_boundary(&self.text, start_raw);
823 let end = next_grapheme_boundary(&self.text, end_raw);
824 if start < end {
825 self.text.replace_range(start..end, "");
826 self.selection = start..start;
827 }
828 self.preferred_x_px = None;
829 self.reset_caret_blink();
830 }
831
832 pub fn begin_drag(&mut self, idx_byte: usize, extend: bool) {
833 let idx = idx_byte.min(self.text.len());
834 if extend {
835 let anchor = self.selection.start;
836 self.selection = anchor.min(idx)..anchor.max(idx);
837 self.drag_anchor = Some(anchor);
838 } else {
839 self.selection = idx..idx;
840 self.drag_anchor = Some(idx);
841 }
842 self.preferred_x_px = None;
843 self.reset_caret_blink();
844 }
845
846 pub fn drag_to(&mut self, idx_byte: usize) {
847 if let Some(anchor) = self.drag_anchor {
848 let i = idx_byte.min(self.text.len());
849 self.selection = anchor.min(i)..anchor.max(i);
850 }
851 self.preferred_x_px = None;
852 self.reset_caret_blink();
853 }
854 pub fn end_drag(&mut self) {
855 self.drag_anchor = None;
856 }
857
858 pub fn caret_index(&self) -> usize {
859 self.selection.end
860 }
861
862 pub fn ensure_caret_visible(&mut self, caret_x_px: f32, inner_width_px: f32, inset_px: f32) {
866 self.ensure_caret_visible_xy(caret_x_px, 0.0, inner_width_px, 1.0, inset_px);
867 }
868
869 pub fn ensure_caret_visible_xy(
872 &mut self,
873 caret_x_px: f32,
874 caret_y_px: f32,
875 inner_w_px: f32,
876 inner_h_px: f32,
877 inset_px: f32,
878 ) {
879 let inset_px = inset_px.max(0.0);
880
881 let left_px = self.scroll_offset + inset_px;
883 let right_px = self.scroll_offset + inner_w_px - inset_px;
884 if caret_x_px < left_px {
885 self.scroll_target = (caret_x_px - inset_px).max(0.0);
886 } else if caret_x_px > right_px {
887 self.scroll_target = (caret_x_px - inner_w_px + inset_px).max(0.0);
888 }
889
890 let top_px = self.scroll_offset_y + inset_px;
892 let bot_px = self.scroll_offset_y + inner_h_px - inset_px;
893 if caret_y_px < top_px {
894 self.scroll_target_y = (caret_y_px - inset_px).max(0.0);
895 } else if caret_y_px > bot_px {
896 self.scroll_target_y = (caret_y_px - inner_h_px + inset_px).max(0.0);
897 }
898 }
899
900 pub fn clamp_scroll(&mut self, content_h_px: f32) {
901 let max_y = (content_h_px - self.inner_height).max(0.0);
902 self.scroll_target_y = self.scroll_target_y.clamp(0.0, max_y);
903 if self.scroll_target_y.is_nan() {
904 self.scroll_target_y = 0.0;
905 }
906 }
907
908 pub fn reset_caret_blink(&mut self) {
909 self.blink_start = Instant::now();
910 }
911 pub fn caret_visible(&self) -> bool {
912 const PERIOD: Duration = Duration::from_millis(500);
913 ((Instant::now() - self.blink_start).as_millis() / PERIOD.as_millis()).is_multiple_of(2)
914 }
915
916 pub fn set_inner_width(&mut self, w_px: f32) {
917 self.inner_width = w_px.max(0.0);
918 if self.scroll_offset.is_nan() {
919 self.scroll_offset = 0.0;
920 }
921 if self.scroll_target.is_nan() {
922 self.scroll_target = 0.0;
923 }
924 }
925 pub fn set_inner_height(&mut self, h_px: f32) {
926 self.inner_height = h_px.max(0.0);
927 if self.scroll_offset_y.is_nan() {
928 self.scroll_offset_y = 0.0;
929 }
930 if self.scroll_target_y.is_nan() {
931 self.scroll_target_y = 0.0;
932 }
933 }
934
935 pub fn tick_scroll_animation(&mut self) {
939 let now = Instant::now();
940 let dt = match self.last_scroll_tick {
941 Some(prev) => {
942 let d = now.saturating_duration_since(prev).as_secs_f32();
943 d.min(0.05) }
945 None => {
946 self.last_scroll_tick = Some(now);
949 self.scroll_offset = self.scroll_target;
950 self.scroll_vel = 0.0;
951 self.scroll_offset_y = self.scroll_target_y;
952 self.scroll_vel_y = 0.0;
953 return;
954 }
955 };
956 self.last_scroll_tick = Some(now);
957
958 if dt > 0.0 {
960 let dx = self.scroll_target - self.scroll_offset;
961 let near_x = dx.abs() < 0.5 && self.scroll_vel.abs() < 0.5;
962 if near_x {
963 self.scroll_offset = self.scroll_target;
964 self.scroll_vel = 0.0;
965 } else {
966 let force_x = SCROLL_STIFFNESS * dx - SCROLL_DAMPING * self.scroll_vel;
967 self.scroll_vel += force_x * dt;
968 self.scroll_offset += self.scroll_vel * dt;
969 if (self.scroll_target - self.scroll_offset).signum() != dx.signum() && dx != 0.0 {
971 self.scroll_offset = self.scroll_target;
972 self.scroll_vel = 0.0;
973 }
974 }
975 }
976
977 if dt > 0.0 {
979 let dy = self.scroll_target_y - self.scroll_offset_y;
980 let near_y = dy.abs() < 0.5 && self.scroll_vel_y.abs() < 0.5;
981 if near_y {
982 self.scroll_offset_y = self.scroll_target_y;
983 self.scroll_vel_y = 0.0;
984 } else {
985 let force_y = SCROLL_STIFFNESS * dy - SCROLL_DAMPING * self.scroll_vel_y;
986 self.scroll_vel_y += force_y * dt;
987 self.scroll_offset_y += self.scroll_vel_y * dt;
988 if (self.scroll_target_y - self.scroll_offset_y).signum() != dy.signum()
989 && dy != 0.0
990 {
991 self.scroll_offset_y = self.scroll_target_y;
992 self.scroll_vel_y = 0.0;
993 }
994 }
995 }
996 }
997}
998
999#[derive(Clone)]
1009pub struct TextFieldConfig {
1010 pub enabled: bool,
1012 pub read_only: bool,
1014 pub input_transformation: Option<Rc<dyn repose_core::InputTransformation>>,
1016 pub text_style: repose_core::TextStyle,
1018 pub keyboard_options: repose_core::KeyboardOptions,
1020 pub on_keyboard_action: Option<Rc<dyn repose_core::KeyboardActionHandler>>,
1022 pub line_limits: repose_core::TextFieldLineLimits,
1024 pub on_text_layout: Option<Rc<dyn Fn(&repose_core::TextLayoutResult)>>,
1026 pub interaction_source: Option<repose_core::MutableInteractionSource>,
1028 pub cursor_brush: Option<repose_core::Brush>,
1030 pub output_transformation: Option<Rc<dyn repose_core::OutputTransformation>>,
1032 pub decorator: Option<Rc<dyn repose_core::TextFieldDecorator>>,
1034 pub codepoint_transformation: Option<repose_core::CodepointTransformation>,
1036 pub text_obfuscation_mode: repose_core::TextObfuscationMode,
1038 pub text_obfuscation_character: char,
1040
1041 pub on_change: Option<Rc<dyn Fn(String)>>,
1043 pub on_submit: Option<Rc<dyn Fn(String)>>,
1044 pub visual_transformation: Option<Rc<dyn repose_core::VisualTransformation>>,
1045 pub decoration_box: Option<Rc<dyn Fn(repose_core::View) -> repose_core::View>>,
1046}
1047
1048impl Default for TextFieldConfig {
1049 fn default() -> Self {
1050 Self {
1051 enabled: true,
1052 read_only: false,
1053 input_transformation: None,
1054 text_style: Default::default(),
1055 keyboard_options: repose_core::KeyboardOptions::DEFAULT.clone(),
1056 on_keyboard_action: None,
1057 line_limits: repose_core::TextFieldLineLimits::MultiLine {
1058 min_height_in_lines: 1,
1059 max_height_in_lines: usize::MAX,
1060 },
1061 on_text_layout: None,
1062 interaction_source: None,
1063 cursor_brush: None,
1064 output_transformation: None,
1065 decorator: None,
1066 codepoint_transformation: None,
1067 text_obfuscation_mode: repose_core::TextObfuscationMode::System,
1068 text_obfuscation_character: '\u{2022}',
1069 on_change: None,
1070 on_submit: None,
1071 visual_transformation: None,
1072 decoration_box: None,
1073 }
1074 }
1075}
1076
1077pub fn BasicTextField(
1091 state: Rc<RefCell<TextFieldState>>,
1092 modifier: repose_core::Modifier,
1093 hint: impl Into<String>,
1094 config: TextFieldConfig,
1095) -> repose_core::View {
1096 let (single_line, max_lines, min_lines) = match config.line_limits {
1097 repose_core::TextFieldLineLimits::SingleLine => (true, 1, 1),
1098 repose_core::TextFieldLineLimits::MultiLine {
1099 min_height_in_lines,
1100 max_height_in_lines,
1101 } => (false, max_height_in_lines, min_height_in_lines),
1102 };
1103
1104 let ka = if let Some(ref handler) = config.on_keyboard_action {
1105 let handler = handler.clone();
1106 repose_core::KeyboardActions {
1107 on_done: Some({
1108 let h = handler.clone();
1109 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1110 h.on_keyboard_action(&|| {})
1111 })
1112 }),
1113 on_go: Some({
1114 let h = handler.clone();
1115 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1116 h.on_keyboard_action(&|| {})
1117 })
1118 }),
1119 on_next: Some({
1120 let h = handler.clone();
1121 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1122 h.on_keyboard_action(&|| {})
1123 })
1124 }),
1125 on_previous: Some({
1126 let h = handler.clone();
1127 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1128 h.on_keyboard_action(&|| {})
1129 })
1130 }),
1131 on_search: Some({
1132 let h = handler.clone();
1133 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1134 h.on_keyboard_action(&|| {})
1135 })
1136 }),
1137 on_send: Some({
1138 Rc::new(move |_: &dyn repose_core::KeyboardActionScope| {
1139 handler.on_keyboard_action(&|| {})
1140 })
1141 }),
1142 }
1143 } else {
1144 repose_core::KeyboardActions::default()
1145 };
1146
1147 let decoration_box = config
1148 .decorator
1149 .map(|d| Rc::new(move |inner: repose_core::View| d.decorate(inner)) as Rc<dyn Fn(_) -> _>);
1150
1151 let cursor_color = config.cursor_brush.and_then(|b| match b {
1152 repose_core::Brush::Solid(c) => Some(c),
1153 _ => None,
1154 });
1155
1156 let value = state.borrow().text.clone();
1157 let key = state.as_ptr() as u64;
1158 set_textfield_state(key, state.clone());
1159
1160 let state_on_change = {
1161 let s = state.clone();
1162 move |new_value: String| {
1163 s.borrow_mut().text = new_value;
1164 }
1165 };
1166
1167 let merged_on_change: Option<Rc<dyn Fn(String)>> =
1168 if let Some(ref cfg_on_change) = config.on_change {
1169 let a = Rc::new(state_on_change) as Rc<dyn Fn(String)>;
1170 let b = cfg_on_change.clone();
1171 Some(Rc::new(move |v: String| {
1172 a(v.clone());
1173 b(v);
1174 }) as Rc<dyn Fn(String)>)
1175 } else {
1176 Some(Rc::new(state_on_change) as Rc<dyn Fn(String)>)
1177 };
1178
1179 text_field_view(
1180 modifier,
1181 hint.into(),
1182 value,
1183 !single_line,
1184 merged_on_change,
1185 config.on_submit,
1186 config.visual_transformation,
1187 config.keyboard_options.keyboard_type,
1188 config.keyboard_options.capitalization,
1189 config.keyboard_options.ime_action,
1190 config.enabled,
1191 config.read_only,
1192 Some(max_lines),
1193 min_lines,
1194 cursor_color,
1195 config.on_text_layout,
1196 config.text_style,
1197 ka,
1198 config.interaction_source,
1199 Some(config.line_limits),
1200 config.input_transformation,
1201 config.output_transformation,
1202 decoration_box,
1203 config.codepoint_transformation,
1204 )
1205}
1206
1207pub fn BasicSecureTextField(
1212 state: Rc<RefCell<TextFieldState>>,
1213 modifier: repose_core::Modifier,
1214 config: TextFieldConfig,
1215) -> repose_core::View {
1216 let mask = config.text_obfuscation_character;
1217 let secure_config = TextFieldConfig {
1218 line_limits: repose_core::TextFieldLineLimits::SingleLine,
1219 keyboard_options: repose_core::KeyboardOptions::SECURE_TEXT_FIELD,
1220 visual_transformation: match config.text_obfuscation_mode {
1221 repose_core::TextObfuscationMode::Visible => None,
1222 _ => Some(Rc::new(repose_core::PasswordVisualTransformation { mask })
1223 as Rc<dyn repose_core::VisualTransformation>),
1224 },
1225 ..config
1226 };
1227 BasicTextField(state, modifier, "", secure_config)
1228}
1229
1230#[derive(Clone, Debug)]
1231pub struct TextAreaLayout {
1232 pub ranges: Vec<(usize, usize)>,
1233 pub line_h_px: f32,
1234}
1235
1236pub fn layout_text_area(
1237 text: &str,
1238 font_px: f32,
1239 wrap_w_px: f32,
1240 font_weight: u16,
1241 font_style: u8,
1242 letter_spacing: f32,
1243 font_variation_settings: Option<&str>,
1244) -> TextAreaLayout {
1245 let line_h = font_px;
1246 let (ranges, _) = repose_text::wrap_line_ranges(
1247 text,
1248 font_px,
1249 wrap_w_px.max(1.0),
1250 None,
1251 true,
1252 font_weight,
1253 font_style,
1254 letter_spacing,
1255 font_variation_settings,
1256 );
1257 TextAreaLayout {
1258 ranges,
1259 line_h_px: line_h,
1260 }
1261}
1262
1263fn locate_byte_in_ranges(ranges: &[(usize, usize)], b: usize) -> (usize, usize, usize) {
1265 if ranges.is_empty() {
1266 return (0, 0, b);
1267 }
1268 for (i, (s, e)) in ranges.iter().enumerate() {
1269 if b < *s {
1270 if i == 0 {
1271 return (0, 0, b);
1272 }
1273 let (ps, pe) = ranges[i - 1];
1274 let local = pe.saturating_sub(ps);
1275 return (i - 1, local, ps + local);
1276 }
1277 if b < *e {
1278 let local = b.saturating_sub(*s).min(e.saturating_sub(*s));
1279 return (i, local, *s + local);
1280 }
1281 if b == *e {
1282 if let Some((ns, _ne)) = ranges.get(i + 1)
1283 && *ns == b
1284 {
1285 return (i + 1, 0, b);
1286 }
1287 let local = e.saturating_sub(*s);
1288 return (i, local, *s + local);
1289 }
1290 }
1291 let (ls, le) = ranges[ranges.len() - 1];
1292 let local = le.saturating_sub(ls);
1293 (ranges.len() - 1, local, ls + local)
1294}
1295
1296pub fn caret_xy_for_byte(
1298 text: &str,
1299 font_px: f32,
1300 wrap_w_px: f32,
1301 byte: usize,
1302) -> (f32, f32, usize) {
1303 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1304 let (ranges, line_h) = (&layout.ranges, layout.line_h_px);
1305 let (li, local, _) = locate_byte_in_ranges(ranges, byte);
1306 let (s, e) = ranges.get(li).copied().unwrap_or((0, 0));
1307 let line = &text[s..e];
1308 let m = measure_text(line, font_px, TextMeasureConfig::default());
1309 let ci = byte_to_char_index(&m, local);
1310 let x = m.positions.get(ci).copied().unwrap_or(0.0);
1311 let y = (li as f32) * line_h;
1312 (x, y, li)
1313}
1314
1315pub fn index_for_xy_bytes(text: &str, font_px: f32, wrap_w_px: f32, x_px: f32, y_px: f32) -> usize {
1317 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1318 let li = ((y_px / layout.line_h_px).floor() as isize).max(0) as usize;
1319 let li = li.min(layout.ranges.len().saturating_sub(1));
1320 let (s, e) = layout.ranges.get(li).copied().unwrap_or((0, 0));
1321 let line = &text[s..e];
1322 let local = index_for_x_bytes(line, font_px, x_px.max(0.0), 400, 0);
1323 (s + local).min(text.len())
1324}
1325
1326pub fn move_caret_vertical(
1328 text: &str,
1329 font_px: f32,
1330 wrap_w_px: f32,
1331 cur_byte: usize,
1332 dir: i32, preferred_x: Option<f32>,
1334) -> (usize, f32) {
1335 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1336 if layout.ranges.is_empty() {
1337 return (cur_byte, preferred_x.unwrap_or(0.0));
1338 }
1339 let (x, _y, li) = caret_xy_for_byte(text, font_px, wrap_w_px, cur_byte);
1340 let px = preferred_x.unwrap_or(x);
1341 let mut nli = li as i32 + dir;
1342 nli = nli.clamp(0, (layout.ranges.len().saturating_sub(1)) as i32);
1343 let nli = nli as usize;
1344 let (s, e) = layout.ranges[nli];
1345 let line = &text[s..e];
1346 let local = index_for_x_bytes(line, font_px, px.max(0.0), 400, 0);
1347 ((s + local).min(text.len()), px)
1348}
1349
1350pub fn line_home_end(
1352 text: &str,
1353 font_px: f32,
1354 wrap_w_px: f32,
1355 cur_byte: usize,
1356 to_end: bool,
1357) -> usize {
1358 let layout = layout_text_area(text, font_px, wrap_w_px, 400, 0, 0.0, None);
1359 let (li, _local, _) = locate_byte_in_ranges(&layout.ranges, cur_byte);
1360 let (s, e) = layout.ranges.get(li).copied().unwrap_or((0, 0));
1361 if to_end { e } else { s }
1362}
1363
1364fn clamp_to_char_boundary(s: &str, i: usize) -> usize {
1365 if i >= s.len() {
1366 return s.len();
1367 }
1368 if s.is_char_boundary(i) {
1369 return i;
1370 }
1371 let mut j = i;
1372 while j > 0 && !s.is_char_boundary(j) {
1373 j -= 1;
1374 }
1375 j
1376}
1377
1378fn char_to_byte(s: &str, ci: usize) -> usize {
1379 if ci == 0 {
1380 0
1381 } else {
1382 s.char_indices().nth(ci).map(|(i, _)| i).unwrap_or(s.len())
1383 }
1384}
1385
1386pub(crate) fn paint_text_field(
1398 scene: &mut Scene,
1399 rect: repose_core::Rect,
1400 text_input: &TextInputConfig,
1401 state: Option<&Rc<RefCell<TextFieldState>>>,
1402 is_focused: bool,
1403 clip_rounded: Option<[f32; 4]>,
1404 alpha_accum: f32,
1405) {
1406 let ts = text_input
1407 .text_style
1408 .as_ref()
1409 .map(|s| s.clone())
1410 .unwrap_or_default();
1411 let font_size_dp = if ts.font_size != 0.0 {
1412 ts.font_size
1413 } else {
1414 TF_FONT_DP
1415 };
1416 let font_val = dp_to_px(font_size_dp) * locals::text_scale().0;
1417 let line_h = if ts.line_height != 0.0 {
1418 dp_to_px(ts.line_height) * locals::text_scale().0
1419 } else if text_input.multiline {
1420 0.0 } else {
1422 font_val };
1424 let text_off_y = (rect.h - line_h.max(font_val)) / 2.0;
1425
1426 let clip_radius = clip_rounded.unwrap_or([0.0; 4]).map(dp_to_px);
1427 scene.nodes.push(SceneNode::PushClip {
1428 rect,
1429 radius: clip_radius,
1430 op: repose_core::ClipOp::Intersect,
1431 });
1432
1433 let th = locals::theme();
1434 let show_selection = text_input.enabled;
1435 let show_cursor = text_input.enabled && !text_input.read_only;
1436 let cursor_color = text_input.cursor_color.unwrap_or(th.on_surface);
1437 let rendered_by_vt = |original: &str| -> String {
1438 if let Some(ref vt) = text_input.visual_transformation {
1439 let annotated = repose_core::AnnotatedString::new(original.to_string(), vec![]);
1440 vt.filter(&annotated).text.text
1441 } else {
1442 original.to_string()
1443 }
1444 };
1445
1446 if let Some(state_rc) = state {
1447 let st = state_rc.borrow();
1448
1449 if !text_input.multiline {
1450 let measure_for = if text_input.visual_transformation.is_some() && !st.text.is_empty() {
1452 rendered_by_vt(&st.text)
1453 } else {
1454 st.text.clone()
1455 };
1456 let has_vt = text_input.visual_transformation.is_some();
1457 let m = measure_text(
1458 &measure_for,
1459 font_val,
1460 TextMeasureConfig {
1461 font_family: ts.font_family,
1462 font_weight: ts.font_weight.unwrap_or(400),
1463 font_style: ts.font_style.unwrap_or(0),
1464 letter_spacing: ts.letter_spacing,
1465 font_variation_settings: None,
1466 },
1467 );
1468
1469 if show_selection && st.selection.start != st.selection.end {
1471 let start_off = if has_vt {
1472 original_offset_to_display(&st.text, &measure_for, st.selection.start)
1473 } else {
1474 st.selection.start
1475 };
1476 let end_off = if has_vt {
1477 original_offset_to_display(&st.text, &measure_for, st.selection.end)
1478 } else {
1479 st.selection.end
1480 };
1481 let sx = m
1482 .positions
1483 .get(byte_to_char_index(&m, start_off))
1484 .copied()
1485 .unwrap_or(0.0)
1486 - st.scroll_offset;
1487 let ex = m
1488 .positions
1489 .get(byte_to_char_index(&m, end_off))
1490 .copied()
1491 .unwrap_or(sx)
1492 - st.scroll_offset;
1493 let selection = th.focus.with_alpha_f32(85.0 / 255.0);
1494 let vis_x = sx.max(0.0);
1495 let vis_ex = ex.max(0.0);
1496 scene.nodes.push(SceneNode::Rect {
1497 rect: repose_core::Rect {
1498 x: rect.x + vis_x,
1499 y: rect.y + text_off_y,
1500 w: (vis_ex - vis_x).max(0.0),
1501 h: line_h.max(font_val),
1502 },
1503 brush: Brush::Solid(selection),
1504 radius: [0.0; 4],
1505 });
1506 }
1507
1508 let txt_col = if st.text.is_empty() {
1510 ts.color.unwrap_or(th.on_surface_variant)
1511 } else {
1512 ts.color.unwrap_or(th.on_surface)
1513 };
1514 let render_txt = if st.text.is_empty() {
1515 text_input.hint.clone()
1516 } else {
1517 rendered_by_vt(&st.text)
1518 };
1519 scene.nodes.push(SceneNode::Text {
1520 rect: repose_core::Rect {
1521 x: rect.x - st.scroll_offset,
1522 y: rect.y + text_off_y,
1523 w: rect.w,
1524 h: line_h,
1525 },
1526 text: Arc::from(render_txt),
1527 color: mul_alpha_color(txt_col, alpha_accum),
1528 size: font_val,
1529 font_family: ts.font_family,
1530 text_align: ts.text_align,
1531 font_weight: FontWeight(ts.font_weight.unwrap_or(400)),
1532 font_style: match ts.font_style.unwrap_or(0) {
1533 1 => FontStyle::Italic,
1534 _ => FontStyle::Normal,
1535 },
1536 text_decoration: ts.text_decoration.unwrap_or_default(),
1537 letter_spacing: ts.letter_spacing,
1538 line_height: ts.line_height,
1539 extra_style: Default::default(),
1540 url: None,
1541 font_variation_settings: None,
1542 });
1543
1544 if show_cursor
1546 && is_focused
1547 && st.selection.start == st.selection.end
1548 && st.caret_visible()
1549 {
1550 let caret_off = if has_vt {
1551 original_offset_to_display(&st.text, &measure_for, st.selection.end)
1552 } else {
1553 st.selection.end
1554 };
1555 let cx = m
1556 .positions
1557 .get(byte_to_char_index(&m, caret_off))
1558 .copied()
1559 .unwrap_or(0.0)
1560 - st.scroll_offset;
1561 let cursor_y = rect.y + text_off_y + (line_h.max(font_val) - font_val) / 2.0;
1562 scene.nodes.push(SceneNode::Rect {
1563 rect: repose_core::Rect {
1564 x: rect.x + cx.max(0.0),
1565 y: cursor_y,
1566 w: dp_to_px(1.0),
1567 h: font_val,
1568 },
1569 brush: Brush::Solid(cursor_color),
1570 radius: [0.0; 4],
1571 });
1572 }
1573 } else {
1574 let render_text = if st.text.is_empty() {
1576 st.text.clone()
1577 } else if let Some(ref vt) = text_input.visual_transformation {
1578 let annotated = repose_core::AnnotatedString::new(st.text.clone(), vec![]);
1579 vt.filter(&annotated).text.text
1580 } else {
1581 st.text.clone()
1582 };
1583 let layout = layout_text_area(
1584 &render_text,
1585 font_val,
1586 rect.w.max(1.0),
1587 400,
1588 0,
1589 ts.letter_spacing,
1590 None,
1591 );
1592 let lh = layout.line_h_px;
1593 let max_line_count = text_input.max_lines.unwrap_or(usize::MAX);
1594
1595 if st.text.is_empty() {
1597 scene.nodes.push(SceneNode::Text {
1598 rect: repose_core::Rect {
1599 x: rect.x,
1600 y: rect.y,
1601 w: rect.w,
1602 h: line_h,
1603 },
1604 text: Arc::from(text_input.hint.clone()),
1605 color: mul_alpha_color(ts.color.unwrap_or(th.on_surface_variant), alpha_accum),
1606 size: font_val,
1607 font_family: ts.font_family,
1608 text_align: ts.text_align,
1609 font_weight: FontWeight(ts.font_weight.unwrap_or(400)),
1610 font_style: match ts.font_style.unwrap_or(0) {
1611 1 => FontStyle::Italic,
1612 _ => FontStyle::Normal,
1613 },
1614 text_decoration: ts.text_decoration.unwrap_or_default(),
1615 letter_spacing: ts.letter_spacing,
1616 line_height: ts.line_height,
1617 extra_style: Default::default(),
1618 url: None,
1619 font_variation_settings: None,
1620 });
1621 } else {
1622 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1623 if i >= max_line_count {
1624 break;
1625 }
1626 let ln = render_text[s..e].to_string();
1627 let draw_y = rect.y + (i as f32) * lh - st.scroll_offset_y;
1628 if draw_y + lh < rect.y - 1.0 || draw_y > rect.y + rect.h + 1.0 {
1629 continue;
1630 }
1631 scene.nodes.push(SceneNode::Text {
1632 rect: repose_core::Rect {
1633 x: rect.x,
1634 y: draw_y,
1635 w: rect.w,
1636 h: lh,
1637 },
1638 text: Arc::<str>::from(ln),
1639 color: mul_alpha_color(ts.color.unwrap_or(th.on_surface), alpha_accum),
1640 size: font_val,
1641 font_family: ts.font_family,
1642 text_align: ts.text_align,
1643 font_weight: FontWeight(ts.font_weight.unwrap_or(400)),
1644 font_style: match ts.font_style.unwrap_or(0) {
1645 1 => FontStyle::Italic,
1646 _ => FontStyle::Normal,
1647 },
1648 text_decoration: ts.text_decoration.unwrap_or_default(),
1649 letter_spacing: ts.letter_spacing,
1650 line_height: ts.line_height,
1651 extra_style: Default::default(),
1652 url: None,
1653 font_variation_settings: None,
1654 });
1655 }
1656 }
1657
1658 if show_selection && st.selection.start != st.selection.end {
1660 let sel_a_orig: usize = st.selection.start.min(st.selection.end);
1661 let sel_b_orig: usize = st.selection.start.max(st.selection.end);
1662 let has_vt = text_input.visual_transformation.is_some();
1663 let sel_a = if has_vt {
1664 original_offset_to_display(&st.text, &render_text, sel_a_orig)
1665 } else {
1666 sel_a_orig
1667 };
1668 let sel_b = if has_vt {
1669 original_offset_to_display(&st.text, &render_text, sel_b_orig)
1670 } else {
1671 sel_b_orig
1672 };
1673 let selection = th.focus.with_alpha_f32(85.0 / 255.0);
1674 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1675 if i >= max_line_count {
1676 break;
1677 }
1678 let os = sel_a.max(s);
1679 let oe = sel_b.min(e);
1680 if os >= oe {
1681 continue;
1682 }
1683 let ln = &render_text[s..e];
1684 let m = measure_text(
1685 ln,
1686 font_val,
1687 TextMeasureConfig {
1688 font_family: ts.font_family,
1689 font_weight: ts.font_weight.unwrap_or(400),
1690 font_style: ts.font_style.unwrap_or(0),
1691 letter_spacing: ts.letter_spacing,
1692 font_variation_settings: None,
1693 },
1694 );
1695 let ls = os - s;
1696 let le = oe - s;
1697 let sx = m
1698 .positions
1699 .get(byte_to_char_index(&m, ls))
1700 .copied()
1701 .unwrap_or(0.0);
1702 let ex = m
1703 .positions
1704 .get(byte_to_char_index(&m, le))
1705 .copied()
1706 .unwrap_or(sx);
1707 let draw_y = rect.y + (i as f32) * lh - st.scroll_offset_y;
1708 scene.nodes.push(SceneNode::Rect {
1709 rect: repose_core::Rect {
1710 x: rect.x + sx,
1711 y: draw_y,
1712 w: (ex - sx).max(0.0),
1713 h: lh,
1714 },
1715 brush: Brush::Solid(selection),
1716 radius: [0.0; 4],
1717 });
1718 }
1719 }
1720
1721 if show_cursor
1723 && is_focused
1724 && st.selection.start == st.selection.end
1725 && st.caret_visible()
1726 {
1727 let caret_orig = st.selection.end.min(st.text.len());
1728 let has_vt = text_input.visual_transformation.is_some();
1729 let caret = if has_vt {
1730 original_offset_to_display(&st.text, &render_text, caret_orig)
1731 } else {
1732 caret_orig
1733 };
1734 let (cx, cy, _li) =
1735 caret_xy_for_byte(&render_text, font_val, rect.w.max(1.0), caret);
1736 let draw_x = rect.x + cx;
1737 let draw_y = rect.y + cy - st.scroll_offset_y;
1738 scene.nodes.push(SceneNode::Rect {
1739 rect: repose_core::Rect {
1740 x: draw_x,
1741 y: draw_y + (lh - font_val) / 2.0,
1742 w: dp_to_px(1.0),
1743 h: font_val,
1744 },
1745 brush: Brush::Solid(cursor_color),
1746 radius: [0.0; 4],
1747 });
1748 }
1749 }
1750 } else {
1751 if text_input.value.is_empty() {
1753 let hint_y = if text_input.multiline {
1754 rect.y
1755 } else {
1756 rect.y + text_off_y
1757 };
1758 scene.nodes.push(SceneNode::Text {
1759 rect: repose_core::Rect {
1760 x: rect.x,
1761 y: hint_y,
1762 w: rect.w,
1763 h: line_h,
1764 },
1765 text: Arc::from(text_input.hint.clone()),
1766 color: mul_alpha_color(th.on_surface_variant, alpha_accum),
1767 size: font_val,
1768 font_family: None,
1769 text_align: TextAlign::Unspecified,
1770 font_weight: FontWeight::NORMAL,
1771 font_style: FontStyle::Normal,
1772 text_decoration: ts.text_decoration.unwrap_or_default(),
1773 letter_spacing: 0.0,
1774 line_height: 0.0,
1775 extra_style: Default::default(),
1776 url: None,
1777 font_variation_settings: None,
1778 });
1779 } else if text_input.multiline {
1780 let render_text = if text_input.value.is_empty() {
1781 text_input.value.clone()
1782 } else if let Some(ref vt) = text_input.visual_transformation {
1783 let annotated = repose_core::AnnotatedString::new(text_input.value.clone(), vec![]);
1784 vt.filter(&annotated).text.text
1785 } else {
1786 text_input.value.clone()
1787 };
1788 let layout = layout_text_area(
1789 &render_text,
1790 font_val,
1791 rect.w.max(1.0),
1792 400,
1793 0,
1794 ts.letter_spacing,
1795 None,
1796 );
1797 let lh = layout.line_h_px;
1798 for (i, (s, e)) in layout.ranges.iter().copied().enumerate() {
1799 let ln = render_text[s..e].to_string();
1800 let draw_y = rect.y + (i as f32) * lh;
1801 if draw_y + lh < rect.y - 1.0 || draw_y > rect.y + rect.h + 1.0 {
1802 continue;
1803 }
1804 scene.nodes.push(SceneNode::Text {
1805 rect: repose_core::Rect {
1806 x: rect.x,
1807 y: draw_y,
1808 w: rect.w,
1809 h: lh,
1810 },
1811 text: Arc::<str>::from(ln),
1812 color: mul_alpha_color(th.on_surface, alpha_accum),
1813 size: font_val,
1814 font_family: None,
1815 text_align: TextAlign::Unspecified,
1816 font_weight: FontWeight::NORMAL,
1817 font_style: FontStyle::Normal,
1818 text_decoration: ts.text_decoration.unwrap_or_default(),
1819 letter_spacing: 0.0,
1820 line_height: 0.0,
1821 extra_style: Default::default(),
1822 url: None,
1823 font_variation_settings: None,
1824 });
1825 }
1826 } else {
1827 scene.nodes.push(SceneNode::Text {
1828 rect: repose_core::Rect {
1829 x: rect.x,
1830 y: rect.y + text_off_y,
1831 w: rect.w,
1832 h: line_h,
1833 },
1834 text: Arc::from(rendered_by_vt(&text_input.value)),
1835 color: mul_alpha_color(th.on_surface, alpha_accum),
1836 size: font_val,
1837 font_family: None,
1838 text_align: TextAlign::Unspecified,
1839 font_weight: FontWeight::NORMAL,
1840 font_style: FontStyle::Normal,
1841 text_decoration: ts.text_decoration.unwrap_or_default(),
1842 letter_spacing: 0.0,
1843 line_height: 0.0,
1844 extra_style: Default::default(),
1845 url: None,
1846 font_variation_settings: None,
1847 });
1848 }
1849 }
1850
1851 if let Some(ref cb) = text_input.on_text_layout {
1853 let (
1854 line_count,
1855 content_w,
1856 content_h,
1857 first_baseline,
1858 last_baseline,
1859 did_overflow_w,
1860 did_overflow_h,
1861 lines,
1862 ) = if let Some(state_rc) = state {
1863 let st = state_rc.borrow();
1864 let display = if st.text.is_empty() {
1865 text_input.hint.clone()
1866 } else if let Some(ref vt) = text_input.visual_transformation {
1867 let annotated = repose_core::AnnotatedString::new(st.text.clone(), vec![]);
1868 vt.filter(&annotated).text.text
1869 } else {
1870 st.text.clone()
1871 };
1872 if text_input.multiline {
1873 let l = layout_text_area(
1874 &display,
1875 font_val,
1876 rect.w.max(1.0),
1877 400,
1878 0,
1879 ts.letter_spacing,
1880 None,
1881 );
1882 let lc = l.ranges.len();
1883 let cw = rect.w.max(0.0);
1884 let ch = (lc as f32 * l.line_h_px).max(0.0);
1885 let line_infos: Vec<_> = l
1886 .ranges
1887 .iter()
1888 .enumerate()
1889 .map(|(i, &(s, e))| {
1890 let top = i as f32 * l.line_h_px;
1891 let bottom = top + l.line_h_px;
1892 let line_text = &display[s..e];
1893 let m = measure_text(line_text, font_val, TextMeasureConfig::default());
1894 let line_w = m.positions.last().copied().unwrap_or(0.0);
1895 TextLineInfo {
1896 start: s,
1897 end: e,
1898 top,
1899 baseline: top + l.line_h_px * 0.8,
1900 bottom,
1901 left: 0.0,
1902 right: line_w,
1903 width: line_w,
1904 }
1905 })
1906 .collect();
1907 let fb = line_infos.first().map(|l| l.baseline).unwrap_or(0.0);
1908 let lb = line_infos.last().map(|l| l.baseline).unwrap_or(0.0);
1909 (lc, cw, ch, fb, lb, cw > rect.w, ch > rect.h, line_infos)
1910 } else {
1911 let m = measure_text(&display, font_val, TextMeasureConfig::default());
1912 let w = m.positions.last().copied().unwrap_or(0.0);
1913 let top = 0.0;
1914 let bottom = line_h.max(font_val);
1915 let baseline = bottom * 0.8;
1916 let line_info = TextLineInfo {
1917 start: 0,
1918 end: display.len(),
1919 top,
1920 baseline,
1921 bottom,
1922 left: 0.0,
1923 right: w,
1924 width: w,
1925 };
1926 (
1927 1,
1928 w.max(0.0),
1929 bottom,
1930 baseline,
1931 baseline,
1932 w > rect.w,
1933 bottom > rect.h,
1934 vec![line_info],
1935 )
1936 }
1937 } else {
1938 (0, 0.0, 0.0, 0.0, 0.0, false, false, vec![])
1939 };
1940 cb(&repose_core::TextLayoutResult {
1941 line_count,
1942 width_px: content_w,
1943 height_px: content_h,
1944 first_baseline,
1945 last_baseline,
1946 did_overflow_width: did_overflow_w,
1947 did_overflow_height: did_overflow_h,
1948 lines,
1949 });
1950 }
1951
1952 scene.nodes.push(SceneNode::PopClip);
1953}
1954
1955fn text_field_view(
1959 modifier: Modifier,
1960 hint: String,
1961 value: String,
1962 multiline: bool,
1963 on_change: Option<Rc<dyn Fn(String)>>,
1964 on_submit: Option<Rc<dyn Fn(String)>>,
1965 visual_transformation: Option<Rc<dyn repose_core::VisualTransformation>>,
1966 keyboard_type: repose_core::KeyboardType,
1967 capitalization: repose_core::KeyboardCapitalization,
1968 ime_action: repose_core::ImeAction,
1969 enabled: bool,
1970 read_only: bool,
1971 max_lines: Option<usize>,
1972 min_lines: usize,
1973 cursor_color: Option<Color>,
1974 on_text_layout: Option<Rc<dyn Fn(&repose_core::TextLayoutResult)>>,
1975 text_style: repose_core::TextStyle,
1976 keyboard_actions: repose_core::KeyboardActions,
1977 interaction_source: Option<repose_core::MutableInteractionSource>,
1978 line_limits: Option<repose_core::TextFieldLineLimits>,
1979 _input_transformation: Option<Rc<dyn repose_core::InputTransformation>>,
1980 _output_transformation: Option<Rc<dyn repose_core::OutputTransformation>>,
1981 _decoration_box: Option<Rc<dyn Fn(repose_core::View) -> repose_core::View>>,
1982 _codepoint_transformation: Option<repose_core::CodepointTransformation>,
1983) -> View {
1984 let modif = modifier.text_input(TextInputConfig {
1985 hint,
1986 multiline,
1987 on_change,
1988 on_submit,
1989 focus_tracker: None,
1990 value,
1991 visual_transformation,
1992 keyboard_type,
1993 capitalization,
1994 ime_action,
1995 enabled,
1996 read_only,
1997 max_lines,
1998 min_lines,
1999 cursor_color,
2000 on_text_layout,
2001 text_style: Some(text_style),
2002 keyboard_actions: Some(keyboard_actions),
2003 interaction_source: interaction_source.as_ref().map(|s| s.source()),
2004 line_limits,
2005 });
2006
2007 View::new(0, ViewKind::Box)
2008 .modifier(modif)
2009 .semantics(Semantics {
2010 role: Role::TextField,
2011 label: None,
2012 focused: false,
2013 enabled,
2014 selectable_group: false,
2015 })
2016}
2017
2018#[cfg(test)]
2019mod tests {
2020 use super::*;
2021
2022 #[test]
2023 fn test_index_for_x_bytes_grapheme() {
2024 let t = "A👍🏽B";
2025 let font_px = 16.0; let m = measure_text(t, font_px, TextMeasureConfig::default());
2027 for i in 0..m.byte_offsets.len() - 1 {
2028 let b = m.byte_offsets[i];
2029 let _ = &t[..b];
2030 }
2031 }
2032}