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