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