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