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)]
93#[derive(Default)]
94pub enum KeyboardType {
95    #[default]
96    Text,
97    Ascii,
98    Number,
99    Phone,
100    Email,
101    Uri,
102    Decimal,
103}
104
105
106/// The action button on the IME (soft keyboard).
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108#[derive(Default)]
109pub enum ImeAction {
110    #[default]
111    Unspecified,
112    None,
113    Go,
114    Search,
115    Send,
116    Next,
117    Done,
118    Previous,
119}
120
121
122#[derive(Debug, Clone, Copy, PartialEq)]
123pub struct SpanStyle {
124    pub color: Option<Color>,
125    pub font_size: Option<f32>,
126}
127
128impl SpanStyle {
129    pub const fn default() -> Self {
130        Self {
131            color: None,
132            font_size: None,
133        }
134    }
135
136    pub fn color(mut self, c: Color) -> Self {
137        self.color = Some(c);
138        self
139    }
140
141    pub fn font_size(mut self, px: f32) -> Self {
142        self.font_size = Some(px);
143        self
144    }
145}
146
147impl Default for SpanStyle {
148    fn default() -> Self {
149        Self::default()
150    }
151}
152
153/// A span of text with an associated style.
154#[derive(Debug, Clone, PartialEq)]
155pub struct TextSpan {
156    /// Byte offset start in the original text.
157    pub start: usize,
158    /// Byte offset end (exclusive) in the original text.
159    pub end: usize,
160    pub style: SpanStyle,
161}
162
163/// Text with multiple styled spans.
164///
165/// Analogous to Compose's `AnnotatedString`.
166#[derive(Debug, Clone)]
167pub struct AnnotatedString {
168    pub text: String,
169    pub spans: Arc<[TextSpan]>,
170}
171
172impl AnnotatedString {
173    pub fn new(text: impl Into<String>, spans: Vec<TextSpan>) -> Self {
174        let text = text.into();
175        Self {
176            text,
177            spans: spans.into(),
178        }
179    }
180
181    pub fn as_str(&self) -> &str {
182        &self.text
183    }
184}
185
186impl From<String> for AnnotatedString {
187    fn from(text: String) -> Self {
188        Self {
189            text,
190            spans: Arc::from([]),
191        }
192    }
193}
194
195impl From<&str> for AnnotatedString {
196    fn from(text: &str) -> Self {
197        Self {
198            text: text.to_string(),
199            spans: Arc::from([]),
200        }
201    }
202}
203
204/// Builder for constructing an `AnnotatedString`.
205#[derive(Default)]
206pub struct AnnotatedStringBuilder {
207    text: String,
208    spans: Vec<TextSpan>,
209}
210
211impl AnnotatedStringBuilder {
212    pub fn new() -> Self {
213        Self::default()
214    }
215
216    /// Append plain text (inherits parent style, or default if at top level).
217    pub fn push(&mut self, text: &str) -> &mut Self {
218        self.text.push_str(text);
219        self
220    }
221
222    /// Append text with a specific style.
223    pub fn push_with_style(&mut self, text: &str, style: SpanStyle) -> &mut Self {
224        let start = self.text.len();
225        self.text.push_str(text);
226        let end = self.text.len();
227        if start < end {
228            self.spans.push(TextSpan { start, end, style });
229        }
230        self
231    }
232
233    /// Append text in a specific color.
234    pub fn push_color(&mut self, text: &str, color: Color) -> &mut Self {
235        self.push_with_style(text, SpanStyle::default().color(color))
236    }
237
238    /// Apply a style to a range of already-appended text.
239    pub fn add_style(&mut self, start: usize, end: usize, style: SpanStyle) -> &mut Self {
240        if start < end && end <= self.text.len() {
241            self.spans.push(TextSpan { start, end, style });
242        }
243        self
244    }
245
246    pub fn build(&mut self) -> AnnotatedString {
247        let text = std::mem::take(&mut self.text);
248        self.spans.sort_by_key(|s| s.start);
249        // Merge overlapping/adjacent spans with same style
250        let mut merged: Vec<TextSpan> = Vec::new();
251        for span in std::mem::take(&mut self.spans) {
252            if let Some(last) = merged.last_mut()
253                && last.end == span.start && last.style == span.style {
254                    last.end = span.end;
255                    continue;
256                }
257            merged.push(span);
258        }
259        AnnotatedString {
260            text,
261            spans: merged.into(),
262        }
263    }
264}
265
266/// Convenience function to build an `AnnotatedString`.
267pub fn build_annotated_string(b: impl FnOnce(&mut AnnotatedStringBuilder)) -> AnnotatedString {
268    let mut builder = AnnotatedStringBuilder::new();
269    b(&mut builder);
270    builder.build()
271}