Skip to main content

decal/builders/
text.rs

1use crate::{
2    attributes::IntoPaintStack,
3    capabilities::*,
4    layout::{
5        Node,
6        NodeKind,
7        StencilScope,
8        StencilType,
9        TextMeta,
10        Typography,
11    },
12    macros::impl_node_builder,
13    paint::{
14        Appearance,
15        IntoResources,
16        Resource,
17    },
18    primitives::{
19        FontStyle,
20        FontWeight,
21        Paint,
22    },
23};
24use taffy::prelude::*;
25
26/// Text node.
27#[derive(Debug, Default)]
28pub struct Text {
29    meta: TextMeta,
30    layout: Style,
31    visual: Appearance,
32    typography: Typography,
33    resources: Vec<Resource>,
34}
35
36impl_node_builder!(
37    Text,
38    build(this) {
39        let mut meta = this.meta;
40        meta.typography(this.typography.clone());
41
42        Node::new(
43            NodeKind::Text(meta),
44            this.layout,
45            this.visual,
46            Some(this.typography),
47            this.resources
48        )
49    }
50);
51
52impl Text {
53    /// Creates a new text node from the provided content.
54    ///
55    /// # Arguments
56    /// - `content`: The text content convertible into one or more [`TextSpan`]
57    ///   values.
58    ///
59    /// # Examples
60    ///
61    /// ```rust
62    /// # use decal::prelude::*;
63    ///
64    /// let scene = decal! {
65    ///     Column {
66    ///         Text("another example text")
67    ///         Text(text! {
68    ///             "some ",
69    ///             ("example ", { weight: FontWeight::Bold }),
70    ///             "text."
71    ///         })
72    ///     }
73    /// };
74    /// ```
75    ///
76    /// See the [`text!`] macro for more advanced span composition examples.
77    ///
78    /// # Returns
79    /// - [`Self`]
80    ///
81    /// [`text!`]: decal_macros::text
82    pub fn new<S>(content: S) -> Self
83    where
84        S: IntoText,
85    {
86        let spans = content.into_text_spans();
87        let mut resources = Vec::new();
88
89        for span in &spans {
90            resources.extend(span.resources.clone());
91        }
92
93        Self {
94            meta: TextMeta::new(spans),
95            resources,
96            ..Default::default()
97        }
98    }
99
100    /// Use this text as a stencil mask to the provided source (paint stack).
101    ///
102    /// # Arguments
103    /// - `value`: The paint stack used as a stencil source, convertible via
104    ///   [`IntoPaintStack`].
105    ///
106    /// # Examples
107    ///
108    /// ```rust
109    /// # use decal::prelude::*;
110    ///
111    /// let gradient = LinearGradient::right().stops([(0.0, rgb(0xff0000)), (1.0, rgb(0x00ff00))]);
112    ///
113    /// let scene = decal! {
114    ///     Text("hello")
115    ///         .stencil(gradient)
116    /// };
117    /// ```
118    ///
119    /// # Returns
120    /// - [`Self`]
121    pub fn stencil<T>(mut self, value: T) -> Self
122    where
123        T: IntoPaintStack,
124    {
125        let paint = value.into_paint_stack();
126        self.meta.stencil_paint(paint.clone());
127        self.add_resources(paint);
128        self
129    }
130
131    /// Sets the scope that determines which glyphs participate in stencil mask
132    /// generation.
133    ///
134    /// # Arguments
135    /// - `scope`: The [`StencilScope`] defining which glyph categories are
136    ///   included in the stencil mask.
137    ///
138    /// # Examples
139    ///
140    /// Exclude bitmap glyphs such as emojis from the stencil mask while still
141    /// rendering them normally.
142    ///
143    /// ```rust
144    /// # use decal::prelude::*;
145    ///
146    /// let gradient = LinearGradient::right().stops([(0.0, rgb(0xff0000)), (1.0, rgb(0x00ff00))]);
147    ///
148    /// let scene = decal! {
149    ///     Text("hello 🐠")
150    ///         .stencil(gradient)
151    ///         .stencil_scope(StencilScope::VectorGlyphs)
152    /// };
153    /// ```
154    ///
155    /// Include all glyphs including bitmap glyphs and emojis in the stencil
156    /// mask.
157    ///
158    /// ```rust
159    /// # use decal::prelude::*;
160    ///
161    /// let gradient = LinearGradient::right().stops([(0.0, rgb(0xff0000)), (1.0, rgb(0x00ff00))]);
162    ///
163    /// let scene = decal! {
164    ///     Text("hello 🐠")
165    ///         .stencil(gradient)
166    ///         .stencil_scope(StencilScope::AllGlyphs)
167    /// };
168    /// ```
169    ///
170    /// # Returns
171    /// - [`Self`]
172    pub fn stencil_scope(mut self, scope: StencilScope) -> Self {
173        self.meta.stencil_scope(scope);
174        self
175    }
176
177    /// Sets how the stencil mask is derived from the rendered glyph content.
178    ///
179    /// This determines which channel of the glyph rendering is sampled to
180    /// construct the stencil mask, which directly influences how strongly paint
181    /// effects are applied across the text.
182    ///
183    /// # Arguments
184    /// - `value`: The [`StencilType`] that defines whether alpha or luminance
185    ///   data is used for stencil generation.
186    ///
187    /// # Examples
188    ///
189    /// Use glyph alpha values to control stencil intensity.
190    ///
191    /// ```rust
192    /// # use decal::prelude::*;
193    ///
194    /// let gradient = LinearGradient::right().stops([(0.0, rgb(0xff0000)), (1.0, rgb(0x00ff00))]);
195    ///
196    /// let scene = decal! {
197    ///     Text("hello 🐠")
198    ///         .stencil(gradient)
199    ///         .stencil_type(StencilType::Alpha)
200    /// };
201    /// ```
202    ///
203    /// Use glyph luminance values to control stencil intensity based on
204    /// brightness.
205    ///
206    /// ```rust
207    /// # use decal::prelude::*;
208    ///
209    /// let gradient = LinearGradient::right().stops([(0.0, rgb(0x0000ff)), (1.0, rgb(0xffff00))]);
210    ///
211    /// let scene = decal! {
212    ///     Text("hello 🐠")
213    ///         .stencil(gradient)
214    ///         .stencil_type(StencilType::Luminance)
215    /// };
216    /// ```
217    ///
218    /// # Returns
219    /// - [`Self`]
220    pub fn stencil_type(mut self, value: StencilType) -> Self {
221        self.meta.stencil_type(value);
222        self
223    }
224}
225
226impl Hideable for Text {
227    fn hidden(mut self, value: bool) -> Self {
228        self.layout.display = if value { Display::None } else { Display::Block };
229        self
230    }
231}
232
233impl Background for Text {}
234impl Dimensions for Text {}
235impl Margin for Text {}
236impl Opacity for Text {}
237impl Positioned for Text {}
238impl Transformation for Text {}
239impl Textual for Text {}
240impl SelfAlignment for Text {}
241impl Visibility for Text {}
242impl FilterEffects for Text {}
243impl Blendable for Text {}
244
245//
246
247#[derive(Debug, Clone)]
248pub struct TextSpan {
249    pub(crate) content: String,
250    pub(crate) typography: Typography,
251    pub(crate) resources: Vec<Resource>,
252    pub(crate) hidden: bool,
253}
254
255impl TextSpan {
256    /// Creates a new text span.
257    ///
258    /// Manual construction of [`TextSpan`] is supported, but the [`text!`]
259    /// macro is the preferred way to build styled text spans in most cases.
260    /// See [`Text::new`] for typical usage patterns.
261    ///
262    /// # Arguments
263    /// - `content`: The content of the span.
264    ///
265    /// # Returns
266    /// - [`Self`]
267    ///
268    /// [`text!`]: decal_macros::text
269    pub fn new(content: String) -> Self {
270        Self {
271            content,
272            typography: Default::default(),
273            resources: Vec::new(),
274            hidden: false,
275        }
276    }
277
278    /// Sets the font family for the text span.
279    ///
280    /// # Arguments
281    /// - `family`: The font family name.
282    ///
283    /// # Returns
284    /// - [`Self`]
285    pub fn family<T>(mut self, family: T) -> Self
286    where
287        T: Into<Option<String>>,
288    {
289        self.typography.family = family.into();
290        self
291    }
292
293    /// Sets the font size for the text span.
294    ///
295    /// # Arguments
296    /// - `size`: The font size.
297    ///
298    /// # Returns
299    /// - [`Self`]
300    pub fn size<T>(mut self, size: T) -> Self
301    where
302        T: Into<Option<f32>>,
303    {
304        self.typography.size = size.into();
305        self
306    }
307
308    /// Sets the line height for the text span.
309    ///
310    /// # Arguments
311    /// - `line_height`: The line height.
312    ///
313    /// # Returns
314    /// - [`Self`]
315    pub fn line_height<T>(mut self, line_height: T) -> Self
316    where
317        T: Into<Option<f32>>,
318    {
319        self.typography.line_height = line_height.into();
320        self
321    }
322
323    /// Sets the font weight for the text span.
324    ///
325    /// # Arguments
326    /// - `weight`: The [`FontWeight`] to apply.
327    ///
328    /// # Returns
329    /// - [`Self`]
330    pub fn weight<T>(mut self, weight: T) -> Self
331    where
332        T: Into<Option<FontWeight>>,
333    {
334        self.typography.weight = weight.into();
335        self
336    }
337
338    /// Sets the text color for the span.
339    ///
340    /// # Arguments
341    /// - `color`: The [`Paint`] value applied to the text.
342    ///
343    /// # Returns
344    /// - [`Self`]
345    pub fn color<T>(mut self, color: T) -> Self
346    where
347        T: Into<Option<Paint>>,
348    {
349        let color = color.into();
350        self.typography.color = color.clone();
351
352        if let Some(resources) = color.map(|c| c.into_resources()) {
353            self.resources.extend(resources);
354        }
355
356        self
357    }
358
359    /// Sets the font style for the text span.
360    ///
361    /// # Arguments
362    /// - `style`: The [`FontStyle`] to apply.
363    ///
364    /// # Returns
365    /// - [`Self`]
366    pub fn style<T>(mut self, style: T) -> Self
367    where
368        T: Into<Option<FontStyle>>,
369    {
370        self.typography.style = style.into();
371        self
372    }
373
374    /// Sets the letter spacing for the text span.
375    ///
376    /// # Arguments
377    /// - `letter_spacing`: The letter spacing.
378    ///
379    /// # Returns
380    /// - [`Self`]
381    pub fn letter_spacing<T>(mut self, letter_spacing: T) -> Self
382    where
383        T: Into<Option<f32>>,
384    {
385        self.typography.letter_spacing = letter_spacing.into();
386        self
387    }
388
389    /// Marks the text span as hidden or visible.
390    ///
391    /// # Arguments
392    /// - `value`: Whether the span should opt out of rendering.
393    ///
394    /// # Returns
395    /// - [`Self`]
396    pub fn hidden(mut self, value: bool) -> Self {
397        self.hidden = value;
398        self
399    }
400}
401
402/// Conversion trait for values that can be interpreted as a text span.
403pub trait IntoTextSpan {
404    /// Converts the value into a [`TextSpan`].
405    fn into_text_span(self) -> TextSpan;
406}
407
408/// Converts a string slice into a text span.
409impl IntoTextSpan for &str {
410    fn into_text_span(self) -> TextSpan {
411        TextSpan::new(self.to_string())
412    }
413}
414
415/// Converts an owned string into a text span.
416impl IntoTextSpan for String {
417    fn into_text_span(self) -> TextSpan {
418        TextSpan::new(self)
419    }
420}
421
422/// Identity conversion for an existing text span.
423impl IntoTextSpan for TextSpan {
424    fn into_text_span(self) -> TextSpan {
425        self
426    }
427}
428
429/// Conversion trait for values that can be expanded into multiple text spans.
430pub trait IntoText {
431    /// Converts the value into a collection of text spans.
432    fn into_text_spans(self) -> Vec<TextSpan>;
433}
434
435/// Converts a string slice into a single text span.
436impl IntoText for &str {
437    fn into_text_spans(self) -> Vec<TextSpan> {
438        vec![self.into_text_span()]
439    }
440}
441
442/// Converts an owned string into a single text span.
443impl IntoText for String {
444    fn into_text_spans(self) -> Vec<TextSpan> {
445        vec![self.into_text_span()]
446    }
447}
448
449/// Converts a collection of span-like values into text spans.
450impl<T> IntoText for Vec<T>
451where
452    T: IntoTextSpan,
453{
454    fn into_text_spans(self) -> Vec<TextSpan> {
455        self.into_iter().map(|x| x.into_text_span()).collect()
456    }
457}