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//! use tessera_ui_basic_components::text::{text, TextArgs};
17//! text("Hello, Tessera!");
18//! ```
19//!
20//! Custom styling:
21//! ```
22//! use tessera_ui_basic_components::text::{text, TextArgsBuilder};
23//! use tessera_ui::{Color, Dp};
24//! let args = TextArgsBuilder::default()
25//! .text("Styled".to_string())
26//! .color(Color::from_rgb(0.2, 0.4, 0.8))
27//! .size(Dp(32.0))
28//! .build()
29//! .unwrap();
30//! text(args);
31//! ```
32use derive_builder::Builder;
33use tessera_ui::{Color, ComputedData, DimensionValue, Dp, Px};
34use tessera_ui_macros::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 pub text: String,
113
114 /// The color of the text.
115 ///
116 /// Defaults to `Color::BLACK` if not specified. The color is applied uniformly
117 /// to all characters in the text string.
118 #[builder(default = "Color::BLACK")]
119 pub color: Color,
120
121 /// The font size in density-independent pixels (dp).
122 ///
123 /// Defaults to `Dp(25.0)` if not specified. This size is automatically scaled
124 /// based on the device's pixel density to ensure consistent visual appearance
125 /// across different screen densities.
126 #[builder(default = "Dp(25.0)")]
127 pub size: Dp,
128
129 /// Optional override for line height in density-independent pixels (dp).
130 ///
131 /// If not specified (None), the line height will automatically be calculated as
132 /// 1.2 times the font size, which provides good readability for most text.
133 ///
134 /// Set this to a specific value if you need precise control over line spacing,
135 /// such as for dense layouts or specific design requirements.
136 ///
137 /// # Example
138 /// ```
139 /// use tessera_ui_basic_components::text::TextArgsBuilder;
140 /// use tessera_ui::Dp;
141 ///
142 /// // Automatic line height (1.2 × size)
143 /// let auto = TextArgsBuilder::default()
144 /// .text("Auto spacing".to_string())
145 /// .size(Dp(20.0)) // line_height will be Dp(24.0)
146 /// .build()
147 /// .unwrap();
148 ///
149 /// // Custom line height
150 /// let custom = TextArgsBuilder::default()
151 /// .text("Custom spacing".to_string())
152 /// .size(Dp(20.0))
153 /// .line_height(Dp(30.0)) // Explicit line height
154 /// .build()
155 /// .unwrap();
156 /// ```
157 #[builder(default, setter(strip_option))]
158 pub line_height: Option<Dp>,
159}
160
161/// Converts a [`String`] into [`TextArgs`] using the builder pattern.
162///
163/// This allows convenient usage of string literals or owned strings as text arguments
164/// for the [`text`] component.
165///
166/// # Example
167/// ```
168/// use tessera_ui_basic_components::text::TextArgs;
169///
170/// let args: TextArgs = "Hello, Tessera!".to_string().into();
171/// ```
172impl From<String> for TextArgs {
173 fn from(val: String) -> Self {
174 TextArgsBuilder::default().text(val).build().unwrap()
175 }
176}
177
178/// Converts a string slice (`&str`) into [`TextArgs`] using the builder pattern.
179///
180/// This enables ergonomic conversion from string literals for the [`text`] component.
181///
182/// # Example
183/// ```
184/// use tessera_ui_basic_components::text::TextArgs;
185///
186/// let args: TextArgs = "Quick text".into();
187/// ```
188impl From<&str> for TextArgs {
189 fn from(val: &str) -> Self {
190 TextArgsBuilder::default()
191 .text(val.to_string())
192 .build()
193 .unwrap()
194 }
195}
196
197/// Basic text component.
198///
199/// # Example
200/// ```
201/// use tessera_ui_basic_components::text::{text, TextArgs, TextArgsBuilder};
202/// use tessera_ui::Dp;
203/// // a simple hello world text, in black
204/// let args = TextArgsBuilder::default()
205/// .text("Hello, World!".to_string())
206/// .size(Dp(50.0)) // Example using Dp
207/// // line_height will be Dp(60.0) (1.2 * size) by default
208/// .build()
209/// .unwrap();
210/// text(args);
211/// ```
212#[tessera]
213pub fn text(args: impl Into<TextArgs>) {
214 let text_args: TextArgs = args.into();
215 measure(Box::new(move |input| {
216 let max_width: Option<Px> = match input.parent_constraint.width {
217 DimensionValue::Fixed(w) => Some(w),
218 DimensionValue::Wrap { max, .. } => max, // Use max from Wrap
219 DimensionValue::Fill { max, .. } => max, // Use max from Fill
220 };
221
222 let max_height: Option<Px> = match input.parent_constraint.height {
223 DimensionValue::Fixed(h) => Some(h),
224 DimensionValue::Wrap { max, .. } => max, // Use max from Wrap
225 DimensionValue::Fill { max, .. } => max, // Use max from Fill
226 };
227
228 let line_height = text_args.line_height.unwrap_or(Dp(text_args.size.0 * 1.2));
229
230 let text_data = TextData::new(
231 text_args.text.clone(),
232 text_args.color,
233 text_args.size.to_pixels_f32(),
234 line_height.to_pixels_f32(),
235 TextConstraint {
236 max_width: max_width.map(|px| px.to_f32()),
237 max_height: max_height.map(|px| px.to_f32()),
238 },
239 );
240
241 let size = text_data.size;
242 let drawable = TextCommand { data: text_data };
243
244 // Use the new unified command system to add the text rendering command
245 input.metadata_mut().push_draw_command(drawable);
246
247 Ok(ComputedData {
248 width: size[0].into(),
249 height: size[1].into(),
250 })
251 }));
252}