Skip to main content

repose_core/
text.rs

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