Skip to main content

repose_core/
text.rs

1use crate::Color;
2use std::rc::Rc;
3use std::sync::Arc;
4
5/// Transforms the visual representation of a text field's text without changing
6/// the underlying value. For example, password masking.
7pub trait VisualTransformation: std::fmt::Debug + Send + Sync + 'static {
8    /// Transform the text for display. Returns the transformed text and an
9    /// offset-translation function that maps offsets in the display text back
10    /// to the original text.
11    fn filter(&self, text: &str) -> TransformedText;
12}
13
14/// The result of applying a `VisualTransformation`.
15pub struct TransformedText {
16    /// The text to display (e.g., "•••••" for a password).
17    pub text: String,
18    /// Maps an offset in `text` back to the original offset.
19    pub offset_map: Rc<dyn Fn(usize) -> usize>,
20}
21
22impl Clone for TransformedText {
23    fn clone(&self) -> Self {
24        Self {
25            text: self.text.clone(),
26            offset_map: self.offset_map.clone(),
27        }
28    }
29}
30
31impl std::fmt::Debug for TransformedText {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.debug_struct("TransformedText")
34            .field("text", &self.text)
35            .finish()
36    }
37}
38
39/// No visual transformation - text is displayed as-is.
40#[derive(Clone, Copy, Debug)]
41pub struct NoVisualTransformation;
42
43impl VisualTransformation for NoVisualTransformation {
44    fn filter(&self, text: &str) -> TransformedText {
45        let len = text.len();
46        TransformedText {
47            text: text.to_string(),
48            offset_map: Rc::new(move |offset| offset.min(len)),
49        }
50    }
51}
52
53/// A `VisualTransformation` that masks all characters with `*`.
54#[derive(Clone, Copy, Debug)]
55pub struct PasswordVisualTransformation {
56    /// The replacement character (default `*`).
57    pub mask_char: char,
58}
59
60impl Default for PasswordVisualTransformation {
61    fn default() -> Self {
62        Self { mask_char: '*' }
63    }
64}
65
66impl VisualTransformation for PasswordVisualTransformation {
67    fn filter(&self, text: &str) -> TransformedText {
68        let masked: String = text.chars().map(|_| self.mask_char).collect();
69        let len = text.len();
70        TransformedText {
71            text: masked,
72            offset_map: Rc::new(move |offset| offset.min(len)),
73        }
74    }
75}
76
77/// Convert a byte offset in the original text to the corresponding byte offset
78/// in the visually-transformed display text, assuming each original character
79/// maps to one or more display characters starting at a predictable position.
80pub fn original_offset_to_display(original: &str, display: &str, original_byte: usize) -> usize {
81    let char_idx = original[..original_byte.min(original.len())]
82        .chars()
83        .count();
84    display
85        .char_indices()
86        .nth(char_idx)
87        .map(|(i, _)| i)
88        .unwrap_or(display.len())
89}
90
91/// Hints the platform about the type of keyboard to show.
92#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
93pub enum KeyboardType {
94    #[default]
95    Text,
96    Ascii,
97    Number,
98    Phone,
99    Email,
100    Uri,
101    Decimal,
102}
103
104/// The action button on the IME (soft keyboard).
105#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
106pub enum ImeAction {
107    #[default]
108    Unspecified,
109    None,
110    Go,
111    Search,
112    Send,
113    Next,
114    Done,
115    Previous,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq)]
119pub struct SpanStyle {
120    pub color: Option<Color>,
121    pub font_size: Option<f32>,
122}
123
124impl SpanStyle {
125    pub const fn default() -> Self {
126        Self {
127            color: None,
128            font_size: None,
129        }
130    }
131
132    pub fn color(mut self, c: Color) -> Self {
133        self.color = Some(c);
134        self
135    }
136
137    pub fn font_size(mut self, px: f32) -> Self {
138        self.font_size = Some(px);
139        self
140    }
141}
142
143impl Default for SpanStyle {
144    fn default() -> Self {
145        Self::default()
146    }
147}
148
149/// A span of text with an associated style.
150#[derive(Debug, Clone, PartialEq)]
151pub struct TextSpan {
152    /// Byte offset start in the original text.
153    pub start: usize,
154    /// Byte offset end (exclusive) in the original text.
155    pub end: usize,
156    pub style: SpanStyle,
157}
158
159/// Text with multiple styled spans.
160///
161/// Analogous to Compose's `AnnotatedString`.
162#[derive(Debug, Clone)]
163pub struct AnnotatedString {
164    pub text: String,
165    pub spans: Arc<[TextSpan]>,
166}
167
168impl AnnotatedString {
169    pub fn new(text: impl Into<String>, spans: Vec<TextSpan>) -> Self {
170        let text = text.into();
171        Self {
172            text,
173            spans: spans.into(),
174        }
175    }
176
177    pub fn as_str(&self) -> &str {
178        &self.text
179    }
180}
181
182impl From<String> for AnnotatedString {
183    fn from(text: String) -> Self {
184        Self {
185            text,
186            spans: Arc::from([]),
187        }
188    }
189}
190
191impl From<&str> for AnnotatedString {
192    fn from(text: &str) -> Self {
193        Self {
194            text: text.to_string(),
195            spans: Arc::from([]),
196        }
197    }
198}
199
200/// Builder for constructing an `AnnotatedString`.
201#[derive(Default)]
202pub struct AnnotatedStringBuilder {
203    text: String,
204    spans: Vec<TextSpan>,
205}
206
207impl AnnotatedStringBuilder {
208    pub fn new() -> Self {
209        Self::default()
210    }
211
212    /// Append plain text (inherits parent style, or default if at top level).
213    pub fn push(&mut self, text: &str) -> &mut Self {
214        self.text.push_str(text);
215        self
216    }
217
218    /// Append text with a specific style.
219    pub fn push_with_style(&mut self, text: &str, style: SpanStyle) -> &mut Self {
220        let start = self.text.len();
221        self.text.push_str(text);
222        let end = self.text.len();
223        if start < end {
224            self.spans.push(TextSpan { start, end, style });
225        }
226        self
227    }
228
229    /// Append text in a specific color.
230    pub fn push_color(&mut self, text: &str, color: Color) -> &mut Self {
231        self.push_with_style(text, SpanStyle::default().color(color))
232    }
233
234    /// Apply a style to a range of already-appended text.
235    pub fn add_style(&mut self, start: usize, end: usize, style: SpanStyle) -> &mut Self {
236        if start < end && end <= self.text.len() {
237            self.spans.push(TextSpan { start, end, style });
238        }
239        self
240    }
241
242    pub fn build(&mut self) -> AnnotatedString {
243        let text = std::mem::take(&mut self.text);
244        self.spans.sort_by_key(|s| s.start);
245        // Merge overlapping/adjacent spans with same style
246        let mut merged: Vec<TextSpan> = Vec::new();
247        for span in std::mem::take(&mut self.spans) {
248            if let Some(last) = merged.last_mut()
249                && last.end == span.start
250                && last.style == span.style
251            {
252                last.end = span.end;
253                continue;
254            }
255            merged.push(span);
256        }
257        AnnotatedString {
258            text,
259            spans: merged.into(),
260        }
261    }
262}
263
264/// Convenience function to build an `AnnotatedString`.
265pub fn build_annotated_string(b: impl FnOnce(&mut AnnotatedStringBuilder)) -> AnnotatedString {
266    let mut builder = AnnotatedStringBuilder::new();
267    b(&mut builder);
268    builder.build()
269}