1use crate::{TextLayout, TextStyle};
4use icu_segmenter::GraphemeClusterSegmenter;
5use incular_core::{RestorationKey, RestorationScope};
6use std::{cell::RefCell, fmt, ops::Range, rc::Rc, time::Instant};
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
11pub enum TextAffinity {
12 #[default]
13 Downstream,
14 Upstream,
15}
16
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
21pub struct TextRange {
22 pub start: usize,
23 pub end: usize,
24}
25
26impl TextRange {
27 pub const EMPTY: Self = Self { start: 0, end: 0 };
28
29 #[must_use]
30 pub const fn new(start: usize, end: usize) -> Self {
31 Self { start, end }
32 }
33
34 #[must_use]
35 pub const fn collapsed(offset: usize) -> Self {
36 Self::new(offset, offset)
37 }
38
39 #[must_use]
40 pub const fn is_collapsed(self) -> bool {
41 self.start == self.end
42 }
43
44 #[must_use]
45 pub const fn is_valid(self) -> bool {
46 self.start <= self.end
47 }
48
49 #[must_use]
50 pub const fn len(self) -> usize {
51 self.end.saturating_sub(self.start)
52 }
53
54 #[must_use]
55 pub const fn is_empty(self) -> bool {
56 self.start == self.end
57 }
58
59 #[must_use]
60 pub const fn as_range(self) -> Range<usize> {
61 self.start..self.end
62 }
63
64 #[must_use]
67 pub fn clamp_to(self, text: &str) -> Self {
68 let start = nearest_char_boundary(text, self.start.min(text.len()));
69 let end = nearest_char_boundary(text, self.end.min(text.len())).max(start);
70 Self::new(start, end)
71 }
72
73 #[must_use]
74 pub fn normalized(self) -> Self {
75 if self.start <= self.end {
76 self
77 } else {
78 Self::new(self.end, self.start)
79 }
80 }
81}
82
83#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
86pub struct TextSelection {
87 pub base: usize,
88 pub extent: usize,
89}
90
91impl TextSelection {
92 #[must_use]
93 pub const fn new(base: usize, extent: usize) -> Self {
94 Self { base, extent }
95 }
96
97 #[must_use]
98 pub const fn collapsed(offset: usize) -> Self {
99 Self::new(offset, offset)
100 }
101
102 #[must_use]
103 pub const fn start(self) -> usize {
104 if self.base < self.extent {
105 self.base
106 } else {
107 self.extent
108 }
109 }
110
111 #[must_use]
112 pub const fn end(self) -> usize {
113 if self.base > self.extent {
114 self.base
115 } else {
116 self.extent
117 }
118 }
119
120 #[must_use]
121 pub const fn is_collapsed(self) -> bool {
122 self.base == self.extent
123 }
124
125 #[must_use]
126 pub const fn is_forward(self) -> bool {
127 self.base <= self.extent
128 }
129
130 #[must_use]
131 pub const fn range(self) -> TextRange {
132 TextRange::new(self.start(), self.end())
133 }
134
135 #[must_use]
140 pub fn clamp_to(self, text: &str) -> Self {
141 let base = nearest_char_boundary(text, self.base.min(text.len()));
142 let extent = nearest_char_boundary(text, self.extent.min(text.len()));
143 Self::new(base, extent)
144 }
145}
146
147pub type ComposingRange = TextRange;
149
150#[derive(Clone, Debug, PartialEq, Eq)]
151pub struct TextEditingValue {
152 pub text: String,
153 pub selection: TextSelection,
154 pub composing: Option<ComposingRange>,
155}
156
157impl TextEditingValue {
158 #[must_use]
159 pub fn new(text: impl Into<String>) -> Self {
160 let text = text.into();
161 let caret = text.len();
162 Self {
163 text,
164 selection: TextSelection::collapsed(caret),
165 composing: None,
166 }
167 }
168
169 #[must_use]
170 pub const fn empty() -> Self {
171 Self {
172 text: String::new(),
173 selection: TextSelection::collapsed(0),
174 composing: None,
175 }
176 }
177
178 #[must_use]
179 pub fn with_selection(mut self, selection: TextSelection) -> Self {
180 self.selection = selection.clamp_to(&self.text);
181 self
182 }
183
184 #[must_use]
185 pub fn with_composing(mut self, composing: Option<ComposingRange>) -> Self {
186 self.composing = composing.map(|range| range.clamp_to(&self.text));
187 self
188 }
189
190 #[must_use]
191 pub fn replace(mut self, range: TextRange, replacement: &str) -> Self {
192 let range = range.normalized().clamp_to(&self.text);
193 self.text.replace_range(range.as_range(), replacement);
194 let caret = range.start + replacement.len();
195 self.selection = TextSelection::collapsed(caret);
196 self.composing = None;
197 self
198 }
199}
200
201impl Default for TextEditingValue {
202 fn default() -> Self {
203 Self::empty()
204 }
205}
206
207#[derive(Clone, Debug, PartialEq, Eq)]
210pub struct TextEditingDelta {
211 pub range: TextRange,
212 pub replacement: String,
213 pub selection: Option<TextSelection>,
214 pub composing: Option<ComposingRange>,
215}
216
217impl TextEditingDelta {
218 #[must_use]
219 pub fn replace(range: TextRange, replacement: impl Into<String>) -> Self {
220 Self {
221 range,
222 replacement: replacement.into(),
223 selection: None,
224 composing: None,
225 }
226 }
227
228 #[must_use]
229 pub fn apply(&self, value: &TextEditingValue) -> TextEditingValue {
230 let mut next = value.clone().replace(self.range, &self.replacement);
231 if let Some(selection) = self.selection {
232 next.selection = selection.clamp_to(&next.text);
233 }
234 next.composing = self.composing.map(|range| range.clamp_to(&next.text));
235 next
236 }
237}
238
239#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
240pub enum SelectionChangedCause {
241 #[default]
242 Unknown,
243 Keyboard,
244 Tap,
245 DoubleTap,
246 LongPress,
247 Drag,
248 Accessibility,
249 Programmatic,
250}
251
252type Listener = Rc<dyn Fn(&TextEditingValue)>;
253type EditTransform = Rc<dyn Fn(&TextEditingValue, &TextEditingValue) -> TextEditingValue>;
254
255#[derive(Default)]
256struct ControllerState {
257 value: TextEditingValue,
258 listeners: Vec<(usize, Listener)>,
259 next_listener: usize,
260 content_revision: u64,
261 visual_revision: u64,
262 caret_reset: Option<Instant>,
263 preferred_caret_x: Option<f32>,
264 caret_affinity: TextAffinity,
265 preedit: Option<String>,
266 preedit_selection: Option<TextRange>,
267 restoration: Option<TextRestoration>,
268 restoration_generation: u64,
269}
270
271#[derive(Clone)]
272struct TextRestoration {
273 scope: RestorationScope,
274 key: RestorationKey,
275}
276
277#[derive(Clone, Default)]
280pub struct TextEditingController {
281 changes: incular_core::reactivity::DependencySource,
282 transforms: incular_core::reactivity::DependencySource,
283 inner: Rc<RefCell<ControllerState>>,
284}
285
286#[must_use = "keep the subscription alive to observe changes"]
288pub struct TextEditingSubscription {
289 _subscription: incular_core::reactivity::Subscription,
290}
291
292impl PartialEq for TextEditingController {
293 fn eq(&self, other: &Self) -> bool {
294 Rc::ptr_eq(&self.inner, &other.inner)
295 }
296}
297
298impl fmt::Debug for TextEditingController {
299 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
300 formatter
301 .debug_struct("TextEditingController")
302 .field("value", &self.value())
303 .finish()
304 }
305}
306
307impl TextEditingController {
308 #[must_use]
309 pub fn new() -> Self {
310 Self::from_value(TextEditingValue::default())
311 }
312
313 #[must_use]
315 pub fn with_text(text: impl Into<String>) -> Self {
316 Self::from_value(TextEditingValue::new(text))
317 }
318
319 #[must_use]
321 pub fn restored(scope: RestorationScope, key: RestorationKey) -> Self {
322 let controller = Self::new();
323 controller.bind_restoration(scope, key);
324 controller
325 }
326
327 pub fn bind_restoration(&self, scope: RestorationScope, key: RestorationKey) {
329 let restored = scope.get_json(&key).and_then(value_from_json);
330 let changed = restored
331 .as_ref()
332 .is_some_and(|value| *value != self.inner.borrow().value);
333 let mut state = self.inner.borrow_mut();
334 state.restoration = Some(TextRestoration { scope, key });
335 state.restoration_generation = state
338 .restoration_generation
339 .checked_add(1)
340 .expect("text restoration generation exhausted");
341 if let Some(value) = restored {
342 state.value = value;
343 state.content_revision = state
344 .content_revision
345 .checked_add(1)
346 .expect("content revision exhausted");
347 state.visual_revision = state
348 .visual_revision
349 .checked_add(1)
350 .expect("visual revision exhausted");
351 state.preedit = None;
352 state.preedit_selection = None;
353 state.caret_reset = None;
354 state.preferred_caret_x = None;
355 state.caret_affinity = TextAffinity::Downstream;
356 }
357 drop(state);
358 if changed {
359 self.notify_listeners();
360 }
361 }
362
363 pub fn unbind_restoration(&self) {
365 self.inner.borrow_mut().restoration = None;
366 }
367
368 #[must_use]
369 pub fn from_value(value: TextEditingValue) -> Self {
370 Self {
371 changes: incular_core::reactivity::DependencySource::default(),
372 transforms: incular_core::reactivity::DependencySource::default(),
373 inner: Rc::new(RefCell::new(ControllerState {
374 value,
375 ..ControllerState::default()
376 })),
377 }
378 }
379
380 #[must_use]
381 pub fn value(&self) -> TextEditingValue {
382 self.changes.track();
383 self.inner.borrow().value.clone()
384 }
385
386 #[must_use]
387 pub fn text(&self) -> String {
388 self.changes.track();
389 self.inner.borrow().value.text.clone()
390 }
391
392 #[must_use]
393 pub fn selection(&self) -> TextSelection {
394 self.changes.track();
395 self.inner.borrow().value.selection
396 }
397
398 #[must_use]
399 pub fn composing(&self) -> Option<ComposingRange> {
400 self.changes.track();
401 self.inner.borrow().value.composing
402 }
403
404 pub fn set_value(&self, value: TextEditingValue) {
405 self.update(value, true);
406 self.inner.borrow_mut().caret_affinity = TextAffinity::Downstream;
407 }
408
409 pub fn set_text(&self, text: impl Into<String>) {
410 self.update(TextEditingValue::new(text), true);
411 self.inner.borrow_mut().caret_affinity = TextAffinity::Downstream;
412 }
413
414 pub fn clear(&self) {
415 self.set_text("");
416 }
417
418 pub fn set_selection(&self, selection: TextSelection) {
419 let mut value = self.value();
420 value.selection = selection.clamp_to(&value.text);
421 self.update(value, false);
422 self.inner.borrow_mut().caret_affinity = TextAffinity::Downstream;
423 }
424
425 pub fn set_selection_with_affinity(&self, selection: TextSelection, affinity: TextAffinity) {
428 let mut value = self.value();
429 value.selection = selection.clamp_to(&value.text);
430 self.update(value, false);
431 self.inner.borrow_mut().caret_affinity = affinity;
432 }
433
434 pub fn set_composing(&self, composing: Option<ComposingRange>) {
435 let mut value = self.value();
436 value.composing = composing.map(|range| range.clamp_to(&value.text));
437 self.update(value, false);
438 }
439
440 pub fn observe(
442 &self,
443 listener: impl Fn(&TextEditingValue) + 'static,
444 ) -> TextEditingSubscription {
445 let state = Rc::downgrade(&self.inner);
446 TextEditingSubscription {
447 _subscription: self.changes.subscribe((), move || {
448 if let Some(state) = state.upgrade() {
449 let value = state.borrow().value.clone();
450 listener(&value);
451 }
452 }),
453 }
454 }
455
456 #[doc(hidden)]
457 pub fn register_edit_transform(
458 &self,
459 transform: impl Fn(&TextEditingValue, &TextEditingValue) -> TextEditingValue + 'static,
460 ) -> incular_core::reactivity::Subscription {
461 let transform: EditTransform = Rc::new(transform);
462 self.transforms.subscribe(transform, || {})
463 }
464
465 pub fn add_listener(&self, listener: impl Fn(&TextEditingValue) + 'static) -> usize {
469 let mut state = self.inner.borrow_mut();
470 let token = state.next_listener;
471 state.next_listener = state
472 .next_listener
473 .checked_add(1)
474 .expect("text editing listener identity space exhausted");
475 state.listeners.push((token, Rc::new(listener)));
476 token
477 }
478
479 pub fn remove_listener(&self, token: usize) -> bool {
480 let mut state = self.inner.borrow_mut();
481 let before = state.listeners.len();
482 state.listeners.retain(|(id, _)| *id != token);
483 before != state.listeners.len()
484 }
485
486 pub fn apply_delta(&self, delta: &TextEditingDelta) {
487 let next = delta.apply(&self.value());
488 self.update(next, true);
489 }
490
491 pub fn replace_selection(&self, replacement: &str) {
492 let value = self.value();
493 let range = value.selection.range();
494 self.update(value.replace(range, replacement), true);
495 }
496
497 pub fn insert_text(&self, text: &str) {
498 self.replace_selection(text);
499 }
500
501 pub fn delete_backward(&self) {
502 self.backspace();
503 }
504
505 pub fn delete_forward(&self) {
506 let value = self.value();
507 let range = if value.selection.is_collapsed() {
508 next_grapheme_boundary(&value.text, value.selection.extent)
509 .map_or(TextRange::collapsed(value.selection.extent), |end| {
510 TextRange::new(value.selection.extent, end)
511 })
512 } else {
513 value.selection.range()
514 };
515 self.update(value.replace(range, ""), true);
516 }
517
518 pub fn insert(&self, text: &str) {
520 self.insert_text(text);
521 }
522
523 pub fn backspace(&self) {
525 let value = self.value();
526 let range = if value.selection.is_collapsed() {
527 previous_grapheme_boundary(&value.text, value.selection.extent)
528 .map_or(TextRange::collapsed(value.selection.extent), |start| {
529 TextRange::new(start, value.selection.extent)
530 })
531 } else {
532 value.selection.range()
533 };
534 self.update(value.replace(range, ""), true);
535 }
536
537 pub fn delete(&self) {
539 self.delete_forward();
540 }
541
542 pub fn move_left(&self, extend: bool) {
543 self.move_cursor(
544 previous_grapheme_boundary(&self.value().text, self.value().selection.extent)
545 .unwrap_or(0),
546 extend,
547 );
548 }
549
550 pub fn move_right(&self, extend: bool) {
551 let value = self.value();
552 self.move_cursor(
553 next_grapheme_boundary(&value.text, value.selection.extent).unwrap_or(value.text.len()),
554 extend,
555 );
556 }
557
558 pub fn move_left_visual(&self, layout: &TextLayout, extend: bool) {
562 self.move_visual(layout, false, extend);
563 }
564
565 pub fn move_right_visual(&self, layout: &TextLayout, extend: bool) {
568 self.move_visual(layout, true, extend);
569 }
570
571 pub fn move_home(&self, extend: bool) {
572 self.move_cursor(0, extend);
573 }
574
575 pub fn move_end(&self, extend: bool) {
576 self.move_cursor(self.text().len(), extend);
577 }
578
579 pub fn select_all(&self) {
580 self.set_selection(TextSelection::new(0, self.text().len()));
581 }
582
583 #[must_use]
584 pub fn selected_text(&self) -> String {
585 let value = self.value();
586 let range = value.selection.range();
587 value.text[range.as_range()].to_owned()
588 }
589
590 pub fn set_preedit(&self, text: impl Into<String>, selection: Option<TextRange>) {
592 incular_core::reactivity::assert_mutation_allowed();
593 let text = text.into();
594 let mut state = self.inner.borrow_mut();
595 state.preedit_selection = selection.map(|range| range.clamp_to(&text));
596 state.preedit = (!text.is_empty()).then_some(text);
597 state.visual_revision = state
598 .visual_revision
599 .checked_add(1)
600 .expect("visual revision exhausted");
601 drop(state);
602 self.notify_listeners();
603 }
604
605 pub fn commit_preedit(&self, text: &str) {
606 let value = self.value();
607 let range = value.selection.range();
608 self.update_committed(value.replace(range, text), true, true);
609 }
610
611 pub fn clear_preedit(&self) {
612 incular_core::reactivity::assert_mutation_allowed();
613 let mut state = self.inner.borrow_mut();
614 if state.preedit.take().is_some() {
615 state.preedit_selection = None;
616 state.visual_revision = state
617 .visual_revision
618 .checked_add(1)
619 .expect("visual revision exhausted");
620 drop(state);
621 self.notify_listeners();
622 }
623 }
624
625 #[must_use]
626 pub fn preedit(&self) -> Option<String> {
627 self.inner.borrow().preedit.clone()
628 }
629
630 #[must_use]
631 pub fn preedit_selection(&self) -> Option<TextRange> {
632 self.inner.borrow().preedit_selection
633 }
634
635 #[doc(hidden)]
637 #[must_use]
638 pub fn revisions(&self) -> (u64, u64) {
639 let state = self.inner.borrow();
640 (state.content_revision, state.visual_revision)
641 }
642
643 #[doc(hidden)]
645 #[must_use]
646 pub fn restoration_generation(&self) -> u64 {
647 self.inner.borrow().restoration_generation
648 }
649
650 #[doc(hidden)]
652 pub fn reset_caret(&self, now: Instant) {
653 let mut state = self.inner.borrow_mut();
654 state.caret_reset = Some(now);
655 state.visual_revision = state
656 .visual_revision
657 .checked_add(1)
658 .expect("visual revision exhausted");
659 }
660
661 #[doc(hidden)]
663 #[must_use]
664 pub fn caret_visible(&self, now: Instant) -> bool {
665 self.inner.borrow().caret_reset.is_some_and(|start| {
666 (now.checked_duration_since(start)
667 .unwrap_or_default()
668 .as_millis()
669 / 500)
670 .is_multiple_of(2)
671 })
672 }
673
674 #[doc(hidden)]
675 #[must_use]
676 pub fn preferred_caret_x(&self) -> Option<f32> {
677 self.inner.borrow().preferred_caret_x
678 }
679
680 #[doc(hidden)]
683 pub fn set_preferred_caret_x(&self, x: f32) {
684 self.inner.borrow_mut().preferred_caret_x = x.is_finite().then_some(x);
685 }
686
687 #[must_use]
689 pub fn caret_affinity(&self) -> TextAffinity {
690 self.inner.borrow().caret_affinity
691 }
692
693 #[doc(hidden)]
694 pub fn move_cursor(&self, target: usize, extend: bool) {
695 self.move_cursor_impl(target, extend, None, TextAffinity::Downstream);
696 }
697
698 #[doc(hidden)]
699 pub fn move_cursor_with_affinity(&self, target: usize, affinity: TextAffinity, extend: bool) {
700 self.move_cursor_impl(target, extend, None, affinity);
701 }
702
703 fn move_cursor_impl(
704 &self,
705 target: usize,
706 extend: bool,
707 preferred_x: Option<f32>,
708 affinity: TextAffinity,
709 ) {
710 let mut value = self.value();
711 let target = nearest_char_boundary(&value.text, target.min(value.text.len()));
712 value.selection = if extend {
713 TextSelection::new(value.selection.base, target)
714 } else {
715 TextSelection::collapsed(target)
716 };
717 self.update(value, false);
718 let mut state = self.inner.borrow_mut();
719 state.preferred_caret_x = preferred_x;
720 state.caret_affinity = affinity;
721 }
722
723 fn move_visual(&self, layout: &TextLayout, right: bool, extend: bool) {
724 let value = self.value();
725 let extent = value.selection.extent;
726 if !extend && !value.selection.is_collapsed() {
729 let target = if right {
730 value.selection.end()
731 } else {
732 value.selection.start()
733 };
734 self.move_cursor_with_affinity(target, TextAffinity::Downstream, false);
735 return;
736 }
737 let line = layout
738 .lines
739 .iter()
740 .position(|line| extent >= line.start && extent <= line.end)
741 .unwrap_or_else(|| layout.lines.len().saturating_sub(1));
742 let stops = layout.line_caret_positions(line);
743 if stops.is_empty() {
744 return;
745 }
746 let affinity = self.caret_affinity();
747 let current = stops
748 .iter()
749 .position(|stop| stop.offset == extent && stop.affinity == affinity)
750 .or_else(|| stops.iter().position(|stop| stop.offset == extent))
751 .unwrap_or_else(|| {
752 if right {
753 0
754 } else {
755 stops.len().saturating_sub(1)
756 }
757 });
758 let target = if right {
759 stops.get(current + 1).copied().or_else(|| {
760 (line + 1..layout.lines.len())
761 .find_map(|next_line| layout.line_caret_positions(next_line).first().copied())
762 })
763 } else {
764 current
765 .checked_sub(1)
766 .and_then(|index| stops.get(index).copied())
767 .or_else(|| {
768 (0..line).rev().find_map(|previous_line| {
769 layout.line_caret_positions(previous_line).last().copied()
770 })
771 })
772 };
773 let Some(target) = target else {
774 return;
775 };
776 self.move_cursor_with_affinity(target.offset, target.affinity, extend);
777 }
778
779 fn update(&self, value: TextEditingValue, content_changed: bool) {
780 self.update_committed(value, content_changed, false);
781 }
782
783 fn update_committed(&self, value: TextEditingValue, content_changed: bool, end_preedit: bool) {
784 incular_core::reactivity::assert_mutation_allowed();
785 let (old, revision) = {
786 let state = self.inner.borrow();
787 (state.value.clone(), state.visual_revision)
788 };
789 let value = {
790 let transforms = self.transforms.subscribers::<EditTransform>();
791 let mut next = value;
792 for transform in transforms {
793 let _guard = incular_core::reactivity::ReadOnlyGuard::enter();
794 next = transform(&old, &next);
795 if self.inner.borrow().visual_revision != revision {
796 return;
797 }
798 }
799 next.selection = next.selection.clamp_to(&next.text);
800 next.composing = next.composing.map(|range| range.clamp_to(&next.text));
801 next
802 };
803 let listeners = {
804 let mut state = self.inner.borrow_mut();
805 if state.value == value && !(end_preedit && state.preedit.is_some()) {
806 return;
807 }
808 state.value = value;
809 if content_changed {
810 state.content_revision = state
811 .content_revision
812 .checked_add(1)
813 .expect("content revision exhausted");
814 }
815 state.visual_revision = state
816 .visual_revision
817 .checked_add(1)
818 .expect("visual revision exhausted");
819 state.preedit = None;
820 state.preedit_selection = None;
821 state.preferred_caret_x = None;
822 state
823 .listeners
824 .iter()
825 .map(|(_, listener)| listener.clone())
826 .collect::<Vec<_>>()
827 };
828 let value = self.inner.borrow().value.clone();
829 for listener in listeners {
830 listener(&value);
831 }
832 self.persist_restoration();
833 self.changes.notify();
834 }
835
836 fn notify_listeners(&self) {
837 let (value, listeners) = {
838 let state = self.inner.borrow();
839 (
840 state.value.clone(),
841 state
842 .listeners
843 .iter()
844 .map(|(_, listener)| listener.clone())
845 .collect::<Vec<_>>(),
846 )
847 };
848 for listener in listeners {
849 listener(&value);
850 }
851 self.changes.notify();
852 }
853
854 fn persist_restoration(&self) {
855 let (restoration, value) = {
856 let state = self.inner.borrow();
857 (state.restoration.clone(), value_to_json(&state.value))
858 };
859 if let Some(restoration) = restoration {
860 restoration.scope.set_json(&restoration.key, value);
861 }
862 }
863}
864
865fn value_to_json(value: &TextEditingValue) -> serde_json::Value {
866 serde_json::json!({
867 "text": value.text,
868 "selection": {
869 "base": value.selection.base,
870 "extent": value.selection.extent,
871 },
872 })
873}
874
875fn value_from_json(value: serde_json::Value) -> Option<TextEditingValue> {
876 let object = value.as_object()?;
877 let text = object.get("text")?.as_str()?.to_owned();
878 let selection = object
879 .get("selection")
880 .and_then(serde_json::Value::as_object)
881 .and_then(|selection| {
882 let base = usize::try_from(selection.get("base")?.as_u64()?).ok()?;
883 let extent = usize::try_from(selection.get("extent")?.as_u64()?).ok()?;
884 Some(TextSelection::new(base, extent))
885 })
886 .unwrap_or_else(|| TextSelection::collapsed(text.len()));
887 Some(TextEditingValue::new(text.clone()).with_selection(selection.clamp_to(&text)))
888}
889
890#[derive(Clone, Debug)]
892pub struct EditableText {
893 pub controller: TextEditingController,
894 pub style: TextStyle,
895 pub read_only: bool,
896 pub obscure_text: bool,
897 pub max_lines: Option<usize>,
898}
899
900impl EditableText {
901 #[must_use]
902 pub fn new(controller: TextEditingController) -> Self {
903 Self {
904 controller,
905 style: TextStyle::default(),
906 read_only: false,
907 obscure_text: false,
908 max_lines: Some(1),
909 }
910 }
911
912 #[must_use]
913 pub fn style(mut self, style: TextStyle) -> Self {
914 self.style = style;
915 self
916 }
917
918 #[must_use]
919 pub fn read_only(mut self, read_only: bool) -> Self {
920 self.read_only = read_only;
921 self
922 }
923
924 #[must_use]
925 pub fn obscure_text(mut self, obscure_text: bool) -> Self {
926 self.obscure_text = obscure_text;
927 self
928 }
929
930 #[must_use]
931 pub fn max_lines(mut self, max_lines: Option<usize>) -> Self {
932 self.max_lines = max_lines;
933 self
934 }
935}
936
937#[derive(Clone, Debug)]
939pub struct TextField {
940 pub editable: EditableText,
941 pub placeholder: String,
942}
943
944impl TextField {
945 #[must_use]
946 pub fn new(controller: TextEditingController) -> Self {
947 Self {
948 editable: EditableText::new(controller),
949 placeholder: String::new(),
950 }
951 }
952
953 #[must_use]
954 pub fn style(mut self, style: TextStyle) -> Self {
955 self.editable = self.editable.style(style);
956 self
957 }
958
959 #[must_use]
960 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
961 self.placeholder = placeholder.into();
962 self
963 }
964
965 #[must_use]
966 pub fn read_only(mut self, read_only: bool) -> Self {
967 self.editable = self.editable.read_only(read_only);
968 self
969 }
970}
971
972fn nearest_char_boundary(text: &str, offset: usize) -> usize {
973 if text.is_char_boundary(offset) {
974 return offset;
975 }
976 let mut boundary = offset;
977 while boundary > 0 && !text.is_char_boundary(boundary) {
978 boundary -= 1;
979 }
980 boundary
981}
982
983fn previous_grapheme_boundary(text: &str, offset: usize) -> Option<usize> {
984 let offset = nearest_char_boundary(text, offset.min(text.len()));
985 (offset > 0).then(|| {
986 GraphemeClusterSegmenter::new()
987 .segment_str(text)
988 .take_while(|index| *index < offset)
989 .last()
990 .unwrap_or(0)
991 })
992}
993
994fn next_grapheme_boundary(text: &str, offset: usize) -> Option<usize> {
995 let offset = nearest_char_boundary(text, offset.min(text.len()));
996 (offset < text.len()).then(|| {
997 GraphemeClusterSegmenter::new()
998 .segment_str(text)
999 .find(|index| *index > offset)
1000 .unwrap_or(text.len())
1001 })
1002}