Skip to main content

repose_core/
text.rs

1use crate::Color;
2use std::fmt::Debug;
3use std::rc::Rc;
4use std::sync::Arc;
5
6/// A range of text measured in byte offsets, matching Compose's `TextRange`.
7///
8/// When `start == end`, the range is collapsed (cursor position).
9/// When `start > end`, the range is reversed (selection direction matters).
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub struct TextRange {
12    pub start: usize,
13    pub end: usize,
14}
15
16impl TextRange {
17    pub const ZERO: TextRange = TextRange { start: 0, end: 0 };
18
19    pub fn new(start: usize, end: usize) -> Self {
20        Self { start, end }
21    }
22
23    pub fn collapsed(at: usize) -> Self {
24        Self { start: at, end: at }
25    }
26
27    pub fn min(self) -> usize {
28        self.start.min(self.end)
29    }
30
31    pub fn max(self) -> usize {
32        self.start.max(self.end)
33    }
34
35    pub fn is_collapsed(self) -> bool {
36        self.start == self.end
37    }
38
39    pub fn reversed(self) -> bool {
40        self.start > self.end
41    }
42
43    pub fn length(self) -> usize {
44        self.max() - self.min()
45    }
46
47    pub fn intersects(self, other: TextRange) -> bool {
48        self.min() < other.max() && other.min() < self.max()
49    }
50
51    pub fn contains(self, offset: usize) -> bool {
52        self.min() <= offset && offset < self.max()
53    }
54
55    pub fn coerce_in(self, min: usize, max: usize) -> Self {
56        Self {
57            start: self.start.clamp(min, max),
58            end: self.end.clamp(min, max),
59        }
60    }
61}
62
63impl From<(usize, usize)> for TextRange {
64    fn from((start, end): (usize, usize)) -> Self {
65        Self { start, end }
66    }
67}
68
69/// Snapshot of a text field's editing state including text, selection, and
70/// IME composition range. Corresponds to Compose's `TextFieldValue`.
71#[derive(Clone, Debug, PartialEq)]
72pub struct TextFieldValue {
73    /// The annotated text. Plain text is accessible via `text()` or `annotated_string.text`.
74    pub annotated_string: AnnotatedString,
75    /// Selection range in byte offsets. Collapsed range (start == end) = cursor.
76    pub selection: TextRange,
77    /// Active IME composition range in byte offsets, or None.
78    pub composition: Option<TextRange>,
79}
80
81impl TextFieldValue {
82    /// Create from plain text (no annotations).
83    pub fn new(text: impl Into<String>) -> Self {
84        let annotated = AnnotatedString::from(text.into());
85        let len = annotated.text.len();
86        Self {
87            selection: TextRange::collapsed(len),
88            annotated_string: annotated,
89            composition: None,
90        }
91    }
92
93    /// Create from an `AnnotatedString` preserving all annotations.
94    pub fn from_annotated(annotated: AnnotatedString) -> Self {
95        let len = annotated.text.len();
96        Self {
97            selection: TextRange::collapsed(len),
98            annotated_string: annotated,
99            composition: None,
100        }
101    }
102
103    /// Convenience: plain text content (delegates to `annotated_string.text`).
104    pub fn text(&self) -> &str {
105        &self.annotated_string.text
106    }
107
108    pub fn with_selection(mut self, start: usize, end: usize) -> Self {
109        let len = self.annotated_string.text.len();
110        self.selection = TextRange::new(start.min(len), end.min(len));
111        self
112    }
113
114    pub fn get_text_before_selection(&self, max_chars: usize) -> AnnotatedString {
115        let sel_min = self.selection.min();
116        let start = sel_min.saturating_sub(max_chars);
117        let text = self.annotated_string.text[start..sel_min].to_string();
118        // Preserve spans that intersect the sub-range
119        let spans: Vec<TextSpan> = self
120            .annotated_string
121            .spans
122            .iter()
123            .filter(|s| s.start >= start && s.end <= sel_min)
124            .map(|s| TextSpan {
125                start: s.start - start,
126                end: s.end - start,
127                style: s.style.clone(),
128                url: s.url.clone(),
129            })
130            .collect();
131        AnnotatedString::new(text, spans)
132    }
133
134    pub fn get_text_after_selection(&self, max_chars: usize) -> AnnotatedString {
135        let sel_max = self.selection.max();
136        let end = (sel_max + max_chars).min(self.annotated_string.text.len());
137        let text = self.annotated_string.text[sel_max..end].to_string();
138        let spans: Vec<TextSpan> = self
139            .annotated_string
140            .spans
141            .iter()
142            .filter(|s| s.start >= sel_max && s.end <= end)
143            .map(|s| TextSpan {
144                start: s.start - sel_max,
145                end: s.end - sel_max,
146                style: s.style.clone(),
147                url: s.url.clone(),
148            })
149            .collect();
150        AnnotatedString::new(text, spans)
151    }
152
153    pub fn get_selected_text(&self) -> AnnotatedString {
154        let r = self.selection.min()..self.selection.max();
155        let text = self.annotated_string.text[r.clone()].to_string();
156        let spans: Vec<TextSpan> = self
157            .annotated_string
158            .spans
159            .iter()
160            .filter(|s| s.start >= r.start && s.end <= r.end)
161            .map(|s| TextSpan {
162                start: s.start - r.start,
163                end: s.end - r.start,
164                style: s.style.clone(),
165                url: s.url.clone(),
166            })
167            .collect();
168        AnnotatedString::new(text, spans)
169    }
170
171    pub fn copy(&self, annotated_string: AnnotatedString) -> Self {
172        TextFieldValue {
173            annotated_string,
174            selection: self.selection,
175            composition: self.composition,
176        }
177    }
178
179    pub fn copy_text(&self, text: String) -> Self {
180        TextFieldValue {
181            annotated_string: AnnotatedString::from(text),
182            selection: self.selection,
183            composition: self.composition,
184        }
185    }
186}
187
188/// Result of text layout for `on_text_layout`.
189#[derive(Clone, Debug)]
190pub struct TextLayoutResult {
191    pub line_count: usize,
192    pub width_px: f32,
193    pub height_px: f32,
194    pub first_baseline: f32,
195    pub last_baseline: f32,
196    pub did_overflow_width: bool,
197    pub did_overflow_height: bool,
198    pub lines: Vec<TextLineInfo>,
199}
200
201/// How text is obfuscated in secure fields.
202#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
203pub enum TextObfuscationMode {
204    /// Text is visible, no obfuscation.
205    Visible,
206    /// Reveal the last typed character briefly, then hide.
207    RevealLastTyped,
208    /// All characters are obfuscated.
209    Hidden,
210    /// Uses the platform's default obfuscation behavior.
211    #[default]
212    System,
213}
214
215/// Layout information for a single line of text.
216#[derive(Clone, Debug)]
217pub struct TextLineInfo {
218    /// Byte offset of the line start in the text.
219    pub start: usize,
220    /// Byte offset of the line end (exclusive) in the text.
221    pub end: usize,
222    /// Top y position in px relative to the text field content area.
223    pub top: f32,
224    /// Baseline y position in px relative to the text field content area.
225    pub baseline: f32,
226    /// Bottom y position in px relative to the text field content area.
227    pub bottom: f32,
228    /// Left x position in px relative to the text field content area.
229    pub left: f32,
230    /// Right x position in px relative to the text field content area.
231    pub right: f32,
232    /// Width of this line in px.
233    pub width: f32,
234}
235
236impl TextLayoutResult {
237    pub fn get_line_start(&self, line_index: usize) -> Option<usize> {
238        self.lines.get(line_index).map(|l| l.start)
239    }
240
241    pub fn get_line_end(&self, line_index: usize) -> Option<usize> {
242        self.lines.get(line_index).map(|l| l.end)
243    }
244
245    pub fn get_line_end_visible(&self, line_index: usize) -> Option<usize> {
246        self.lines.get(line_index).map(|l| l.end)
247    }
248
249    pub fn is_line_ellipsized(&self, _line_index: usize) -> bool {
250        false
251    }
252
253    pub fn get_line_top(&self, line_index: usize) -> Option<f32> {
254        self.lines.get(line_index).map(|l| l.top)
255    }
256
257    pub fn get_line_baseline(&self, line_index: usize) -> Option<f32> {
258        self.lines.get(line_index).map(|l| l.baseline)
259    }
260
261    pub fn get_line_bottom(&self, line_index: usize) -> Option<f32> {
262        self.lines.get(line_index).map(|l| l.bottom)
263    }
264
265    pub fn get_line_left(&self, line_index: usize) -> Option<f32> {
266        self.lines.get(line_index).map(|l| l.left)
267    }
268
269    pub fn get_line_right(&self, line_index: usize) -> Option<f32> {
270        self.lines.get(line_index).map(|l| l.right)
271    }
272
273    pub fn get_line_for_offset(&self, offset: usize) -> usize {
274        for (i, line) in self.lines.iter().enumerate() {
275            if offset >= line.start && offset < line.end {
276                return i;
277            }
278        }
279        self.line_count.saturating_sub(1)
280    }
281
282    pub fn get_line_for_vertical_position(&self, vertical: f32) -> usize {
283        for (i, line) in self.lines.iter().enumerate() {
284            if vertical >= line.top && vertical < line.bottom {
285                return i;
286            }
287        }
288        self.line_count.saturating_sub(1)
289    }
290
291    pub fn get_horizontal_position(&self, offset: usize, _use_primary_direction: bool) -> f32 {
292        self.lines
293            .iter()
294            .find(|l| offset >= l.start && offset <= l.end)
295            .map(|l| l.left)
296            .unwrap_or(0.0)
297    }
298
299    pub fn has_visual_overflow(&self) -> bool {
300        self.did_overflow_width || self.did_overflow_height
301    }
302
303    pub fn get_bounding_box(&self, offset: usize) -> crate::Rect {
304        if self.lines.is_empty() || offset > self.lines.last().unwrap().end {
305            return crate::Rect::default();
306        }
307        let line = self
308            .lines
309            .iter()
310            .find(|l| offset >= l.start && offset < l.end)
311            .unwrap_or_else(|| {
312                if offset >= self.lines.last().unwrap().end {
313                    self.lines.last().unwrap()
314                } else {
315                    &self.lines[0]
316                }
317            });
318        let _line_idx = self.get_line_for_offset(offset);
319        let char_in_line = offset - line.start;
320        let pos_in_line = if char_in_line == 0 {
321            line.left
322        } else {
323            let chars = line.end - line.start;
324            if chars == 0 {
325                line.left
326            } else {
327                line.left + (line.width * (char_in_line as f32) / (chars as f32))
328            }
329        };
330        crate::Rect {
331            x: pos_in_line,
332            y: line.top,
333            w: if char_in_line < (line.end - line.start) {
334                line.width / (line.end - line.start).max(1) as f32
335            } else {
336                1.0
337            },
338            h: line.bottom - line.top,
339        }
340    }
341
342    pub fn get_cursor_rect(&self, offset: usize) -> crate::Rect {
343        let line = self
344            .lines
345            .iter()
346            .find(|l| offset >= l.start && offset <= l.end)
347            .unwrap_or_else(|| {
348                if offset >= self.lines.last().map(|l| l.end).unwrap_or(0) {
349                    self.lines.last().unwrap()
350                } else {
351                    &self.lines[0]
352                }
353            });
354        let char_in_line = offset.saturating_sub(line.start);
355        let chars = (line.end - line.start).max(1);
356        let x = line.left + (line.width * (char_in_line as f32) / (chars as f32));
357        crate::Rect {
358            x,
359            y: line.top,
360            w: 1.0,
361            h: line.bottom - line.top,
362        }
363    }
364
365    pub fn get_offset_for_position(&self, position: (f32, f32)) -> Option<usize> {
366        let line_idx = self.get_line_for_vertical_position(position.1);
367        let line = self.lines.get(line_idx)?;
368        if position.0 <= line.left {
369            return Some(line.start);
370        }
371        if position.0 >= line.right {
372            return Some(line.end);
373        }
374        let fraction = (position.0 - line.left) / line.width.max(1.0);
375        let offset_in_line = ((line.end - line.start) as f32 * fraction).round() as usize;
376        Some((line.start + offset_in_line).min(line.end))
377    }
378
379    pub fn get_word_boundary(&self, _offset: usize) -> super::TextRange {
380        // Stub: layout results don't retain the full text, so real word
381        // boundaries can't be computed yet.
382        super::TextRange::new(0, 0)
383    }
384
385    pub fn get_paragraph_direction(&self, _offset: usize) -> u8 {
386        0 // LTR
387    }
388
389    pub fn get_bidi_run_direction(&self, _offset: usize) -> u8 {
390        0 // LTR
391    }
392}
393
394pub trait OffsetMapping: Debug + Send + Sync + 'static {
395    fn original_to_transformed(&self, offset: usize) -> usize;
396    fn transformed_to_original(&self, offset: usize) -> usize;
397    fn clone_box(&self) -> Box<dyn OffsetMapping>;
398}
399
400/// Identity offset mapping: original and transformed offsets are the same.
401#[derive(Clone, Copy, Debug)]
402pub struct IdentityOffsetMapping;
403
404impl OffsetMapping for IdentityOffsetMapping {
405    fn original_to_transformed(&self, offset: usize) -> usize {
406        offset
407    }
408    fn transformed_to_original(&self, offset: usize) -> usize {
409        offset
410    }
411    fn clone_box(&self) -> Box<dyn OffsetMapping> {
412        Box::new(*self)
413    }
414}
415
416/// Transforms display text without changing the underlying value (e.g. password masking).
417pub trait VisualTransformation: Debug + Send + Sync + 'static {
418    /// Transform the text for display. Takes the original `AnnotatedString` and returns
419    /// the transformed `TransformedText` with an offset mapping.
420    fn filter(&self, text: &AnnotatedString) -> TransformedText;
421}
422
423/// Output of `VisualTransformation::filter`.
424pub struct TransformedText {
425    pub text: AnnotatedString,
426    pub offset_mapping: Box<dyn OffsetMapping>,
427}
428
429impl TransformedText {
430    pub fn new(text: AnnotatedString, offset_mapping: Box<dyn OffsetMapping>) -> Self {
431        TransformedText {
432            text,
433            offset_mapping,
434        }
435    }
436}
437
438impl Clone for TransformedText {
439    fn clone(&self) -> Self {
440        Self {
441            text: self.text.clone(),
442            offset_mapping: self.offset_mapping.clone_box(),
443        }
444    }
445}
446
447impl Debug for TransformedText {
448    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
449        f.debug_struct("TransformedText")
450            .field("text", &self.text.text)
451            .finish()
452    }
453}
454
455#[derive(Clone, Copy, Debug)]
456pub struct IdentityVisualTransformation;
457
458impl VisualTransformation for IdentityVisualTransformation {
459    fn filter(&self, text: &AnnotatedString) -> TransformedText {
460        TransformedText {
461            text: text.clone(),
462            offset_mapping: Box::new(IdentityOffsetMapping),
463        }
464    }
465}
466
467/// A `VisualTransformation` that masks all characters with a given character.
468/// Matches Compose's `PasswordVisualTransformation`.
469#[derive(Clone, Copy, Debug)]
470pub struct PasswordVisualTransformation {
471    pub mask: char,
472}
473
474impl Default for PasswordVisualTransformation {
475    fn default() -> Self {
476        Self { mask: '\u{2022}' }
477    }
478}
479
480/// Char-index offset mapping for masked text: the Nth original char maps to
481/// the Nth mask char (byte offsets differ when widths differ, e.g. `a\U0001F44D`
482/// (5B) masked to `\u{2022}\u{2022}` (6B)). Identity mapping here silently
483/// mis-placed carets mid-mask-char for non-BMP input.
484#[derive(Clone, Debug)]
485pub struct PasswordOffsetMapping {
486    original: Vec<usize>,
487    transformed: Vec<usize>,
488}
489
490impl PasswordOffsetMapping {
491    pub fn new(original: &str, mask: char, masked: &str) -> Self {
492        let mut o = Vec::with_capacity(original.chars().count() + 1);
493        for (i, _) in original.char_indices() {
494            o.push(i);
495        }
496        o.push(original.len());
497        let mut t = vec![0usize];
498        let n = original.chars().count();
499        let mut byte = 0usize;
500        for _ in 0..n {
501            byte += mask.len_utf8();
502            t.push(byte);
503        }
504        if byte != masked.len() {
505            t.clear();
506            t.push(0);
507            let mut b = 0usize;
508            for ch in masked.chars() {
509                b += ch.len_utf8();
510                t.push(b);
511            }
512        }
513        Self {
514            original: o,
515            transformed: t,
516        }
517    }
518
519    fn snap_back(offsets: &[usize], text_len: usize, offset: usize) -> usize {
520        let o = offset.min(text_len);
521        match offsets.binary_search(&o) {
522            Ok(_) => o,
523            Err(i) => offsets.get(i.saturating_sub(1)).copied().unwrap_or(0),
524        }
525    }
526    fn index_of(offsets: &[usize], snapped: usize) -> usize {
527        match offsets.binary_search(&snapped) {
528            Ok(i) => i,
529            Err(i) => i.min(offsets.len().saturating_sub(1)),
530        }
531    }
532}
533
534impl OffsetMapping for PasswordOffsetMapping {
535    fn original_to_transformed(&self, offset: usize) -> usize {
536        let o_text_len = *self.original.last().unwrap_or(&0);
537        let snapped = Self::snap_back(&self.original, o_text_len, offset);
538        let idx = Self::index_of(&self.original, snapped);
539        self.transformed.get(idx).copied().unwrap_or(0)
540    }
541    fn transformed_to_original(&self, offset: usize) -> usize {
542        let t_text_len = *self.transformed.last().unwrap_or(&0);
543        let snapped = Self::snap_back(&self.transformed, t_text_len, offset);
544        let idx = Self::index_of(&self.transformed, snapped);
545        self.original.get(idx).copied().unwrap_or(0)
546    }
547    fn clone_box(&self) -> Box<dyn OffsetMapping> {
548        Box::new(self.clone())
549    }
550}
551
552impl VisualTransformation for PasswordVisualTransformation {
553    fn filter(&self, text: &AnnotatedString) -> TransformedText {
554        let masked_text: String = text.text.chars().map(|_| self.mask).collect();
555        let mapping = PasswordOffsetMapping::new(&text.text, self.mask, &masked_text);
556        TransformedText {
557            text: AnnotatedString::new(masked_text, vec![]),
558            offset_mapping: Box::new(mapping),
559        }
560    }
561}
562
563/// Convert a byte offset in the original text to the corresponding byte offset
564/// in the visually-transformed display text.
565pub fn original_offset_to_display(original: &str, display: &str, original_byte: usize) -> usize {
566    original_offset_to_display_with_mapping(original, display, original_byte, None)
567}
568
569/// Convert a byte offset in the original text to the corresponding byte offset
570/// in the visually-transformed display text, using the provided `OffsetMapping` if available.
571pub fn original_offset_to_display_with_mapping(
572    original: &str,
573    display: &str,
574    original_byte: usize,
575    offset_mapping: Option<&dyn OffsetMapping>,
576) -> usize {
577    if let Some(om) = offset_mapping {
578        om.original_to_transformed(original_byte)
579    } else {
580        let char_idx = original[..original_byte.min(original.len())]
581            .chars()
582            .count();
583        display
584            .char_indices()
585            .nth(char_idx)
586            .map(|(i, _)| i)
587            .unwrap_or(display.len())
588    }
589}
590
591/// Configures automatic capitalization behavior for the keyboard.
592/// Corresponds to Compose's `KeyboardCapitalization`.
593#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
594pub enum KeyboardCapitalization {
595    #[default]
596    Unspecified,
597    None,
598    Characters,
599    Words,
600    Sentences,
601}
602
603/// Text shadow. Offsets and blur are in [`Dp`](crate::units::Dp),
604/// converted to px at the paint boundary.
605#[derive(Clone, Copy, Debug, PartialEq)]
606pub struct Shadow {
607    pub color: Color,
608    /// Horizontal offset in [`Dp`](crate::units::Dp).
609    pub offset_x: crate::units::Dp,
610    /// Vertical offset in [`Dp`](crate::units::Dp).
611    pub offset_y: crate::units::Dp,
612    /// Blur radius in [`Dp`](crate::units::Dp).
613    pub blur_radius: crate::units::Dp,
614}
615
616/// Font synthesis controls whether the font renderer may synthesize
617/// bold, italic, or small-caps variants when the font lacks them.
618#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
619pub enum FontSynthesis {
620    #[default]
621    Unspecified,
622    None,
623    Weight,
624    Style,
625    SmallCaps,
626    All,
627}
628
629/// Baseline shift for subscript/superscript.
630/// Wraps a multiplier applied against font_size for glyph y-offset.
631#[derive(Clone, Copy, Debug, PartialEq)]
632pub struct BaselineShift(pub f32);
633
634#[allow(non_upper_case_globals)]
635impl BaselineShift {
636    /// No baseline shift (zero offset).
637    pub const Unspecified: BaselineShift = BaselineShift(0.0);
638    /// Default superscript offset: shift up by 33.3% of font_size (CSS standard).
639    pub const Superscript: BaselineShift = BaselineShift(-0.333);
640    /// Default subscript offset: shift down by 20% of font_size (CSS standard).
641    pub const Subscript: BaselineShift = BaselineShift(0.2);
642}
643
644impl Default for BaselineShift {
645    fn default() -> Self {
646        BaselineShift::Unspecified
647    }
648}
649
650/// Hyphenation behavior.
651#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
652pub enum Hyphens {
653    #[default]
654    Unspecified,
655    None,
656    Auto,
657}
658
659/// Line break behavior.
660#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
661pub enum LineBreak {
662    #[default]
663    Unspecified,
664    Simple,
665    Heading,
666    Paragraph,
667}
668
669/// First-line and rest-line indent in [`Dp`](crate::units::Dp).
670#[derive(Clone, Copy, Debug, PartialEq)]
671pub struct TextIndent {
672    pub first_line: crate::units::Dp,
673    pub rest_lines: crate::units::Dp,
674}
675
676impl Default for TextIndent {
677    fn default() -> Self {
678        Self {
679            first_line: crate::units::Dp::ZERO,
680            rest_lines: crate::units::Dp::ZERO,
681        }
682    }
683}
684
685/// Path effect applied to a stroked path.
686/// Mirrors Compose's `PathEffect`.
687#[derive(Clone, Debug, PartialEq)]
688pub enum PathEffect {
689    /// Replace sharp corners with rounded arcs of the given radius.
690    Corner {
691        /// Corner radius in em-units.
692        radius: f32,
693    },
694    /// Draw the path as a dashed line.
695    Dash {
696        /// Interleaved on/off lengths in em-units.
697        intervals: Vec<f32>,
698        /// Starting phase offset in em-units.
699        phase: f32,
700    },
701}
702
703/// Draw style for text (fill or stroke).
704#[derive(Clone, Debug, PartialEq, Default)]
705pub enum DrawStyle {
706    #[default]
707    Fill,
708    Stroke {
709        /// Stroke width in em-units (fraction of font size). 0.05 = 5% of em.
710        width: f32,
711        /// Line cap style for stroke endpoints.
712        cap: crate::StrokeCap,
713        /// Line join style for stroke segment joins.
714        join: crate::StrokeJoin,
715        /// Miter limit for miter joins.
716        miter: f32,
717        /// Optional path effect (dash, corner rounding, etc.).
718        path_effect: Option<PathEffect>,
719    },
720}
721
722impl DrawStyle {
723    /// Create a `Stroke` variant with default cap, join, miter, and no path effect.
724    pub const fn stroke(width: f32) -> Self {
725        Self::Stroke {
726            width,
727            cap: crate::StrokeCap::Butt,
728            join: crate::StrokeJoin::Miter,
729            miter: 4.0,
730            path_effect: None,
731        }
732    }
733}
734
735/// Style configuration for text displayed in a text field.
736/// Corresponds to Compose's `TextStyle` (font sizes are `TextUnit` Sp there).
737#[derive(Clone, Debug)]
738pub struct TextStyle {
739    /// Font size in [`Sp`](crate::units::Sp). `Sp::ZERO` = use default (16sp for TextField).
740    pub font_size: crate::units::Sp,
741    /// Text color. None = use theme default.
742    pub color: Option<Color>,
743    /// Font weight. None = NORMAL.
744    pub font_weight: Option<u16>,
745    /// Font family. None = use default (sans-serif).
746    pub font_family: Option<&'static str>,
747    /// Font style. None = Normal.
748    pub font_style: Option<u8>,
749    /// Text alignment. Unspecified = inherit.
750    pub text_align: crate::TextAlign,
751    /// Letter spacing in [`Sp`](crate::units::Sp). `Sp::ZERO` = no extra spacing.
752    pub letter_spacing: crate::units::Sp,
753    /// Line height in [`Sp`](crate::units::Sp). `Sp::ZERO` = default (font_size).
754    pub line_height: crate::units::Sp,
755    /// Text background color. None = transparent.
756    pub background: Option<Color>,
757    /// Text decoration (underline, strikethrough). None = no decoration.
758    pub text_decoration: Option<crate::TextDecoration>,
759    /// Text shadow. None = no shadow.
760    pub shadow: Option<Shadow>,
761    /// Text direction. None = inherit from thread-local default (usually LTR).
762    pub text_direction: Option<crate::TextDirection>,
763    /// Font synthesis policy (synthesize missing bold/italic/small-caps).
764    pub font_synthesis: FontSynthesis,
765    /// Baseline shift (superscript/subscript).
766    pub baseline_shift: BaselineShift,
767    /// Hyphenation behavior.
768    pub hyphens: Hyphens,
769    /// Line break behavior.
770    pub line_break: LineBreak,
771    /// First-line and rest-line indent in dp.
772    pub text_indent: Option<TextIndent>,
773    /// Draw style (fill or stroke).
774    pub draw_style: DrawStyle,
775    /// Text opacity (0.0-1.0). 0.0 = use default (fully opaque).
776    pub alpha: f32,
777    /// Locale hint for text shaping. Empty = use default.
778    pub locale_list: Option<String>,
779    /// OpenType font feature settings (e.g. "liga", "kern").
780    pub font_feature_settings: Option<String>,
781    /// OpenType font variation settings (e.g. "wght 700, opsz 24").
782    pub font_variation_settings: Option<String>,
783}
784
785impl Default for TextStyle {
786    fn default() -> Self {
787        Self {
788            font_size: crate::units::Sp::ZERO,
789            color: None,
790            font_weight: None,
791            font_family: Some("sans-serif"),
792            font_style: None,
793            text_align: crate::TextAlign::Unspecified,
794            letter_spacing: crate::units::Sp::ZERO,
795            line_height: crate::units::Sp::ZERO,
796            background: None,
797            text_decoration: None,
798            shadow: None,
799            text_direction: None,
800            font_synthesis: FontSynthesis::Unspecified,
801            baseline_shift: BaselineShift::Unspecified,
802            hyphens: Hyphens::Unspecified,
803            line_break: LineBreak::Unspecified,
804            text_indent: None,
805            draw_style: DrawStyle::Fill,
806            alpha: 0.0,
807            locale_list: None,
808            font_feature_settings: None,
809            font_variation_settings: None,
810        }
811    }
812}
813
814/// Hints the platform about the type of keyboard to show.
815#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
816pub enum KeyboardType {
817    #[default]
818    Unspecified,
819    Text,
820    Ascii,
821    Number,
822    Phone,
823    Uri,
824    Email,
825    Password,
826    NumberPassword,
827    Decimal,
828    PasswordVisible,
829    PostalAddress,
830    PersonName,
831    EmailSubject,
832    ShortMessage,
833    LongMessage,
834    Filter,
835    Phonetic,
836    DateTime,
837    Date,
838    Time,
839    NumberSigned,
840    DecimalSigned,
841    DecimalPassword,
842    NumberPasswordSigned,
843    DecimalPasswordSigned,
844}
845
846/// The action button on the IME (soft keyboard).
847#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
848pub enum ImeAction {
849    #[default]
850    Unspecified,
851    None,
852    Default,
853    Go,
854    Search,
855    Send,
856    Previous,
857    Next,
858    Done,
859}
860
861/// High-level IME purpose for the platform's soft keyboard.
862///
863/// Unlike [`KeyboardType`], which enumerates the many Compose-style keyboard
864/// layouts, this is the small set of intents platforms can actually react (for NativeA) to
865/// (password fields, e-mail addresses, URLs, phone numbers, plain text).
866/// On the web it also selects the `inputmode` attribute for mobile browsers.
867#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
868pub enum ImePurposeHint {
869    #[default]
870    Normal,
871    Password,
872    Email,
873    Url,
874    Phone,
875    Number,
876}
877
878impl KeyboardType {
879    /// Map this keyboard type to the coarser platform IME purpose.
880    pub fn ime_purpose_hint(self) -> ImePurposeHint {
881        use KeyboardType::*;
882        match self {
883            Password
884            | NumberPassword
885            | DecimalPassword
886            | NumberPasswordSigned
887            | DecimalPasswordSigned => ImePurposeHint::Password,
888            Email | EmailSubject => ImePurposeHint::Email,
889            Uri => ImePurposeHint::Url,
890            Phone => ImePurposeHint::Phone,
891            Number | NumberSigned | Decimal | DecimalSigned => ImePurposeHint::Number,
892            _ => ImePurposeHint::Normal,
893        }
894    }
895}
896
897/// Scope provided to `KeyboardActions` callbacks, allowing fallback to the
898/// platform's default IME action behavior. Corresponds to Compose's `KeyboardActionScope`.
899pub trait KeyboardActionScope {
900    fn default_keyboard_action(&self, action: ImeAction);
901}
902
903/// Callbacks for IME action button presses on the soft keyboard.
904/// Corresponds to Compose's legacy `KeyboardActions`.
905#[derive(Clone, Default)]
906pub struct KeyboardActions {
907    pub on_done: Option<Rc<dyn Fn(&dyn KeyboardActionScope)>>,
908    pub on_go: Option<Rc<dyn Fn(&dyn KeyboardActionScope)>>,
909    pub on_next: Option<Rc<dyn Fn(&dyn KeyboardActionScope)>>,
910    pub on_previous: Option<Rc<dyn Fn(&dyn KeyboardActionScope)>>,
911    pub on_search: Option<Rc<dyn Fn(&dyn KeyboardActionScope)>>,
912    pub on_send: Option<Rc<dyn Fn(&dyn KeyboardActionScope)>>,
913}
914
915impl KeyboardActions {
916    pub fn on_any(f: impl Fn(ImeAction, &dyn KeyboardActionScope) + 'static) -> Self {
917        let f = Rc::new(f);
918        KeyboardActions {
919            on_done: Some({
920                let f = f.clone();
921                Rc::new(move |scope| f(ImeAction::Done, scope))
922            }),
923            on_go: Some({
924                let f = f.clone();
925                Rc::new(move |scope| f(ImeAction::Go, scope))
926            }),
927            on_next: Some({
928                let f = f.clone();
929                Rc::new(move |scope| f(ImeAction::Next, scope))
930            }),
931            on_previous: Some({
932                let f = f.clone();
933                Rc::new(move |scope| f(ImeAction::Previous, scope))
934            }),
935            on_search: Some({
936                let f = f.clone();
937                Rc::new(move |scope| f(ImeAction::Search, scope))
938            }),
939            on_send: Some(Rc::new(move |scope| f(ImeAction::Send, scope))),
940        }
941    }
942}
943
944/// Handles IME action button presses. Single-callback interface used by the
945/// new `BasicTextField(state, ...)` API. Corresponds to Compose's `KeyboardActionHandler`.
946pub trait KeyboardActionHandler: Debug + 'static {
947    fn on_keyboard_action(&self, perform_default: &dyn Fn());
948}
949
950/// A simple `KeyboardActionHandler` that only executes the default behavior.
951#[derive(Clone, Copy, Debug)]
952pub struct DefaultKeyboardActionHandler;
953
954impl KeyboardActionHandler for DefaultKeyboardActionHandler {
955    fn on_keyboard_action(&self, perform_default: &dyn Fn()) {
956        perform_default();
957    }
958}
959
960/// Mutable text buffer used as the scope for `InputTransformation` and
961/// `OutputTransformation`. Corresponds to Compose's `TextFieldBuffer`.
962pub trait TextFieldBuffer {
963    fn text(&self) -> &str;
964    fn set_text(&mut self, text: &str);
965    fn selection(&self) -> TextRange;
966    fn set_selection(&mut self, sel: TextRange);
967    fn length(&self) -> usize;
968    fn replace(&mut self, start: usize, end: usize, text: &str);
969    fn insert(&mut self, index: usize, text: &str);
970    fn delete(&mut self, start: usize, end: usize);
971    fn place_cursor_before_char_at(&mut self, index: usize);
972    fn place_cursor_at_end(&mut self);
973    fn select_all(&mut self);
974    fn revert_all_changes(&mut self);
975    fn original_text(&self) -> &str;
976    fn original_selection(&self) -> TextRange;
977    fn has_selection(&self) -> bool;
978}
979
980/// Limits on the number of visible lines in a text field.
981/// Corresponds to Compose's `TextFieldLineLimits`.
982#[derive(Clone, Copy, Debug, PartialEq, Eq)]
983pub enum TextFieldLineLimits {
984    SingleLine,
985    MultiLine {
986        min_height_in_lines: usize,
987        max_height_in_lines: usize,
988    },
989}
990
991impl TextFieldLineLimits {
992    pub fn default() -> Self {
993        TextFieldLineLimits::MultiLine {
994            min_height_in_lines: 1,
995            max_height_in_lines: usize::MAX,
996        }
997    }
998}
999
1000/// Wraps the inner text field with custom decorations.
1001/// Corresponds to Compose's `TextFieldDecorator`.
1002pub trait TextFieldDecorator: Debug + 'static {
1003    fn decorate(&self, inner: crate::View) -> crate::View;
1004}
1005
1006/// A `TextFieldDecorator` that passes through the inner text field unchanged.
1007#[derive(Clone, Copy, Debug)]
1008pub struct DefaultTextFieldDecorator;
1009
1010impl TextFieldDecorator for DefaultTextFieldDecorator {
1011    fn decorate(&self, inner: crate::View) -> crate::View {
1012        inner
1013    }
1014}
1015
1016/// Transforms user input before it is applied to the text field.
1017/// Corresponds to Compose's `InputTransformation`.
1018pub trait InputTransformation: Debug + 'static {
1019    fn keyboard_options(&self) -> Option<KeyboardOptions> {
1020        None
1021    }
1022    fn transform_input(&self, buffer: &mut dyn TextFieldBuffer);
1023}
1024
1025/// Transforms text output for display.
1026/// Corresponds to Compose's `OutputTransformation`.
1027pub trait OutputTransformation: Debug + 'static {
1028    fn transform_output(&self, buffer: &mut dyn TextFieldBuffer);
1029}
1030
1031/// Internal 1-to-1 codepoint transformation for password obfuscation.
1032/// Corresponds to Compose's `CodepointTransformation`.
1033pub struct CodepointTransformation {
1034    pub transform: Box<dyn Fn(usize, char) -> char>,
1035}
1036
1037impl Clone for CodepointTransformation {
1038    fn clone(&self) -> Self {
1039        // Cannot clone Box<dyn Fn>; this is for internal use only.
1040        // In practice, CodepointTransformation is passed as an Option and
1041        // constructed fresh each time. If clone is needed, wrap the Fn in Rc.
1042        panic!("CodepointTransformation::clone() is not supported -> use Rc instead");
1043    }
1044}
1045
1046impl CodepointTransformation {
1047    pub fn new(transform: impl Fn(usize, char) -> char + 'static) -> Self {
1048        CodepointTransformation {
1049            transform: Box::new(transform),
1050        }
1051    }
1052
1053    pub fn transform(&self, codepoint_index: usize, codepoint: char) -> char {
1054        (self.transform)(codepoint_index, codepoint)
1055    }
1056}
1057
1058/// Input transformation settings: keyboard type, capitalization, and IME action.
1059/// Corresponds to Compose's `KeyboardOptions`.
1060#[derive(Clone, Copy, Debug, PartialEq)]
1061pub struct KeyboardOptions {
1062    pub keyboard_type: KeyboardType,
1063    pub capitalization: KeyboardCapitalization,
1064    pub ime_action: ImeAction,
1065    pub auto_correct_enabled: Option<bool>,
1066    pub show_keyboard_on_focus: Option<bool>,
1067    pub platform_ime_options: Option<&'static str>,
1068    pub hint_locales: Option<&'static str>,
1069}
1070
1071impl KeyboardOptions {
1072    pub const DEFAULT: KeyboardOptions = KeyboardOptions {
1073        keyboard_type: KeyboardType::Text,
1074        capitalization: KeyboardCapitalization::Unspecified,
1075        ime_action: ImeAction::Unspecified,
1076        auto_correct_enabled: None,
1077        show_keyboard_on_focus: None,
1078        platform_ime_options: None,
1079        hint_locales: None,
1080    };
1081
1082    pub const SECURE_TEXT_FIELD: KeyboardOptions = KeyboardOptions {
1083        keyboard_type: KeyboardType::Password,
1084        capitalization: KeyboardCapitalization::Unspecified,
1085        ime_action: ImeAction::Unspecified,
1086        auto_correct_enabled: Some(false),
1087        show_keyboard_on_focus: None,
1088        platform_ime_options: None,
1089        hint_locales: None,
1090    };
1091
1092    pub fn fill_unspecified_values_with(&self, other: Option<&KeyboardOptions>) -> KeyboardOptions {
1093        let other = match other {
1094            Some(o) => o,
1095            None => return *self,
1096        };
1097        KeyboardOptions {
1098            keyboard_type: if self.keyboard_type == KeyboardType::Unspecified {
1099                other.keyboard_type
1100            } else {
1101                self.keyboard_type
1102            },
1103            capitalization: if self.capitalization == KeyboardCapitalization::Unspecified {
1104                other.capitalization
1105            } else {
1106                self.capitalization
1107            },
1108            ime_action: if self.ime_action == ImeAction::Unspecified {
1109                other.ime_action
1110            } else {
1111                self.ime_action
1112            },
1113            auto_correct_enabled: self.auto_correct_enabled.or(other.auto_correct_enabled),
1114            show_keyboard_on_focus: self.show_keyboard_on_focus.or(other.show_keyboard_on_focus),
1115            platform_ime_options: self.platform_ime_options.or(other.platform_ime_options),
1116            hint_locales: self.hint_locales.or(other.hint_locales),
1117        }
1118    }
1119
1120    /// Returns a new [KeyboardOptions] that merges this with [other].
1121    /// [other]'s null or Unspecified values are replaced with this object's values.
1122    /// Corresponds to Compose's `KeyboardOptions.merge()`.
1123    pub fn merge(&self, other: Option<&KeyboardOptions>) -> KeyboardOptions {
1124        other.map_or(*self, |o| o.fill_unspecified_values_with(Some(self)))
1125    }
1126}
1127
1128impl Default for KeyboardOptions {
1129    fn default() -> Self {
1130        Self::DEFAULT
1131    }
1132}
1133
1134#[derive(Debug, Clone, PartialEq)]
1135pub struct SpanStyle {
1136    pub color: Option<Color>,
1137    pub font_size: Option<crate::units::Sp>,
1138    pub font_weight: Option<u16>,
1139    pub font_family: Option<&'static str>,
1140    pub font_style: Option<u8>,
1141    pub text_align: Option<crate::TextAlign>,
1142    pub letter_spacing: Option<crate::units::Sp>,
1143    pub line_height: Option<crate::units::Sp>,
1144    pub background: Option<Color>,
1145    pub text_decoration: Option<crate::TextDecoration>,
1146    pub text_direction: Option<crate::TextDirection>,
1147    pub font_synthesis: Option<FontSynthesis>,
1148    pub baseline_shift: Option<BaselineShift>,
1149    pub hyphens: Option<Hyphens>,
1150    pub line_break: Option<LineBreak>,
1151    pub text_indent: Option<TextIndent>,
1152    pub draw_style: Option<DrawStyle>,
1153    pub alpha: f32,
1154    /// OpenType font variation settings (e.g. "wght 700, opsz 24").
1155    pub font_variation_settings: Option<String>,
1156}
1157
1158impl SpanStyle {
1159    pub const fn default() -> Self {
1160        Self {
1161            color: None,
1162            font_size: None,
1163            font_weight: None,
1164            font_family: None,
1165            font_style: None,
1166            text_align: None,
1167            letter_spacing: None,
1168            line_height: None,
1169            background: None,
1170            text_decoration: None,
1171            text_direction: None,
1172            font_synthesis: None,
1173            baseline_shift: None,
1174            hyphens: None,
1175            line_break: None,
1176            text_indent: None,
1177            draw_style: None,
1178            alpha: 0.0,
1179            font_variation_settings: None,
1180        }
1181    }
1182
1183    pub fn color(mut self, c: Color) -> Self {
1184        self.color = Some(c);
1185        self
1186    }
1187
1188    pub fn font_size(mut self, size: crate::units::Sp) -> Self {
1189        self.font_size = Some(size);
1190        self
1191    }
1192
1193    pub fn text_decoration(mut self, d: TextDecoration) -> Self {
1194        self.text_decoration = Some(d);
1195        self
1196    }
1197
1198    pub fn font_weight(mut self, w: u16) -> Self {
1199        self.font_weight = Some(w);
1200        self
1201    }
1202
1203    pub fn font_style(mut self, s: u8) -> Self {
1204        self.font_style = Some(s);
1205        self
1206    }
1207
1208    pub fn draw_style(mut self, s: DrawStyle) -> Self {
1209        self.draw_style = Some(s);
1210        self
1211    }
1212
1213    pub fn background(mut self, c: Color) -> Self {
1214        self.background = Some(c);
1215        self
1216    }
1217
1218    pub fn baseline_shift(mut self, s: BaselineShift) -> Self {
1219        self.baseline_shift = Some(s);
1220        self
1221    }
1222}
1223
1224impl Default for SpanStyle {
1225    fn default() -> Self {
1226        Self::default()
1227    }
1228}
1229
1230/// A span of text with an associated style.
1231#[derive(Debug, Clone, PartialEq)]
1232pub struct TextSpan {
1233    /// Byte offset start in the original text.
1234    pub start: usize,
1235    /// Byte offset end (exclusive) in the original text.
1236    pub end: usize,
1237    pub style: SpanStyle,
1238    /// URL for clickable links.
1239    pub url: Option<Arc<str>>,
1240}
1241
1242/// Text with multiple styled spans.
1243///
1244/// Analogous to Compose's `AnnotatedString`.
1245#[derive(Debug, Clone, PartialEq)]
1246pub struct AnnotatedString {
1247    pub text: String,
1248    pub spans: Arc<[TextSpan]>,
1249}
1250
1251impl AnnotatedString {
1252    pub fn new(text: impl Into<String>, spans: Vec<TextSpan>) -> Self {
1253        let text = text.into();
1254        Self {
1255            text,
1256            spans: spans.into(),
1257        }
1258    }
1259
1260    pub fn as_str(&self) -> &str {
1261        &self.text
1262    }
1263}
1264
1265impl From<String> for AnnotatedString {
1266    fn from(text: String) -> Self {
1267        Self {
1268            text,
1269            spans: Arc::from([]),
1270        }
1271    }
1272}
1273
1274impl From<&str> for AnnotatedString {
1275    fn from(text: &str) -> Self {
1276        Self {
1277            text: text.to_string(),
1278            spans: Arc::from([]),
1279        }
1280    }
1281}
1282
1283/// Builder for constructing an `AnnotatedString`.
1284#[derive(Default)]
1285pub struct AnnotatedStringBuilder {
1286    text: String,
1287    spans: Vec<TextSpan>,
1288}
1289
1290impl AnnotatedStringBuilder {
1291    pub fn new() -> Self {
1292        Self::default()
1293    }
1294
1295    /// Append plain text (inherits parent style, or default if at top level).
1296    pub fn push(&mut self, text: &str) -> &mut Self {
1297        self.text.push_str(text);
1298        self
1299    }
1300
1301    /// Append text with a specific style.
1302    pub fn push_with_style(&mut self, text: &str, style: SpanStyle) -> &mut Self {
1303        let start = self.text.len();
1304        self.text.push_str(text);
1305        let end = self.text.len();
1306        if start < end {
1307            self.spans.push(TextSpan {
1308                start,
1309                end,
1310                style,
1311                url: None,
1312            });
1313        }
1314        self
1315    }
1316
1317    /// Append text in a specific color.
1318    pub fn push_color(&mut self, text: &str, color: Color) -> &mut Self {
1319        self.push_with_style(text, SpanStyle::default().color(color))
1320    }
1321
1322    /// Append text with a clickable link URL (auto-applies underline + blue color).
1323    pub fn push_link(&mut self, text: &str, url: impl Into<Arc<str>>) -> &mut Self {
1324        let start = self.text.len();
1325        self.text.push_str(text);
1326        let end = self.text.len();
1327        if start < end {
1328            self.spans.push(TextSpan {
1329                start,
1330                end,
1331                style: SpanStyle::default()
1332                    .color(Color::from_rgba(0x15, 0x76, 0xFF, 255))
1333                    .text_decoration(TextDecoration::UNDERLINE),
1334                url: Some(url.into()),
1335            });
1336        }
1337        self
1338    }
1339
1340    /// Apply a style to a range of already-appended text.
1341    pub fn add_style(&mut self, start: usize, end: usize, style: SpanStyle) -> &mut Self {
1342        if start < end && end <= self.text.len() {
1343            self.spans.push(TextSpan {
1344                start,
1345                end,
1346                style,
1347                url: None,
1348            });
1349        }
1350        self
1351    }
1352
1353    pub fn build(&mut self) -> AnnotatedString {
1354        let text = std::mem::take(&mut self.text);
1355        self.spans.sort_by_key(|s| s.start);
1356        // Merge overlapping/adjacent spans with same style
1357        let mut merged: Vec<TextSpan> = Vec::new();
1358        for span in std::mem::take(&mut self.spans) {
1359            if let Some(last) = merged.last_mut()
1360                && last.end == span.start
1361                && last.style == span.style
1362            {
1363                last.end = span.end;
1364                continue;
1365            }
1366            merged.push(span);
1367        }
1368        AnnotatedString {
1369            text,
1370            spans: merged.into(),
1371        }
1372    }
1373}
1374
1375/// Horizontal text alignment.
1376#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)]
1377pub enum TextAlign {
1378    Left,
1379    Right,
1380    Center,
1381    Justify,
1382    Start,
1383    End,
1384    #[default]
1385    Unspecified,
1386}
1387
1388/// Font weight as a numeric value 100-900, matching CSS `font-weight`.
1389#[derive(Clone, Copy, Debug, PartialEq)]
1390pub struct FontWeight(pub u16);
1391
1392impl FontWeight {
1393    pub const THIN: FontWeight = FontWeight(100);
1394    pub const EXTRA_LIGHT: FontWeight = FontWeight(200);
1395    pub const LIGHT: FontWeight = FontWeight(300);
1396    pub const NORMAL: FontWeight = FontWeight(400);
1397    pub const MEDIUM: FontWeight = FontWeight(500);
1398    pub const SEMI_BOLD: FontWeight = FontWeight(600);
1399    pub const BOLD: FontWeight = FontWeight(700);
1400    pub const EXTRA_BOLD: FontWeight = FontWeight(800);
1401    pub const BLACK: FontWeight = FontWeight(900);
1402}
1403
1404impl Default for FontWeight {
1405    fn default() -> Self {
1406        FontWeight::NORMAL
1407    }
1408}
1409
1410/// Font style: normal or italic.
1411#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)]
1412pub enum FontStyle {
1413    #[default]
1414    Normal,
1415    Italic,
1416}
1417
1418/// Text decoration state.
1419#[derive(Clone, Copy, Debug, PartialEq, Default)]
1420pub struct TextDecoration {
1421    pub underline: bool,
1422    pub strikethrough: bool,
1423    pub color: Option<Color>,
1424}
1425
1426impl TextDecoration {
1427    pub const UNDERLINE: TextDecoration = TextDecoration {
1428        underline: true,
1429        strikethrough: false,
1430        color: None,
1431    };
1432    pub const STRIKETHROUGH: TextDecoration = TextDecoration {
1433        underline: false,
1434        strikethrough: true,
1435        color: None,
1436    };
1437}
1438
1439/// Convenience function to build an `AnnotatedString`.
1440pub fn build_annotated_string(b: impl FnOnce(&mut AnnotatedStringBuilder)) -> AnnotatedString {
1441    let mut builder = AnnotatedStringBuilder::new();
1442    b(&mut builder);
1443    builder.build()
1444}