Skip to main content

carnelian/
drawing.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Functions for drawing in Carnelian
6//! Carnelian uses the Render abstraction over Forma and Spinel
7//! to put pixels on screen. The items in this module are higher-
8//! level drawing primitives.
9
10use 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/// Some Fuchsia device displays are mounted rotated. This value represents
27/// The supported rotations and can be used by views to rotate their content
28/// to display appropriately when running on the frame buffer.
29#[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    /// Create a transformation to accommodate the screen rotation.
40    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    /// Create a transformation to undo the screen rotation.
52    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
110/// Create a render path for the specified rectangle.
111pub 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
122/// Create a render path for the specified rounded rectangle.
123pub 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
172/// Create a render path for the specified circle.
173pub 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
211/// Create a render path for the specified polygon.
212pub 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
230/// Create a path for knocking out the points of a rectangle, giving it a
231/// rounded appearance.
232pub 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
289/// Create a render path for a fuchsia-style teardrop cursor.
290pub 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/// Struct combining a foreground and background color.
316#[derive(Hash, Eq, PartialEq, Debug, Clone, Copy)]
317pub struct Paint {
318    /// Color for foreground painting
319    pub fg: Color,
320    /// Color for background painting
321    pub bg: Color,
322}
323
324impl Paint {
325    /// Create a paint from a pair of hash codes
326    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
331/// Load a font from the provided path.
332pub 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/// Struct containing a font data.
350#[derive(Clone)]
351pub struct FontFace {
352    /// Font.
353    pub face: Face<'static>,
354}
355
356impl FontFace {
357    /// Create a new FontFace.
358    pub fn new(data: &'static [u8]) -> Result<FontFace, Error> {
359        let face = Face::parse(data, 0)?;
360        Ok(FontFace { face })
361    }
362
363    /// Get the ascent, in pixels, for this font at the specified size.
364    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    /// Get the descent, in pixels, for this font at the specified size.
370    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    /// Get the capital height, in pixels, for this font at the specified size.
376    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
383/// Return the width in pixels for the specified text, face and size.
384pub fn measure_text_width(face: &FontFace, font_size: f32, text: &str) -> f32 {
385    measure_text_size(face, font_size, text, false).width
386}
387
388/// Return the size in pixels for the specified text, face and size.
389pub 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
424/// Break up text into chunks guaranteed to be no wider than max_width when rendered with
425/// face at font_size.
426pub fn linebreak_text(face: &FontFace, font_size: f32, text: &str, max_width: f32) -> Vec<String> {
427    let chunks: Vec<&str> = text.split_whitespace().collect();
428    let space_width = measure_text_width(face, font_size, " ");
429    let breaks: Vec<usize> = chunks
430        .iter()
431        .enumerate()
432        .scan(0.0, |width, (index, word)| {
433            let word_width = measure_text_width(face, font_size, word);
434            let resulting_line_len = *width + word_width + space_width;
435            if resulting_line_len > max_width {
436                *width = word_width + space_width;
437                Some(Some(index))
438            } else {
439                *width += word_width;
440                *width += space_width;
441                Some(None)
442            }
443        })
444        .flatten()
445        .chain(std::iter::once(chunks.len()))
446        .collect();
447    let lines: Vec<String> = breaks
448        .iter()
449        .scan(0, |first_word_index, last_word_index| {
450            let first = *first_word_index;
451            *first_word_index = *last_word_index;
452            let line = &chunks[first..*last_word_index];
453            let line_str = String::from(line.join(" "));
454            Some(line_str)
455        })
456        .collect();
457
458    lines
459}
460
461fn pixel_size_rect(face: &FontFace, font_size: f32, value: ttf_parser::Rect) -> Rect {
462    let units_per_em = face.face.units_per_em();
463    let scale = font_size / units_per_em as f32;
464    let min_x = value.x_min as f32 * scale;
465    let max_y = -value.y_min as f32 * scale;
466    let max_x = value.x_max as f32 * scale;
467    let min_y = -value.y_max as f32 * scale;
468    Box2D::new(point2(min_x, min_y), point2(max_x, max_y)).to_rect()
469}
470
471fn scaled_point2(x: f32, y: f32, scale: &Vector2D<f32>) -> Point {
472    point2(x * scale.x, y * scale.y)
473}
474
475const ZERO_RECT: ttf_parser::Rect = ttf_parser::Rect { x_max: 0, x_min: 0, y_max: 0, y_min: 0 };
476
477struct GlyphBuilder<'a> {
478    scale: Vector2D<f32>,
479    offset: Vector2D<f32>,
480    context: &'a RenderContext,
481    path_builder: Option<PathBuilder>,
482    raster_builder: RasterBuilder,
483}
484
485impl<'a> GlyphBuilder<'a> {
486    fn new(scale: Vector2D<f32>, offset: Vector2D<f32>, context: &'a mut RenderContext) -> Self {
487        let path_builder = context.path_builder().expect("path_builder");
488        let raster_builder = context.raster_builder().expect("raster_builder");
489        Self { scale, offset, context, path_builder: Some(path_builder), raster_builder }
490    }
491
492    fn path_builder(&mut self) -> &mut PathBuilder {
493        self.path_builder.as_mut().expect("path_builder() PathBuilder")
494    }
495
496    fn raster(self) -> Raster {
497        self.raster_builder.build()
498    }
499}
500
501impl ttf_parser::OutlineBuilder for GlyphBuilder<'_> {
502    fn move_to(&mut self, x: f32, y: f32) {
503        let p = scaled_point2(x, -y, &self.scale) + self.offset;
504        self.path_builder().move_to(p);
505    }
506
507    fn line_to(&mut self, x: f32, y: f32) {
508        let p = scaled_point2(x, -y, &self.scale) + self.offset;
509        self.path_builder().line_to(p);
510    }
511
512    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
513        let p1 = scaled_point2(x1, -y1, &self.scale) + self.offset;
514        let p = scaled_point2(x, -y, &self.scale) + self.offset;
515        self.path_builder().quad_to(p1, p);
516    }
517
518    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
519        let p1 = scaled_point2(x1, -y1, &self.scale) + self.offset;
520        let p2 = scaled_point2(x2, -y2, &self.scale) + self.offset;
521        let p = scaled_point2(x, -y, &self.scale) + self.offset;
522        self.path_builder().cubic_to(p1, p2, p);
523    }
524
525    fn close(&mut self) {
526        let path_builder = self.path_builder.take().expect("take PathBuilder");
527        let path = path_builder.build();
528        self.raster_builder.add(&path, None);
529        let path_builder = self.context.path_builder().expect("path_builder");
530        self.path_builder = Some(path_builder);
531    }
532}
533
534#[derive(Debug)]
535#[allow(missing_docs)]
536pub struct Glyph {
537    pub raster: Raster,
538    pub bounding_box: Rect,
539}
540
541impl Glyph {
542    #[allow(missing_docs)]
543    pub fn new(
544        context: &mut RenderContext,
545        face: &FontFace,
546        size: f32,
547        id: Option<ttf_parser::GlyphId>,
548    ) -> Self {
549        let units_per_em = face.face.units_per_em();
550        let xy_scale = size / units_per_em as f32;
551        let scale = vec2(xy_scale, xy_scale);
552        let offset = Vector2D::zero();
553        if let Some(id) = id {
554            Self::with_scale_and_offset(context, face, scale, offset, id)
555        } else {
556            let path_builder = context.path_builder().expect("path_builder");
557            let path = path_builder.build();
558            let mut raster_builder = context.raster_builder().expect("raster_builder");
559            raster_builder.add(&path, None);
560            let raster = raster_builder.build();
561
562            Self { bounding_box: Rect::zero(), raster }
563        }
564    }
565
566    #[allow(missing_docs)]
567    pub fn with_scale_and_offset(
568        context: &mut RenderContext,
569        face: &FontFace,
570        scale: Vector2D<f32>,
571        offset: Vector2D<f32>,
572        id: ttf_parser::GlyphId,
573    ) -> Self {
574        let mut builder = GlyphBuilder::new(scale, offset, context);
575        let glyph_bounding_box = &face.face.outline_glyph(id, &mut builder).unwrap_or(ZERO_RECT);
576        let min_x = glyph_bounding_box.x_min as f32 * scale.x + offset.x;
577        let max_y = -glyph_bounding_box.y_min as f32 * scale.y + offset.y;
578        let max_x = glyph_bounding_box.x_max as f32 * scale.x + offset.x;
579        let min_y = -glyph_bounding_box.y_max as f32 * scale.y + offset.y;
580        let bounding_box = Box2D::new(point2(min_x, min_y), point2(max_x, max_y)).to_rect();
581
582        Self { bounding_box, raster: builder.raster() }
583    }
584}
585
586#[derive(Debug)]
587#[allow(missing_docs)]
588pub struct GlyphMap {
589    pub glyphs: BTreeMap<ttf_parser::GlyphId, Glyph>,
590}
591
592impl GlyphMap {
593    #[allow(missing_docs)]
594    pub fn new() -> Self {
595        Self { glyphs: BTreeMap::new() }
596    }
597}
598
599#[allow(missing_docs)]
600pub struct Text {
601    pub raster: Raster,
602    pub bounding_box: Rect,
603}
604
605impl Text {
606    #[allow(missing_docs)]
607    pub fn new_with_lines(
608        context: &mut RenderContext,
609        lines: &Vec<String>,
610        size: f32,
611        face: &FontFace,
612        glyph_map: &mut GlyphMap,
613    ) -> Self {
614        let glyphs = &mut glyph_map.glyphs;
615        let mut bounding_box = Rect::zero();
616        let mut raster_union = {
617            let raster_builder = context.raster_builder().unwrap();
618            raster_builder.build()
619        };
620        let ascent = face.ascent(size);
621        let descent = face.descent(size);
622        let units_per_em = face.face.units_per_em();
623        let scale = size / units_per_em as f32;
624        let mut y_offset = vec2(0.0, ascent).to_i32();
625        for line in lines.iter() {
626            let chars = line.chars();
627            let mut x: f32 = 0.0;
628
629            for c in chars {
630                if let Some(glyph_index) = face.face.glyph_index(c) {
631                    let horizontal_advance = face.face.glyph_hor_advance(glyph_index).unwrap_or(0);
632                    let w = horizontal_advance as f32 * scale;
633                    let position = y_offset + vec2(x, 0.0).to_i32();
634                    let glyph = glyphs
635                        .entry(glyph_index)
636                        .or_insert_with(|| Glyph::new(context, face, size, Some(glyph_index)));
637                    if !glyph.bounding_box.is_empty() {
638                        // Clone and translate raster.
639                        let raster = glyph
640                            .raster
641                            .clone()
642                            .translate(position.cast_unit::<euclid::UnknownUnit>());
643                        raster_union = raster_union + raster;
644
645                        // Expand bounding box.
646                        let glyph_bounding_box = &glyph.bounding_box.translate(position.to_f32());
647
648                        if bounding_box.is_empty() {
649                            bounding_box = *glyph_bounding_box;
650                        } else {
651                            bounding_box = bounding_box.union(&glyph_bounding_box);
652                        }
653                    }
654
655                    x += w;
656                }
657            }
658            y_offset += vec2(0, (ascent - descent) as i32);
659        }
660
661        Self { raster: raster_union, bounding_box }
662    }
663
664    #[allow(missing_docs)]
665    pub fn new(
666        context: &mut RenderContext,
667        text: &str,
668        size: f32,
669        wrap: f32,
670        face: &FontFace,
671        glyph_map: &mut GlyphMap,
672    ) -> Self {
673        let lines = linebreak_text(face, size, text, wrap);
674        Self::new_with_lines(context, &lines, size, face, glyph_map)
675    }
676}
677
678/// Struct containing text grid details.
679#[allow(missing_docs)]
680pub struct TextGrid {
681    pub scale: Vector2D<f32>,
682    pub offset: Vector2D<f32>,
683    pub cell_size: Size,
684}
685
686impl TextGrid {
687    /// Creates a new text grid using the specified font and cell size.
688    /// This determines the appropriate scale and offset for outlines
689    /// to provide optimal glyph layout in cells.
690    pub fn new(face: &FontFace, cell_size: &Size) -> Self {
691        let units_per_em = face.face.units_per_em() as f32;
692        let ascent = face.face.ascender() as f32 / units_per_em;
693        let descent = face.face.descender() as f32 / units_per_em;
694
695        // Use a font size that allows the full ascent and descent to fit in cell.
696        let font_size = cell_size.height / (ascent - descent);
697        let y_scale = font_size / units_per_em;
698
699        // Use the horizontal advance for character '0' as the reference cell
700        // width and scale glyph outlines horizontally to match this size.
701        // This is important in order to allow glyph outlines to align with
702        // the right edge of the cell.
703        let x_scale = face.face.glyph_index('0').map_or(y_scale, |glyph_index| {
704            let horizontal_advance =
705                face.face.glyph_hor_advance(glyph_index).expect("glyph_hor_advance") as f32
706                    / units_per_em
707                    * font_size;
708            y_scale * cell_size.width / horizontal_advance
709        });
710
711        let scale = vec2(x_scale, y_scale);
712        // Offset glyph outlines by ascent so integer translation to cell origin
713        // is enough for correct placement.
714        let offset = vec2(0.0, ascent * font_size);
715
716        Self { scale, offset, cell_size: *cell_size }
717    }
718}
719
720#[cfg(test)]
721mod tests {
722    use super::{GlyphMap, Size, Text, TextGrid};
723    use crate::drawing::{DisplayRotation, FontFace, measure_text_size};
724    use crate::render::generic::{self, Backend};
725    use crate::render::{Context as RenderContext, ContextInner};
726    use euclid::approxeq::ApproxEq;
727    use euclid::{size2, vec2};
728    use fuchsia_async::{self as fasync, MonotonicInstant, TimeoutExt};
729    use fuchsia_framebuffer::FrameUsage;
730    use fuchsia_framebuffer::sysmem::BufferCollectionAllocator;
731    use std::sync::LazyLock;
732
733    const DEFAULT_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(5);
734
735    // This font creation method isn't ideal. The correct method would be to ask the Fuchsia
736    // font service for the font data.
737    static FONT_DATA: &'static [u8] = include_bytes!(
738        "../../../../../prebuilt/third_party/fonts/robotoslab/RobotoSlab-Regular.ttf"
739    );
740    static FONT_FACE: LazyLock<FontFace> =
741        LazyLock::new(|| FontFace::new(&FONT_DATA).expect("Failed to create font"));
742
743    #[fasync::run_singlethreaded(test)]
744    async fn test_text_bounding_box() {
745        let size = size2(800, 800);
746        let mut buffer_allocator = BufferCollectionAllocator::new(
747            size.width,
748            size.height,
749            fidl_fuchsia_images2::PixelFormat::B8G8R8A8,
750            FrameUsage::Cpu,
751            3,
752        )
753        .expect("BufferCollectionAllocator::new");
754        let context_token = buffer_allocator
755            .duplicate_token()
756            .on_timeout(MonotonicInstant::after(DEFAULT_TIMEOUT), || {
757                panic!("Timed out while waiting for duplicate_token")
758            })
759            .await
760            .expect("token");
761        let forma_context = generic::Forma::new_context(context_token, size, DisplayRotation::Deg0);
762        let _buffers_result = buffer_allocator
763            .allocate_buffers(true)
764            .on_timeout(MonotonicInstant::after(DEFAULT_TIMEOUT), || {
765                panic!("Timed out while waiting for sysmem bufers")
766            })
767            .await;
768        let mut render_context = RenderContext { inner: ContextInner::Forma(forma_context) };
769        let mut glyphs = GlyphMap::new();
770        let text =
771            Text::new(&mut render_context, "Good Morning", 20.0, 200.0, &FONT_FACE, &mut glyphs);
772
773        let expected_origin = euclid::point2(0.5371094, 4.765625);
774        let expected_size = vec2(132.33594, 19.501953);
775        let epsilon_point = euclid::point2(1.0, 1.0);
776        let epsilon_vec = euclid::vec2(1.0, 1.0);
777        assert!(
778            text.bounding_box.origin.approx_eq_eps(&expected_origin, &epsilon_point),
779            "Expected bounding box origin to be close to {:?} but found {:?}",
780            expected_origin,
781            text.bounding_box.origin
782        );
783        assert!(
784            text.bounding_box.size.to_vector().approx_eq_eps(&expected_size, &epsilon_vec),
785            "Expected bounding box origin to be close to {:?} but found {:?}",
786            expected_size,
787            text.bounding_box.size
788        );
789    }
790
791    #[fasync::run_singlethreaded(test)]
792    async fn test_textgridcell() {
793        let cell_size = Size::new(16.0, 32.0);
794        let grid = TextGrid::new(&FONT_FACE, &Size::new(16.0, 32.0));
795        assert_eq!(grid.cell_size, cell_size);
796        assert!(grid.offset != vec2(0.0, 0.0));
797        assert!(grid.scale != vec2(0.0, 0.0));
798    }
799
800    #[test]
801    fn test_measure_text() {
802        assert_eq!(measure_text_size(&FONT_FACE, 32.0, "", false).width, 0.0);
803        assert_eq!(measure_text_size(&FONT_FACE, 32.0, "", true).width, 0.0);
804        assert!(measure_text_size(&FONT_FACE, 32.0, "ahoy", false).width > 0.0);
805        assert!(measure_text_size(&FONT_FACE, 32.0, "ahoy", true).width > 0.0);
806    }
807}