1 use crate::assets::Assets;
2 use crate::{
3     svg, Color, DeferDraw, EventCtx, GeomBatch, GfxCtx, JustDraw, MultiKey, Prerender, ScreenDims,
4     Widget,
5 };
6 use geom::{PolyLine, Polygon};
7 use std::collections::hash_map::DefaultHasher;
8 use std::fmt::Write;
9 use std::hash::Hasher;
10 
11 // Same as body()
12 pub const DEFAULT_FONT: Font = Font::OverpassRegular;
13 pub const DEFAULT_FONT_SIZE: usize = 21;
14 const DEFAULT_FG_COLOR: Color = Color::WHITE;
15 
16 pub const BG_COLOR: Color = Color::grey(0.3);
17 pub const SELECTED_COLOR: Color = Color::grey(0.5);
18 pub const INACTIVE_CHOICE_COLOR: Color = Color::grey(0.8);
19 pub const SCALE_LINE_HEIGHT: f64 = 1.2;
20 
21 // TODO Almost gone!
22 pub const MAX_CHAR_WIDTH: f64 = 25.0;
23 
24 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25 pub enum Font {
26     BungeeInlineRegular,
27     BungeeRegular,
28     OverpassBold,
29     OverpassRegular,
30     OverpassSemiBold,
31     OverpassMonoBold,
32     ZcoolXiaoWei,
33 }
34 
35 impl Font {
family(self) -> &'static str36     pub fn family(self) -> &'static str {
37         match self {
38             Font::BungeeInlineRegular => "Bungee Inline",
39             Font::BungeeRegular => "Bungee",
40             Font::OverpassBold => "Overpass",
41             Font::OverpassRegular => "Overpass",
42             Font::OverpassSemiBold => "Overpass",
43             Font::OverpassMonoBold => "Overpass Mono",
44             Font::ZcoolXiaoWei => "ZCOOL XiaoWei",
45         }
46     }
47 }
48 
49 #[derive(Debug, Clone)]
50 pub struct TextSpan {
51     text: String,
52     fg_color: Color,
53     size: usize,
54     font: Font,
55     underlined: bool,
56 }
57 
58 impl TextSpan {
fg(mut self, color: Color) -> TextSpan59     pub fn fg(mut self, color: Color) -> TextSpan {
60         assert_eq!(self.fg_color, DEFAULT_FG_COLOR);
61         self.fg_color = color;
62         self
63     }
64 
draw(self, ctx: &EventCtx) -> Widget65     pub fn draw(self, ctx: &EventCtx) -> Widget {
66         Text::from(self).draw(ctx)
67     }
batch(self, ctx: &EventCtx) -> Widget68     pub fn batch(self, ctx: &EventCtx) -> Widget {
69         Text::from(self).batch(ctx)
70     }
71 
72     // Yuwen's new styles, defined in Figma. Should document them in Github better.
73 
display_title(mut self) -> TextSpan74     pub fn display_title(mut self) -> TextSpan {
75         self.font = Font::BungeeInlineRegular;
76         self.size = 64;
77         self
78     }
big_heading_styled(mut self) -> TextSpan79     pub fn big_heading_styled(mut self) -> TextSpan {
80         self.font = Font::BungeeRegular;
81         self.size = 32;
82         self
83     }
big_heading_plain(mut self) -> TextSpan84     pub fn big_heading_plain(mut self) -> TextSpan {
85         self.font = Font::OverpassBold;
86         self.size = 32;
87         self
88     }
small_heading(mut self) -> TextSpan89     pub fn small_heading(mut self) -> TextSpan {
90         self.font = Font::OverpassSemiBold;
91         self.size = 26;
92         self
93     }
94     // The default
body(mut self) -> TextSpan95     pub fn body(mut self) -> TextSpan {
96         self.font = Font::OverpassRegular;
97         self.size = 21;
98         self
99     }
secondary(mut self) -> TextSpan100     pub fn secondary(mut self) -> TextSpan {
101         self.font = Font::OverpassRegular;
102         self.size = 21;
103         self.fg_color = Color::hex("#A3A3A3");
104         self
105     }
small(mut self) -> TextSpan106     pub fn small(mut self) -> TextSpan {
107         self.font = Font::OverpassRegular;
108         self.size = 16;
109         self
110     }
big_monospaced(mut self) -> TextSpan111     pub fn big_monospaced(mut self) -> TextSpan {
112         self.font = Font::OverpassMonoBold;
113         self.size = 32;
114         self
115     }
small_monospaced(mut self) -> TextSpan116     pub fn small_monospaced(mut self) -> TextSpan {
117         self.font = Font::OverpassMonoBold;
118         self.size = 16;
119         self
120     }
121 
underlined(mut self) -> TextSpan122     pub fn underlined(mut self) -> TextSpan {
123         self.underlined = true;
124         self
125     }
126 }
127 
128 // TODO What's the better way of doing this? Also "Line" is a bit of a misnomer
129 #[allow(non_snake_case)]
Line<S: Into<String>>(text: S) -> TextSpan130 pub fn Line<S: Into<String>>(text: S) -> TextSpan {
131     TextSpan {
132         text: text.into(),
133         fg_color: DEFAULT_FG_COLOR,
134         size: DEFAULT_FONT_SIZE,
135         font: DEFAULT_FONT,
136         underlined: false,
137     }
138 }
139 
140 #[derive(Debug, Clone)]
141 pub struct Text {
142     // The bg_color will cover the entire block, but some lines can have extra highlighting.
143     lines: Vec<(Option<Color>, Vec<TextSpan>)>,
144     // TODO Stop using this as much as possible.
145     bg_color: Option<Color>,
146 }
147 
148 impl Text {
new() -> Text149     pub fn new() -> Text {
150         Text {
151             lines: Vec::new(),
152             bg_color: None,
153         }
154     }
155 
from(line: TextSpan) -> Text156     pub fn from(line: TextSpan) -> Text {
157         let mut txt = Text::new();
158         txt.add(line);
159         txt
160     }
161 
from_all(lines: Vec<TextSpan>) -> Text162     pub fn from_all(lines: Vec<TextSpan>) -> Text {
163         let mut txt = Text::new();
164         for l in lines {
165             txt.append(l);
166         }
167         txt
168     }
169 
from_multiline(lines: Vec<TextSpan>) -> Text170     pub fn from_multiline(lines: Vec<TextSpan>) -> Text {
171         let mut txt = Text::new();
172         for l in lines {
173             txt.add(l);
174         }
175         txt
176     }
177 
178     // TODO Remove this
with_bg(mut self) -> Text179     pub fn with_bg(mut self) -> Text {
180         assert!(self.bg_color.is_none());
181         self.bg_color = Some(BG_COLOR);
182         self
183     }
184 
bg(mut self, bg: Color) -> Text185     pub fn bg(mut self, bg: Color) -> Text {
186         assert!(self.bg_color.is_none());
187         self.bg_color = Some(bg);
188         self
189     }
190 
191     // TODO Not exactly sure this is the right place for this, but better than code duplication
tooltip(ctx: &EventCtx, hotkey: Option<MultiKey>, action: &str) -> Text192     pub fn tooltip(ctx: &EventCtx, hotkey: Option<MultiKey>, action: &str) -> Text {
193         if let Some(ref key) = hotkey {
194             Text::from_all(vec![
195                 Line(key.describe()).fg(ctx.style().hotkey_color).small(),
196                 Line(format!(" - {}", action)).small(),
197             ])
198         } else {
199             Text::from(Line(action).small())
200         }
201     }
202 
change_fg(mut self, fg: Color) -> Text203     pub fn change_fg(mut self, fg: Color) -> Text {
204         for (_, spans) in self.lines.iter_mut() {
205             for span in spans {
206                 span.fg_color = fg;
207             }
208         }
209         self
210     }
211 
add(&mut self, line: TextSpan)212     pub fn add(&mut self, line: TextSpan) {
213         self.lines.push((None, vec![line]));
214     }
215 
add_highlighted(&mut self, line: TextSpan, highlight: Color)216     pub fn add_highlighted(&mut self, line: TextSpan, highlight: Color) {
217         self.lines.push((Some(highlight), vec![line]));
218     }
219 
220     // TODO Just one user...
highlight_last_line(&mut self, highlight: Color)221     pub(crate) fn highlight_last_line(&mut self, highlight: Color) {
222         self.lines.last_mut().unwrap().0 = Some(highlight);
223     }
224 
append(&mut self, line: TextSpan)225     pub fn append(&mut self, line: TextSpan) {
226         if self.lines.is_empty() {
227             self.add(line);
228             return;
229         }
230 
231         // Can't override the size or font mid-line.
232         let last = self.lines.last().unwrap().1.last().unwrap();
233         assert_eq!(line.size, last.size);
234         assert_eq!(line.font, last.font);
235 
236         self.lines.last_mut().unwrap().1.push(line);
237     }
238 
add_appended(&mut self, lines: Vec<TextSpan>)239     pub fn add_appended(&mut self, lines: Vec<TextSpan>) {
240         for (idx, l) in lines.into_iter().enumerate() {
241             if idx == 0 {
242                 self.add(l);
243             } else {
244                 self.append(l);
245             }
246         }
247     }
248 
append_all(&mut self, lines: Vec<TextSpan>)249     pub fn append_all(&mut self, lines: Vec<TextSpan>) {
250         for l in lines {
251             self.append(l);
252         }
253     }
254 
is_empty(&self) -> bool255     pub fn is_empty(&self) -> bool {
256         self.lines.is_empty()
257     }
258 
extend(&mut self, other: Text)259     pub fn extend(&mut self, other: Text) {
260         self.lines.extend(other.lines);
261     }
262 
dims(self, assets: &Assets) -> ScreenDims263     pub(crate) fn dims(self, assets: &Assets) -> ScreenDims {
264         self.render(assets).get_dims()
265     }
266 
render(self, assets: &Assets) -> GeomBatch267     pub fn render(self, assets: &Assets) -> GeomBatch {
268         self.inner_render(assets, svg::HIGH_QUALITY)
269     }
270 
render_g(self, g: &GfxCtx) -> GeomBatch271     pub fn render_g(self, g: &GfxCtx) -> GeomBatch {
272         self.render(&g.prerender.assets)
273     }
render_ctx(self, ctx: &EventCtx) -> GeomBatch274     pub fn render_ctx(self, ctx: &EventCtx) -> GeomBatch {
275         self.render(&ctx.prerender.assets)
276     }
277 
inner_render(self, assets: &Assets, tolerance: f32) -> GeomBatch278     pub(crate) fn inner_render(self, assets: &Assets, tolerance: f32) -> GeomBatch {
279         let hash_key = self.hash_key();
280         if let Some(batch) = assets.get_cached_text(&hash_key) {
281             return batch;
282         }
283 
284         let mut output_batch = GeomBatch::new();
285         let mut master_batch = GeomBatch::new();
286 
287         let mut y = 0.0;
288         let mut max_width = 0.0_f64;
289         for (line_color, line) in self.lines {
290             // Assume size doesn't change mid-line. Always use this fixed line height per font
291             // size.
292             let line_height = assets.line_height(line[0].font, line[0].size);
293 
294             let line_batch = render_line(line, tolerance, assets);
295             let line_dims = if line_batch.is_empty() {
296                 ScreenDims::new(0.0, line_height)
297             } else {
298                 // Also lie a little about width to make things look reasonable. TODO Probably
299                 // should tune based on font size.
300                 ScreenDims::new(line_batch.get_dims().width + 5.0, line_height)
301             };
302 
303             if let Some(c) = line_color {
304                 master_batch.push(
305                     c,
306                     Polygon::rectangle(line_dims.width, line_dims.height).translate(0.0, y),
307                 );
308             }
309 
310             y += line_dims.height;
311 
312             // Add all of the padding at the bottom of the line.
313             let offset = line_height / SCALE_LINE_HEIGHT * 0.2;
314             master_batch.append(line_batch.translate(0.0, y - offset));
315 
316             max_width = max_width.max(line_dims.width);
317         }
318 
319         if let Some(c) = self.bg_color {
320             output_batch.push(c, Polygon::rectangle(max_width, y));
321         }
322         output_batch.append(master_batch);
323         output_batch.autocrop_dims = false;
324 
325         assets.cache_text(hash_key, output_batch.clone());
326         output_batch
327     }
328 
render_to_batch(self, prerender: &Prerender) -> GeomBatch329     pub fn render_to_batch(self, prerender: &Prerender) -> GeomBatch {
330         let mut batch = self.render(&prerender.assets);
331         batch.autocrop_dims = true;
332         batch.autocrop()
333     }
334 
hash_key(&self) -> String335     fn hash_key(&self) -> String {
336         let mut hasher = DefaultHasher::new();
337         hasher.write(format!("{:?}", self).as_ref());
338         format!("{:x}", hasher.finish())
339     }
340 
draw(self, ctx: &EventCtx) -> Widget341     pub fn draw(self, ctx: &EventCtx) -> Widget {
342         JustDraw::wrap(ctx, self.render_ctx(ctx))
343     }
batch(self, ctx: &EventCtx) -> Widget344     pub fn batch(self, ctx: &EventCtx) -> Widget {
345         DeferDraw::new(self.render_ctx(ctx))
346     }
347 
wrap_to_pct(self, ctx: &EventCtx, pct: usize) -> Text348     pub fn wrap_to_pct(self, ctx: &EventCtx, pct: usize) -> Text {
349         self.inner_wrap_to_pct(
350             (pct as f64) / 100.0 * ctx.canvas.window_width,
351             &ctx.prerender.assets,
352         )
353     }
354 
inner_wrap_to_pct(mut self, limit: f64, assets: &Assets) -> Text355     pub(crate) fn inner_wrap_to_pct(mut self, limit: f64, assets: &Assets) -> Text {
356         let mut lines = Vec::new();
357         for (bg, spans) in self.lines.drain(..) {
358             // First optimistically assume everything just fits.
359             if render_line(spans.clone(), svg::LOW_QUALITY, assets)
360                 .get_dims()
361                 .width
362                 < limit
363             {
364                 lines.push((bg, spans));
365                 continue;
366             }
367 
368             // Greedy approach, fit as many words on a line as possible. Don't do all of that
369             // hyphenation nonsense.
370             let mut width_left = limit;
371             let mut current_line = Vec::new();
372             for span in spans {
373                 let mut current_span = span.clone();
374                 current_span.text = String::new();
375                 for word in span.text.split_whitespace() {
376                     let width = render_line(
377                         vec![TextSpan {
378                             text: word.to_string(),
379                             size: span.size,
380                             font: span.font,
381                             fg_color: span.fg_color,
382                             underlined: false,
383                         }],
384                         svg::LOW_QUALITY,
385                         assets,
386                     )
387                     .get_dims()
388                     .width;
389                     if width_left > width {
390                         current_span.text.push(' ');
391                         current_span.text.push_str(word);
392                         width_left -= width;
393                     } else {
394                         current_line.push(current_span);
395                         lines.push((bg, current_line.drain(..).collect()));
396 
397                         current_span = span.clone();
398                         current_span.text = word.to_string();
399                         width_left = limit;
400                     }
401                 }
402                 if !current_span.text.is_empty() {
403                     current_line.push(current_span);
404                 }
405             }
406             if !current_line.is_empty() {
407                 lines.push((bg, current_line));
408             }
409         }
410         self.lines = lines;
411         self
412     }
413 }
414 
render_line(spans: Vec<TextSpan>, tolerance: f32, assets: &Assets) -> GeomBatch415 fn render_line(spans: Vec<TextSpan>, tolerance: f32, assets: &Assets) -> GeomBatch {
416     // TODO This assumes size and font don't change mid-line. We might be able to support that now,
417     // actually.
418     // https://www.oreilly.com/library/view/svg-text-layout/9781491933817/ch04.html
419 
420     // Just set a sufficiently large view box
421     let mut svg = r##"<svg width="9999" height="9999" viewBox="0 0 9999 9999" xmlns="http://www.w3.org/2000/svg">"##.to_string();
422 
423     write!(
424         &mut svg,
425         r##"<text x="0" y="0" xml:space="preserve" font-size="{}" font-family="{}" {}>"##,
426         spans[0].size,
427         spans[0].font.family(),
428         match spans[0].font {
429             Font::OverpassBold => "font-weight=\"bold\"",
430             Font::OverpassSemiBold => "font-weight=\"600\"",
431             _ => "",
432         }
433     )
434     .unwrap();
435 
436     let mut contents = String::new();
437     for span in spans {
438         write!(
439             &mut contents,
440             r##"<tspan fill="{}" {}>{}</tspan>"##,
441             // TODO Doesn't support alpha
442             span.fg_color.to_hex(),
443             if span.underlined {
444                 "text-decoration=\"underline\""
445             } else {
446                 ""
447             },
448             htmlescape::encode_minimal(&span.text)
449         )
450         .unwrap();
451     }
452     write!(&mut svg, "{}</text></svg>", contents).unwrap();
453 
454     let svg_tree = match usvg::Tree::from_str(&svg, &assets.text_opts) {
455         Ok(t) => t,
456         Err(err) => panic!("render_line({}): {}", contents, err),
457     };
458     let mut batch = GeomBatch::new();
459     match crate::svg::add_svg_inner(&mut batch, svg_tree, tolerance) {
460         Ok(_) => batch,
461         Err(err) => panic!("render_line({}): {}", contents, err),
462     }
463 }
464 
465 pub trait TextExt {
draw_text(self, ctx: &EventCtx) -> Widget466     fn draw_text(self, ctx: &EventCtx) -> Widget;
batch_text(self, ctx: &EventCtx) -> Widget467     fn batch_text(self, ctx: &EventCtx) -> Widget;
468 }
469 
470 impl TextExt for &str {
draw_text(self, ctx: &EventCtx) -> Widget471     fn draw_text(self, ctx: &EventCtx) -> Widget {
472         Line(self).draw(ctx)
473     }
batch_text(self, ctx: &EventCtx) -> Widget474     fn batch_text(self, ctx: &EventCtx) -> Widget {
475         Line(self).batch(ctx)
476     }
477 }
478 
479 impl TextExt for String {
draw_text(self, ctx: &EventCtx) -> Widget480     fn draw_text(self, ctx: &EventCtx) -> Widget {
481         Line(self).draw(ctx)
482     }
batch_text(self, ctx: &EventCtx) -> Widget483     fn batch_text(self, ctx: &EventCtx) -> Widget {
484         Line(self).batch(ctx)
485     }
486 }
487 
488 impl TextSpan {
489     // TODO Copies from render_line a fair amount
render_curvey(self, prerender: &Prerender, path: &PolyLine, scale: f64) -> GeomBatch490     pub fn render_curvey(self, prerender: &Prerender, path: &PolyLine, scale: f64) -> GeomBatch {
491         let assets = &prerender.assets;
492         let tolerance = svg::HIGH_QUALITY;
493 
494         // Just set a sufficiently large view box
495         let mut svg = r##"<svg width="9999" height="9999" viewBox="0 0 9999 9999" xmlns="http://www.w3.org/2000/svg">"##.to_string();
496 
497         write!(
498             &mut svg,
499             r##"<path id="txtpath" fill="none" stroke="none" d=""##
500         )
501         .unwrap();
502         write!(
503             &mut svg,
504             "M {} {}",
505             path.points()[0].x(),
506             path.points()[0].y()
507         )
508         .unwrap();
509         for pt in path.points().into_iter().skip(1) {
510             write!(&mut svg, " L {} {}", pt.x(), pt.y()).unwrap();
511         }
512         write!(&mut svg, "\" />").unwrap();
513         // We need to subtract and account for the length of the text
514         let start_offset = (path.length() / 2.0).inner_meters()
515             - (Text::from(Line(&self.text)).dims(assets).width * scale) / 2.0;
516         write!(
517             &mut svg,
518             r##"<text xml:space="preserve" font-size="{}" font-family="{}" {} fill="{}" startOffset="{}">"##,
519             // This is seemingly the easiest way to do this. We could .scale() the whole batch
520             // after, but then we have to re-translate it to the proper spot
521             (self.size as f64) * scale,
522             self.font.family(),
523             match self.font {
524                 Font::OverpassBold => "font-weight=\"bold\"",
525                 Font::OverpassSemiBold => "font-weight=\"600\"",
526                 _ => "",
527             },
528             self.fg_color.to_hex(),
529             start_offset,
530         )
531         .unwrap();
532 
533         write!(
534             &mut svg,
535             r##"<textPath href="#txtpath">{}</textPath></text></svg>"##,
536             self.text
537         )
538         .unwrap();
539 
540         let svg_tree = match usvg::Tree::from_str(&svg, &assets.text_opts) {
541             Ok(t) => t,
542             Err(err) => panic!("curvey({}): {}", self.text, err),
543         };
544         let mut batch = GeomBatch::new();
545         match crate::svg::add_svg_inner(&mut batch, svg_tree, tolerance) {
546             Ok(_) => batch,
547             Err(err) => panic!("curvey({}): {}", self.text, err),
548         }
549     }
550 }
551