1use crate::color::Color;
11use crate::geometry::{Coord, Corners, Point, Rect, Size};
12use crate::render::{Context as RenderContext, Path, PathBuilder, Raster, RasterBuilder};
13use anyhow::{Context, Error, anyhow};
14use euclid::default::{Box2D, Size2D, Transform2D, Vector2D};
15use euclid::{Angle, point2, size2, vec2};
16
17use serde::{Deserialize, Serialize};
18use std::collections::BTreeMap;
19use std::convert::TryFrom;
20use std::fs::File;
21use std::path::PathBuf;
22use std::slice;
23use std::str::FromStr;
24use ttf_parser::Face;
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
30#[allow(missing_docs)]
31pub enum DisplayRotation {
32 Deg0,
33 Deg90,
34 Deg180,
35 Deg270,
36}
37
38impl DisplayRotation {
39 pub fn transform(&self, target_size: &Size2D<Coord>) -> Transform2D<Coord> {
41 let w = target_size.width;
42 let h = target_size.height;
43 match self {
44 Self::Deg0 => Transform2D::identity(),
45 Self::Deg90 => Transform2D::from_array([0.0, -1.0, 1.0, 0.0, 0.0, h]),
46 Self::Deg180 => Transform2D::from_array([-1.0, 0.0, 0.0, -1.0, w, h]),
47 Self::Deg270 => Transform2D::from_array([0.0, 1.0, -1.0, 0.0, w, 0.0]),
48 }
49 }
50
51 pub fn inv_transform(&self, target_size: &Size2D<Coord>) -> Transform2D<Coord> {
53 let w = target_size.width;
54 let h = target_size.height;
55 match self {
56 Self::Deg0 => Transform2D::identity(),
57 Self::Deg90 => Transform2D::from_array([0.0, 1.0, -1.0, 0.0, h, 0.0]),
58 Self::Deg180 => Transform2D::from_array([-1.0, 0.0, 0.0, -1.0, w, h]),
59 Self::Deg270 => Transform2D::from_array([0.0, -1.0, 1.0, 0.0, 0.0, w]),
60 }
61 }
62}
63
64impl Default for DisplayRotation {
65 fn default() -> Self {
66 Self::Deg0
67 }
68}
69
70impl From<DisplayRotation> for Angle<Coord> {
71 fn from(display_rotation: DisplayRotation) -> Self {
72 let degrees = match display_rotation {
73 DisplayRotation::Deg0 => 0.0,
74 DisplayRotation::Deg90 => 90.0,
75 DisplayRotation::Deg180 => 180.0,
76 DisplayRotation::Deg270 => 270.0,
77 };
78 Angle::degrees(degrees)
79 }
80}
81
82impl TryFrom<u32> for DisplayRotation {
83 type Error = Error;
84
85 fn try_from(num: u32) -> Result<Self, Self::Error> {
86 match num {
87 0 => Ok(DisplayRotation::Deg0),
88 90 => Ok(DisplayRotation::Deg90),
89 180 => Ok(DisplayRotation::Deg180),
90 270 => Ok(DisplayRotation::Deg270),
91 _ => Err(anyhow!("Invalid DisplayRotation {}", num)),
92 }
93 }
94}
95
96impl FromStr for DisplayRotation {
97 type Err = Error;
98
99 fn from_str(s: &str) -> Result<Self, Self::Err> {
100 match s {
101 "0" => Ok(DisplayRotation::Deg0),
102 "90" => Ok(DisplayRotation::Deg90),
103 "180" => Ok(DisplayRotation::Deg180),
104 "270" => Ok(DisplayRotation::Deg270),
105 _ => Err(anyhow!("Invalid DisplayRotation {}", s)),
106 }
107 }
108}
109
110pub fn path_for_rectangle(bounds: &Rect, render_context: &mut RenderContext) -> Path {
112 let mut path_builder = render_context.path_builder().expect("path_builder");
113 path_builder
114 .move_to(bounds.origin)
115 .line_to(bounds.top_right())
116 .line_to(bounds.bottom_right())
117 .line_to(bounds.bottom_left())
118 .line_to(bounds.origin);
119 path_builder.build()
120}
121
122pub fn path_for_rounded_rectangle(
124 bounds: &Rect,
125 corner_radius: Coord,
126 render_context: &mut RenderContext,
127) -> Path {
128 let kappa = 4.0 / 3.0 * (std::f32::consts::PI / 8.0).tan();
129 let control_dist = kappa * corner_radius;
130
131 let top_left_arc_start = bounds.origin + vec2(0.0, corner_radius);
132 let top_left_arc_end = bounds.origin + vec2(corner_radius, 0.0);
133 let top_left_curve_center = bounds.origin + vec2(corner_radius, corner_radius);
134 let top_left_p1 = top_left_curve_center + vec2(-corner_radius, -control_dist);
135 let top_left_p2 = top_left_curve_center + vec2(-control_dist, -corner_radius);
136
137 let top_right = bounds.top_right();
138 let top_right_arc_start = top_right + vec2(-corner_radius, 0.0);
139 let top_right_arc_end = top_right + vec2(0.0, corner_radius);
140 let top_right_curve_center = top_right + vec2(-corner_radius, corner_radius);
141 let top_right_p1 = top_right_curve_center + vec2(control_dist, -corner_radius);
142 let top_right_p2 = top_right_curve_center + vec2(corner_radius, -control_dist);
143
144 let bottom_right = bounds.bottom_right();
145 let bottom_right_arc_start = bottom_right + vec2(0.0, -corner_radius);
146 let bottom_right_arc_end = bottom_right + vec2(-corner_radius, 0.0);
147 let bottom_right_curve_center = bottom_right + vec2(-corner_radius, -corner_radius);
148 let bottom_right_p1 = bottom_right_curve_center + vec2(corner_radius, control_dist);
149 let bottom_right_p2 = bottom_right_curve_center + vec2(control_dist, corner_radius);
150
151 let bottom_left = bounds.bottom_left();
152 let bottom_left_arc_start = bottom_left + vec2(corner_radius, 0.0);
153 let bottom_left_arc_end = bottom_left + vec2(0.0, -corner_radius);
154 let bottom_left_curve_center = bottom_left + vec2(corner_radius, -corner_radius);
155 let bottom_left_p1 = bottom_left_curve_center + vec2(-control_dist, corner_radius);
156 let bottom_left_p2 = bottom_left_curve_center + vec2(-corner_radius, control_dist);
157
158 let mut path_builder = render_context.path_builder().expect("path_builder");
159 path_builder
160 .move_to(top_left_arc_start)
161 .cubic_to(top_left_p1, top_left_p2, top_left_arc_end)
162 .line_to(top_right_arc_start)
163 .cubic_to(top_right_p1, top_right_p2, top_right_arc_end)
164 .line_to(bottom_right_arc_start)
165 .cubic_to(bottom_right_p1, bottom_right_p2, bottom_right_arc_end)
166 .line_to(bottom_left_arc_start)
167 .cubic_to(bottom_left_p1, bottom_left_p2, bottom_left_arc_end)
168 .line_to(top_left_arc_start);
169 path_builder.build()
170}
171
172pub fn path_for_circle(center: Point, radius: Coord, render_context: &mut RenderContext) -> Path {
174 let kappa = 4.0 / 3.0 * (std::f32::consts::PI / 8.0).tan();
175 let control_dist = kappa * radius;
176
177 let mut path_builder = render_context.path_builder().expect("path_builder");
178 let left = center + vec2(-radius, 0.0);
179 let top = center + vec2(0.0, -radius);
180 let right = center + vec2(radius, 0.0);
181 let bottom = center + vec2(0.0, radius);
182 let left_p1 = center + vec2(-radius, -control_dist);
183 let left_p2 = center + vec2(-control_dist, -radius);
184 let top_p1 = center + vec2(control_dist, -radius);
185 let top_p2 = center + vec2(radius, -control_dist);
186 let right_p1 = center + vec2(radius, control_dist);
187 let right_p2 = center + vec2(control_dist, radius);
188 let bottom_p1 = center + vec2(-control_dist, radius);
189 let bottom_p2 = center + vec2(-radius, control_dist);
190 path_builder
191 .move_to(left)
192 .cubic_to(left_p1, left_p2, top)
193 .cubic_to(top_p1, top_p2, right)
194 .cubic_to(right_p1, right_p2, bottom)
195 .cubic_to(bottom_p1, bottom_p2, left);
196 path_builder.build()
197}
198
199fn point_for_segment_index(
200 index: usize,
201 center: Point,
202 radius: Coord,
203 segment_angle: f32,
204) -> Point {
205 let angle = index as f32 * segment_angle;
206 let x = radius * angle.cos();
207 let y = radius * angle.sin();
208 center + vec2(x, y)
209}
210
211pub fn path_for_polygon(
213 center: Point,
214 radius: Coord,
215 segment_count: usize,
216 render_context: &mut RenderContext,
217) -> Path {
218 let segment_angle = (2.0 * std::f32::consts::PI) / segment_count as f32;
219 let mut path_builder = render_context.path_builder().expect("path_builder");
220 let first_point = point_for_segment_index(0, center, radius, segment_angle);
221 path_builder.move_to(first_point);
222 for index in 1..segment_count {
223 let pt = point_for_segment_index(index, center, radius, segment_angle);
224 path_builder.line_to(pt);
225 }
226 path_builder.line_to(first_point);
227 path_builder.build()
228}
229
230pub fn path_for_corner_knockouts(
233 bounds: &Rect,
234 corner_radius: Coord,
235 render_context: &mut RenderContext,
236) -> Path {
237 let kappa = 4.0 / 3.0 * (std::f32::consts::PI / 8.0).tan();
238 let control_dist = kappa * corner_radius;
239
240 let top_left = bounds.top_left();
241 let top_left_arc_start = bounds.origin + vec2(0.0, corner_radius);
242 let top_left_arc_end = bounds.origin + vec2(corner_radius, 0.0);
243 let top_left_curve_center = bounds.origin + vec2(corner_radius, corner_radius);
244 let top_left_p1 = top_left_curve_center + vec2(-corner_radius, -control_dist);
245 let top_left_p2 = top_left_curve_center + vec2(-control_dist, -corner_radius);
246
247 let top_right = bounds.top_right();
248 let top_right_arc_start = top_right + vec2(-corner_radius, 0.0);
249 let top_right_arc_end = top_right + vec2(0.0, corner_radius);
250 let top_right_curve_center = top_right + vec2(-corner_radius, corner_radius);
251 let top_right_p1 = top_right_curve_center + vec2(control_dist, -corner_radius);
252 let top_right_p2 = top_right_curve_center + vec2(corner_radius, -control_dist);
253
254 let bottom_right = bounds.bottom_right();
255 let bottom_right_arc_start = bottom_right + vec2(0.0, -corner_radius);
256 let bottom_right_arc_end = bottom_right + vec2(-corner_radius, 0.0);
257 let bottom_right_curve_center = bottom_right + vec2(-corner_radius, -corner_radius);
258 let bottom_right_p1 = bottom_right_curve_center + vec2(corner_radius, control_dist);
259 let bottom_right_p2 = bottom_right_curve_center + vec2(control_dist, corner_radius);
260
261 let bottom_left = bounds.bottom_left();
262 let bottom_left_arc_start = bottom_left + vec2(corner_radius, 0.0);
263 let bottom_left_arc_end = bottom_left + vec2(0.0, -corner_radius);
264 let bottom_left_curve_center = bottom_left + vec2(corner_radius, -corner_radius);
265 let bottom_left_p1 = bottom_left_curve_center + vec2(-control_dist, corner_radius);
266 let bottom_left_p2 = bottom_left_curve_center + vec2(-corner_radius, control_dist);
267
268 let mut path_builder = render_context.path_builder().expect("path_builder");
269 path_builder
270 .move_to(top_left)
271 .line_to(top_left_arc_start)
272 .cubic_to(top_left_p1, top_left_p2, top_left_arc_end)
273 .line_to(top_left)
274 .move_to(top_right)
275 .line_to(top_right_arc_start)
276 .cubic_to(top_right_p1, top_right_p2, top_right_arc_end)
277 .line_to(top_right)
278 .move_to(bottom_right)
279 .line_to(bottom_right_arc_start)
280 .cubic_to(bottom_right_p1, bottom_right_p2, bottom_right_arc_end)
281 .line_to(bottom_right)
282 .move_to(bottom_left)
283 .line_to(bottom_left_arc_start)
284 .cubic_to(bottom_left_p1, bottom_left_p2, bottom_left_arc_end)
285 .line_to(bottom_left);
286 path_builder.build()
287}
288
289pub fn path_for_cursor(hot_spot: Point, radius: Coord, render_context: &mut RenderContext) -> Path {
291 let kappa = 4.0 / 3.0 * (std::f32::consts::PI / 8.0).tan();
292 let control_dist = kappa * radius;
293 let mut path_builder = render_context.path_builder().expect("path_builder");
294 let center = hot_spot + vec2(radius, radius);
295 let left = center + vec2(-radius, 0.0);
296 let top = center + vec2(0.0, -radius);
297 let right = center + vec2(radius, 0.0);
298 let bottom = center + vec2(0.0, radius);
299 let top_p1 = center + vec2(control_dist, -radius);
300 let top_p2 = center + vec2(radius, -control_dist);
301 let right_p1 = center + vec2(radius, control_dist);
302 let right_p2 = center + vec2(control_dist, radius);
303 let bottom_p1 = center + vec2(-control_dist, radius);
304 let bottom_p2 = center + vec2(-radius, control_dist);
305 path_builder
306 .move_to(hot_spot)
307 .line_to(top)
308 .cubic_to(top_p1, top_p2, right)
309 .cubic_to(right_p1, right_p2, bottom)
310 .cubic_to(bottom_p1, bottom_p2, left)
311 .line_to(hot_spot);
312 path_builder.build()
313}
314
315#[derive(Hash, Eq, PartialEq, Debug, Clone, Copy)]
317pub struct Paint {
318 pub fg: Color,
320 pub bg: Color,
322}
323
324impl Paint {
325 pub fn from_hash_codes(fg: &str, bg: &str) -> Result<Paint, Error> {
327 Ok(Paint { fg: Color::from_hash_code(fg)?, bg: Color::from_hash_code(bg)? })
328 }
329}
330
331pub fn load_font(path: PathBuf) -> Result<FontFace, Error> {
333 let file = File::open(path).context("File::open")?;
334 let vmo = fdio::get_vmo_copy_from_file(&file).context("fdio::get_vmo_copy_from_file")?;
335 let size = file.metadata()?.len() as usize;
336 let root_vmar = fuchsia_runtime::vmar_root_self();
337 let address = root_vmar.map(
338 0,
339 &vmo,
340 0,
341 size,
342 zx::VmarFlags::PERM_READ | zx::VmarFlags::MAP_RANGE | zx::VmarFlags::REQUIRE_NON_RESIZABLE,
343 )?;
344
345 let mapped_font_data = unsafe { slice::from_raw_parts(address as *mut u8, size) };
346 Ok(FontFace::new(&*mapped_font_data)?)
347}
348
349#[derive(Clone)]
351pub struct FontFace {
352 pub face: Face<'static>,
354}
355
356impl FontFace {
357 pub fn new(data: &'static [u8]) -> Result<FontFace, Error> {
359 let face = Face::parse(data, 0)?;
360 Ok(FontFace { face })
361 }
362
363 pub fn ascent(&self, size: f32) -> f32 {
365 let ascent = self.face.ascender();
366 (ascent as f32 / self.face.units_per_em() as f32) * size
367 }
368
369 pub fn descent(&self, size: f32) -> f32 {
371 let descender = self.face.descender();
372 (descender as f32 / self.face.units_per_em() as f32) * size
373 }
374
375 pub fn capital_height(&self, size: f32) -> Option<f32> {
377 self.face
378 .capital_height()
379 .map(|capital_height| (capital_height as f32 / self.face.units_per_em() as f32) * size)
380 }
381}
382
383pub fn measure_text_width(face: &FontFace, font_size: f32, text: &str) -> f32 {
385 measure_text_size(face, font_size, text, false).width
386}
387
388pub fn measure_text_size(face: &FontFace, size: f32, text: &str, visual: bool) -> Size {
390 let mut bounding_box = Rect::zero();
391 let ascent = face.ascent(size);
392 let units_per_em = face.face.units_per_em();
393 let scale = size / units_per_em as f32;
394 let y_offset = vec2(0.0, ascent).to_i32();
395 let chars = text.chars();
396 let mut x: f32 = 0.0;
397
398 for c in chars {
399 if let Some(glyph_index) = face.face.glyph_index(c) {
400 let glyph_bounding_box = face
401 .face
402 .glyph_bounding_box(glyph_index)
403 .and_then(|bounding_box| Some(pixel_size_rect(face, size, bounding_box)))
404 .unwrap_or_else(Rect::zero);
405 let horizontal_advance = face.face.glyph_hor_advance(glyph_index).unwrap_or(0);
406 let w = horizontal_advance as f32 * scale;
407 let position = y_offset + vec2(x, 0.0).to_i32();
408 if !glyph_bounding_box.is_empty() {
409 let glyph_bounding_box = &glyph_bounding_box.translate(position.to_f32());
410
411 if bounding_box.is_empty() {
412 bounding_box = *glyph_bounding_box;
413 } else {
414 bounding_box = bounding_box.union(&glyph_bounding_box);
415 }
416 }
417
418 x += w;
419 }
420 }
421 if visual { bounding_box.size } else { size2(x, size) }
422}
423
424pub fn linebreak_text(face: &FontFace, font_size: f32, text: &str, max_width: f32) -> Vec<String> {
427 let space_width = measure_text_width(face, font_size, " ");
428 let mut lines = Vec::new();
429 let mut current_line = String::new();
430 let mut current_width = 0.0;
431
432 for word in text.split_whitespace() {
433 let word_width = measure_text_width(face, font_size, word);
434 if !current_line.is_empty() {
435 if current_width + space_width + word_width <= max_width {
436 current_line.push(' ');
437 current_line.push_str(word);
438 current_width += space_width + word_width;
439 continue;
440 }
441 lines.push(std::mem::take(&mut current_line));
442 current_width = 0.0;
443 }
444
445 if word_width <= max_width {
446 current_line.push_str(word);
447 current_width = word_width;
448 } else {
449 let mut buf = [0u8; 4];
450 for c in word.chars() {
451 let char_width = measure_text_width(face, font_size, c.encode_utf8(&mut buf));
452 if !current_line.is_empty() && current_width + char_width > max_width {
453 lines.push(std::mem::take(&mut current_line));
454 current_width = 0.0;
455 }
456 current_line.push(c);
457 current_width += char_width;
458 }
459 }
460 }
461
462 if !current_line.is_empty() || lines.is_empty() {
463 lines.push(current_line);
464 }
465
466 lines
467}
468
469fn pixel_size_rect(face: &FontFace, font_size: f32, value: ttf_parser::Rect) -> Rect {
470 let units_per_em = face.face.units_per_em();
471 let scale = font_size / units_per_em as f32;
472 let min_x = value.x_min as f32 * scale;
473 let max_y = -value.y_min as f32 * scale;
474 let max_x = value.x_max as f32 * scale;
475 let min_y = -value.y_max as f32 * scale;
476 Box2D::new(point2(min_x, min_y), point2(max_x, max_y)).to_rect()
477}
478
479fn scaled_point2(x: f32, y: f32, scale: &Vector2D<f32>) -> Point {
480 point2(x * scale.x, y * scale.y)
481}
482
483const ZERO_RECT: ttf_parser::Rect = ttf_parser::Rect { x_max: 0, x_min: 0, y_max: 0, y_min: 0 };
484
485struct GlyphBuilder<'a> {
486 scale: Vector2D<f32>,
487 offset: Vector2D<f32>,
488 context: &'a RenderContext,
489 path_builder: Option<PathBuilder>,
490 raster_builder: RasterBuilder,
491}
492
493impl<'a> GlyphBuilder<'a> {
494 fn new(scale: Vector2D<f32>, offset: Vector2D<f32>, context: &'a mut RenderContext) -> Self {
495 let path_builder = context.path_builder().expect("path_builder");
496 let raster_builder = context.raster_builder().expect("raster_builder");
497 Self { scale, offset, context, path_builder: Some(path_builder), raster_builder }
498 }
499
500 fn path_builder(&mut self) -> &mut PathBuilder {
501 self.path_builder.as_mut().expect("path_builder() PathBuilder")
502 }
503
504 fn raster(self) -> Raster {
505 self.raster_builder.build()
506 }
507}
508
509impl ttf_parser::OutlineBuilder for GlyphBuilder<'_> {
510 fn move_to(&mut self, x: f32, y: f32) {
511 let p = scaled_point2(x, -y, &self.scale) + self.offset;
512 self.path_builder().move_to(p);
513 }
514
515 fn line_to(&mut self, x: f32, y: f32) {
516 let p = scaled_point2(x, -y, &self.scale) + self.offset;
517 self.path_builder().line_to(p);
518 }
519
520 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
521 let p1 = scaled_point2(x1, -y1, &self.scale) + self.offset;
522 let p = scaled_point2(x, -y, &self.scale) + self.offset;
523 self.path_builder().quad_to(p1, p);
524 }
525
526 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
527 let p1 = scaled_point2(x1, -y1, &self.scale) + self.offset;
528 let p2 = scaled_point2(x2, -y2, &self.scale) + self.offset;
529 let p = scaled_point2(x, -y, &self.scale) + self.offset;
530 self.path_builder().cubic_to(p1, p2, p);
531 }
532
533 fn close(&mut self) {
534 let path_builder = self.path_builder.take().expect("take PathBuilder");
535 let path = path_builder.build();
536 self.raster_builder.add(&path, None);
537 let path_builder = self.context.path_builder().expect("path_builder");
538 self.path_builder = Some(path_builder);
539 }
540}
541
542#[derive(Debug)]
543#[allow(missing_docs)]
544pub struct Glyph {
545 pub raster: Raster,
546 pub bounding_box: Rect,
547}
548
549impl Glyph {
550 #[allow(missing_docs)]
551 pub fn new(
552 context: &mut RenderContext,
553 face: &FontFace,
554 size: f32,
555 id: Option<ttf_parser::GlyphId>,
556 ) -> Self {
557 let units_per_em = face.face.units_per_em();
558 let xy_scale = size / units_per_em as f32;
559 let scale = vec2(xy_scale, xy_scale);
560 let offset = Vector2D::zero();
561 if let Some(id) = id {
562 Self::with_scale_and_offset(context, face, scale, offset, id)
563 } else {
564 let path_builder = context.path_builder().expect("path_builder");
565 let path = path_builder.build();
566 let mut raster_builder = context.raster_builder().expect("raster_builder");
567 raster_builder.add(&path, None);
568 let raster = raster_builder.build();
569
570 Self { bounding_box: Rect::zero(), raster }
571 }
572 }
573
574 #[allow(missing_docs)]
575 pub fn with_scale_and_offset(
576 context: &mut RenderContext,
577 face: &FontFace,
578 scale: Vector2D<f32>,
579 offset: Vector2D<f32>,
580 id: ttf_parser::GlyphId,
581 ) -> Self {
582 let mut builder = GlyphBuilder::new(scale, offset, context);
583 let glyph_bounding_box = &face.face.outline_glyph(id, &mut builder).unwrap_or(ZERO_RECT);
584 let min_x = glyph_bounding_box.x_min as f32 * scale.x + offset.x;
585 let max_y = -glyph_bounding_box.y_min as f32 * scale.y + offset.y;
586 let max_x = glyph_bounding_box.x_max as f32 * scale.x + offset.x;
587 let min_y = -glyph_bounding_box.y_max as f32 * scale.y + offset.y;
588 let bounding_box = Box2D::new(point2(min_x, min_y), point2(max_x, max_y)).to_rect();
589
590 Self { bounding_box, raster: builder.raster() }
591 }
592}
593
594#[derive(Debug)]
595#[allow(missing_docs)]
596pub struct GlyphMap {
597 pub glyphs: BTreeMap<ttf_parser::GlyphId, Glyph>,
598}
599
600impl GlyphMap {
601 #[allow(missing_docs)]
602 pub fn new() -> Self {
603 Self { glyphs: BTreeMap::new() }
604 }
605}
606
607#[allow(missing_docs)]
608pub struct Text {
609 pub raster: Raster,
610 pub bounding_box: Rect,
611}
612
613impl Text {
614 #[allow(missing_docs)]
615 pub fn new_with_lines(
616 context: &mut RenderContext,
617 lines: &Vec<String>,
618 size: f32,
619 face: &FontFace,
620 glyph_map: &mut GlyphMap,
621 ) -> Self {
622 let glyphs = &mut glyph_map.glyphs;
623 let mut bounding_box = Rect::zero();
624 let mut raster_union = {
625 let raster_builder = context.raster_builder().unwrap();
626 raster_builder.build()
627 };
628 let ascent = face.ascent(size);
629 let descent = face.descent(size);
630 let units_per_em = face.face.units_per_em();
631 let scale = size / units_per_em as f32;
632 let mut y_offset = vec2(0.0, ascent).to_i32();
633 for line in lines.iter() {
634 let chars = line.chars();
635 let mut x: f32 = 0.0;
636
637 for c in chars {
638 if let Some(glyph_index) = face.face.glyph_index(c) {
639 let horizontal_advance = face.face.glyph_hor_advance(glyph_index).unwrap_or(0);
640 let w = horizontal_advance as f32 * scale;
641 let position = y_offset + vec2(x, 0.0).to_i32();
642 let glyph = glyphs
643 .entry(glyph_index)
644 .or_insert_with(|| Glyph::new(context, face, size, Some(glyph_index)));
645 if !glyph.bounding_box.is_empty() {
646 let raster = glyph
648 .raster
649 .clone()
650 .translate(position.cast_unit::<euclid::UnknownUnit>());
651 raster_union = raster_union + raster;
652
653 let glyph_bounding_box = &glyph.bounding_box.translate(position.to_f32());
655
656 if bounding_box.is_empty() {
657 bounding_box = *glyph_bounding_box;
658 } else {
659 bounding_box = bounding_box.union(&glyph_bounding_box);
660 }
661 }
662
663 x += w;
664 }
665 }
666 y_offset += vec2(0, (ascent - descent) as i32);
667 }
668
669 Self { raster: raster_union, bounding_box }
670 }
671
672 #[allow(missing_docs)]
673 pub fn new(
674 context: &mut RenderContext,
675 text: &str,
676 size: f32,
677 wrap: f32,
678 face: &FontFace,
679 glyph_map: &mut GlyphMap,
680 ) -> Self {
681 let lines = linebreak_text(face, size, text, wrap);
682 Self::new_with_lines(context, &lines, size, face, glyph_map)
683 }
684}
685
686#[allow(missing_docs)]
688pub struct TextGrid {
689 pub scale: Vector2D<f32>,
690 pub offset: Vector2D<f32>,
691 pub cell_size: Size,
692}
693
694impl TextGrid {
695 pub fn new(face: &FontFace, cell_size: &Size) -> Self {
699 let units_per_em = face.face.units_per_em() as f32;
700 let ascent = face.face.ascender() as f32 / units_per_em;
701 let descent = face.face.descender() as f32 / units_per_em;
702
703 let font_size = cell_size.height / (ascent - descent);
705 let y_scale = font_size / units_per_em;
706
707 let x_scale = face.face.glyph_index('0').map_or(y_scale, |glyph_index| {
712 let horizontal_advance =
713 face.face.glyph_hor_advance(glyph_index).expect("glyph_hor_advance") as f32
714 / units_per_em
715 * font_size;
716 y_scale * cell_size.width / horizontal_advance
717 });
718
719 let scale = vec2(x_scale, y_scale);
720 let offset = vec2(0.0, ascent * font_size);
723
724 Self { scale, offset, cell_size: *cell_size }
725 }
726}
727
728#[cfg(test)]
729mod tests {
730 use super::{GlyphMap, Size, Text, TextGrid};
731 use crate::drawing::{
732 DisplayRotation, FontFace, linebreak_text, measure_text_size, measure_text_width,
733 };
734 use crate::render::generic::{self, Backend};
735 use crate::render::{Context as RenderContext, ContextInner};
736 use euclid::approxeq::ApproxEq;
737 use euclid::{size2, vec2};
738 use fuchsia_async::{self as fasync, MonotonicInstant, TimeoutExt};
739 use fuchsia_framebuffer::FrameUsage;
740 use fuchsia_framebuffer::sysmem::BufferCollectionAllocator;
741 use std::sync::LazyLock;
742
743 const DEFAULT_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(5);
744
745 static FONT_DATA: &'static [u8] = include_bytes!(
748 "../../../../../prebuilt/third_party/fonts/robotoslab/RobotoSlab-Regular.ttf"
749 );
750 static FONT_FACE: LazyLock<FontFace> =
751 LazyLock::new(|| FontFace::new(&FONT_DATA).expect("Failed to create font"));
752
753 #[fasync::run_singlethreaded(test)]
754 async fn test_text_bounding_box() {
755 let size = size2(800, 800);
756 let mut buffer_allocator = BufferCollectionAllocator::new(
757 size.width,
758 size.height,
759 fidl_fuchsia_images2::PixelFormat::B8G8R8A8,
760 FrameUsage::Cpu,
761 3,
762 )
763 .expect("BufferCollectionAllocator::new");
764 let context_token = buffer_allocator
765 .duplicate_token()
766 .on_timeout(MonotonicInstant::after(DEFAULT_TIMEOUT), || {
767 panic!("Timed out while waiting for duplicate_token")
768 })
769 .await
770 .expect("token");
771 let forma_context = generic::Forma::new_context(context_token, size, DisplayRotation::Deg0);
772 let _buffers_result = buffer_allocator
773 .allocate_buffers(true)
774 .on_timeout(MonotonicInstant::after(DEFAULT_TIMEOUT), || {
775 panic!("Timed out while waiting for sysmem bufers")
776 })
777 .await;
778 let mut render_context = RenderContext { inner: ContextInner::Forma(forma_context) };
779 let mut glyphs = GlyphMap::new();
780 let text =
781 Text::new(&mut render_context, "Good Morning", 20.0, 200.0, &FONT_FACE, &mut glyphs);
782
783 let expected_origin = euclid::point2(0.5371094, 4.765625);
784 let expected_size = vec2(132.33594, 19.501953);
785 let epsilon_point = euclid::point2(1.0, 1.0);
786 let epsilon_vec = euclid::vec2(1.0, 1.0);
787 assert!(
788 text.bounding_box.origin.approx_eq_eps(&expected_origin, &epsilon_point),
789 "Expected bounding box origin to be close to {:?} but found {:?}",
790 expected_origin,
791 text.bounding_box.origin
792 );
793 assert!(
794 text.bounding_box.size.to_vector().approx_eq_eps(&expected_size, &epsilon_vec),
795 "Expected bounding box origin to be close to {:?} but found {:?}",
796 expected_size,
797 text.bounding_box.size
798 );
799 }
800
801 #[fasync::run_singlethreaded(test)]
802 async fn test_textgridcell() {
803 let cell_size = Size::new(16.0, 32.0);
804 let grid = TextGrid::new(&FONT_FACE, &Size::new(16.0, 32.0));
805 assert_eq!(grid.cell_size, cell_size);
806 assert!(grid.offset != vec2(0.0, 0.0));
807 assert!(grid.scale != vec2(0.0, 0.0));
808 }
809
810 #[test]
811 fn test_measure_text() {
812 assert_eq!(measure_text_size(&FONT_FACE, 32.0, "", false).width, 0.0);
813 assert_eq!(measure_text_size(&FONT_FACE, 32.0, "", true).width, 0.0);
814 assert!(measure_text_size(&FONT_FACE, 32.0, "ahoy", false).width > 0.0);
815 assert!(measure_text_size(&FONT_FACE, 32.0, "ahoy", true).width > 0.0);
816 }
817
818 #[test]
819 fn test_linebreak_text() {
820 assert_eq!(linebreak_text(&FONT_FACE, 20.0, "", 100.0), vec![""]);
821 assert_eq!(linebreak_text(&FONT_FACE, 20.0, "Good Morning", 200.0), vec!["Good Morning"]);
822 assert_eq!(
823 linebreak_text(&FONT_FACE, 20.0, "Good Morning Morning", 200.0),
824 vec!["Good Morning", "Morning"]
825 );
826
827 let morning_width = measure_text_width(&FONT_FACE, 20.0, "Morning");
828 assert_eq!(
829 linebreak_text(&FONT_FACE, 20.0, "Good Morning", morning_width + 1.0),
830 vec!["Good", "Morning"]
831 );
832
833 let long_word = "product/board:product_version/platform_version";
834 let max_width = 120.0;
835 let lines = linebreak_text(&FONT_FACE, 20.0, long_word, max_width);
836 assert!(lines.len() > 1);
837 assert_eq!(lines.concat(), long_word);
838 for line in &lines {
839 assert!(!line.is_empty());
840 assert!(measure_text_width(&FONT_FACE, 20.0, line) <= max_width);
841 }
842 for pair in lines.windows(2) {
843 let next_char = pair[1].chars().next().unwrap();
844 assert!(
845 measure_text_width(&FONT_FACE, 20.0, &format!("{}{next_char}", pair[0]))
846 > max_width
847 );
848 }
849
850 let last_chunk = lines.last().unwrap();
851 let combined_last = format!("{last_chunk} Morning");
852 let mut expected_mixed = vec!["Good".to_string()];
853 expected_mixed.extend_from_slice(&lines[..lines.len() - 1]);
854 if measure_text_width(&FONT_FACE, 20.0, &combined_last) <= max_width {
855 expected_mixed.push(combined_last);
856 } else {
857 expected_mixed.push(last_chunk.clone());
858 expected_mixed.push("Morning".to_string());
859 }
860 assert_eq!(
861 linebreak_text(&FONT_FACE, 20.0, &format!("Good {long_word} Morning"), max_width),
862 expected_mixed
863 );
864 }
865}