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 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 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}