Skip to main content

scena/scene/
labels.rs

1use crate::diagnostics::LookupError;
2use crate::material::Color;
3
4use super::{LabelKey, NodeKey, NodeKind, Scene, Transform};
5
6mod font;
7
8pub use font::{LabelFontError, LabelFontFace};
9use font::{
10    default_label_font_face, truetype_glyph_rasters, truetype_metrics, validate_basic_latin_text,
11};
12
13#[derive(Debug, Clone, PartialEq)]
14pub struct LabelDesc {
15    text: String,
16    font: LabelFontFace,
17    billboard: LabelBillboard,
18    color: Color,
19    background: Option<Color>,
20    halo: Option<Color>,
21    size: f32,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub struct LabelMetrics {
26    pub glyph_count: usize,
27    pub width_px: f32,
28    pub height_px: f32,
29    pub baseline_px: f32,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum LabelBillboard {
34    ScreenAligned,
35}
36
37impl Scene {
38    pub fn add_label(
39        &mut self,
40        parent: NodeKey,
41        label: LabelDesc,
42        transform: Transform,
43    ) -> Result<LabelKey, LookupError> {
44        Ok(self.add_label_node(parent, label, transform)?.0)
45    }
46
47    pub(crate) fn add_label_node(
48        &mut self,
49        parent: NodeKey,
50        label: LabelDesc,
51        transform: Transform,
52    ) -> Result<(LabelKey, NodeKey), LookupError> {
53        label.validate_style()?;
54        let label_key = self.labels.insert(label);
55        match self.insert_node(parent, NodeKind::Label(label_key), transform) {
56            Ok(node) => Ok((label_key, node)),
57            Err(error) => {
58                self.labels.remove(label_key);
59                Err(error)
60            }
61        }
62    }
63
64    pub fn label(&self, label: LabelKey) -> Option<&LabelDesc> {
65        self.labels.get(label)
66    }
67
68    pub fn set_label_text(
69        &mut self,
70        label: LabelKey,
71        text: impl Into<String>,
72    ) -> Result<(), LookupError> {
73        let label_key = label;
74        let label = self
75            .labels
76            .get_mut(label_key)
77            .ok_or(LookupError::LabelNotFound(label_key))?;
78        let text = text.into();
79        if let Err(error) = label.validate_text(&text) {
80            return Err(LookupError::UnsupportedLabelText {
81                label: label_key,
82                reason: error.to_string(),
83            });
84        }
85        if label.text != text {
86            label.text = text;
87            self.structure_revision = self.structure_revision.saturating_add(1);
88        }
89        Ok(())
90    }
91}
92
93impl LabelDesc {
94    /// Creates a label rendered through scena's embedded TrueType font.
95    pub fn new(text: impl Into<String>) -> Self {
96        Self {
97            text: text.into(),
98            font: default_label_font_face(),
99            billboard: LabelBillboard::ScreenAligned,
100            color: Color::WHITE,
101            background: None,
102            halo: None,
103            size: 14.0,
104        }
105    }
106
107    /// Creates a label rendered through a caller-supplied TrueType/OpenType font.
108    ///
109    /// This first font path intentionally supports basic Latin text only. Complex
110    /// shaping scripts are rejected instead of being approximated by broken glyph
111    /// order or fallback boxes.
112    pub fn truetype(
113        text: impl Into<String>,
114        font_bytes: impl AsRef<[u8]>,
115    ) -> Result<Self, LabelFontError> {
116        let text = text.into();
117        validate_basic_latin_text(&text)?;
118        let font = LabelFontFace::from_truetype_bytes(font_bytes)?;
119        Self::truetype_face(text, font)
120    }
121
122    pub fn truetype_face(
123        text: impl Into<String>,
124        font: LabelFontFace,
125    ) -> Result<Self, LabelFontError> {
126        let text = text.into();
127        validate_basic_latin_text(&text)?;
128        Ok(Self {
129            text,
130            font,
131            billboard: LabelBillboard::ScreenAligned,
132            color: Color::WHITE,
133            background: None,
134            halo: None,
135            size: 14.0,
136        })
137    }
138
139    pub fn text(&self) -> &str {
140        &self.text
141    }
142
143    pub const fn billboard(&self) -> LabelBillboard {
144        self.billboard
145    }
146
147    pub const fn color(&self) -> Color {
148        self.color
149    }
150
151    pub const fn background(&self) -> Option<Color> {
152        self.background
153    }
154
155    pub const fn halo(&self) -> Option<Color> {
156        self.halo
157    }
158
159    pub const fn size(&self) -> f32 {
160        self.size
161    }
162
163    pub fn metrics(&self) -> LabelMetrics {
164        truetype_metrics(&self.font, &self.text, self.size)
165    }
166
167    pub fn with_color(mut self, color: Color) -> Self {
168        self.color = color;
169        self
170    }
171
172    pub fn with_background(mut self, color: Color) -> Self {
173        self.background = Some(color);
174        self
175    }
176
177    pub const fn without_background(mut self) -> Self {
178        self.background = None;
179        self
180    }
181
182    pub fn with_halo(mut self, color: Color) -> Self {
183        self.halo = Some(color);
184        self
185    }
186
187    pub const fn without_halo(mut self) -> Self {
188        self.halo = None;
189        self
190    }
191
192    pub(crate) fn glyph_rasters(&self) -> Vec<LabelGlyphRaster> {
193        truetype_glyph_rasters(&self.font, &self.text, self.size)
194    }
195
196    pub fn with_size(mut self, size: f32) -> Self {
197        self.size = readable_size_or(size, self.size);
198        self
199    }
200
201    pub const fn with_billboard(mut self, billboard: LabelBillboard) -> Self {
202        self.billboard = billboard;
203        self
204    }
205
206    fn validate_text(&self, text: &str) -> Result<(), LabelFontError> {
207        validate_basic_latin_text(text)
208    }
209
210    fn validate_style(&self) -> Result<(), LookupError> {
211        if self.validate_text(&self.text).is_err() {
212            return Err(LookupError::InvalidLabelStyle {
213                field: "label text",
214                reason: "embedded label font supports basic Latin text only",
215            });
216        }
217        validate_opaque_color("label text color", self.color)?;
218        if let Some(background) = self.background {
219            validate_opaque_color("label background color", background)?;
220        }
221        if let Some(halo) = self.halo {
222            validate_opaque_color("label halo color", halo)?;
223        }
224        Ok(())
225    }
226}
227
228#[derive(Debug, Clone, PartialEq)]
229pub(crate) struct LabelGlyphRaster {
230    pub(crate) x_px: f32,
231    pub(crate) y_px: f32,
232    pub(crate) width_px: f32,
233    pub(crate) height_px: f32,
234    pub(crate) alpha_width: u32,
235    pub(crate) alpha_height: u32,
236    pub(crate) alpha: Vec<u8>,
237}
238
239fn readable_size_or(value: f32, fallback: f32) -> f32 {
240    if value.is_finite() && value >= 4.0 {
241        value
242    } else {
243        fallback
244    }
245}
246
247fn validate_opaque_color(field: &'static str, color: Color) -> Result<(), LookupError> {
248    if color.r.is_finite()
249        && color.g.is_finite()
250        && color.b.is_finite()
251        && color.a.is_finite()
252        && color.a == 1.0
253    {
254        Ok(())
255    } else {
256        Err(LookupError::InvalidLabelStyle {
257            field,
258            reason: "label billboard colors must be finite and opaque until a transparent label path exists",
259        })
260    }
261}