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