Skip to main content

repose_core/
text.rs

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