Skip to main content

repose_core/
text.rs

1use crate::Color;
2use std::rc::Rc;
3use std::sync::Arc;
4
5/// Result of text layout computation, provided to the `on_text_layout` callback.
6/// Exposes key information about the rendered text layout.
7#[derive(Clone, Debug)]
8pub struct TextLayoutResult {
9    /// Number of visual lines in the layout.
10    pub line_count: usize,
11    /// Total content width in px.
12    pub width_px: f32,
13    /// Total content height in px.
14    pub height_px: f32,
15}
16
17/// Transforms the visual representation of a text field's text without changing
18/// the underlying value. For example, password masking.
19pub trait VisualTransformation: std::fmt::Debug + Send + Sync + 'static {
20    /// Transform the text for display. Returns the transformed text and an
21    /// offset-translation function that maps offsets in the display text back
22    /// to the original text.
23    fn filter(&self, text: &str) -> TransformedText;
24}
25
26/// The result of applying a `VisualTransformation`.
27pub struct TransformedText {
28    /// The text to display (e.g., "•••••" for a password).
29    pub text: String,
30    /// Maps an offset in `text` back to the original offset.
31    pub offset_map: Rc<dyn Fn(usize) -> usize>,
32}
33
34impl Clone for TransformedText {
35    fn clone(&self) -> Self {
36        Self {
37            text: self.text.clone(),
38            offset_map: self.offset_map.clone(),
39        }
40    }
41}
42
43impl std::fmt::Debug for TransformedText {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        f.debug_struct("TransformedText")
46            .field("text", &self.text)
47            .finish()
48    }
49}
50
51/// No visual transformation - text is displayed as-is.
52#[derive(Clone, Copy, Debug)]
53pub struct NoVisualTransformation;
54
55impl VisualTransformation for NoVisualTransformation {
56    fn filter(&self, text: &str) -> TransformedText {
57        let len = text.len();
58        TransformedText {
59            text: text.to_string(),
60            offset_map: Rc::new(move |offset| offset.min(len)),
61        }
62    }
63}
64
65/// A `VisualTransformation` that masks all characters with `*`.
66#[derive(Clone, Copy, Debug)]
67pub struct PasswordVisualTransformation {
68    /// The replacement character (default `*`).
69    pub mask_char: char,
70}
71
72impl Default for PasswordVisualTransformation {
73    fn default() -> Self {
74        Self { mask_char: '*' }
75    }
76}
77
78impl VisualTransformation for PasswordVisualTransformation {
79    fn filter(&self, text: &str) -> TransformedText {
80        let masked: String = text.chars().map(|_| self.mask_char).collect();
81        let len = text.len();
82        TransformedText {
83            text: masked,
84            offset_map: Rc::new(move |offset| offset.min(len)),
85        }
86    }
87}
88
89/// Convert a byte offset in the original text to the corresponding byte offset
90/// in the visually-transformed display text, assuming each original character
91/// maps to one or more display characters starting at a predictable position.
92pub fn original_offset_to_display(original: &str, display: &str, original_byte: usize) -> usize {
93    let char_idx = original[..original_byte.min(original.len())]
94        .chars()
95        .count();
96    display
97        .char_indices()
98        .nth(char_idx)
99        .map(|(i, _)| i)
100        .unwrap_or(display.len())
101}
102
103/// Hints the platform about the type of keyboard to show.
104#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
105pub enum KeyboardType {
106    #[default]
107    Text,
108    Ascii,
109    Number,
110    Phone,
111    Email,
112    Uri,
113    Decimal,
114}
115
116/// The action button on the IME (soft keyboard).
117#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
118pub enum ImeAction {
119    #[default]
120    Unspecified,
121    None,
122    Go,
123    Search,
124    Send,
125    Next,
126    Done,
127    Previous,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq)]
131pub struct SpanStyle {
132    pub color: Option<Color>,
133    pub font_size: Option<f32>,
134}
135
136impl SpanStyle {
137    pub const fn default() -> Self {
138        Self {
139            color: None,
140            font_size: None,
141        }
142    }
143
144    pub fn color(mut self, c: Color) -> Self {
145        self.color = Some(c);
146        self
147    }
148
149    pub fn font_size(mut self, px: f32) -> Self {
150        self.font_size = Some(px);
151        self
152    }
153}
154
155impl Default for SpanStyle {
156    fn default() -> Self {
157        Self::default()
158    }
159}
160
161/// A span of text with an associated style.
162#[derive(Debug, Clone, PartialEq)]
163pub struct TextSpan {
164    /// Byte offset start in the original text.
165    pub start: usize,
166    /// Byte offset end (exclusive) in the original text.
167    pub end: usize,
168    pub style: SpanStyle,
169}
170
171/// Text with multiple styled spans.
172///
173/// Analogous to Compose's `AnnotatedString`.
174#[derive(Debug, Clone)]
175pub struct AnnotatedString {
176    pub text: String,
177    pub spans: Arc<[TextSpan]>,
178}
179
180impl AnnotatedString {
181    pub fn new(text: impl Into<String>, spans: Vec<TextSpan>) -> Self {
182        let text = text.into();
183        Self {
184            text,
185            spans: spans.into(),
186        }
187    }
188
189    pub fn as_str(&self) -> &str {
190        &self.text
191    }
192}
193
194impl From<String> for AnnotatedString {
195    fn from(text: String) -> Self {
196        Self {
197            text,
198            spans: Arc::from([]),
199        }
200    }
201}
202
203impl From<&str> for AnnotatedString {
204    fn from(text: &str) -> Self {
205        Self {
206            text: text.to_string(),
207            spans: Arc::from([]),
208        }
209    }
210}
211
212/// Builder for constructing an `AnnotatedString`.
213#[derive(Default)]
214pub struct AnnotatedStringBuilder {
215    text: String,
216    spans: Vec<TextSpan>,
217}
218
219impl AnnotatedStringBuilder {
220    pub fn new() -> Self {
221        Self::default()
222    }
223
224    /// Append plain text (inherits parent style, or default if at top level).
225    pub fn push(&mut self, text: &str) -> &mut Self {
226        self.text.push_str(text);
227        self
228    }
229
230    /// Append text with a specific style.
231    pub fn push_with_style(&mut self, text: &str, style: SpanStyle) -> &mut Self {
232        let start = self.text.len();
233        self.text.push_str(text);
234        let end = self.text.len();
235        if start < end {
236            self.spans.push(TextSpan { start, end, style });
237        }
238        self
239    }
240
241    /// Append text in a specific color.
242    pub fn push_color(&mut self, text: &str, color: Color) -> &mut Self {
243        self.push_with_style(text, SpanStyle::default().color(color))
244    }
245
246    /// Apply a style to a range of already-appended text.
247    pub fn add_style(&mut self, start: usize, end: usize, style: SpanStyle) -> &mut Self {
248        if start < end && end <= self.text.len() {
249            self.spans.push(TextSpan { start, end, style });
250        }
251        self
252    }
253
254    pub fn build(&mut self) -> AnnotatedString {
255        let text = std::mem::take(&mut self.text);
256        self.spans.sort_by_key(|s| s.start);
257        // Merge overlapping/adjacent spans with same style
258        let mut merged: Vec<TextSpan> = Vec::new();
259        for span in std::mem::take(&mut self.spans) {
260            if let Some(last) = merged.last_mut()
261                && last.end == span.start
262                && last.style == span.style
263            {
264                last.end = span.end;
265                continue;
266            }
267            merged.push(span);
268        }
269        AnnotatedString {
270            text,
271            spans: merged.into(),
272        }
273    }
274}
275
276/// Horizontal text alignment.
277#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
278pub enum TextAlign {
279    Left,
280    Right,
281    Center,
282    Justify,
283    Start,
284    End,
285    Unspecified,
286}
287
288impl Default for TextAlign {
289    fn default() -> Self {
290        TextAlign::Unspecified
291    }
292}
293
294/// Font weight as a numeric value 100-900, matching CSS `font-weight`.
295#[derive(Clone, Copy, Debug, PartialEq)]
296pub struct FontWeight(pub u16);
297
298impl FontWeight {
299    pub const THIN: FontWeight = FontWeight(100);
300    pub const EXTRA_LIGHT: FontWeight = FontWeight(200);
301    pub const LIGHT: FontWeight = FontWeight(300);
302    pub const NORMAL: FontWeight = FontWeight(400);
303    pub const MEDIUM: FontWeight = FontWeight(500);
304    pub const SEMI_BOLD: FontWeight = FontWeight(600);
305    pub const BOLD: FontWeight = FontWeight(700);
306    pub const EXTRA_BOLD: FontWeight = FontWeight(800);
307    pub const BLACK: FontWeight = FontWeight(900);
308}
309
310impl Default for FontWeight {
311    fn default() -> Self {
312        FontWeight::NORMAL
313    }
314}
315
316/// Font style: normal or italic.
317#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
318pub enum FontStyle {
319    Normal,
320    Italic,
321}
322
323impl Default for FontStyle {
324    fn default() -> Self {
325        FontStyle::Normal
326    }
327}
328
329/// Text decoration state.
330#[derive(Clone, Copy, Debug, PartialEq)]
331pub struct TextDecoration {
332    pub underline: bool,
333    pub strikethrough: bool,
334    pub color: Option<Color>,
335}
336
337impl Default for TextDecoration {
338    fn default() -> Self {
339        Self {
340            underline: false,
341            strikethrough: false,
342            color: None,
343        }
344    }
345}
346
347/// Convenience function to build an `AnnotatedString`.
348pub fn build_annotated_string(b: impl FnOnce(&mut AnnotatedStringBuilder)) -> AnnotatedString {
349    let mut builder = AnnotatedStringBuilder::new();
350    b(&mut builder);
351    builder.build()
352}