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