Skip to main content

repose_core/
text.rs

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