blob: f5b9bce3bb4ed6dde5d49d19e5dfd98369861149 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Functions for drawing in Carnelian
//! Carnelian uses the Render abstraction over Mold and Spinel
//! to put pixels on screen. The items in this module are higher-
//! level drawing primitives.
use crate::{
color::Color,
geometry::{Coord, Corners, Point, Rect},
render::{Context as RenderContext, Path, PathBuilder, Raster, RasterBuilder},
};
use anyhow::{Context, Error};
use euclid::{
default::{Box2D, Size2D, Transform2D},
point2, vec2, Angle,
};
use fuchsia_zircon::{self as zx};
use std::{collections::BTreeMap, fs::File, path::PathBuf, slice};
use ttf_parser::Face;
/// Some Fuchsia device displays are mounted rotated. This value represents
/// The supported rotations and can be used by views to rotate their content
/// to display appropriately when running on the frame buffer.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DisplayRotation {
Deg0,
Deg90,
Deg180,
Deg270,
}
impl DisplayRotation {
pub fn transform(&self, target_size: &Size2D<Coord>) -> Transform2D<Coord> {
let w = target_size.width;
let h = target_size.height;
match self {
Self::Deg0 => Transform2D::identity(),
Self::Deg90 => Transform2D::from_array([0.0, -1.0, 1.0, 0.0, 0.0, h]),
Self::Deg180 => Transform2D::from_array([-1.0, 0.0, 0.0, -1.0, w, h]),
Self::Deg270 => Transform2D::from_array([0.0, 1.0, -1.0, 0.0, w, 0.0]),
}
}
pub fn inv_transform(&self, target_size: &Size2D<Coord>) -> Transform2D<Coord> {
let w = target_size.width;
let h = target_size.height;
match self {
Self::Deg0 => Transform2D::identity(),
Self::Deg90 => Transform2D::from_array([0.0, 1.0, -1.0, 0.0, h, 0.0]),
Self::Deg180 => Transform2D::from_array([-1.0, 0.0, 0.0, -1.0, w, h]),
Self::Deg270 => Transform2D::from_array([0.0, -1.0, 1.0, 0.0, 0.0, w]),
}
}
pub fn rotation(&self) -> Option<Transform2D<Coord>> {
match self {
Self::Deg0 => None,
_ => {
let display_rotation = *self;
let angle: Angle<Coord> = display_rotation.into();
Some(Transform2D::rotation(angle))
}
}
}
}
impl Default for DisplayRotation {
fn default() -> Self {
Self::Deg0
}
}
impl From<DisplayRotation> for Angle<Coord> {
fn from(display_rotation: DisplayRotation) -> Self {
let degrees = match display_rotation {
DisplayRotation::Deg0 => 0.0,
DisplayRotation::Deg90 => 90.0,
DisplayRotation::Deg180 => 180.0,
DisplayRotation::Deg270 => 270.0,
};
Angle::degrees(degrees)
}
}
/// Create a render path for the specified rectangle.
pub fn path_for_rectangle(bounds: &Rect, render_context: &mut RenderContext) -> Path {
let mut path_builder = render_context.path_builder().expect("path_builder");
path_builder
.move_to(bounds.origin)
.line_to(bounds.top_right())
.line_to(bounds.bottom_right())
.line_to(bounds.bottom_left())
.line_to(bounds.origin);
path_builder.build()
}
/// Create a render path for the specified rounded rectangle.
pub fn path_for_rounded_rectangle(
bounds: &Rect,
corner_radius: Coord,
render_context: &mut RenderContext,
) -> Path {
let kappa = 4.0 / 3.0 * (std::f32::consts::PI / 8.0).tan();
let control_dist = kappa * corner_radius;
let top_left_arc_start = bounds.origin + vec2(0.0, corner_radius);
let top_left_arc_end = bounds.origin + vec2(corner_radius, 0.0);
let top_left_curve_center = bounds.origin + vec2(corner_radius, corner_radius);
let top_left_p1 = top_left_curve_center + vec2(-corner_radius, -control_dist);
let top_left_p2 = top_left_curve_center + vec2(-control_dist, -corner_radius);
let top_right = bounds.top_right();
let top_right_arc_start = top_right + vec2(-corner_radius, 0.0);
let top_right_arc_end = top_right + vec2(0.0, corner_radius);
let top_right_curve_center = top_right + vec2(-corner_radius, corner_radius);
let top_right_p1 = top_right_curve_center + vec2(control_dist, -corner_radius);
let top_right_p2 = top_right_curve_center + vec2(corner_radius, -control_dist);
let bottom_right = bounds.bottom_right();
let bottom_right_arc_start = bottom_right + vec2(0.0, -corner_radius);
let bottom_right_arc_end = bottom_right + vec2(-corner_radius, 0.0);
let bottom_right_curve_center = bottom_right + vec2(-corner_radius, -corner_radius);
let bottom_right_p1 = bottom_right_curve_center + vec2(corner_radius, control_dist);
let bottom_right_p2 = bottom_right_curve_center + vec2(control_dist, corner_radius);
let bottom_left = bounds.bottom_left();
let bottom_left_arc_start = bottom_left + vec2(corner_radius, 0.0);
let bottom_left_arc_end = bottom_left + vec2(0.0, -corner_radius);
let bottom_left_curve_center = bottom_left + vec2(corner_radius, -corner_radius);
let bottom_left_p1 = bottom_left_curve_center + vec2(-control_dist, corner_radius);
let bottom_left_p2 = bottom_left_curve_center + vec2(-corner_radius, control_dist);
let mut path_builder = render_context.path_builder().expect("path_builder");
path_builder
.move_to(top_left_arc_start)
.cubic_to(top_left_p1, top_left_p2, top_left_arc_end)
.line_to(top_right_arc_start)
.cubic_to(top_right_p1, top_right_p2, top_right_arc_end)
.line_to(bottom_right_arc_start)
.cubic_to(bottom_right_p1, bottom_right_p2, bottom_right_arc_end)
.line_to(bottom_left_arc_start)
.cubic_to(bottom_left_p1, bottom_left_p2, bottom_left_arc_end)
.line_to(top_left_arc_start);
path_builder.build()
}
/// Create a render path for the specified circle.
pub fn path_for_circle(center: Point, radius: Coord, render_context: &mut RenderContext) -> Path {
let kappa = 4.0 / 3.0 * (std::f32::consts::PI / 8.0).tan();
let control_dist = kappa * radius;
let mut path_builder = render_context.path_builder().expect("path_builder");
let left = center + vec2(-radius, 0.0);
let top = center + vec2(0.0, -radius);
let right = center + vec2(radius, 0.0);
let bottom = center + vec2(0.0, radius);
let left_p1 = center + vec2(-radius, -control_dist);
let left_p2 = center + vec2(-control_dist, -radius);
let top_p1 = center + vec2(control_dist, -radius);
let top_p2 = center + vec2(radius, -control_dist);
let right_p1 = center + vec2(radius, control_dist);
let right_p2 = center + vec2(control_dist, radius);
let bottom_p1 = center + vec2(-control_dist, radius);
let bottom_p2 = center + vec2(-radius, control_dist);
path_builder
.move_to(left)
.cubic_to(left_p1, left_p2, top)
.cubic_to(top_p1, top_p2, right)
.cubic_to(right_p1, right_p2, bottom)
.cubic_to(bottom_p1, bottom_p2, left);
path_builder.build()
}
fn point_for_segment_index(
index: usize,
center: Point,
radius: Coord,
segment_angle: f32,
) -> Point {
let angle = index as f32 * segment_angle;
let x = radius * angle.cos();
let y = radius * angle.sin();
center + vec2(x, y)
}
/// Create a render path for the specified polygon.
pub fn path_for_polygon(
center: Point,
radius: Coord,
segment_count: usize,
render_context: &mut RenderContext,
) -> Path {
let segment_angle = (2.0 * std::f32::consts::PI) / segment_count as f32;
let mut path_builder = render_context.path_builder().expect("path_builder");
let first_point = point_for_segment_index(0, center, radius, segment_angle);
path_builder.move_to(first_point);
for index in 1..segment_count {
let pt = point_for_segment_index(index, center, radius, segment_angle);
path_builder.line_to(pt);
}
path_builder.line_to(first_point);
path_builder.build()
}
/// Create a path for knocking out the points of a rectangle, giving it a
/// rounded appearance.
pub fn path_for_corner_knockouts(
bounds: &Rect,
corner_radius: Coord,
render_context: &mut RenderContext,
) -> Path {
let kappa = 4.0 / 3.0 * (std::f32::consts::PI / 8.0).tan();
let control_dist = kappa * corner_radius;
let top_left = bounds.top_left();
let top_left_arc_start = bounds.origin + vec2(0.0, corner_radius);
let top_left_arc_end = bounds.origin + vec2(corner_radius, 0.0);
let top_left_curve_center = bounds.origin + vec2(corner_radius, corner_radius);
let top_left_p1 = top_left_curve_center + vec2(-corner_radius, -control_dist);
let top_left_p2 = top_left_curve_center + vec2(-control_dist, -corner_radius);
let top_right = bounds.top_right();
let top_right_arc_start = top_right + vec2(-corner_radius, 0.0);
let top_right_arc_end = top_right + vec2(0.0, corner_radius);
let top_right_curve_center = top_right + vec2(-corner_radius, corner_radius);
let top_right_p1 = top_right_curve_center + vec2(control_dist, -corner_radius);
let top_right_p2 = top_right_curve_center + vec2(corner_radius, -control_dist);
let bottom_right = bounds.bottom_right();
let bottom_right_arc_start = bottom_right + vec2(0.0, -corner_radius);
let bottom_right_arc_end = bottom_right + vec2(-corner_radius, 0.0);
let bottom_right_curve_center = bottom_right + vec2(-corner_radius, -corner_radius);
let bottom_right_p1 = bottom_right_curve_center + vec2(corner_radius, control_dist);
let bottom_right_p2 = bottom_right_curve_center + vec2(control_dist, corner_radius);
let bottom_left = bounds.bottom_left();
let bottom_left_arc_start = bottom_left + vec2(corner_radius, 0.0);
let bottom_left_arc_end = bottom_left + vec2(0.0, -corner_radius);
let bottom_left_curve_center = bottom_left + vec2(corner_radius, -corner_radius);
let bottom_left_p1 = bottom_left_curve_center + vec2(-control_dist, corner_radius);
let bottom_left_p2 = bottom_left_curve_center + vec2(-corner_radius, control_dist);
let mut path_builder = render_context.path_builder().expect("path_builder");
path_builder
.move_to(top_left)
.line_to(top_left_arc_start)
.cubic_to(top_left_p1, top_left_p2, top_left_arc_end)
.line_to(top_left)
.move_to(top_right)
.line_to(top_right_arc_start)
.cubic_to(top_right_p1, top_right_p2, top_right_arc_end)
.line_to(top_right)
.move_to(bottom_right)
.line_to(bottom_right_arc_start)
.cubic_to(bottom_right_p1, bottom_right_p2, bottom_right_arc_end)
.line_to(bottom_right)
.move_to(bottom_left)
.line_to(bottom_left_arc_start)
.cubic_to(bottom_left_p1, bottom_left_p2, bottom_left_arc_end)
.line_to(bottom_left);
path_builder.build()
}
/// Create a render path for a fuchsia-style teardrop cursor.
pub fn path_for_cursor(hot_spot: Point, radius: Coord, render_context: &mut RenderContext) -> Path {
let kappa = 4.0 / 3.0 * (std::f32::consts::PI / 8.0).tan();
let control_dist = kappa * radius;
let mut path_builder = render_context.path_builder().expect("path_builder");
let center = hot_spot + vec2(radius, radius);
let left = center + vec2(-radius, 0.0);
let top = center + vec2(0.0, -radius);
let right = center + vec2(radius, 0.0);
let bottom = center + vec2(0.0, radius);
let top_p1 = center + vec2(control_dist, -radius);
let top_p2 = center + vec2(radius, -control_dist);
let right_p1 = center + vec2(radius, control_dist);
let right_p2 = center + vec2(control_dist, radius);
let bottom_p1 = center + vec2(-control_dist, radius);
let bottom_p2 = center + vec2(-radius, control_dist);
path_builder
.move_to(hot_spot)
.line_to(top)
.cubic_to(top_p1, top_p2, right)
.cubic_to(right_p1, right_p2, bottom)
.cubic_to(bottom_p1, bottom_p2, left)
.line_to(hot_spot);
path_builder.build()
}
/// Struct combining a foreground and background color.
#[derive(Hash, Eq, PartialEq, Debug, Clone, Copy)]
pub struct Paint {
/// Color for foreground painting
pub fg: Color,
/// Color for background painting
pub bg: Color,
}
impl Paint {
/// Create a paint from a pair of hash codes
pub fn from_hash_codes(fg: &str, bg: &str) -> Result<Paint, Error> {
Ok(Paint { fg: Color::from_hash_code(fg)?, bg: Color::from_hash_code(bg)? })
}
}
pub fn load_font(path: PathBuf) -> Result<FontFace, Error> {
let file = File::open(path).context("File::open")?;
let vmo = fdio::get_vmo_copy_from_file(&file).context("fdio::get_vmo_copy_from_file")?;
let size = file.metadata()?.len() as usize;
let root_vmar = fuchsia_runtime::vmar_root_self();
let address = root_vmar.map(
0,
&vmo,
0,
size,
zx::VmarFlags::PERM_READ | zx::VmarFlags::MAP_RANGE | zx::VmarFlags::REQUIRE_NON_RESIZABLE,
)?;
let mapped_font_data = unsafe { slice::from_raw_parts(address as *mut u8, size) };
Ok(FontFace::new(&*mapped_font_data)?)
}
/// Struct containing a font and a cache of rendered glyphs.
#[derive(Clone)]
pub struct FontFace {
/// Font.
pub face: Face<'static>,
}
impl FontFace {
pub fn new(data: &'static [u8]) -> Result<FontFace, Error> {
let face = Face::from_slice(data, 0)?;
Ok(FontFace { face })
}
pub fn ascent(&self, size: f32) -> f32 {
let ascent = self.face.ascender();
self.face
.units_per_em()
.and_then(|units_per_em| Some((ascent as f32 / units_per_em as f32) * size))
.expect("units_per_em")
}
pub fn descent(&self, size: f32) -> f32 {
let descender = self.face.descender();
self.face
.units_per_em()
.and_then(|units_per_em| Some((descender as f32 / units_per_em as f32) * size))
.expect("units_per_em")
}
pub fn capital_height(&self, size: f32) -> Option<f32> {
self.face.capital_height().and_then(|capital_height| {
self.face
.units_per_em()
.and_then(|units_per_em| Some((capital_height as f32 / units_per_em as f32) * size))
})
}
}
pub fn measure_text_width(face: &FontFace, font_size: f32, text: &str) -> f32 {
text.chars()
.filter_map(|c| {
let glyph_index = face.face.glyph_index(c);
glyph_index.and_then(|glyph_index| {
let hor_advance = face
.face
.glyph_hor_advance(glyph_index)
.and_then(|hor_advance| pixel_size(face, font_size, hor_advance as i16))
.expect("hor_advance");
Some(hor_advance)
})
})
.sum()
}
pub fn linebreak_text(face: &FontFace, font_size: f32, text: &str, max_width: f32) -> Vec<String> {
let chunks: Vec<&str> = text.split_whitespace().collect();
let space_width = measure_text_width(face, font_size, " ");
let breaks: Vec<usize> = chunks
.iter()
.enumerate()
.scan(0.0, |width, (index, word)| {
let word_width = measure_text_width(face, font_size, word);
let resulting_line_len = *width + word_width + space_width;
if resulting_line_len > max_width {
*width = word_width + space_width;
Some(Some(index))
} else {
*width += word_width;
*width += space_width;
Some(None)
}
})
.flatten()
.chain(std::iter::once(chunks.len()))
.collect();
let lines: Vec<String> = breaks
.iter()
.scan(0, |first_word_index, last_word_index| {
let first = *first_word_index;
*first_word_index = *last_word_index;
let line = &chunks[first..*last_word_index];
let line_str = String::from(line.join(" "));
Some(line_str)
})
.collect();
lines
}
fn pixel_size(face: &FontFace, font_size: f32, value: i16) -> Option<f32> {
face.face
.units_per_em()
.and_then(|units_per_em| Some((value as f32 / units_per_em as f32) * font_size))
}
fn scaled_point2(x: f32, y: f32, scale: f32) -> Point {
point2(x * scale, -y * scale)
}
const ZERO_RECT: ttf_parser::Rect = ttf_parser::Rect { x_max: 0, x_min: 0, y_max: 0, y_min: 0 };
struct GlyphBuilder<'a> {
scale: f32,
context: &'a RenderContext,
path_builder: Option<PathBuilder>,
raster_builder: RasterBuilder,
}
impl<'a> GlyphBuilder<'a> {
fn new(scale: f32, context: &'a mut RenderContext) -> Self {
let path_builder = context.path_builder().expect("path_builder");
let raster_builder = context.raster_builder().expect("raster_builder");
Self { scale, context, path_builder: Some(path_builder), raster_builder }
}
fn path_builder(&mut self) -> &mut PathBuilder {
self.path_builder.as_mut().expect("path_builder() PathBuilder")
}
fn raster(self) -> Raster {
self.raster_builder.build()
}
}
impl ttf_parser::OutlineBuilder for GlyphBuilder<'_> {
fn move_to(&mut self, x: f32, y: f32) {
let scale = self.scale;
self.path_builder().move_to(scaled_point2(x, y, scale));
}
fn line_to(&mut self, x: f32, y: f32) {
let scale = self.scale;
self.path_builder().line_to(scaled_point2(x, y, scale));
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
let scale = self.scale;
self.path_builder().quad_to(scaled_point2(x1, y1, scale), scaled_point2(x, y, scale));
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
let scale = self.scale;
self.path_builder().cubic_to(
scaled_point2(x1, y1, scale),
scaled_point2(x2, y2, scale),
scaled_point2(x, y, scale),
);
}
fn close(&mut self) {
let path_builder = self.path_builder.take().expect("take PathBuilder");
let path = path_builder.build();
self.raster_builder.add(&path, None);
let path_builder = self.context.path_builder().expect("path_builder");
self.path_builder = Some(path_builder);
}
}
#[derive(Debug)]
pub struct Glyph {
pub raster: Raster,
pub bounding_box: Rect,
}
impl Glyph {
pub fn new(
context: &mut RenderContext,
face: &FontFace,
size: f32,
id: Option<ttf_parser::GlyphId>,
) -> Self {
if let Some(id) = id {
let units_per_em = face.face.units_per_em().expect("units_per_em");
let scale = size / units_per_em as f32;
let mut builder = GlyphBuilder::new(scale, context);
let glyph_bounding_box =
&face.face.outline_glyph(id, &mut builder).unwrap_or_else(|| ZERO_RECT);
let min_x = glyph_bounding_box.x_min as f32 * scale;
let max_y = -glyph_bounding_box.y_min as f32 * scale;
let max_x = glyph_bounding_box.x_max as f32 * scale;
let min_y = -glyph_bounding_box.y_max as f32 * scale;
let bounding_box = Box2D::new(point2(min_x, min_y), point2(max_x, max_y)).to_rect();
Self { bounding_box, raster: builder.raster() }
} else {
let path_builder = context.path_builder().expect("path_builder");
let path = path_builder.build();
let mut raster_builder = context.raster_builder().expect("raster_builder");
raster_builder.add(&path, None);
let raster = raster_builder.build();
Self { bounding_box: Rect::zero(), raster }
}
}
}
#[derive(Debug)]
pub struct GlyphMap {
glyphs: BTreeMap<ttf_parser::GlyphId, Glyph>,
}
impl GlyphMap {
pub fn new() -> Self {
Self { glyphs: BTreeMap::new() }
}
}
pub struct Text {
pub raster: Raster,
pub bounding_box: Rect,
}
impl Text {
pub fn new_with_lines(
context: &mut RenderContext,
lines: &Vec<String>,
size: f32,
face: &FontFace,
glyph_map: &mut GlyphMap,
) -> Self {
let glyphs = &mut glyph_map.glyphs;
let mut bounding_box = Rect::zero();
let mut raster_union = None;
let ascent = face.ascent(size);
let units_per_em = face.face.units_per_em().expect("units_per_em");
let scale = size / units_per_em as f32;
let mut y_offset = vec2(0.0, ascent).to_i32();
for line in lines.iter() {
let chars = line.chars();
let mut x: f32 = 0.0;
for c in chars {
if let Some(glyph_index) = face.face.glyph_index(c) {
let horizontal_advance = face.face.glyph_hor_advance(glyph_index).unwrap_or(0);
let w = horizontal_advance as f32 * scale;
let position = y_offset + vec2(x, 0.0).to_i32();
let glyph = glyphs
.entry(glyph_index)
.or_insert_with(|| Glyph::new(context, face, size, Some(glyph_index)));
// Clone and translate raster.
let raster =
glyph.raster.clone().translate(position.cast_unit::<euclid::UnknownUnit>());
raster_union = if let Some(raster_union) = raster_union {
Some(raster_union + raster)
} else {
Some(raster)
};
// Expand bounding box.
let glyph_bounding_box = &glyph.bounding_box.translate(position.to_f32());
if bounding_box.is_empty() {
bounding_box = *glyph_bounding_box;
} else {
bounding_box = bounding_box.union(&glyph_bounding_box);
}
x += w;
}
}
y_offset += vec2(0, size as i32);
}
Self { raster: raster_union.expect("raster_union"), bounding_box }
}
pub fn new(
context: &mut RenderContext,
text: &str,
size: f32,
wrap: f32,
face: &FontFace,
glyph_map: &mut GlyphMap,
) -> Self {
let lines = linebreak_text(face, size, text, wrap);
Self::new_with_lines(context, &lines, size, face, glyph_map)
}
}
#[cfg(test)]
mod tests {
use super::{GlyphMap, Text};
use crate::{
drawing::{DisplayRotation, FontFace},
render::{
generic::{self, Backend},
Context as RenderContext, ContextInner,
},
};
use euclid::{approxeq::ApproxEq, size2, vec2};
use fuchsia_async::{self as fasync, Time, TimeoutExt};
use fuchsia_framebuffer::{sysmem::BufferCollectionAllocator, FrameUsage};
use lazy_static::lazy_static;
const DEFAULT_TIMEOUT: fuchsia_zircon::Duration = fuchsia_zircon::Duration::from_seconds(5);
// This font creation method isn't ideal. The correct method would be to ask the Fuchsia
// font service for the font data.
static FONT_DATA: &'static [u8] = include_bytes!(
"../../../../../prebuilt/third_party/fonts/robotoslab/RobotoSlab-Regular.ttf"
);
lazy_static! {
pub static ref FONT_FACE: FontFace =
FontFace::new(&FONT_DATA).expect("Failed to create font");
}
#[fasync::run_singlethreaded(test)]
async fn test_text_bounding_box() {
let size = size2(800, 800);
let mut buffer_allocator = BufferCollectionAllocator::new(
size.width,
size.height,
fidl_fuchsia_sysmem::PixelFormatType::Bgra32,
FrameUsage::Cpu,
3,
)
.expect("BufferCollectionAllocator::new");
let context_token = buffer_allocator
.duplicate_token()
.on_timeout(Time::after(DEFAULT_TIMEOUT), || {
panic!("Timed out while waiting for duplicate_token")
})
.await
.expect("token");
let mold_context = generic::Mold::new_context(context_token, size, DisplayRotation::Deg0);
let _buffers_result = buffer_allocator
.allocate_buffers(true)
.on_timeout(Time::after(DEFAULT_TIMEOUT), || {
panic!("Timed out while waiting for sysmem bufers")
})
.await;
let mut render_context = RenderContext { inner: ContextInner::Mold(mold_context) };
let mut glyphs = GlyphMap::new();
let text =
Text::new(&mut render_context, "Good Morning", 20.0, 200.0, &FONT_FACE, &mut glyphs);
let expected_origin = euclid::point2(0.5371094, 4.765625);
let expected_size = vec2(132.33594, 19.501953);
assert!(
text.bounding_box.origin.approx_eq(&expected_origin),
"Expected bounding box origin to be close to {:?} but found {:?}",
expected_origin,
text.bounding_box.origin
);
assert!(
text.bounding_box.size.to_vector().approx_eq(&expected_size),
"Expected bounding box origin to be close to {:?} but found {:?}",
expected_size,
text.bounding_box.size
);
}
}