1use std::collections::hash_map::DefaultHasher;
2use std::fmt::Write;
3use std::hash::Hasher;
4
5use usvg::TreeParsing;
6use usvg_text_layout::TreeTextToPath;
7
8use geom::{PolyLine, Polygon};
9
10use crate::assets::Assets;
11use crate::{
12 svg, Color, DeferDraw, EventCtx, GeomBatch, JustDraw, MultiKey, ScreenDims, Style, Widget,
13};
14
15pub const DEFAULT_FONT: Font = Font::OverpassRegular;
17pub const DEFAULT_FONT_SIZE: usize = 21;
18
19pub const SCALE_LINE_HEIGHT: f64 = 1.2;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum Font {
23 BungeeInlineRegular,
24 BungeeRegular,
25 OverpassBold,
26 OverpassRegular,
27 OverpassSemiBold,
28 OverpassMonoBold,
29}
30
31impl Font {
32 pub fn family(self) -> &'static str {
33 match self {
34 Font::BungeeInlineRegular => "Bungee Inline",
35 Font::BungeeRegular => "Bungee",
36 Font::OverpassBold => "Overpass",
37 Font::OverpassRegular => "Overpass",
38 Font::OverpassSemiBold => "Overpass",
39 Font::OverpassMonoBold => "Overpass Mono",
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
45pub struct TextSpan {
46 text: String,
47 fg_color: Option<Color>,
48 outline_color: Option<Color>,
49 size: usize,
50 font: Font,
51 underlined: bool,
52}
53
54impl<AsStrRef: AsRef<str>> From<AsStrRef> for TextSpan {
55 fn from(line: AsStrRef) -> Self {
56 Line(line.as_ref())
57 }
58}
59
60impl TextSpan {
61 pub fn fg(mut self, color: Color) -> TextSpan {
62 assert_eq!(self.fg_color, None);
63 self.fg_color = Some(color);
64 self
65 }
66
67 pub fn maybe_fg(mut self, color: Option<Color>) -> TextSpan {
68 assert_eq!(self.fg_color, None);
69 self.fg_color = color;
70 self
71 }
72
73 pub fn fg_color_for_style(&self, style: &Style) -> Color {
74 self.fg_color.unwrap_or(style.text_primary_color)
75 }
76
77 pub fn outlined(mut self, color: Color) -> TextSpan {
78 assert_eq!(self.outline_color, None);
79 self.outline_color = Some(color);
80 self
81 }
82
83 pub fn into_widget(self, ctx: &EventCtx) -> Widget {
84 Text::from(self).into_widget(ctx)
85 }
86 pub fn batch(self, ctx: &EventCtx) -> Widget {
87 Text::from(self).batch(ctx)
88 }
89
90 pub fn display_title(mut self) -> TextSpan {
93 self.font = Font::BungeeInlineRegular;
94 self.size = 64;
95 self
96 }
97 pub fn big_heading_styled(mut self) -> TextSpan {
98 self.font = Font::BungeeRegular;
99 self.size = 32;
100 self
101 }
102 pub fn big_heading_plain(mut self) -> TextSpan {
103 self.font = Font::OverpassBold;
104 self.size = 32;
105 self
106 }
107 pub fn small_heading(mut self) -> TextSpan {
108 self.font = Font::OverpassSemiBold;
109 self.size = 26;
110 self
111 }
112 pub fn body(mut self) -> TextSpan {
114 self.font = Font::OverpassRegular;
115 self.size = 21;
116 self
117 }
118 pub fn bold_body(mut self) -> TextSpan {
119 self.font = Font::OverpassBold;
120 self.size = 21;
121 self
122 }
123 pub fn secondary(mut self) -> TextSpan {
124 self.font = Font::OverpassRegular;
125 self.size = 21;
126 self.fg_color = Some(Color::hex("#A3A3A3"));
128 self
129 }
130 pub fn small(mut self) -> TextSpan {
131 self.font = Font::OverpassRegular;
132 self.size = 16;
133 self
134 }
135 pub fn big_monospaced(mut self) -> TextSpan {
136 self.font = Font::OverpassMonoBold;
137 self.size = 32;
138 self
139 }
140 pub fn small_monospaced(mut self) -> TextSpan {
141 self.font = Font::OverpassMonoBold;
142 self.size = 16;
143 self
144 }
145
146 pub fn underlined(mut self) -> TextSpan {
147 self.underlined = true;
148 self
149 }
150
151 pub fn size(mut self, size: usize) -> TextSpan {
152 self.size = size;
153 self
154 }
155
156 pub fn font(mut self, font: Font) -> TextSpan {
157 self.font = font;
158 self
159 }
160}
161
162#[allow(non_snake_case)]
164pub fn Line<S: Into<String>>(text: S) -> TextSpan {
165 TextSpan {
166 text: text.into(),
167 fg_color: None,
168 outline_color: None,
169 size: DEFAULT_FONT_SIZE,
170 font: DEFAULT_FONT,
171 underlined: false,
172 }
173}
174
175#[derive(Debug, Clone)]
176pub struct Text {
177 lines: Vec<(Option<Color>, Vec<TextSpan>)>,
179 bg_color: Option<Color>,
181}
182
183impl From<TextSpan> for Text {
184 fn from(line: TextSpan) -> Text {
185 let mut txt = Text::new();
186 txt.add_line(line);
187 txt
188 }
189}
190
191impl<AsStrRef: AsRef<str>> From<AsStrRef> for Text {
192 fn from(line: AsStrRef) -> Text {
193 let mut txt = Text::new();
194 txt.add_line(Line(line.as_ref()));
195 txt
196 }
197}
198
199impl Text {
200 pub fn new() -> Text {
201 Text {
202 lines: Vec::new(),
203 bg_color: None,
204 }
205 }
206
207 pub fn from_all(lines: Vec<TextSpan>) -> Text {
208 let mut txt = Text::new();
209 for l in lines {
210 txt.append(l);
211 }
212 txt
213 }
214
215 pub fn from_multiline(lines: Vec<impl Into<TextSpan>>) -> Text {
216 let mut txt = Text::new();
217 for l in lines {
218 txt.add_line(l.into());
219 }
220 txt
221 }
222
223 pub fn bg(mut self, bg: Color) -> Text {
224 assert!(self.bg_color.is_none());
225 self.bg_color = Some(bg);
226 self
227 }
228
229 pub fn tooltip<MK: Into<Option<MultiKey>>>(ctx: &EventCtx, hotkey: MK, action: &str) -> Text {
231 if let Some(ref key) = hotkey.into() {
232 Text::from_all(vec![
233 Line(key.describe())
234 .fg(ctx.style().text_hotkey_color)
235 .small(),
236 Line(format!(" - {}", action)).small(),
237 ])
238 } else {
239 Text::from(Line(action).small())
240 }
241 }
242
243 pub fn change_fg(mut self, fg: Color) -> Text {
244 for (_, spans) in self.lines.iter_mut() {
245 for span in spans {
246 span.fg_color = Some(fg);
247 }
248 }
249 self
250 }
251
252 pub fn default_fg(mut self, fg: Color) -> Text {
253 for (_, spans) in self.lines.iter_mut() {
254 for span in spans {
255 if span.fg_color.is_none() {
256 span.fg_color = Some(fg);
257 }
258 }
259 }
260 self
261 }
262
263 pub fn add_line(&mut self, line: impl Into<TextSpan>) {
264 self.lines.push((None, vec![line.into()]));
265 }
266
267 pub(crate) fn highlight_last_line(&mut self, highlight: Color) {
269 self.lines.last_mut().unwrap().0 = Some(highlight);
270 }
271
272 pub fn append(&mut self, line: TextSpan) {
273 if self.lines.is_empty() {
274 self.add_line(line);
275 return;
276 }
277
278 self.lines.last_mut().unwrap().1.push(line);
279 }
280
281 pub fn add_appended(&mut self, lines: Vec<TextSpan>) {
282 for (idx, l) in lines.into_iter().enumerate() {
283 if idx == 0 {
284 self.add_line(l);
285 } else {
286 self.append(l);
287 }
288 }
289 }
290
291 pub fn append_all(&mut self, lines: Vec<TextSpan>) {
292 for l in lines {
293 self.append(l);
294 }
295 }
296
297 pub fn remove_colors_from_last_line(&mut self) {
298 let (_, spans) = self.lines.last_mut().unwrap();
299 for span in spans {
300 span.fg_color = None;
301 span.outline_color = None;
302 }
303 }
304
305 pub fn is_empty(&self) -> bool {
306 self.lines.is_empty()
307 }
308
309 pub fn extend(&mut self, other: Text) {
310 self.lines.extend(other.lines);
311 }
312
313 pub(crate) fn dims(self, assets: &Assets) -> ScreenDims {
314 self.render(assets).get_dims()
315 }
316
317 pub fn rendered_width<A: AsRef<Assets>>(self, assets: &A) -> f64 {
318 self.dims(assets.as_ref()).width
319 }
320
321 pub fn render<A: AsRef<Assets>>(self, assets: &A) -> GeomBatch {
323 let assets: &Assets = assets.as_ref();
324 self.inner_render(assets, svg::HIGH_QUALITY)
325 }
326
327 pub(crate) fn inner_render(self, assets: &Assets, tolerance: f32) -> GeomBatch {
328 let hash_key = self.hash_key();
329 if let Some(batch) = assets.get_cached_text(&hash_key) {
330 return batch;
331 }
332
333 let mut output_batch = GeomBatch::new();
334 let mut master_batch = GeomBatch::new();
335
336 let mut y = 0.0;
337 let mut max_width = 0.0_f64;
338 for (line_color, line) in self.lines {
341 let mut line_height = 0.0_f64;
344 for span in &line {
345 line_height = line_height.max(assets.line_height(span.font, span.size));
346 }
347
348 let line_batch = render_line(line, tolerance, assets);
349 let line_dims = if line_batch.is_empty() {
350 ScreenDims::new(0.0, line_height)
351 } else {
352 ScreenDims::new(line_batch.get_dims().width + 5.0, line_height)
355 };
356
357 if let Some(c) = line_color {
358 master_batch.push(
359 c,
360 Polygon::rectangle(line_dims.width, line_dims.height).translate(0.0, y),
361 );
362 }
363
364 y += line_dims.height;
365
366 let offset = line_height / SCALE_LINE_HEIGHT * 0.2;
368 master_batch.append(line_batch.translate(0.0, y - offset));
369
370 max_width = max_width.max(line_dims.width);
371 }
372
373 if let Some(c) = self.bg_color {
374 output_batch.push(c, Polygon::rectangle(max_width, y));
375 }
376 output_batch.append(master_batch);
377 output_batch.autocrop_dims = false;
378
379 assets.cache_text(hash_key, output_batch.clone());
380 output_batch
381 }
382
383 pub fn render_autocropped<A: AsRef<Assets>>(self, assets: &A) -> GeomBatch {
386 let mut batch = self.render(assets);
387 batch.autocrop_dims = true;
388 batch.autocrop()
389 }
390
391 fn hash_key(&self) -> String {
392 let mut hasher = DefaultHasher::new();
393 hasher.write(format!("{:?}", self).as_ref());
394 format!("{:x}", hasher.finish())
395 }
396
397 pub fn into_widget(self, ctx: &EventCtx) -> Widget {
398 JustDraw::wrap(ctx, self.render(ctx))
399 }
400 pub fn batch(self, ctx: &EventCtx) -> Widget {
401 DeferDraw::new_widget(self.render(ctx))
402 }
403
404 pub fn wrap_to_pct(self, ctx: &EventCtx, pct: usize) -> Text {
405 self.wrap_to_pixels(ctx, (pct as f64) / 100.0 * ctx.canvas.window_width)
406 }
407
408 pub fn wrap_to_pixels(self, ctx: &EventCtx, limit: f64) -> Text {
409 self.inner_wrap_to_pixels(limit, &ctx.prerender.assets)
410 }
411
412 pub(crate) fn inner_wrap_to_pixels(mut self, limit: f64, assets: &Assets) -> Text {
413 let mut lines = Vec::new();
414 for (bg, spans) in self.lines.drain(..) {
415 if render_line(spans.clone(), svg::LOW_QUALITY, assets)
417 .get_dims()
418 .width
419 < limit
420 {
421 lines.push((bg, spans));
422 continue;
423 }
424
425 let mut width_left = limit;
428 let mut current_line = Vec::new();
429 for span in spans {
430 let mut current_span = span.clone();
431 current_span.text = String::new();
432 for word in span.text.split_whitespace() {
433 let width = render_line(
434 vec![TextSpan {
435 text: word.to_string(),
436 size: span.size,
437 font: span.font,
438 fg_color: span.fg_color,
439 outline_color: span.outline_color,
440 underlined: span.underlined,
441 }],
442 svg::LOW_QUALITY,
443 assets,
444 )
445 .get_dims()
446 .width;
447 if width_left > width {
448 current_span.text.push(' ');
449 current_span.text.push_str(word);
450 width_left -= width;
451 } else {
452 current_line.push(current_span);
453 lines.push((bg, current_line.drain(..).collect()));
454
455 current_span = span.clone();
456 current_span.text = word.to_string();
457 width_left = limit;
458 }
459 }
460 if !current_span.text.is_empty() {
461 current_line.push(current_span);
462 }
463 }
464 if !current_line.is_empty() {
465 lines.push((bg, current_line));
466 }
467 }
468 self.lines = lines;
469 self
470 }
471}
472
473fn render_line(spans: Vec<TextSpan>, tolerance: f32, assets: &Assets) -> GeomBatch {
474 let mut svg = r##"<svg width="9999" height="9999" viewBox="0 0 9999 9999" xmlns="http://www.w3.org/2000/svg">"##.to_string();
476
477 write!(&mut svg, r##"<text x="0" y="0" xml:space="preserve">"##,).unwrap();
478
479 let mut contents = String::new();
480 for span in spans {
481 let fg_color = span.fg_color_for_style(&assets.style.borrow());
482 write!(
483 &mut contents,
484 r##"<tspan font-size="{}" font-family="{}" {} fill="{}" fill-opacity="{}" {}{}>{}</tspan>"##,
485 span.size,
486 span.font.family(),
487 match span.font {
488 Font::OverpassBold => "font-weight=\"bold\"",
489 Font::OverpassSemiBold => "font-weight=\"600\"",
490 _ => "",
491 },
492 fg_color.as_hex(),
493 fg_color.a,
494 if span.underlined {
495 "text-decoration=\"underline\""
496 } else {
497 ""
498 },
499 if let Some(c) = span.outline_color {
500 format!("stroke=\"{}\"", c.as_hex())
501 } else {
502 String::new()
503 },
504 htmlescape::encode_minimal(&span.text)
505 )
506 .unwrap();
507 }
508 write!(&mut svg, "{}</text></svg>", contents).unwrap();
509
510 let mut svg_tree = match usvg::Tree::from_str(&svg, &usvg::Options::default()) {
511 Ok(t) => t,
512 Err(err) => panic!("render_line({}): {}", contents, err),
513 };
514 svg_tree.convert_text(&assets.fontdb.borrow());
515 let mut batch = GeomBatch::new();
516 match crate::svg::add_svg_inner(&mut batch, svg_tree, tolerance) {
517 Ok(_) => batch,
518 Err(err) => {
519 error!("render_line({}): {}", contents, err);
520 batch
522 }
523 }
524}
525
526pub trait TextExt {
527 fn text_widget(self, ctx: &EventCtx) -> Widget;
528 fn batch_text(self, ctx: &EventCtx) -> Widget;
529}
530
531impl TextExt for &str {
532 fn text_widget(self, ctx: &EventCtx) -> Widget {
533 Line(self).into_widget(ctx)
534 }
535 fn batch_text(self, ctx: &EventCtx) -> Widget {
536 Line(self).batch(ctx)
537 }
538}
539
540impl TextExt for String {
541 fn text_widget(self, ctx: &EventCtx) -> Widget {
542 Line(self).into_widget(ctx)
543 }
544 fn batch_text(self, ctx: &EventCtx) -> Widget {
545 Line(self).batch(ctx)
546 }
547}
548
549impl TextSpan {
550 pub fn render_curvey<A: AsRef<Assets>>(
552 self,
553 assets: &A,
554 path: &PolyLine,
555 scale: f64,
556 ) -> GeomBatch {
557 let assets = assets.as_ref();
558 let tolerance = svg::HIGH_QUALITY;
559 let mut stroke_parameters = String::new();
560
561 if let Some(c) = self.outline_color {
562 stroke_parameters = format!("stroke=\"{}\" stroke-width=\".1\"", c.as_hex());
563 };
564
565 let mut svg = r##"<svg width="9999" height="9999" viewBox="0 0 9999 9999" xmlns="http://www.w3.org/2000/svg">"##.to_string();
567
568 write!(
569 &mut svg,
570 r##"<path id="txtpath" fill="none" stroke="none" d=""##
571 )
572 .unwrap();
573 write!(
574 &mut svg,
575 "M {} {}",
576 path.points()[0].x(),
577 path.points()[0].y()
578 )
579 .unwrap();
580 for pt in path.points().iter().skip(1) {
581 write!(&mut svg, " L {} {}", pt.x(), pt.y()).unwrap();
582 }
583 write!(&mut svg, "\" />").unwrap();
584 let start_offset = (path.length().inner_meters()
586 - scale * Text::from(&self.text).rendered_width(&assets))
587 / 2.0;
588
589 let fg_color = self.fg_color_for_style(&assets.style.borrow());
590
591 write!(
592 &mut svg,
593 r##"<text xml:space="preserve" font-size="{}" font-family="{}" {} fill="{}" fill-opacity="{}" startOffset="{}" {}>"##,
594 (self.size as f64) * scale,
597 self.font.family(),
598 match self.font {
599 Font::OverpassBold => "font-weight=\"bold\"",
600 Font::OverpassSemiBold => "font-weight=\"600\"",
601 _ => "",
602 },
603 fg_color.as_hex(),
604 fg_color.a,
605 start_offset,
606 stroke_parameters,
607 )
608 .unwrap();
609
610 write!(
611 &mut svg,
612 r##"<textPath href="#txtpath">{}</textPath></text></svg>"##,
613 htmlescape::encode_minimal(&self.text)
614 )
615 .unwrap();
616
617 let mut svg_tree = match usvg::Tree::from_str(&svg, &usvg::Options::default()) {
618 Ok(t) => t,
619 Err(err) => panic!("curvey({}): {}", self.text, err),
620 };
621 svg_tree.convert_text(&assets.fontdb.borrow());
622 let mut batch = GeomBatch::new();
623 match crate::svg::add_svg_inner(&mut batch, svg_tree, tolerance) {
624 Ok(_) => batch,
625 Err(err) => {
626 error!("render_curvey({}): {}", self.text, err);
627 batch
628 }
629 }
630 }
631}