waterui_text/text.rs
1use crate::font::FontWeight;
2use crate::locale::Formatter;
3use crate::{font::Font, styled::StyledStr};
4use alloc::string::ToString;
5use core::fmt::Display;
6use nami::impl_constant;
7use nami::signal::IntoSignal;
8use nami::{Computed, Signal, SignalExt, signal::IntoComputed};
9use waterui_color::Color;
10use waterui_core::configurable;
11
12configurable!(
13 /// A view that displays one or more lines of read-only text.
14 ///
15 /// 
16 ///
17 /// Text sizes itself to fit its content and never stretches to fill extra space.
18 /// When the available width is limited, it wraps to multiple lines automatically.
19 /// If both width and height are constrained, it truncates with "..." at the end.
20 ///
21 /// # Layout Behavior
22 ///
23 /// - **Sizing:** Fits its content naturally, like a label
24 /// - **In stacks:** Takes only the space it needs, leaving room for siblings
25 /// - **Wrapping:** Automatically wraps when width is constrained via `.frame()`
26 ///
27 /// # Examples
28 ///
29 /// ```ignore
30 /// // Simple text
31 /// text("Hello, World!")
32 ///
33 /// // Styled text
34 /// text("Important").bold().title()
35 ///
36 /// // Enable wrapping with fixed width
37 /// text("Long paragraph...").frame().width(200.0)
38 ///
39 /// // Push text apart in a row
40 /// hstack((text("Name"), spacer(), text("Value")))
41 /// ```
42 //
43 // ═══════════════════════════════════════════════════════════════════════════
44 // INTERNAL: Layout Contract for Backend Implementers
45 // ═══════════════════════════════════════════════════════════════════════════
46 //
47
48 //
49 // Measurement Protocol (multi-pass):
50 // Pass 1 - PROBE: proposal(nil, nil) → (single_line_width, line_height)
51 // Pass 2 - WRAP: proposal(w, nil) → (actual_width ≤ w, wrapped_height)
52 // Pass 3 - TRUNCATE: proposal(w, h) → (w, h) with ellipsis if needed
53 //
54 // ═══════════════════════════════════════════════════════════════════════════
55 //
56 #[derive(Debug)]
57 Text,
58 TextConfig
59);
60
61#[derive(Debug, Clone)]
62#[non_exhaustive]
63/// Configuration for text components.
64///
65/// This struct contains all the properties needed to render text,
66/// including the content string and font styling information.
67pub struct TextConfig {
68 /// The rich text content to be displayed.
69 pub content: Computed<StyledStr>,
70}
71
72impl Clone for Text {
73 fn clone(&self) -> Self {
74 Self(self.0.clone())
75 }
76}
77
78impl core::cmp::PartialEq for Text {
79 fn eq(&self, _other: &Self) -> bool {
80 false
81 }
82}
83
84impl core::cmp::PartialOrd for Text {
85 fn partial_cmp(&self, _other: &Self) -> Option<core::cmp::Ordering> {
86 None
87 }
88}
89
90impl Default for Text {
91 fn default() -> Self {
92 text("")
93 }
94}
95
96impl Text {
97 /// Creates a new text component.
98 pub fn new(content: impl IntoComputed<StyledStr>) -> Self {
99 Self(TextConfig {
100 content: content.into_signal().map(StyledStr::from).computed(),
101 })
102 }
103
104 /// Creates a text component from any type implementing `Display`.
105 ///
106 /// This is a convenience method for creating text from values like
107 /// numbers, booleans, or other displayable types.
108 pub fn display<T: Display>(source: impl Signal<Output = T>) -> Self {
109 Self::new(source.map(|value| value.to_string()))
110 }
111
112 /// Creates a text component using a custom formatter.
113 ///
114 /// This allows for specialized formatting of values, such as
115 /// locale-specific number or date formatting.
116 pub fn format<T>(value: impl IntoComputed<T>, formatter: impl Formatter<T> + 'static) -> Self {
117 Self::new(
118 value
119 .into_signal()
120 .map(move |value| formatter.format(&value)),
121 )
122 }
123
124 /// Returns the computed content of this text component.
125 ///
126 /// This provides access to the reactive text content that will
127 /// automatically update when the underlying data changes.
128 #[must_use]
129 pub fn content(&self) -> Computed<StyledStr> {
130 self.0.content.clone()
131 }
132
133 /// Sets the font for this text component.
134 ///
135 /// This allows customizing the typography, including size, weight,
136 /// style, and other font properties.
137 #[must_use]
138 pub fn font(mut self, font: impl IntoSignal<Font>) -> Self {
139 let font = font.into_signal();
140 self.0.content = self
141 .0
142 .content
143 .zip(font)
144 .map(|(content, font)| content.font(&font))
145 .computed();
146 self
147 }
148
149 /// Sets the font size.
150 #[must_use]
151 pub fn size(mut self, size: impl IntoSignal<f64>) -> Self {
152 // A little sad we have to do this conversion here
153 #[allow(clippy::cast_possible_truncation)]
154 let size = size.into_signal().map(|s| s as f32);
155 self.0.content = self
156 .0
157 .content
158 .zip(size)
159 .map(|(content, size)| content.size(size))
160 .computed();
161 self
162 }
163
164 /// Sets the font weight.
165 #[must_use]
166 pub fn weight(mut self, weight: impl IntoSignal<FontWeight>) -> Self {
167 let weight = weight.into_signal();
168 self.0.content = self
169 .0
170 .content
171 .zip(weight)
172 .map(|(content, weight)| content.weight(weight))
173 .computed();
174 self
175 }
176
177 /// Applies an underline to the text.
178 #[must_use]
179 pub fn underline(mut self, underline: impl IntoSignal<bool>) -> Self {
180 let underline = underline.into_signal();
181 self.0.content = self
182 .0
183 .content
184 .zip(underline)
185 .map(|(content, underline)| content.underline(underline))
186 .computed();
187 self
188 }
189
190 /// Sets the foreground (text) color.
191 #[must_use]
192 pub fn foreground(mut self, color: impl Into<Color>) -> Self {
193 let color = color.into();
194 self.0.content = self
195 .0
196 .content
197 .map(move |content| content.foreground(color.clone()))
198 .computed();
199 self
200 }
201
202 /// Sets the background color for the text.
203 #[must_use]
204 pub fn background_color(mut self, color: impl Into<Color>) -> Self {
205 let color = color.into();
206 self.0.content = self
207 .0
208 .content
209 .map(move |content| content.background_color(color.clone()))
210 .computed();
211 self
212 }
213
214 /// Sets the font to bold.
215 #[must_use]
216 pub fn bold(self) -> Self {
217 self.weight(FontWeight::Bold)
218 }
219
220 /// Sets the italic style.
221 #[must_use]
222 pub fn italic(mut self, is_italic: impl Signal<Output = bool>) -> Self {
223 self.0.content = self
224 .0
225 .content
226 .zip(is_italic)
227 .map(|(content, is_italic)| content.italic(is_italic))
228 .computed();
229 self
230 }
231}
232
233macro_rules! impl_text_font {
234 ($(($name:ident, $value:expr)),+) => {
235 $(
236 impl Text {
237 #[doc = concat!("Sets the font to ", stringify!($name), " style.")]
238 #[must_use]
239 pub fn $name(self) -> Self {
240 self.font($value)
241 }
242 }
243 )+
244 };
245}
246
247impl_text_font!(
248 (body, crate::font::Body),
249 (title, crate::font::Title),
250 (headline, crate::font::Headline),
251 (subheadline, crate::font::Subheadline),
252 (caption, crate::font::Caption),
253 (footnote, crate::font::Footnote)
254);
255/// Creates a new text component with the given content.
256///
257/// This is a convenience function equivalent to `Text::new(text)`.
258///
259/// # Tip
260/// If you need formatted text, please use the `text!` macro instead.
261#[must_use]
262pub fn text(text: impl IntoComputed<StyledStr>) -> Text {
263 Text::new(text)
264}
265
266impl<T> From<T> for Text
267where
268 T: IntoComputed<StyledStr>,
269{
270 fn from(value: T) -> Self {
271 Self::new(value)
272 }
273}
274
275impl_constant!(Text, TextConfig);