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}