tessera_ui_basic_components/
text.rs

1//! Text component module for Tessera UI.
2//!
3//! This module provides the [`text`] component and its configuration types for rendering styled, single-style text within the Tessera UI framework.
4//! It is designed for displaying static or dynamic text content with customizable color, font size, and line height, supporting Unicode and DPI scaling.
5//!
6//! Typical use cases include labels, headings, captions, and any UI element requiring straightforward text rendering.
7//! The component is stateless and integrates with Tessera's layout and rendering systems, automatically adapting to parent constraints and device pixel density.
8//!
9//! The builder-pattern [`TextArgs`] struct allows ergonomic and flexible configuration, with sensible defaults for most properties.
10//! Conversions from `String` and `&str` are supported for convenience.
11//!
12//! # Examples
13//!
14//! Basic usage:
15//!
16//! ```
17//! use tessera_ui_basic_components::text::{text, TextArgs};
18//! text("Hello, Tessera!");
19//! ```
20//!
21//! Custom styling:
22//! ```
23//! use tessera_ui_basic_components::text::{text, TextArgsBuilder};
24//! use tessera_ui::{Color, Dp};
25//! let args = TextArgsBuilder::default()
26//!     .text("Styled".to_string())
27//!     .color(Color::from_rgb(0.2, 0.4, 0.8))
28//!     .size(Dp(32.0))
29//!     .build()
30//!     .unwrap();
31//! text(args);
32//! ```
33use derive_builder::Builder;
34use tessera_ui::{Color, ComputedData, DimensionValue, Dp, Px, tessera};
35
36use crate::pipelines::{TextCommand, TextConstraint, TextData};
37
38/// Configuration arguments for the `text` component.
39///
40/// `TextArgs` defines the visual properties and content for rendering text in the Tessera UI framework.
41/// It uses the builder pattern for convenient construction and provides sensible defaults for all styling properties.
42///
43/// # Fields
44///
45/// - `text`: The string content to be displayed
46/// - `color`: Text color (defaults to black)
47/// - `size`: Font size in density-independent pixels (defaults to 25.0 dp)
48/// - `line_height`: Optional line height override (defaults to 1.2 × font size)
49///
50/// # Builder Pattern
51///
52/// This struct uses the `derive_builder` crate to provide a fluent builder API. All fields except `text`
53/// have sensible defaults, making it easy to create text with minimal configuration.
54///
55/// # Examples
56///
57/// ## Basic text with defaults
58/// ```
59/// use tessera_ui_basic_components::text::{TextArgs, TextArgsBuilder};
60///
61/// let args = TextArgsBuilder::default()
62///     .text("Hello, World!".to_string())
63///     .build()
64///     .unwrap();
65/// // Uses: black color, 25.0 dp size, 30.0 dp line height (1.2 × size)
66/// ```
67///
68/// ## Customized text styling
69/// ```
70/// use tessera_ui_basic_components::text::{TextArgs, TextArgsBuilder};
71/// use tessera_ui::{Color, Dp};
72///
73/// let args = TextArgsBuilder::default()
74///     .text("Styled Text".to_string())
75///     .color(Color::from_rgb(0.2, 0.4, 0.8)) // Blue color
76///     .size(Dp(32.0))                        // Larger font
77///     .line_height(Dp(40.0))                 // Custom line height
78///     .build()
79///     .unwrap();
80/// ```
81///
82/// ## Using automatic line height calculation
83/// ```
84/// use tessera_ui_basic_components::text::{TextArgs, TextArgsBuilder};
85/// use tessera_ui::Dp;
86///
87/// let args = TextArgsBuilder::default()
88///     .text("Auto Line Height".to_string())
89///     .size(Dp(50.0))
90///     // line_height will automatically be Dp(60.0) (1.2 × 50.0)
91///     .build()
92///     .unwrap();
93/// ```
94///
95/// ## Converting from string types
96/// ```
97/// use tessera_ui_basic_components::text::TextArgs;
98///
99/// // From String
100/// let args1: TextArgs = "Hello".to_string().into();
101///
102/// // From &str
103/// let args2: TextArgs = "World".into();
104/// ```
105#[derive(Debug, Default, Builder, Clone)]
106#[builder(pattern = "owned")]
107pub struct TextArgs {
108    /// The text content to be rendered.
109    ///
110    /// This is the actual string that will be displayed on screen. It can contain
111    /// Unicode characters and will be rendered using the specified font properties.
112    #[builder(setter(into))]
113    pub text: String,
114
115    /// The color of the text.
116    ///
117    /// Defaults to `Color::BLACK` if not specified. The color is applied uniformly
118    /// to all characters in the text string.
119    #[builder(default = "Color::BLACK")]
120    pub color: Color,
121
122    /// The font size in density-independent pixels (dp).
123    ///
124    /// Defaults to `Dp(25.0)` if not specified. This size is automatically scaled
125    /// based on the device's pixel density to ensure consistent visual appearance
126    /// across different screen densities.
127    #[builder(default = "Dp(25.0)")]
128    pub size: Dp,
129
130    /// Optional override for line height in density-independent pixels (dp).
131    ///
132    /// If not specified (None), the line height will automatically be calculated as
133    /// 1.2 times the font size, which provides good readability for most text.
134    ///
135    /// Set this to a specific value if you need precise control over line spacing,
136    /// such as for dense layouts or specific design requirements.
137    ///
138    /// # Example
139    /// ```
140    /// use tessera_ui_basic_components::text::TextArgsBuilder;
141    /// use tessera_ui::Dp;
142    ///
143    /// // Automatic line height (1.2 × size)
144    /// let auto = TextArgsBuilder::default()
145    ///     .text("Auto spacing".to_string())
146    ///     .size(Dp(20.0))  // line_height will be Dp(24.0)
147    ///     .build()
148    ///     .unwrap();
149    ///
150    /// // Custom line height
151    /// let custom = TextArgsBuilder::default()
152    ///     .text("Custom spacing".to_string())
153    ///     .size(Dp(20.0))
154    ///     .line_height(Dp(30.0))  // Explicit line height
155    ///     .build()
156    ///     .unwrap();
157    /// ```
158    #[builder(default, setter(strip_option))]
159    pub line_height: Option<Dp>,
160}
161
162/// Converts a [`String`] into [`TextArgs`] using the builder pattern.
163///
164/// This allows convenient usage of string literals or owned strings as text arguments
165/// for the [`text`] component.
166///
167/// # Example
168/// ```
169/// use tessera_ui_basic_components::text::TextArgs;
170///
171/// let args: TextArgs = "Hello, Tessera!".to_string().into();
172/// ```
173impl From<String> for TextArgs {
174    fn from(val: String) -> Self {
175        TextArgsBuilder::default().text(val).build().unwrap()
176    }
177}
178
179/// Converts a string slice (`&str`) into [`TextArgs`] using the builder pattern.
180///
181/// This enables ergonomic conversion from string literals for the [`text`] component.
182///
183/// # Example
184/// ```
185/// use tessera_ui_basic_components::text::TextArgs;
186///
187/// let args: TextArgs = "Quick text".into();
188/// ```
189impl From<&str> for TextArgs {
190    fn from(val: &str) -> Self {
191        TextArgsBuilder::default()
192            .text(val.to_string())
193            .build()
194            .unwrap()
195    }
196}
197
198/// Basic text component.
199///
200/// # Example
201/// ```
202/// use tessera_ui_basic_components::text::{text, TextArgs, TextArgsBuilder};
203/// use tessera_ui::Dp;
204/// // a simple hello world text, in black
205/// let args = TextArgsBuilder::default()
206///     .text("Hello, World!".to_string())
207///     .size(Dp(50.0)) // Example using Dp
208///     // line_height will be Dp(60.0) (1.2 * size) by default
209///     .build()
210///     .unwrap();
211/// text(args);
212/// ```
213#[tessera]
214pub fn text(args: impl Into<TextArgs>) {
215    let text_args: TextArgs = args.into();
216    measure(Box::new(move |input| {
217        let max_width: Option<Px> = match input.parent_constraint.width {
218            DimensionValue::Fixed(w) => Some(w),
219            DimensionValue::Wrap { max, .. } => max, // Use max from Wrap
220            DimensionValue::Fill { max, .. } => max, // Use max from Fill
221        };
222
223        let max_height: Option<Px> = match input.parent_constraint.height {
224            DimensionValue::Fixed(h) => Some(h),
225            DimensionValue::Wrap { max, .. } => max, // Use max from Wrap
226            DimensionValue::Fill { max, .. } => max, // Use max from Fill
227        };
228
229        let line_height = text_args.line_height.unwrap_or(Dp(text_args.size.0 * 1.2));
230
231        let text_data = TextData::new(
232            text_args.text.clone(),
233            text_args.color,
234            text_args.size.to_pixels_f32(),
235            line_height.to_pixels_f32(),
236            TextConstraint {
237                max_width: max_width.map(|px| px.to_f32()),
238                max_height: max_height.map(|px| px.to_f32()),
239            },
240        );
241
242        let size = text_data.size;
243        let drawable = TextCommand { data: text_data };
244
245        // Use the new unified command system to add the text rendering command
246        input.metadata_mut().push_draw_command(drawable);
247
248        Ok(ComputedData {
249            width: size[0].into(),
250            height: size[1].into(),
251        })
252    }));
253}