blob: 8df7ec568b6bac9988c7c86e0cbd875195f7567d [file] [log] [blame]
// Copyright 2021 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.
use {
crate::paths::{
maybe_path_for_char, maybe_path_for_cursor_style, path_for_strikeout, path_for_underline,
Line,
},
carnelian::{
color::Color,
drawing::{FontFace, Glyph, TextGrid},
render::{BlendMode, Context as RenderContext, Fill, FillRule, Layer, Raster, Style},
scene::{LayerGroup, SceneOrder},
Size,
},
euclid::{point2, Rect},
rustc_hash::{FxHashMap, FxHashSet},
std::{
collections::{hash_map::Entry, BTreeSet},
mem,
},
term_model::{
ansi::{CursorStyle, TermInfo},
config::Config,
term::{cell::Flags, color::Rgb, RenderableCell, RenderableCellContent, Term},
},
};
// Supported scale factors.
//
// These values are hard-coded in order to ensure that we use a grid size
// that is efficient and aligns with physical pixels.
const SCALE_FACTORS: &[f32] = &[1.0, 1.25, 2.0, 3.0, 4.0];
/// Returns a scale factor given a set of DPI buckets and an actual DPI value.
pub fn get_scale_factor(dpi: &BTreeSet<u32>, actual_dpi: f32) -> f32 {
let mut scale_factor = 0;
for value in dpi.iter() {
if *value as f32 > actual_dpi {
break;
}
scale_factor += 1;
}
*SCALE_FACTORS.get(scale_factor).unwrap_or(SCALE_FACTORS.last().unwrap())
}
/// Returns the cell size given a cell height.
pub fn cell_size_from_cell_height(font_set: &FontSet, height: f32) -> Size {
let rounded_height = height.round();
// Use a cell width that matches the horizontal advance of character
// '0' as closely as possible. This minimizes the amount of horizontal
// stretching used for glyph outlines. Fallback to half of cell height
// if glyph '0' is missing.
let face = &font_set.font.face;
let width = face.glyph_index('0').map_or(height / 2.0, |glyph_index| {
let ascent = face.ascender() as f32;
let descent = face.descender() as f32;
let horizontal_advance =
face.glyph_hor_advance(glyph_index).expect("glyph_hor_advance") as f32;
rounded_height * horizontal_advance / (ascent - descent)
});
Size::new(width.round(), rounded_height)
}
#[derive(Clone)]
pub struct FontSet {
font: FontFace,
bold_font: Option<FontFace>,
italic_font: Option<FontFace>,
bold_italic_font: Option<FontFace>,
fallback_fonts: Vec<FontFace>,
}
impl FontSet {
pub fn new(
font: FontFace,
bold_font: Option<FontFace>,
italic_font: Option<FontFace>,
bold_italic_font: Option<FontFace>,
fallback_fonts: Vec<FontFace>,
) -> Self {
Self { font, bold_font, italic_font, bold_italic_font, fallback_fonts }
}
}
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub enum LayerContent {
Cursor(CursorStyle),
Char((char, Flags)),
}
// The term-model library gives us zero-width characters in our array of chars. However,
// we do not support this at this point so we just pull out the first char for rendering.
impl From<RenderableCell> for LayerContent {
fn from(cell: RenderableCell) -> Self {
match cell.inner {
RenderableCellContent::Cursor(cursor_key) => Self::Cursor(cursor_key.style),
RenderableCellContent::Chars(chars) => {
let flags = cell.flags & (Flags::BOLD_ITALIC | Flags::UNDERLINE | Flags::STRIKEOUT);
// Ignore hidden cells and render tabs as spaces to prevent font issues.
if chars[0] == '\t' || cell.flags.contains(Flags::HIDDEN) {
Self::Char((' ', flags))
} else {
Self::Char((chars[0], flags))
}
}
}
}
}
#[derive(PartialEq)]
struct LayerId {
content: LayerContent,
rgb: Rgb,
}
fn maybe_raster_for_cursor_style(
render_context: &mut RenderContext,
cursor_style: CursorStyle,
cell_size: &Size,
) -> Option<Raster> {
maybe_path_for_cursor_style(render_context, cursor_style, cell_size).as_ref().map(|p| {
let mut raster_builder = render_context.raster_builder().expect("raster_builder");
raster_builder.add(p, None);
raster_builder.build()
})
}
fn maybe_fallback_glyph_for_char(
render_context: &mut RenderContext,
c: char,
cell_size: &Size,
) -> Option<Glyph> {
maybe_path_for_char(render_context, c, cell_size).as_ref().map(|p| {
let mut raster_builder = render_context.raster_builder().expect("raster_builder");
raster_builder.add(p, None);
let raster = raster_builder.build();
let bounding_box = Rect::from_size(*cell_size);
Glyph { raster, bounding_box }
})
}
fn maybe_glyph_for_char(
context: &mut RenderContext,
c: char,
flags: Flags,
textgrid: &TextGrid,
font_set: &FontSet,
) -> Option<Glyph> {
let maybe_bold_italic_font = match flags & Flags::BOLD_ITALIC {
Flags::BOLD => font_set.bold_font.as_ref(),
Flags::ITALIC => font_set.italic_font.as_ref(),
Flags::BOLD_ITALIC => font_set.bold_italic_font.as_ref(),
_ => None,
};
let scale = textgrid.scale;
let offset = textgrid.offset;
// Glyph search order:
//
// 1. Bold/italic font first if appropriate.
// 2. Regular font.
// 3. Fallback fonts.
//
// The fallback font can be used to provide icons/emojis
// that are not expected to be part of the regular font.
for font in maybe_bold_italic_font
.iter()
.map(|font| *font)
.chain(std::iter::once(&font_set.font))
.chain(font_set.fallback_fonts.iter())
{
if let Some(glyph_index) = font.face.glyph_index(c) {
let glyph = Glyph::with_scale_and_offset(context, font, scale, offset, glyph_index);
return Some(glyph);
}
}
// Try fallback glyph if we failed to locate glyph in fonts.
maybe_fallback_glyph_for_char(context, c, &textgrid.cell_size)
}
fn maybe_raster_for_char(
context: &mut RenderContext,
c: char,
flags: Flags,
textgrid: &TextGrid,
font_set: &FontSet,
) -> Option<Raster> {
// Get a potential glyph for this character.
let maybe_glyph = maybe_glyph_for_char(context, c, flags, textgrid, font_set);
// Create an extra raster if underline or strikeout flag is set.
let maybe_extra_raster = if flags.intersects(Flags::UNDERLINE | Flags::STRIKEOUT) {
let mut raster_builder = context.raster_builder().expect("raster_builder");
if flags.contains(Flags::UNDERLINE) {
// TODO(https://fxbug.dev/42172477): Avoid glyph overlap.
let line_metrics = font_set.font.face.underline_metrics();
raster_builder.add(
&path_for_underline(
&textgrid.cell_size,
context,
line_metrics.map(|line_metrics| Line::new(line_metrics, textgrid)),
),
None,
);
}
if flags.contains(Flags::STRIKEOUT) {
let line_metrics = font_set.font.face.strikeout_metrics();
raster_builder.add(
&path_for_strikeout(
&textgrid.cell_size,
context,
line_metrics.map(|line_metrics| Line::new(line_metrics, textgrid)),
),
None,
);
}
Some(raster_builder.build())
} else {
None
};
// Return a union of glyph raster and extra raster.
match (maybe_glyph, maybe_extra_raster) {
(Some(glyph), Some(extra_raster)) => Some(glyph.raster + extra_raster),
(Some(glyph), None) => Some(glyph.raster),
(None, Some(extra_raster)) => Some(extra_raster),
_ => None,
}
}
fn maybe_raster_for_layer_content(
render_context: &mut RenderContext,
content: &LayerContent,
column: usize,
row: usize,
textgrid: &TextGrid,
font_set: &FontSet,
raster_cache: &mut FxHashMap<LayerContent, Option<Raster>>,
) -> Option<Raster> {
raster_cache
.entry(*content)
.or_insert_with(|| match content {
LayerContent::Cursor(cursor_style) => {
maybe_raster_for_cursor_style(render_context, *cursor_style, &textgrid.cell_size)
}
LayerContent::Char((c, flags)) => {
maybe_raster_for_char(render_context, *c, *flags, textgrid, font_set)
}
})
.as_ref()
.map(|r| {
let cell_size = &textgrid.cell_size;
let cell_position =
point2(cell_size.width * column as f32, cell_size.height * row as f32);
let raster = r.clone().translate(cell_position.to_vector().to_i32());
// Add empty raster to enable caching of the translated cursor.
// TODO: add more appropriate API for this.
let empty_raster = {
let raster_builder = render_context.raster_builder().unwrap();
raster_builder.build()
};
raster + empty_raster
})
}
fn make_color(term_color: &Rgb) -> Color {
Color { r: term_color.r, g: term_color.g, b: term_color.b, a: 0xff }
}
#[derive(PartialEq, Debug)]
pub struct RenderableLayer {
pub order: usize,
pub column: usize,
pub row: usize,
pub content: LayerContent,
pub rgb: Rgb,
}
pub struct Offset {
pub column: usize,
pub row: usize,
}
pub fn renderable_layers<'b, T, C>(
term: &'b Term<T>,
config: &'b Config<C>,
offset: &'b Offset,
) -> impl Iterator<Item = RenderableLayer> + 'b {
let columns = term.cols().0;
// renderable_cells() returns cells in painter's algorithm order, we
// convert that into a retained scene by assuming that we have at most
// 4 layers per cell:
//
// 1: Cursor background
// 2: Cursor foreground
// 3: Background
// 4: Foreground
let stride = columns * 4;
term.renderable_cells(config).flat_map(move |cell| {
let row = cell.line.0 + offset.row;
let cell_order = row * stride + (cell.column.0 + offset.column);
let content: LayerContent = cell.into();
let order = match content {
LayerContent::Cursor(_) => cell_order,
LayerContent::Char(_) => cell_order + columns * 2,
};
if cell.bg_alpha != 0.0 {
assert!(cell.bg_alpha == 1.0, "unsupported bg_alpha: {}", cell.bg_alpha);
Some(RenderableLayer {
order: order,
column: cell.column.0,
row,
content: LayerContent::Cursor(CursorStyle::Block),
rgb: cell.bg,
})
} else {
None
}
.into_iter()
.chain(std::iter::once(RenderableLayer {
order: order + columns,
column: cell.column.0,
row,
content,
rgb: cell.fg,
}))
})
}
pub struct Renderer {
textgrid: TextGrid,
raster_cache: FxHashMap<LayerContent, Option<Raster>>,
layers: FxHashMap<SceneOrder, LayerId>,
old_layers: FxHashSet<SceneOrder>,
new_layers: FxHashSet<SceneOrder>,
}
impl Renderer {
pub fn new(font_set: &FontSet, cell_size: &Size) -> Self {
let textgrid = TextGrid::new(&font_set.font, cell_size);
let raster_cache = FxHashMap::default();
let layers = FxHashMap::default();
let old_layers = FxHashSet::default();
let new_layers = FxHashSet::default();
Self { textgrid, raster_cache, layers, old_layers, new_layers }
}
pub fn render<I>(
&mut self,
layer_group: &mut dyn LayerGroup,
render_context: &mut RenderContext,
font_set: &FontSet,
layers: I,
) where
I: IntoIterator<Item = RenderableLayer>,
{
let raster_cache = &mut self.raster_cache;
let textgrid = &self.textgrid;
// Process all layers and update the layer group as needed.
for RenderableLayer { order, column, row, content, rgb } in layers.into_iter() {
let id = LayerId { content, rgb };
let order = SceneOrder::try_from(order).unwrap_or_else(|e| panic!("{}", e));
// Remove from old layers.
self.old_layers.remove(&order);
match self.layers.entry(order) {
Entry::Occupied(entry) => {
if *entry.get() != id {
let raster = maybe_raster_for_layer_content(
render_context,
&id.content,
column,
row,
textgrid,
font_set,
raster_cache,
);
if let Some(raster) = raster {
let value = entry.into_mut();
*value = id;
let did_not_exist = self.new_layers.insert(order);
assert!(
did_not_exist,
"multiple layers with order: {}",
order.as_u32()
);
layer_group.insert(
order,
Layer {
raster,
clip: None,
style: Style {
fill_rule: FillRule::NonZero,
fill: Fill::Solid(make_color(&rgb)),
blend_mode: BlendMode::Over,
},
},
);
} else {
entry.remove_entry();
layer_group.remove(order);
}
} else {
let did_not_exist = self.new_layers.insert(order);
assert!(did_not_exist, "multiple layers with order: {}", order.as_u32());
}
}
Entry::Vacant(entry) => {
let raster = maybe_raster_for_layer_content(
render_context,
&id.content,
column,
row,
textgrid,
font_set,
raster_cache,
);
if let Some(raster) = raster {
entry.insert(id);
let did_not_exist = self.new_layers.insert(order);
assert!(did_not_exist, "multiple layers with order: {}", order.as_u32());
layer_group.insert(
order,
Layer {
raster,
clip: None,
style: Style {
fill_rule: FillRule::NonZero,
fill: Fill::Solid(make_color(&rgb)),
blend_mode: BlendMode::Over,
},
},
);
}
}
}
}
// Remove any remaining old layers.
for order in self.old_layers.drain() {
self.layers.remove(&order);
layer_group.remove(order);
}
// Swap old layers for new layers.
mem::swap(&mut self.old_layers, &mut self.new_layers);
}
}
#[cfg(test)]
mod tests {
use {
super::*,
anyhow::Error,
carnelian::{
drawing::DisplayRotation,
render::{generic, Context as RenderContext, ContextInner},
},
euclid::size2,
fuchsia_async as fasync,
once_cell::sync::Lazy,
std::collections::BTreeMap,
term_model::{
ansi::Processor,
clipboard::Clipboard,
event::{Event, EventListener},
term::SizeInfo,
},
};
struct TermConfig;
impl Default for TermConfig {
fn default() -> TermConfig {
TermConfig
}
}
struct EventProxy;
impl EventListener for EventProxy {
fn send_event(&self, _event: Event) {}
}
// 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/robotomono/RobotoMono-Regular.ttf"
);
static FONT_SET: Lazy<FontSet> = Lazy::new(|| {
FontSet::new(
FontFace::new(&FONT_DATA).expect("Failed to create font"),
None,
None,
None,
vec![],
)
});
struct TestLayerGroup<'a>(&'a mut BTreeMap<SceneOrder, Layer>);
impl LayerGroup for TestLayerGroup<'_> {
fn clear(&mut self) {
self.0.clear();
}
fn insert(&mut self, order: SceneOrder, layer: Layer) {
self.0.insert(order, layer);
}
fn remove(&mut self, order: SceneOrder) {
self.0.remove(&order);
}
}
#[test]
fn check_scale_factors() {
let dpi = BTreeSet::from([160, 240, 320]);
assert_eq!(get_scale_factor(&dpi, 100.0), 1.0);
assert_eq!(get_scale_factor(&dpi, 180.0), 1.25);
assert_eq!(get_scale_factor(&dpi, 240.0), 2.0);
assert_eq!(get_scale_factor(&dpi, 319.0), 2.0);
assert_eq!(get_scale_factor(&dpi, 400.0), 3.0);
}
#[test]
fn can_create_renderable_layers() -> Result<(), Error> {
let cell_size = Size::new(8.0, 16.0);
let size_info = SizeInfo {
width: cell_size.width * 2.0,
height: cell_size.height,
cell_width: cell_size.width,
cell_height: cell_size.height,
padding_x: 0.0,
padding_y: 0.0,
dpr: 1.0,
};
let bg = Rgb { r: 0, g: 0, b: 0 };
let fg = Rgb { r: 255, g: 255, b: 255 };
let config = {
let mut config = Config::<TermConfig>::default();
config.colors.primary.background = bg;
config.colors.primary.foreground = fg;
config
};
let mut term = Term::new(&config, &size_info, Clipboard::new(), EventProxy {});
let mut parser = Processor::new();
let mut output = vec![];
parser.advance(&mut term, 'A' as u8, &mut output);
let offset = Offset { column: 0, row: 0 };
let result = renderable_layers(&term, &config, &offset).collect::<Vec<_>>();
assert_eq!(
result,
vec![
RenderableLayer {
order: 6,
column: 0,
row: 0,
content: LayerContent::Char(('A', Flags::empty())),
rgb: fg
},
RenderableLayer {
order: 3,
column: 1,
row: 0,
content: LayerContent::Cursor(CursorStyle::Block),
rgb: fg
},
RenderableLayer {
order: 7,
column: 1,
row: 0,
content: LayerContent::Char((' ', Flags::empty())),
rgb: bg
}
],
"unexpected layers"
);
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn can_render_cell() {
let size = size2(64, 64);
let forma_context = generic::Forma::new_context_without_token(size, DisplayRotation::Deg0);
let mut render_context = RenderContext { inner: ContextInner::Forma(forma_context) };
let mut renderer = Renderer::new(&FONT_SET, &Size::new(8.0, 16.0));
let layers = vec![
RenderableLayer {
order: 0,
column: 0,
row: 0,
content: LayerContent::Cursor(CursorStyle::Block),
rgb: Rgb { r: 0xff, g: 0xff, b: 0xff },
},
RenderableLayer {
order: 1,
column: 0,
row: 0,
content: LayerContent::Char(('A', Flags::empty())),
rgb: Rgb { r: 0, g: 0, b: 0xff },
},
];
let mut result = BTreeMap::new();
let mut layer_group = TestLayerGroup(&mut result);
renderer.render(&mut layer_group, &mut render_context, &FONT_SET, layers.into_iter());
assert_eq!(result.len(), 2, "expected two layers");
}
}