blob: fe212d9aecccc37cde0ca3de41dee9e5abf20803 [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::colors::ColorScheme,
carnelian::{
color::Color,
drawing::{FontFace, GlyphMap, TextGrid, TextGridCell},
render::{BlendMode, Context as RenderContext, Fill, FillRule, Layer, Path, Raster, Style},
scene::{facets::Facet, LayerGroup},
Size, ViewAssistantContext,
},
euclid::point2,
fuchsia_trace::duration,
rustc_hash::{FxHashMap, FxHashSet},
std::{any::Any, cell::RefCell, collections::hash_map::Entry, convert::TryFrom, mem, rc::Rc},
term_model::{
ansi::{CursorStyle, TermInfo},
term::{color::Rgb, RenderableCellContent},
Term,
},
};
pub fn font_to_cell_size(font_size: f32, cell_padding: f32) -> Size {
let height = font_size + cell_padding;
let width = height / 2.0;
// Round to the smallest size equal or greater.
Size::new(width, height).ceil()
}
#[derive(PartialEq)]
enum CellContent {
Cursor(CursorStyle),
Char(char),
}
// 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<RenderableCellContent> for CellContent {
fn from(content: RenderableCellContent) -> Self {
match content {
RenderableCellContent::Cursor(cursor_key) => Self::Cursor(cursor_key.style),
RenderableCellContent::Chars(chars) => Self::Char(chars[0]),
}
}
}
#[derive(PartialEq)]
struct CellId {
content: CellContent,
rgb: Rgb,
}
// Thickness of cursor lines is determined by multiplying thickness factor
// with the cell height. 1/16 has been chosen as that results in 1px thick
// lines for a 16px cell height.
const CURSOR_LINE_THICKNESS_FACTOR: f32 = 1.0 / 16.0;
fn path_for_block(size: &Size, render_context: &mut RenderContext) -> Path {
let mut path_builder = render_context.path_builder().expect("path_builder");
path_builder
.move_to(point2(0.0, 0.0))
.line_to(point2(size.width, 0.0))
.line_to(point2(size.width, size.height))
.line_to(point2(0.0, size.height))
.line_to(point2(0.0, 0.0));
path_builder.build()
}
fn path_for_underline(size: &Size, render_context: &mut RenderContext) -> Path {
let mut path_builder = render_context.path_builder().expect("path_builder");
let top = size.height - size.height * CURSOR_LINE_THICKNESS_FACTOR;
path_builder
.move_to(point2(0.0, top))
.line_to(point2(size.width, top))
.line_to(point2(size.width, size.height))
.line_to(point2(0.0, size.height))
.line_to(point2(0.0, top));
path_builder.build()
}
fn path_for_beam(size: &Size, render_context: &mut RenderContext) -> Path {
let mut path_builder = render_context.path_builder().expect("path_builder");
let right = size.height * CURSOR_LINE_THICKNESS_FACTOR;
path_builder
.move_to(point2(0.0, 0.0))
.line_to(point2(right, 0.0))
.line_to(point2(right, size.height))
.line_to(point2(0.0, size.height))
.line_to(point2(0.0, 0.0));
path_builder.build()
}
fn path_for_hollow_block(size: &Size, render_context: &mut RenderContext) -> Path {
let mut path_builder = render_context.path_builder().expect("path_builder");
let inset = size.height * CURSOR_LINE_THICKNESS_FACTOR;
let bottom_start = size.height - inset;
let right_start = size.width - inset;
path_builder
// top
.move_to(point2(0.0, 0.0))
.line_to(point2(size.width, 0.0))
.line_to(point2(size.width, inset))
.line_to(point2(0.0, inset))
.line_to(point2(0.0, 0.0))
// bottom
.move_to(point2(0.0, bottom_start))
.line_to(point2(size.width, bottom_start))
.line_to(point2(size.width, size.height))
.line_to(point2(0.0, size.height))
.line_to(point2(0.0, bottom_start))
// left
.move_to(point2(0.0, inset))
.line_to(point2(inset, inset))
.line_to(point2(inset, bottom_start))
.line_to(point2(0.0, bottom_start))
.line_to(point2(0.0, inset))
// right
.move_to(point2(right_start, inset))
.line_to(point2(size.width, inset))
.line_to(point2(size.width, bottom_start))
.line_to(point2(right_start, bottom_start))
.line_to(point2(right_start, inset));
path_builder.build()
}
fn maybe_path_for_cursor_style(
render_context: &mut RenderContext,
cursor_style: CursorStyle,
cell_size: &Size,
) -> Option<Path> {
match cursor_style {
CursorStyle::Block => Some(path_for_block(cell_size, render_context)),
CursorStyle::Underline => Some(path_for_underline(cell_size, render_context)),
CursorStyle::Beam => Some(path_for_beam(cell_size, render_context)),
CursorStyle::HollowBlock => Some(path_for_hollow_block(cell_size, render_context)),
CursorStyle::Hidden => None,
}
}
fn maybe_raster_for_cursor_style(
render_context: &mut RenderContext,
cursor_style: CursorStyle,
column: usize,
row: usize,
cell_size: &Size,
cursors: &mut FxHashMap<CursorStyle, Option<Raster>>,
) -> Option<Raster> {
cursors
.entry(cursor_style)
.or_insert_with(|| {
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()
})
})
.as_ref()
.map(|r| {
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 maybe_raster_for_cell_content(
render_context: &mut RenderContext,
content: &CellContent,
column: usize,
row: usize,
cell_size: &Size,
textgrid: &TextGrid,
font: &FontFace,
glyphs: &mut GlyphMap,
cursors: &mut FxHashMap<CursorStyle, Option<Raster>>,
) -> Option<Raster> {
match content {
CellContent::Cursor(cursor_style) => maybe_raster_for_cursor_style(
render_context,
*cursor_style,
column,
row,
cell_size,
cursors,
),
CellContent::Char(c) => {
let grid_cell =
TextGridCell::new(render_context, column, row, *c, textgrid, font, glyphs);
grid_cell.raster
}
}
}
fn make_color(term_color: &Rgb) -> Color {
Color { r: term_color.r, g: term_color.g, b: term_color.b, a: 0xff }
}
fn make_rgb(color: &Color) -> Rgb {
Rgb { r: color.r, g: color.g, b: color.b }
}
/// Facet that implements a virtcon-style text grid with a status bar
/// and terminal output.
pub struct TextGridFacet<T> {
cell_size: Size,
textgrid: TextGrid,
font: FontFace,
glyphs: GlyphMap,
cursors: FxHashMap<CursorStyle, Option<Raster>>,
color_scheme: ColorScheme,
size: Size,
term: Option<Rc<RefCell<Term<T>>>>,
status: Vec<(String, Rgb)>,
status_tab_width: usize,
cells: FxHashMap<u16, CellId>,
old_cells: FxHashSet<u16>,
new_cells: FxHashSet<u16>,
}
pub enum TextGridMessages<T> {
SetTermMessage(Rc<RefCell<Term<T>>>),
ChangeStatusMessage(Vec<(String, Rgb)>),
}
const STATUS_BG: Rgb = Rgb { r: 0, g: 0, b: 0 };
impl<T> TextGridFacet<T> {
pub fn new(
font: FontFace,
font_size: f32,
color_scheme: ColorScheme,
term: Option<Rc<RefCell<Term<T>>>>,
status: Vec<(String, Rgb)>,
status_tab_width: usize,
cell_padding: f32,
) -> Self {
let cell_size = font_to_cell_size(font_size, cell_padding);
let textgrid = TextGrid::new(cell_size, cell_padding);
let glyphs = GlyphMap::new();
let cursors = FxHashMap::default();
let cells = FxHashMap::default();
let old_cells = FxHashSet::default();
let new_cells = FxHashSet::default();
Self {
cell_size,
textgrid,
font,
glyphs,
cursors,
color_scheme,
size: Size::zero(),
term,
status,
status_tab_width,
cells,
old_cells,
new_cells,
}
}
}
impl<T: 'static> Facet for TextGridFacet<T> {
fn update_layers(
&mut self,
_: Size,
layer_group: &mut dyn LayerGroup,
render_context: &mut RenderContext,
view_context: &ViewAssistantContext,
) -> std::result::Result<(), anyhow::Error> {
duration!("gfx", "TextGrid::update_layers");
self.size = view_context.size;
let config = self.color_scheme.into();
let term = self.term.as_ref().map(|t| t.borrow());
let status_tab_width = self.status_tab_width;
let columns = term.as_ref().map(|t| t.cols().0).unwrap_or(1);
let bg = make_rgb(&self.color_scheme.back);
// Create an iterator over cells used for the status bar followed by active
// terminal cells. Each layer has an order, contents, and color.
//
// The order of layers will be stable unless the number of columns change. Each
// row of background layers is followed by one row of foreground layers.
let cells = (0..columns)
.into_iter()
.map(|x| {
(
x,
x,
0,
if STATUS_BG != bg {
CellContent::Cursor(CursorStyle::Block)
} else {
CellContent::Cursor(CursorStyle::Hidden)
},
STATUS_BG,
)
})
.chain(self.status.iter().enumerate().flat_map(|(i, (s, rgb))| {
let start = i * status_tab_width;
let order = columns + start;
s.chars()
.enumerate()
.map(move |(x, c)| (order + x, start + x, 0, CellContent::Char(c), *rgb))
}))
.chain(term.iter().flat_map(|term| {
let stride = columns * 2;
term.renderable_cells(&config).flat_map(move |cell| {
let row = cell.line.0 + 1;
let order = row * stride + cell.column.0;
std::iter::once((
order,
cell.column.0,
row,
if cell.bg != bg {
CellContent::Cursor(CursorStyle::Block)
} else {
CellContent::Cursor(CursorStyle::Hidden)
},
cell.bg,
))
.chain(std::iter::once((
order + columns,
cell.column.0,
row,
cell.inner.into(),
cell.fg,
)))
})
}));
let glyphs = &mut self.glyphs;
let cursors = &mut self.cursors;
let font = &self.font;
let textgrid = &self.textgrid;
let cell_size = &self.cell_size;
// Process all cells and update the layer group as needed.
for (order, column, row, content, rgb) in cells.into_iter() {
let id = CellId { content, rgb };
let order = u16::try_from(order).expect("too many layers");
// Remove from old cells.
self.old_cells.remove(&order);
match self.cells.entry(order) {
Entry::Occupied(entry) => {
if *entry.get() != id {
let raster = maybe_raster_for_cell_content(
render_context,
&id.content,
column,
row,
cell_size,
textgrid,
font,
glyphs,
cursors,
);
if let Some(raster) = raster {
let value = entry.into_mut();
*value = id;
self.new_cells.insert(order);
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 {
self.new_cells.insert(order);
}
}
Entry::Vacant(entry) => {
let raster = maybe_raster_for_cell_content(
render_context,
&id.content,
column,
row,
cell_size,
textgrid,
font,
glyphs,
cursors,
);
if let Some(raster) = raster {
entry.insert(id);
self.new_cells.insert(order);
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 cells.
for order in self.old_cells.drain() {
self.cells.remove(&order);
layer_group.remove(order);
}
// Swap old cells for new cells.
mem::swap(&mut self.old_cells, &mut self.new_cells);
Ok(())
}
fn handle_message(&mut self, message: Box<dyn Any>) {
if let Some(message) = message.downcast_ref::<TextGridMessages<T>>() {
match message {
TextGridMessages::SetTermMessage(term) => {
self.term = Some(Rc::clone(term));
}
TextGridMessages::ChangeStatusMessage(status) => {
self.status = status.clone();
}
}
}
}
fn calculate_size(&self, _: Size) -> Size {
self.size
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::colors::ColorScheme,
anyhow::Error,
carnelian::drawing::load_font,
std::path::PathBuf,
term_model::event::{Event, EventListener},
};
#[derive(Default)]
struct TestListener;
impl EventListener for TestListener {
fn send_event(&self, _event: Event) {}
}
const FONT: &'static str = "/pkg/data/font.ttf";
#[test]
fn can_create_text_grid() -> Result<(), Error> {
let font = load_font(PathBuf::from(FONT))?;
let _ = TextGridFacet::<TestListener>::new(
font,
14.0,
ColorScheme::default(),
None,
vec![],
24,
2.0,
);
Ok(())
}
}