tessera_ui_basic_components/
text.rs

1//! A component for rendering single-style text.
2//!
3//! ## Usage
4//!
5//! Use to display labels, headings, or other static or dynamic text content.
6use derive_builder::Builder;
7use tessera_ui::{Color, ComputedData, DimensionValue, Dp, Px, accesskit::Role, tessera};
8
9use crate::pipelines::text::{
10    command::{TextCommand, TextConstraint},
11    pipeline::TextData,
12};
13
14pub use crate::pipelines::text::pipeline::{read_font_system, write_font_system};
15
16/// Configuration arguments for the `text` component.
17#[derive(Debug, Builder, Clone)]
18#[builder(pattern = "owned")]
19pub struct TextArgs {
20    /// The text content to be rendered.
21    #[builder(setter(into))]
22    pub text: String,
23
24    /// The color of the text.
25    #[builder(default = "crate::material_color::global_material_scheme().on_surface")]
26    pub color: Color,
27
28    /// The font size in density-independent pixels (dp).
29    #[builder(default = "Dp(25.0)")]
30    pub size: Dp,
31
32    /// Optional override for line height in density-independent pixels (dp).
33    #[builder(default, setter(strip_option))]
34    pub line_height: Option<Dp>,
35
36    /// Optional label announced by assistive technologies. Defaults to the text content.
37    #[builder(default, setter(strip_option, into))]
38    pub accessibility_label: Option<String>,
39
40    /// Optional description announced by assistive technologies.
41    #[builder(default, setter(strip_option, into))]
42    pub accessibility_description: Option<String>,
43}
44
45impl Default for TextArgs {
46    fn default() -> Self {
47        TextArgsBuilder::default()
48            .text("")
49            .build()
50            .expect("builder construction failed")
51    }
52}
53
54impl From<String> for TextArgs {
55    fn from(val: String) -> Self {
56        TextArgsBuilder::default()
57            .text(val)
58            .build()
59            .expect("builder construction failed")
60    }
61}
62
63impl From<&str> for TextArgs {
64    fn from(val: &str) -> Self {
65        TextArgsBuilder::default()
66            .text(val.to_string())
67            .build()
68            .expect("builder construction failed")
69    }
70}
71
72/// # text
73///
74/// Renders a block of text with a single, uniform style.
75///
76/// ## Usage
77///
78/// Display simple text content. For more complex styling or editing, see other components.
79///
80/// ## Parameters
81///
82/// - `args` — configures the text content and styling; see [`TextArgs`]. Can be converted from a `String` or `&str`.
83///
84/// ## Examples
85///
86/// ```
87/// use tessera_ui_basic_components::text::{text, TextArgsBuilder};
88/// use tessera_ui::{Color, Dp};
89///
90/// // Simple text from a string literal
91/// text("Hello, world!");
92///
93/// // Styled text using the builder
94/// text(
95///     TextArgsBuilder::default()
96///         .text("Styled Text")
97///         .color(Color::new(0.2, 0.5, 0.8, 1.0))
98///         .size(Dp(32.0))
99///         .build()
100///         .unwrap(),
101/// );
102/// ```
103#[tessera]
104pub fn text(args: impl Into<TextArgs>) {
105    let text_args: TextArgs = args.into();
106    let accessibility_label = text_args.accessibility_label.clone();
107    let accessibility_description = text_args.accessibility_description.clone();
108    let text_for_accessibility = text_args.text.clone();
109
110    input_handler(Box::new(move |input| {
111        let mut builder = input.accessibility().role(Role::Label);
112
113        if let Some(label) = accessibility_label.as_ref() {
114            builder = builder.label(label.clone());
115        } else if !text_for_accessibility.is_empty() {
116            builder = builder.label(text_for_accessibility.clone());
117        }
118
119        if let Some(description) = accessibility_description.as_ref() {
120            builder = builder.description(description.clone());
121        }
122
123        builder.commit();
124    }));
125    measure(Box::new(move |input| {
126        let max_width: Option<Px> = match input.parent_constraint.width {
127            DimensionValue::Fixed(w) => Some(w),
128            DimensionValue::Wrap { max, .. } => max, // Use max from Wrap
129            DimensionValue::Fill { max, .. } => max, // Use max from Fill
130        };
131
132        let max_height: Option<Px> = match input.parent_constraint.height {
133            DimensionValue::Fixed(h) => Some(h),
134            DimensionValue::Wrap { max, .. } => max, // Use max from Wrap
135            DimensionValue::Fill { max, .. } => max, // Use max from Fill
136        };
137
138        let line_height = text_args.line_height.unwrap_or(Dp(text_args.size.0 * 1.2));
139
140        let text_data = TextData::new(
141            text_args.text.clone(),
142            text_args.color,
143            text_args.size.to_pixels_f32(),
144            line_height.to_pixels_f32(),
145            TextConstraint {
146                max_width: max_width.map(|px| px.to_f32()),
147                max_height: max_height.map(|px| px.to_f32()),
148            },
149        );
150
151        let size = text_data.size;
152        let drawable = TextCommand { data: text_data };
153
154        // Use the new unified command system to add the text rendering command
155        input.metadata_mut().push_draw_command(drawable);
156
157        Ok(ComputedData {
158            width: size[0].into(),
159            height: size[1].into(),
160        })
161    }));
162}