Skip to main content

inset_services/
text_formatter.rs

1//! Flutter counterpart: `services/text_formatter.dart`.
2
3use std::rc::Rc;
4
5use inset_embedder::{TargetPlatform, TextAffinity, TextRange, TextSelection};
6
7use crate::TextEditingValue;
8
9/// Mechanisms for enforcing maximum length limits.
10///
11/// This is used by `TextField` to specify how the `TextField.maxLength` should
12/// be applied.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub enum MaxLengthEnforcement {
15    /// No enforcement applied to the editing value. It's possible to exceed the
16    /// max length.
17    None,
18
19    /// Keep the length of the text input from exceeding the max length even when
20    /// the text has an unfinished composing region.
21    Enforced,
22
23    /// Users can still input text if the current value is composing even after
24    /// reaching the max length limit. After composing ends, the value will be
25    /// truncated.
26    TruncateAfterCompositionEnds,
27}
28
29/// A [`TextInputFormatter`] can be optionally injected into an `EditableText`
30/// to provide as-you-type validation and formatting of the text being edited.
31pub trait TextInputFormatter {
32    /// Called when text is being typed or cut/copy/pasted in the `EditableText`.
33    fn format_edit_update(
34        &self,
35        old_value: &TextEditingValue,
36        new_value: &TextEditingValue,
37    ) -> TextEditingValue;
38}
39
40/// Function signature expected for creating custom [`TextInputFormatter`]
41/// shorthands via [`TextInputFormatterRef::with_function`].
42pub type TextInputFormatFunction =
43    Rc<dyn Fn(&TextEditingValue, &TextEditingValue) -> TextEditingValue>;
44
45/// A shared [`TextInputFormatter`].
46#[derive(Clone)]
47pub struct TextInputFormatterRef(pub Rc<dyn TextInputFormatter>);
48
49impl TextInputFormatterRef {
50    /// A shorthand to creating a custom [`TextInputFormatter`] which formats
51    /// incoming text input changes with the given function.
52    pub fn with_function(format_function: TextInputFormatFunction) -> TextInputFormatterRef {
53        TextInputFormatterRef(Rc::new(SimpleTextInputFormatter { format_function }))
54    }
55}
56
57impl TextInputFormatter for TextInputFormatterRef {
58    fn format_edit_update(
59        &self,
60        old_value: &TextEditingValue,
61        new_value: &TextEditingValue,
62    ) -> TextEditingValue {
63        self.0.format_edit_update(old_value, new_value)
64    }
65}
66
67struct SimpleTextInputFormatter {
68    format_function: TextInputFormatFunction,
69}
70
71impl TextInputFormatter for SimpleTextInputFormatter {
72    fn format_edit_update(
73        &self,
74        old_value: &TextEditingValue,
75        new_value: &TextEditingValue,
76    ) -> TextEditingValue {
77        (self.format_function)(old_value, new_value)
78    }
79}
80
81/// A mutable, half-open range [`base`, `extent`) within a string.
82struct MutableTextRange {
83    base: i32,
84    extent: i32,
85}
86
87impl MutableTextRange {
88    fn from_composing_range(range: TextRange) -> Option<MutableTextRange> {
89        if range.is_valid() && !range.is_collapsed() {
90            Some(MutableTextRange {
91                base: range.start,
92                extent: range.end,
93            })
94        } else {
95            None
96        }
97    }
98
99    fn from_text_selection(selection: TextSelection) -> Option<MutableTextRange> {
100        if selection.is_valid() {
101            Some(MutableTextRange {
102                base: selection.base_offset,
103                extent: selection.extent_offset,
104            })
105        } else {
106            None
107        }
108    }
109}
110
111/// The intermediate state of a [`FilteringTextInputFormatter`] when it's
112/// formatting a new user input.
113struct TextEditingValueAccumulator {
114    input_value: TextEditingValue,
115    string_buffer: String,
116    selection: Option<MutableTextRange>,
117    composing_region: Option<MutableTextRange>,
118}
119
120impl TextEditingValueAccumulator {
121    fn new(input_value: TextEditingValue) -> TextEditingValueAccumulator {
122        let selection = MutableTextRange::from_text_selection(input_value.selection);
123        let composing_region = MutableTextRange::from_composing_range(input_value.composing);
124        TextEditingValueAccumulator {
125            input_value,
126            string_buffer: String::new(),
127            selection,
128            composing_region,
129        }
130    }
131
132    fn finalize(self) -> TextEditingValue {
133        let selection = self.selection;
134        let composing_region = self.composing_region;
135        TextEditingValue::new()
136            .text(self.string_buffer)
137            .composing(match &composing_region {
138                None => TextRange::EMPTY,
139                Some(range) if range.base == range.extent => TextRange::EMPTY,
140                Some(range) => TextRange::new(range.base, range.extent),
141            })
142            .selection(match selection {
143                None => TextSelection::collapsed(-1, TextAffinity::Downstream),
144                Some(range) => TextSelection {
145                    base_offset: range.base,
146                    extent_offset: range.extent,
147                    affinity: self.input_value.selection.affinity,
148                    is_directional: self.input_value.selection.is_directional,
149                },
150            })
151    }
152}
153
154/// A pattern [`FilteringTextInputFormatter`] matches against incoming text.
155///
156/// Dart's `Pattern` is a string or a `RegExp`. There is no regex crate here;
157/// [`Self::Digits`] is `RegExp(r'[0-9]')`.
158#[derive(Clone, Debug)]
159pub enum FilterPattern {
160    /// Dart `String` pattern: every occurrence of this substring.
161    Literal(String),
162    /// Dart `RegExp(r'[0-9]')`: every ASCII digit.
163    Digits,
164}
165
166impl FilterPattern {
167    fn all_matches(&self, text: &str) -> Vec<(i32, i32)> {
168        match self {
169            FilterPattern::Literal(needle) => {
170                if needle.is_empty() {
171                    return Vec::new();
172                }
173                let mut matches = Vec::new();
174                let mut search_from = 0usize;
175                while let Some(byte) = text[search_from..].find(needle) {
176                    let start_byte = search_from + byte;
177                    let end_byte = start_byte + needle.len();
178                    matches.push((utf16_len(&text[..start_byte]), utf16_len(&text[..end_byte])));
179                    search_from = end_byte;
180                }
181                matches
182            }
183            FilterPattern::Digits => {
184                let mut matches = Vec::new();
185                let mut utf16 = 0i32;
186                for ch in text.chars() {
187                    let width = ch.len_utf16() as i32;
188                    if ch.is_ascii_digit() {
189                        matches.push((utf16, utf16 + width));
190                    }
191                    utf16 += width;
192                }
193                matches
194            }
195        }
196    }
197}
198
199/// A [`TextInputFormatter`] that prevents the insertion of characters matching
200/// (or not matching) a particular pattern, by replacing the characters with the
201/// given [`replacement_string`](Self::replacement_string).
202#[derive(Clone, Debug)]
203pub struct FilteringTextInputFormatter {
204    /// A pattern to match or replace in incoming [`TextEditingValue`]s.
205    pub filter_pattern: FilterPattern,
206    /// Whether the pattern is an allow list or not.
207    pub allow: bool,
208    /// String used to replace banned patterns.
209    pub replacement_string: String,
210}
211
212impl FilteringTextInputFormatter {
213    /// Creates a formatter that replaces banned patterns with the given
214    /// `replacement_string`.
215    pub fn new(filter_pattern: FilterPattern, allow: bool) -> FilteringTextInputFormatter {
216        FilteringTextInputFormatter {
217            filter_pattern,
218            allow,
219            replacement_string: String::new(),
220        }
221    }
222
223    /// Dart `FilteringTextInputFormatter(replacementString:)`.
224    pub fn replacement_string(mut self, replacement_string: impl Into<String>) -> Self {
225        self.replacement_string = replacement_string.into();
226        self
227    }
228
229    /// Creates a formatter that only allows characters matching a pattern.
230    pub fn allow(filter_pattern: FilterPattern) -> FilteringTextInputFormatter {
231        FilteringTextInputFormatter::new(filter_pattern, true)
232    }
233
234    /// Creates a formatter that blocks characters matching a pattern.
235    pub fn deny(filter_pattern: FilterPattern) -> FilteringTextInputFormatter {
236        FilteringTextInputFormatter::new(filter_pattern, false)
237    }
238
239    /// A [`TextInputFormatter`] that forces input to be a single line.
240    pub fn single_line_formatter() -> TextInputFormatterRef {
241        TextInputFormatterRef(Rc::new(FilteringTextInputFormatter::deny(
242            FilterPattern::Literal("\n".into()),
243        )))
244    }
245
246    /// A [`TextInputFormatter`] that takes in digits `[0-9]` only.
247    pub fn digits_only() -> TextInputFormatterRef {
248        TextInputFormatterRef(Rc::new(FilteringTextInputFormatter::allow(
249            FilterPattern::Digits,
250        )))
251    }
252
253    fn process_region(
254        &self,
255        is_banned_region: bool,
256        region_start: i32,
257        region_end: i32,
258        state: &mut TextEditingValueAccumulator,
259    ) {
260        let replacement_string = if is_banned_region {
261            if region_start == region_end {
262                String::new()
263            } else {
264                self.replacement_string.clone()
265            }
266        } else {
267            utf16_slice(&state.input_value.text, region_start, region_end).to_owned()
268        };
269
270        let region_len = region_end - region_start;
271        let replacement_len = utf16_len(&replacement_string);
272        state.string_buffer.push_str(&replacement_string);
273
274        if replacement_len == region_len {
275            return;
276        }
277
278        let adjust_index = |original_index: i32| -> i32 {
279            let replaced_length = if original_index <= region_start && original_index < region_end {
280                0
281            } else {
282                replacement_len
283            };
284            let removed_length = original_index.clamp(region_start, region_end) - region_start;
285            replaced_length - removed_length
286        };
287
288        if let Some(selection) = state.selection.as_mut() {
289            selection.base += adjust_index(state.input_value.selection.base_offset);
290            selection.extent += adjust_index(state.input_value.selection.extent_offset);
291        }
292        if let Some(composing) = state.composing_region.as_mut() {
293            composing.base += adjust_index(state.input_value.composing.start);
294            composing.extent += adjust_index(state.input_value.composing.end);
295        }
296    }
297}
298
299impl TextInputFormatter for FilteringTextInputFormatter {
300    fn format_edit_update(
301        &self,
302        _old_value: &TextEditingValue,
303        new_value: &TextEditingValue,
304    ) -> TextEditingValue {
305        let mut format_state = TextEditingValueAccumulator::new(new_value.clone());
306        let matches = self.filter_pattern.all_matches(&new_value.text);
307        let mut previous_end = 0i32;
308        for (start, end) in matches {
309            self.process_region(self.allow, previous_end, start, &mut format_state);
310            self.process_region(!self.allow, start, end, &mut format_state);
311            previous_end = end;
312        }
313        self.process_region(
314            self.allow,
315            previous_end,
316            utf16_len(&new_value.text),
317            &mut format_state,
318        );
319        format_state.finalize()
320    }
321}
322
323/// A [`TextInputFormatter`] that prevents the insertion of more characters
324/// than allowed.
325pub struct LengthLimitingTextInputFormatter {
326    /// The limit on the number of user-perceived characters that this formatter
327    /// will allow.
328    pub max_length: Option<i32>,
329    /// Determines how the [`max_length`](Self::max_length) limit should be enforced.
330    pub max_length_enforcement: Option<MaxLengthEnforcement>,
331}
332
333impl LengthLimitingTextInputFormatter {
334    /// Creates a formatter that prevents the insertion of more characters than a
335    /// limit.
336    pub fn new(max_length: Option<i32>) -> LengthLimitingTextInputFormatter {
337        debug_assert!(
338            max_length.is_none_or(|n| n == -1 || n > 0),
339            "maxLength must be null, -1 or greater than zero"
340        );
341        LengthLimitingTextInputFormatter {
342            max_length,
343            max_length_enforcement: None,
344        }
345    }
346
347    /// Dart `LengthLimitingTextInputFormatter(maxLengthEnforcement:)`.
348    pub fn max_length_enforcement(mut self, value: MaxLengthEnforcement) -> Self {
349        self.max_length_enforcement = Some(value);
350        self
351    }
352
353    /// Returns a [`MaxLengthEnforcement`] that follows the specified `platform`'s
354    /// convention.
355    pub fn get_default_max_length_enforcement(
356        platform: Option<TargetPlatform>,
357    ) -> MaxLengthEnforcement {
358        match platform.unwrap_or(TargetPlatform::Android) {
359            TargetPlatform::Android | TargetPlatform::Windows => MaxLengthEnforcement::Enforced,
360            TargetPlatform::IOS
361            | TargetPlatform::MacOS
362            | TargetPlatform::Linux
363            | TargetPlatform::Fuchsia => MaxLengthEnforcement::TruncateAfterCompositionEnds,
364        }
365    }
366
367    /// Truncate the given [`TextEditingValue`] to `max_length` Unicode scalar
368    /// values.
369    pub fn truncate(value: &TextEditingValue, max_length: i32) -> TextEditingValue {
370        let truncated: String = value.text.chars().take(max_length as usize).collect();
371        let truncated_len = utf16_len(&truncated);
372        TextEditingValue::new()
373            .text(truncated)
374            .selection(
375                value
376                    .selection
377                    .copy_with()
378                    .base_offset(value.selection.start().min(truncated_len))
379                    .extent_offset(value.selection.end().min(truncated_len)),
380            )
381            .composing(
382                if !value.composing.is_collapsed() && truncated_len > value.composing.start {
383                    TextRange::new(
384                        value.composing.start,
385                        value.composing.end.min(truncated_len),
386                    )
387                } else {
388                    TextRange::EMPTY
389                },
390            )
391    }
392
393    fn character_count(text: &str) -> i32 {
394        text.chars().count() as i32
395    }
396}
397
398impl TextInputFormatter for LengthLimitingTextInputFormatter {
399    fn format_edit_update(
400        &self,
401        old_value: &TextEditingValue,
402        new_value: &TextEditingValue,
403    ) -> TextEditingValue {
404        let Some(max_length) = self.max_length else {
405            return new_value.clone();
406        };
407        if max_length == -1 || Self::character_count(&new_value.text) <= max_length {
408            return new_value.clone();
409        }
410        debug_assert!(max_length > 0);
411        match self
412            .max_length_enforcement
413            .unwrap_or(Self::get_default_max_length_enforcement(None))
414        {
415            MaxLengthEnforcement::None => new_value.clone(),
416            MaxLengthEnforcement::Enforced => {
417                if Self::character_count(&old_value.text) == max_length
418                    && old_value.selection.is_collapsed()
419                {
420                    return old_value.clone();
421                }
422                Self::truncate(new_value, max_length)
423            }
424            MaxLengthEnforcement::TruncateAfterCompositionEnds => {
425                if Self::character_count(&old_value.text) == max_length
426                    && !old_value.composing.is_valid()
427                {
428                    return old_value.clone();
429                }
430                if new_value.composing.is_valid() {
431                    return new_value.clone();
432                }
433                Self::truncate(new_value, max_length)
434            }
435        }
436    }
437}
438
439fn utf16_len(text: &str) -> i32 {
440    text.encode_utf16().count() as i32
441}
442
443fn utf16_slice(text: &str, start: i32, end: i32) -> &str {
444    let start = start.max(0) as usize;
445    let end = end.max(0) as usize;
446    let mut byte_start = None;
447    let mut byte_end = None;
448    let mut units = 0usize;
449    for (i, ch) in text.char_indices() {
450        if byte_start.is_none() && units == start {
451            byte_start = Some(i);
452        }
453        if units == end {
454            byte_end = Some(i);
455            break;
456        }
457        units += ch.len_utf16();
458    }
459    let byte_start = byte_start.unwrap_or(text.len());
460    let byte_end = byte_end.unwrap_or(text.len());
461    &text[byte_start.min(byte_end)..byte_end]
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    #[test]
469    fn single_line_formatter_strips_newlines() {
470        let formatter = FilteringTextInputFormatter::single_line_formatter();
471        let old = TextEditingValue::EMPTY;
472        let new = TextEditingValue::new()
473            .text("a\nb")
474            .selection(TextSelection::collapsed(3, TextAffinity::Downstream));
475        let out = formatter.format_edit_update(&old, &new);
476        assert_eq!(out.text, "ab");
477        assert_eq!(out.selection.extent_offset, 2);
478    }
479
480    #[test]
481    fn digits_only_keeps_digits() {
482        let formatter = FilteringTextInputFormatter::digits_only();
483        let old = TextEditingValue::EMPTY;
484        let new = TextEditingValue::new().text("a1b2");
485        let out = formatter.format_edit_update(&old, &new);
486        assert_eq!(out.text, "12");
487    }
488}