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,
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,
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,
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#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
587pub enum BaselineShift {
588    #[default]
589    Unspecified,
590    Superscript,
591    Subscript,
592}
593
594/// Hyphenation behavior.
595#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
596pub enum Hyphens {
597    #[default]
598    Unspecified,
599    None,
600    Auto,
601}
602
603/// Line break behavior.
604#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
605pub enum LineBreak {
606    #[default]
607    Unspecified,
608    Simple,
609    Heading,
610    Paragraph,
611}
612
613/// First-line and rest-line indent in dp.
614#[derive(Clone, Copy, Debug, PartialEq)]
615pub struct TextIndent {
616    pub first_line: f32,
617    pub rest_lines: f32,
618}
619
620impl Default for TextIndent {
621    fn default() -> Self {
622        Self {
623            first_line: 0.0,
624            rest_lines: 0.0,
625        }
626    }
627}
628
629/// Draw style for text (fill or stroke).
630#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
631pub enum DrawStyle {
632    #[default]
633    Fill,
634    Stroke,
635}
636
637/// Style configuration for text displayed in a text field.
638/// Corresponds to Compose's `TextStyle`.
639#[derive(Clone, Debug)]
640pub struct TextStyle {
641    /// Font size in dp. 0 = use default (16dp for TextField).
642    pub font_size: f32,
643    /// Text color. None = use theme default.
644    pub color: Option<Color>,
645    /// Font weight. None = NORMAL.
646    pub font_weight: Option<u16>,
647    /// Font family. None = use default.
648    pub font_family: Option<&'static str>,
649    /// Font style. None = Normal.
650    pub font_style: Option<u8>,
651    /// Text alignment. Unspecified = inherit.
652    pub text_align: crate::TextAlign,
653    /// Letter spacing in dp. 0 = no extra spacing.
654    pub letter_spacing: f32,
655    /// Line height in dp. 0 = default (font_size * 1.2).
656    pub line_height: f32,
657    /// Text background color. None = transparent.
658    pub background: Option<Color>,
659    /// Text decoration (underline, strikethrough). None = no decoration.
660    pub text_decoration: Option<crate::TextDecoration>,
661    /// Text shadow. None = no shadow.
662    pub shadow: Option<Shadow>,
663    /// Text direction. None = inherit from thread-local default (usually LTR).
664    pub text_direction: Option<crate::TextDirection>,
665    /// Font synthesis policy (synthesize missing bold/italic/small-caps).
666    pub font_synthesis: FontSynthesis,
667    /// Baseline shift (superscript/subscript).
668    pub baseline_shift: BaselineShift,
669    /// Hyphenation behavior.
670    pub hyphens: Hyphens,
671    /// Line break behavior.
672    pub line_break: LineBreak,
673    /// First-line and rest-line indent in dp.
674    pub text_indent: Option<TextIndent>,
675    /// Draw style (fill or stroke).
676    pub draw_style: DrawStyle,
677    /// Text opacity (0.0-1.0). 0.0 = use default (fully opaque).
678    pub alpha: f32,
679    /// Locale hint for text shaping. Empty = use default.
680    pub locale_list: Option<String>,
681    /// OpenType font feature settings (e.g. "liga", "kern").
682    pub font_feature_settings: Option<String>,
683}
684
685impl Default for TextStyle {
686    fn default() -> Self {
687        Self {
688            font_size: 0.0,
689            color: None,
690            font_weight: None,
691            font_family: None,
692            font_style: None,
693            text_align: crate::TextAlign::Unspecified,
694            letter_spacing: 0.0,
695            line_height: 0.0,
696            background: None,
697            text_decoration: None,
698            shadow: None,
699            text_direction: None,
700            font_synthesis: FontSynthesis::Unspecified,
701            baseline_shift: BaselineShift::Unspecified,
702            hyphens: Hyphens::Unspecified,
703            line_break: LineBreak::Unspecified,
704            text_indent: None,
705            draw_style: DrawStyle::Fill,
706            alpha: 0.0,
707            locale_list: None,
708            font_feature_settings: None,
709        }
710    }
711}
712
713/// Hints the platform about the type of keyboard to show.
714#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
715pub enum KeyboardType {
716    #[default]
717    Unspecified,
718    Text,
719    Ascii,
720    Number,
721    Phone,
722    Uri,
723    Email,
724    Password,
725    NumberPassword,
726    Decimal,
727    PasswordVisible,
728    PostalAddress,
729    PersonName,
730    EmailSubject,
731    ShortMessage,
732    LongMessage,
733    Filter,
734    Phonetic,
735    DateTime,
736    Date,
737    Time,
738    NumberSigned,
739    DecimalSigned,
740    DecimalPassword,
741    NumberPasswordSigned,
742    DecimalPasswordSigned,
743}
744
745/// The action button on the IME (soft keyboard).
746#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
747pub enum ImeAction {
748    #[default]
749    Unspecified,
750    None,
751    Default,
752    Go,
753    Search,
754    Send,
755    Previous,
756    Next,
757    Done,
758}
759
760/// Scope provided to `KeyboardActions` callbacks, allowing fallback to the
761/// platform's default IME action behavior. Corresponds to Compose's `KeyboardActionScope`.
762pub trait KeyboardActionScope {
763    fn default_keyboard_action(&self, action: ImeAction);
764}
765
766/// Callbacks for IME action button presses on the soft keyboard.
767/// Corresponds to Compose's legacy `KeyboardActions`.
768#[derive(Clone)]
769pub struct KeyboardActions {
770    pub on_done: Option<Rc<dyn Fn(&dyn KeyboardActionScope)>>,
771    pub on_go: Option<Rc<dyn Fn(&dyn KeyboardActionScope)>>,
772    pub on_next: Option<Rc<dyn Fn(&dyn KeyboardActionScope)>>,
773    pub on_previous: Option<Rc<dyn Fn(&dyn KeyboardActionScope)>>,
774    pub on_search: Option<Rc<dyn Fn(&dyn KeyboardActionScope)>>,
775    pub on_send: Option<Rc<dyn Fn(&dyn KeyboardActionScope)>>,
776}
777
778impl KeyboardActions {
779    pub fn on_any(f: impl Fn(ImeAction, &dyn KeyboardActionScope) + 'static) -> Self {
780        let f = Rc::new(f);
781        KeyboardActions {
782            on_done: Some({
783                let f = f.clone();
784                Rc::new(move |scope| f(ImeAction::Done, scope))
785            }),
786            on_go: Some({
787                let f = f.clone();
788                Rc::new(move |scope| f(ImeAction::Go, scope))
789            }),
790            on_next: Some({
791                let f = f.clone();
792                Rc::new(move |scope| f(ImeAction::Next, scope))
793            }),
794            on_previous: Some({
795                let f = f.clone();
796                Rc::new(move |scope| f(ImeAction::Previous, scope))
797            }),
798            on_search: Some({
799                let f = f.clone();
800                Rc::new(move |scope| f(ImeAction::Search, scope))
801            }),
802            on_send: Some({ Rc::new(move |scope| f(ImeAction::Send, scope)) }),
803        }
804    }
805}
806
807impl Default for KeyboardActions {
808    fn default() -> Self {
809        KeyboardActions {
810            on_done: None,
811            on_go: None,
812            on_next: None,
813            on_previous: None,
814            on_search: None,
815            on_send: None,
816        }
817    }
818}
819
820struct NoopKeyboardActionScope;
821impl KeyboardActionScope for NoopKeyboardActionScope {
822    fn default_keyboard_action(&self, _action: ImeAction) {}
823}
824
825/// Handles IME action button presses. Single-callback interface used by the
826/// new `BasicTextField(state, ...)` API. Corresponds to Compose's `KeyboardActionHandler`.
827pub trait KeyboardActionHandler: Debug + 'static {
828    fn on_keyboard_action(&self, perform_default: &dyn Fn());
829}
830
831/// A simple `KeyboardActionHandler` that only executes the default behavior.
832#[derive(Clone, Copy, Debug)]
833pub struct DefaultKeyboardActionHandler;
834
835impl KeyboardActionHandler for DefaultKeyboardActionHandler {
836    fn on_keyboard_action(&self, perform_default: &dyn Fn()) {
837        perform_default();
838    }
839}
840
841/// Mutable text buffer used as the scope for `InputTransformation` and
842/// `OutputTransformation`. Corresponds to Compose's `TextFieldBuffer`.
843pub trait TextFieldBuffer {
844    fn text(&self) -> &str;
845    fn set_text(&mut self, text: &str);
846    fn selection(&self) -> TextRange;
847    fn set_selection(&mut self, sel: TextRange);
848    fn length(&self) -> usize;
849    fn replace(&mut self, start: usize, end: usize, text: &str);
850    fn insert(&mut self, index: usize, text: &str);
851    fn delete(&mut self, start: usize, end: usize);
852    fn place_cursor_before_char_at(&mut self, index: usize);
853    fn place_cursor_at_end(&mut self);
854    fn select_all(&mut self);
855    fn revert_all_changes(&mut self);
856    fn original_text(&self) -> &str;
857    fn original_selection(&self) -> TextRange;
858    fn has_selection(&self) -> bool;
859}
860
861/// Limits on the number of visible lines in a text field.
862/// Corresponds to Compose's `TextFieldLineLimits`.
863#[derive(Clone, Copy, Debug, PartialEq, Eq)]
864pub enum TextFieldLineLimits {
865    SingleLine,
866    MultiLine {
867        min_height_in_lines: usize,
868        max_height_in_lines: usize,
869    },
870}
871
872impl TextFieldLineLimits {
873    pub fn default() -> Self {
874        TextFieldLineLimits::MultiLine {
875            min_height_in_lines: 1,
876            max_height_in_lines: usize::MAX,
877        }
878    }
879}
880
881/// Wraps the inner text field with custom decorations.
882/// Corresponds to Compose's `TextFieldDecorator`.
883pub trait TextFieldDecorator: Debug + 'static {
884    fn decorate(&self, inner: crate::View) -> crate::View;
885}
886
887/// A `TextFieldDecorator` that passes through the inner text field unchanged.
888#[derive(Clone, Copy, Debug)]
889pub struct DefaultTextFieldDecorator;
890
891impl TextFieldDecorator for DefaultTextFieldDecorator {
892    fn decorate(&self, inner: crate::View) -> crate::View {
893        inner
894    }
895}
896
897/// Transforms user input before it is applied to the text field.
898/// Corresponds to Compose's `InputTransformation`.
899pub trait InputTransformation: Debug + 'static {
900    fn keyboard_options(&self) -> Option<KeyboardOptions> {
901        None
902    }
903    fn transform_input(&self, buffer: &mut dyn TextFieldBuffer);
904}
905
906/// Transforms text output for display.
907/// Corresponds to Compose's `OutputTransformation`.
908pub trait OutputTransformation: Debug + 'static {
909    fn transform_output(&self, buffer: &mut dyn TextFieldBuffer);
910}
911
912/// Internal 1-to-1 codepoint transformation for password obfuscation.
913/// Corresponds to Compose's `CodepointTransformation`.
914pub struct CodepointTransformation {
915    pub transform: Box<dyn Fn(usize, char) -> char>,
916}
917
918impl Clone for CodepointTransformation {
919    fn clone(&self) -> Self {
920        // Cannot clone Box<dyn Fn>; this is for internal use only.
921        // In practice, CodepointTransformation is passed as an Option and
922        // constructed fresh each time. If clone is needed, wrap the Fn in Rc.
923        panic!("CodepointTransformation::clone() is not supported -> use Rc instead");
924    }
925}
926
927impl CodepointTransformation {
928    pub fn new(transform: impl Fn(usize, char) -> char + 'static) -> Self {
929        CodepointTransformation {
930            transform: Box::new(transform),
931        }
932    }
933
934    pub fn transform(&self, codepoint_index: usize, codepoint: char) -> char {
935        (self.transform)(codepoint_index, codepoint)
936    }
937}
938
939/// Input transformation settings: keyboard type, capitalization, and IME action.
940/// Corresponds to Compose's `KeyboardOptions`.
941#[derive(Clone, Copy, Debug, PartialEq)]
942pub struct KeyboardOptions {
943    pub keyboard_type: KeyboardType,
944    pub capitalization: KeyboardCapitalization,
945    pub ime_action: ImeAction,
946    pub auto_correct_enabled: Option<bool>,
947    pub show_keyboard_on_focus: Option<bool>,
948    pub platform_ime_options: Option<&'static str>,
949    pub hint_locales: Option<&'static str>,
950}
951
952impl KeyboardOptions {
953    pub const DEFAULT: KeyboardOptions = KeyboardOptions {
954        keyboard_type: KeyboardType::Text,
955        capitalization: KeyboardCapitalization::Unspecified,
956        ime_action: ImeAction::Unspecified,
957        auto_correct_enabled: None,
958        show_keyboard_on_focus: None,
959        platform_ime_options: None,
960        hint_locales: None,
961    };
962
963    pub const SECURE_TEXT_FIELD: KeyboardOptions = KeyboardOptions {
964        keyboard_type: KeyboardType::Password,
965        capitalization: KeyboardCapitalization::Unspecified,
966        ime_action: ImeAction::Unspecified,
967        auto_correct_enabled: Some(false),
968        show_keyboard_on_focus: None,
969        platform_ime_options: None,
970        hint_locales: None,
971    };
972
973    pub fn fill_unspecified_values_with(&self, other: Option<&KeyboardOptions>) -> KeyboardOptions {
974        let other = match other {
975            Some(o) => o,
976            None => return *self,
977        };
978        KeyboardOptions {
979            keyboard_type: if self.keyboard_type == KeyboardType::Unspecified {
980                other.keyboard_type
981            } else {
982                self.keyboard_type
983            },
984            capitalization: if self.capitalization == KeyboardCapitalization::Unspecified {
985                other.capitalization
986            } else {
987                self.capitalization
988            },
989            ime_action: if self.ime_action == ImeAction::Unspecified {
990                other.ime_action
991            } else {
992                self.ime_action
993            },
994            auto_correct_enabled: self.auto_correct_enabled.or(other.auto_correct_enabled),
995            show_keyboard_on_focus: self.show_keyboard_on_focus.or(other.show_keyboard_on_focus),
996            platform_ime_options: self.platform_ime_options.or(other.platform_ime_options),
997            hint_locales: self.hint_locales.or(other.hint_locales),
998        }
999    }
1000
1001    /// Returns a new [KeyboardOptions] that merges this with [other].
1002    /// [other]'s null or Unspecified values are replaced with this object's values.
1003    /// Corresponds to Compose's `KeyboardOptions.merge()`.
1004    pub fn merge(&self, other: Option<&KeyboardOptions>) -> KeyboardOptions {
1005        other.map_or(*self, |o| o.fill_unspecified_values_with(Some(self)))
1006    }
1007}
1008
1009impl Default for KeyboardOptions {
1010    fn default() -> Self {
1011        Self::DEFAULT
1012    }
1013}
1014
1015#[derive(Debug, Clone, Copy, PartialEq)]
1016pub struct SpanStyle {
1017    pub color: Option<Color>,
1018    pub font_size: Option<f32>,
1019    pub font_weight: Option<u16>,
1020    pub font_family: Option<&'static str>,
1021    pub font_style: Option<u8>,
1022    pub text_align: Option<crate::TextAlign>,
1023    pub letter_spacing: Option<f32>,
1024    pub line_height: Option<f32>,
1025    pub background: Option<Color>,
1026    pub text_decoration: Option<crate::TextDecoration>,
1027    pub text_direction: Option<crate::TextDirection>,
1028    pub font_synthesis: Option<FontSynthesis>,
1029    pub baseline_shift: Option<BaselineShift>,
1030    pub hyphens: Option<Hyphens>,
1031    pub line_break: Option<LineBreak>,
1032    pub text_indent: Option<TextIndent>,
1033    pub draw_style: Option<DrawStyle>,
1034    pub alpha: f32,
1035}
1036
1037impl SpanStyle {
1038    pub const fn default() -> Self {
1039        Self {
1040            color: None,
1041            font_size: None,
1042            font_weight: None,
1043            font_family: None,
1044            font_style: None,
1045            text_align: None,
1046            letter_spacing: None,
1047            line_height: None,
1048            background: None,
1049            text_decoration: None,
1050            text_direction: None,
1051            font_synthesis: None,
1052            baseline_shift: None,
1053            hyphens: None,
1054            line_break: None,
1055            text_indent: None,
1056            draw_style: None,
1057            alpha: 0.0,
1058        }
1059    }
1060
1061    pub fn color(mut self, c: Color) -> Self {
1062        self.color = Some(c);
1063        self
1064    }
1065
1066    pub fn font_size(mut self, px: f32) -> Self {
1067        self.font_size = Some(px);
1068        self
1069    }
1070
1071    pub fn text_decoration(mut self, d: TextDecoration) -> Self {
1072        self.text_decoration = Some(d);
1073        self
1074    }
1075
1076    pub fn font_weight(mut self, w: u16) -> Self {
1077        self.font_weight = Some(w);
1078        self
1079    }
1080
1081    pub fn font_style(mut self, s: u8) -> Self {
1082        self.font_style = Some(s);
1083        self
1084    }
1085
1086    pub fn background(mut self, c: Color) -> Self {
1087        self.background = Some(c);
1088        self
1089    }
1090}
1091
1092impl Default for SpanStyle {
1093    fn default() -> Self {
1094        Self::default()
1095    }
1096}
1097
1098/// A span of text with an associated style.
1099#[derive(Debug, Clone, PartialEq)]
1100pub struct TextSpan {
1101    /// Byte offset start in the original text.
1102    pub start: usize,
1103    /// Byte offset end (exclusive) in the original text.
1104    pub end: usize,
1105    pub style: SpanStyle,
1106    /// URL for clickable links.
1107    pub url: Option<Arc<str>>,
1108}
1109
1110/// Text with multiple styled spans.
1111///
1112/// Analogous to Compose's `AnnotatedString`.
1113#[derive(Debug, Clone, PartialEq)]
1114pub struct AnnotatedString {
1115    pub text: String,
1116    pub spans: Arc<[TextSpan]>,
1117}
1118
1119impl AnnotatedString {
1120    pub fn new(text: impl Into<String>, spans: Vec<TextSpan>) -> Self {
1121        let text = text.into();
1122        Self {
1123            text,
1124            spans: spans.into(),
1125        }
1126    }
1127
1128    pub fn as_str(&self) -> &str {
1129        &self.text
1130    }
1131}
1132
1133impl From<String> for AnnotatedString {
1134    fn from(text: String) -> Self {
1135        Self {
1136            text,
1137            spans: Arc::from([]),
1138        }
1139    }
1140}
1141
1142impl From<&str> for AnnotatedString {
1143    fn from(text: &str) -> Self {
1144        Self {
1145            text: text.to_string(),
1146            spans: Arc::from([]),
1147        }
1148    }
1149}
1150
1151/// Builder for constructing an `AnnotatedString`.
1152#[derive(Default)]
1153pub struct AnnotatedStringBuilder {
1154    text: String,
1155    spans: Vec<TextSpan>,
1156}
1157
1158impl AnnotatedStringBuilder {
1159    pub fn new() -> Self {
1160        Self::default()
1161    }
1162
1163    /// Append plain text (inherits parent style, or default if at top level).
1164    pub fn push(&mut self, text: &str) -> &mut Self {
1165        self.text.push_str(text);
1166        self
1167    }
1168
1169    /// Append text with a specific style.
1170    pub fn push_with_style(&mut self, text: &str, style: SpanStyle) -> &mut Self {
1171        let start = self.text.len();
1172        self.text.push_str(text);
1173        let end = self.text.len();
1174        if start < end {
1175            self.spans.push(TextSpan {
1176                start,
1177                end,
1178                style,
1179                url: None,
1180            });
1181        }
1182        self
1183    }
1184
1185    /// Append text in a specific color.
1186    pub fn push_color(&mut self, text: &str, color: Color) -> &mut Self {
1187        self.push_with_style(text, SpanStyle::default().color(color))
1188    }
1189
1190    /// Append text with a clickable link URL (auto-applies underline + blue color).
1191    pub fn push_link(&mut self, text: &str, url: impl Into<Arc<str>>) -> &mut Self {
1192        let start = self.text.len();
1193        self.text.push_str(text);
1194        let end = self.text.len();
1195        if start < end {
1196            self.spans.push(TextSpan {
1197                start,
1198                end,
1199                style: SpanStyle::default()
1200                    .color(Color::from_rgba(0x15, 0x76, 0xFF, 255))
1201                    .text_decoration(TextDecoration::UNDERLINE),
1202                url: Some(url.into()),
1203            });
1204        }
1205        self
1206    }
1207
1208    /// Apply a style to a range of already-appended text.
1209    pub fn add_style(&mut self, start: usize, end: usize, style: SpanStyle) -> &mut Self {
1210        if start < end && end <= self.text.len() {
1211            self.spans.push(TextSpan {
1212                start,
1213                end,
1214                style,
1215                url: None,
1216            });
1217        }
1218        self
1219    }
1220
1221    pub fn build(&mut self) -> AnnotatedString {
1222        let text = std::mem::take(&mut self.text);
1223        self.spans.sort_by_key(|s| s.start);
1224        // Merge overlapping/adjacent spans with same style
1225        let mut merged: Vec<TextSpan> = Vec::new();
1226        for span in std::mem::take(&mut self.spans) {
1227            if let Some(last) = merged.last_mut()
1228                && last.end == span.start
1229                && last.style == span.style
1230            {
1231                last.end = span.end;
1232                continue;
1233            }
1234            merged.push(span);
1235        }
1236        AnnotatedString {
1237            text,
1238            spans: merged.into(),
1239        }
1240    }
1241}
1242
1243/// Horizontal text alignment.
1244#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
1245pub enum TextAlign {
1246    Left,
1247    Right,
1248    Center,
1249    Justify,
1250    Start,
1251    End,
1252    Unspecified,
1253}
1254
1255impl Default for TextAlign {
1256    fn default() -> Self {
1257        TextAlign::Unspecified
1258    }
1259}
1260
1261/// Font weight as a numeric value 100-900, matching CSS `font-weight`.
1262#[derive(Clone, Copy, Debug, PartialEq)]
1263pub struct FontWeight(pub u16);
1264
1265impl FontWeight {
1266    pub const THIN: FontWeight = FontWeight(100);
1267    pub const EXTRA_LIGHT: FontWeight = FontWeight(200);
1268    pub const LIGHT: FontWeight = FontWeight(300);
1269    pub const NORMAL: FontWeight = FontWeight(400);
1270    pub const MEDIUM: FontWeight = FontWeight(500);
1271    pub const SEMI_BOLD: FontWeight = FontWeight(600);
1272    pub const BOLD: FontWeight = FontWeight(700);
1273    pub const EXTRA_BOLD: FontWeight = FontWeight(800);
1274    pub const BLACK: FontWeight = FontWeight(900);
1275}
1276
1277impl Default for FontWeight {
1278    fn default() -> Self {
1279        FontWeight::NORMAL
1280    }
1281}
1282
1283/// Font style: normal or italic.
1284#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
1285pub enum FontStyle {
1286    Normal,
1287    Italic,
1288}
1289
1290impl Default for FontStyle {
1291    fn default() -> Self {
1292        FontStyle::Normal
1293    }
1294}
1295
1296/// Text decoration state.
1297#[derive(Clone, Copy, Debug, PartialEq)]
1298pub struct TextDecoration {
1299    pub underline: bool,
1300    pub strikethrough: bool,
1301    pub color: Option<Color>,
1302}
1303
1304impl TextDecoration {
1305    pub const UNDERLINE: TextDecoration = TextDecoration {
1306        underline: true,
1307        strikethrough: false,
1308        color: None,
1309    };
1310    pub const STRIKETHROUGH: TextDecoration = TextDecoration {
1311        underline: false,
1312        strikethrough: true,
1313        color: None,
1314    };
1315}
1316
1317impl Default for TextDecoration {
1318    fn default() -> Self {
1319        Self {
1320            underline: false,
1321            strikethrough: false,
1322            color: None,
1323        }
1324    }
1325}
1326
1327/// Convenience function to build an `AnnotatedString`.
1328pub fn build_annotated_string(b: impl FnOnce(&mut AnnotatedStringBuilder)) -> AnnotatedString {
1329    let mut builder = AnnotatedStringBuilder::new();
1330    b(&mut builder);
1331    builder.build()
1332}
1333
1334