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