1use std::fmt::Display;
2use std::ops::Rem;
3use ab_glyph::{Font, FontArc, Glyph, point, PxScale, ScaleFont};
4use crate::{color, xml};
5use crate::xml::Pusher;
6use crate::plastic_style::Plastic;
7use crate::flat_style::Flat;
8use crate::flat_square_style::FlatSquare;
9
10fn measure_line(font: FontArc, text: &str, scale: PxScale) -> (f32, f32) {
11 let font = font.as_scaled(scale);
12
13 let mut caret = point(0.0, font.ascent());
14 let mut first_glyph: Option<Glyph> = None;
15 let mut last_glyph: Option<Glyph> = None;
16 for c in text.chars().filter(|c| !c.is_control()) {
17 let mut glyph = font.scaled_glyph(c);
18 if let Some(prev) = last_glyph.take() {
19 caret.x += font.kern(prev.id, glyph.id);
20 }
21 glyph.position = caret;
22
23 if first_glyph.is_none() {
24 first_glyph = Some(glyph.clone());
25 }
26 last_glyph = Some(glyph.clone());
27 caret.x += font.h_advance(glyph.id);
28 }
29
30 let height = font.ascent() - font.descent() + font.line_gap();
31 let width = {
32 let min_x = first_glyph.unwrap().position.x;
33 let last_glyph = last_glyph.unwrap();
34 let max_x = last_glyph.position.x + font.h_advance(last_glyph.id);
35 (max_x - min_x).ceil()
36 };
37
38 (width, height)
39}
40
41const FONT_FAMILY: &str = "Verdana,Geneva,DejaVu Sans,sans-serif";
42const FONT_SCALE_UP_FACTOR: f32 = 10.0;
43const FONT_SCALE_DOWN_VALUE: &str = "scale(.1)";
44
45const WIDTH_FONT_SCALE: f32 = 11.0;
46
47#[derive(Copy, Clone)]
49pub enum Style {
50 Plastic,
53
54 Flat,
56
57 FlatSquare,
59}
60
61pub enum FontFamily {
63 Default,
66
67 Custom(String),
69}
70
71impl FontFamily {
72 fn string(&self) -> String {
73 match self {
74 FontFamily::Default => FONT_FAMILY.into(),
75 FontFamily::Custom(val) => val.clone(),
76 }
77 }
78}
79
80pub struct Metadata<'a> {
82 pub style: Style,
84
85 pub label: &'a str,
87
88 pub message: &'a str,
90
91 pub font: FontArc,
94
95 pub font_family: FontFamily,
98
99 pub label_color: Option<&'a str>,
102
103 pub color: Option<&'a str>,
106}
107
108pub(crate) struct GradientStop<'a> {
109 pub(crate) offset: &'a str,
110 pub(crate) stop_color: &'a str,
111 pub(crate) stop_opacity: &'a str,
112}
113
114impl GradientStop<'_> {
115 pub(crate) fn into_attributes(self, of: &mut xml::Node) {
116 of.add_attrs(&[
117 ("offset", self.offset),
118 ("stop-color", self.stop_color),
119 ("stop-opacity", self.stop_opacity),
120 ]);
121 }
122}
123
124fn round_up_to_odd(val: f32) -> f32 {
125 if val.rem(2.0) as i32 == 0 {
126 val + 1.0
127 } else {
128 val
129 }.round()
130}
131
132fn preferred_width_of(text: &str, font: FontArc, scale: PxScale) -> f32 {
133 let (w, _) = measure_line(font, text, scale);
134 let val = round_up_to_odd(w);
135 val * 1.0345
136}
137
138fn colors_for_background(color_str: &str) -> Option<(&str, &str)> {
139 const BRIGHTNESS_THRESHOLD: f32 = 0.69;
140 let parsed_color = match color::color_by_name(Some(color_str)) {
141 Some(c) => c,
142 None => return None,
143 };
144
145 if color::brightness(parsed_color) <= BRIGHTNESS_THRESHOLD {
146 return Some(("#fff", "#010101"));
147 }
148
149 Some(("#333", "#ccc"))
150}
151
152pub(crate) trait Badger {
153 fn vertical_margin(&self) -> f32;
154 fn height(&self) -> f32;
155 fn shadow(&self) -> bool;
156 fn render(&self, parent: &Renderer) -> Vec<xml::Node>;
157}
158
159pub struct Renderer<'a> {
162 horizontal_padding: f32,
163
164 label_margin: f32,
165 message_margin: f32,
166 label_width: f32,
167 message_width: f32,
168
169 left_width: f32,
170 right_width: f32,
171 font_family: String,
172
173 width: f32,
174 label_color: css_color_parser::Color,
175 color: css_color_parser::Color,
176 label: &'a str,
177 message: &'a str,
178 accessible_text: String,
179
180 style: Box<dyn Badger>,
181}
182
183impl Renderer<'_> {
184 fn new<'a>(info: &'a Metadata<'a>) -> Renderer<'a> {
185 let horizontal_padding = 5.0;
186
187 let label_margin = 1.0;
188 let scale = PxScale::from(WIDTH_FONT_SCALE);
189 let label_width = preferred_width_of(info.label, info.font.clone(), scale);
190 let left_width = label_width + 2.0 * horizontal_padding;
191
192 let message_width = preferred_width_of(info.message, info.font.clone(), scale);
193 let message_margin = left_width - 1.0;
194 let right_width = message_width + 2.0 * horizontal_padding;
195 let width = left_width + right_width;
196 let label_color = color::color_by_name(info.label_color).unwrap_or_else(|| color::color_by_name(Some("#555")).unwrap());
197 let color = color::color_by_name(info.color).unwrap_or_else(|| color::color_by_name(Some("#4c1")).unwrap());
198
199 let accessible_text = format!("{}: {}", info.label, info.message);
200
201 let styler: Box<dyn Badger> = match info.style {
202 Style::Plastic => Box::new(Plastic {}),
203 Style::Flat => Box::new(Flat {}),
204 Style::FlatSquare => Box::new(FlatSquare {}),
205 };
206
207 Renderer {
208 horizontal_padding,
209 label_margin,
210 message_margin,
211 label_width,
212 message_width,
213 left_width,
214 right_width,
215 font_family: info.font_family.string(),
216 width,
217 label_color,
218 color,
219 label: info.label,
220 message: info.message,
221 accessible_text,
222 style: styler,
223 }
224 }
225
226 pub fn render(info: &Metadata) -> String {
228 let mut render = Renderer::new(info);
229 render.internal_render()
230 }
231
232 fn internal_render(&mut self) -> String {
233 let title = xml::Node::with_name_and("title",
234 |n| n.push_text(&self.accessible_text));
235
236 let mut svg = xml::Node::with_attributes("svg", &[
237 ("xmlns", "http://www.w3.org/2000/svg"),
238 ("xmlns:xlink", "http://www.w3.org/1999/xlink"),
239 ("width", &format!("{}", self.width)),
240 ("height", &format!("{}", self.style.height())),
241 ("role", "img"),
242 ("aria-label", &self.accessible_text),
243 ]);
244 svg.push_node(title);
245 svg.push_nodes(self.style.render(self));
246
247 let mut doc = xml::Document::new();
248 doc.push_node(svg);
249 xml::Renderer::render(&doc)
250 }
251
252 fn make_text_element(&self, left_margin: f32, content: &str, color: &str, text_width: f32) -> Vec<xml::Node> {
253 let (text_color, shadow_color) = colors_for_background(color).unwrap_or(("", ""));
254
255 let x = FONT_SCALE_UP_FACTOR * (left_margin + 0.5 * text_width + self.horizontal_padding);
256 let mut result = vec![];
257
258 if self.style.shadow() {
259 let shadow = xml::Node::with_name_and("text", |n| {
260 n.add_attrs(&[
261 ("aria-hidden", "true"),
262 ("fill", shadow_color),
263 ("x", &format!("{}", x)),
264 ("y", &format!("{}", 150.0 + self.style.vertical_margin())),
265 ("fill-opacity", ".3"),
266 ("transform", FONT_SCALE_DOWN_VALUE),
267 ("textLength", &format!("{}", FONT_SCALE_UP_FACTOR * text_width)),
268 ]);
269 n.push_text(content);
270 });
271 result.push(shadow);
272 }
273
274 result.push(xml::Node::with_name_and("text", |n| {
275 n.add_attrs(&[
276 ("fill", text_color),
277 ("x", &format!("{}", x)),
278 ("y", &format!("{}", 140.0 + self.style.vertical_margin())),
279 ("transform", FONT_SCALE_DOWN_VALUE),
280 ("textLength", &format!("{}", FONT_SCALE_UP_FACTOR * text_width)),
281 ]);
282 n.push_text(content);
283 }));
284
285 result
286 }
287
288 fn make_label_element(&self) -> Vec<xml::Node> {
289 self.make_text_element(self.label_margin, self.label, &color::color_to_string(self.label_color), self.label_width)
290 }
291
292 fn make_message_element(&self) -> Vec<xml::Node> {
293 self.make_text_element(self.message_margin, self.message, &color::color_to_string(self.color), self.message_width)
294 }
295
296 pub(crate) fn make_clip_path_element(&self, radius: f32) -> xml::Node {
297 xml::Node::with_name_and("clipPath", |n| {
298 n.add_attr("id", "r");
299 n.push_node_named("rect", |n| {
300 n.add_attrs(&[
301 ("fill", "#fff"),
302 ("width", &format!("{}", self.width)),
303 ("height", &format!("{}", self.style.height())),
304 ("rx", &format!("{}", radius)),
305 ]);
306 });
307 })
308 }
309
310 pub(crate) fn make_background_group_element<V: Display + ?Sized>(&self, with_gradient: bool, attributes: &[(&str, &V)]) -> xml::Node {
311 xml::Node::with_name_and("g", |n| {
312 n.add_attrs(attributes);
313 let height = format!("{}", self.style.height());
314 let left_width = format!("{}", self.left_width);
315
316 n.push_node_named("rect", |r| {
318 r.add_attrs(&[
319 ("width", &left_width),
320 ("height", &height),
321 ("fill", &color::color_to_string(self.label_color)),
322 ]);
323 });
324
325 n.push_node_named("rect", |r| {
327 r.add_attrs(&[
328 ("x", &left_width),
329 ("width", &format!("{}", self.right_width)),
330 ("height", &height),
331 ("fill", &color::color_to_string(self.color)),
332 ]);
333 });
334
335 if with_gradient {
336 n.push_node_named("rect", |r| {
337 r.add_attrs(&[
338 ("fill", "url(#s)"),
339 ("width", &format!("{}", self.width)),
340 ("height", &height),
341 ]);
342 })
343 }
344 })
345 }
346
347 pub(crate) fn make_foreground_group_element(&self) -> xml::Node {
348 xml::Node::with_name_and("g", |n| {
349 n.push_nodes(self.make_label_element());
350 n.push_nodes(self.make_message_element());
351 n.add_attrs(&[
352 ("fill", "#fff"),
353 ("text-anchor", "middle"),
354 ("font-family", &self.font_family),
355 ("text-rendering", "geometricPrecision"),
356 ("font-size", "110"),
357 ]);
358 })
359 }
360}