Skip to main content

repose_core/
text.rs

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