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    /// ![Text](https://raw.githubusercontent.com/water-rs/waterui/dev/docs/illustrations/text.svg)
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);