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}