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