blob: 32619506efdaeca0777176e47a963c6bdc6a8489 [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 {
anyhow::Error,
argh::FromArgs,
carnelian::{
app::Config,
color::Color,
drawing::{load_font, DisplayRotation, FontFace, Glyph, GlyphMap, TextGrid},
make_app_assistant,
render::{BlendMode, Context as RenderContext, Fill, FillRule, Layer, Raster, Style},
scene::{
facets::{Facet, FacetId},
scene::{Scene, SceneBuilder, SceneOrder},
LayerGroup,
},
App, AppAssistant, Point, Size, ViewAssistant, ViewAssistantContext, ViewAssistantPtr,
ViewKey,
},
fuchsia_trace::duration,
fuchsia_zircon::Event,
rustc_hash::FxHashMap,
std::{
any::Any,
collections::hash_map::Entry,
f32,
fs::File,
io::{prelude::*, BufReader},
path::Path,
path::PathBuf,
},
};
/// Text Grid.
#[derive(Debug, FromArgs)]
#[argh(name = "textgrid_rs")]
struct Args {
/// display rotatation
#[argh(option)]
rotation: Option<DisplayRotation>,
/// text file to load (default is nyancat.txt)
#[argh(option, default = "String::from(\"nyancat.txt\")")]
file: String,
/// background color (default is black)
#[argh(option, from_str_fn(color_from_str))]
background: Option<Color>,
/// foreground color (default is white)
#[argh(option, from_str_fn(color_from_str))]
foreground: Option<Color>,
/// cell size (default is 8x16)
#[argh(option, from_str_fn(size_from_str))]
cell_size: Option<Size>,
}
fn color_from_str(value: &str) -> Result<Color, String> {
Color::from_hash_code(value).map_err(|err| err.to_string())
}
fn size_from_str(value: &str) -> Result<Size, String> {
let pair: Vec<_> = value.splitn(2, "x").collect();
let width = pair[0].parse::<f32>().map_err(|err| err.to_string())?;
let height = pair[1].parse::<f32>().map_err(|err| err.to_string())?;
Ok(Size::new(width, height))
}
struct TextGridAppAssistant {
display_rotation: DisplayRotation,
filename: String,
background: Color,
foreground: Color,
cell_size: Size,
}
impl Default for TextGridAppAssistant {
fn default() -> Self {
const BLACK_COLOR: Color = Color { r: 0, g: 0, b: 0, a: 255 };
let args: Args = argh::from_env();
let display_rotation = args.rotation.unwrap_or(DisplayRotation::Deg0);
let filename = args.file;
let background = args.background.unwrap_or(BLACK_COLOR);
let foreground = args.foreground.unwrap_or(Color::white());
let cell_size = args.cell_size.unwrap_or(Size::new(8.0, 16.0));
Self { display_rotation, filename, background, foreground, cell_size }
}
}
impl AppAssistant for TextGridAppAssistant {
fn setup(&mut self) -> Result<(), Error> {
Ok(())
}
fn create_view_assistant(&mut self, _: ViewKey) -> Result<ViewAssistantPtr, Error> {
let filename = self.filename.clone();
let background = self.background;
let foreground = self.foreground;
let cell_size = self.cell_size;
Ok(Box::new(TextGridViewAssistant::new(filename, background, foreground, cell_size)))
}
fn filter_config(&mut self, config: &mut Config) {
config.display_rotation = self.display_rotation;
}
}
struct TextGridFacet {
textgrid: TextGrid,
font: FontFace,
glyphs: GlyphMap,
foreground: Color,
pages: Vec<Vec<(u16, u16, char)>>,
size: Size,
current_page: usize,
cells: FxHashMap<(u16, u16), char>,
}
/// Message used to advance to next page
struct NextPageMessage {}
impl TextGridFacet {
fn new(
font: FontFace,
cell_size: Size,
foreground: Color,
pages: Vec<Vec<(u16, u16, char)>>,
) -> Self {
let textgrid = TextGrid::new(&font, &cell_size);
let glyphs = GlyphMap::new();
let cells: FxHashMap<(u16, u16), char> = FxHashMap::default();
Self {
textgrid,
font,
glyphs,
foreground,
pages,
size: Size::zero(),
current_page: 0,
cells,
}
}
fn maybe_raster_for_cell(
context: &mut RenderContext,
textgrid: &TextGrid,
column: usize,
row: usize,
c: char,
face: &FontFace,
glyph_map: &mut GlyphMap,
) -> Option<Raster> {
let glyph_index = face.face.glyph_index(c).expect("glyph_index");
let glyphs = &mut glyph_map.glyphs;
let scale = textgrid.scale;
let offset = textgrid.offset;
let glyph = glyphs.entry(glyph_index).or_insert_with(|| {
Glyph::with_scale_and_offset(context, face, scale, offset, glyph_index)
});
if glyph.bounding_box.is_empty() {
None
} else {
let cell_position = Point::new(
textgrid.cell_size.width * column as f32,
textgrid.cell_size.height * row as f32,
);
Some(glyph.raster.clone().translate(cell_position.to_vector().to_i32()))
}
}
}
impl Facet for TextGridFacet {
fn update_layers(
&mut self,
size: Size,
layer_group: &mut dyn LayerGroup,
render_context: &mut RenderContext,
_view_context: &ViewAssistantContext,
) -> std::result::Result<(), anyhow::Error> {
duration!(c"gfx", c"TextGridFacet::update_layers");
self.size = size;
let glyphs = &mut self.glyphs;
let font = &self.font;
let textgrid = &self.textgrid;
let foreground = &self.foreground;
let page = &self.pages[self.current_page];
const MAX_ROWS: u16 = 128;
const MAX_COLUMNS_PER_ROW: u16 = 256;
for (column, row, c) in page {
assert_eq!(*row < MAX_ROWS, true);
assert_eq!(*column < MAX_COLUMNS_PER_ROW, true);
let order = SceneOrder::try_from((*row * MAX_COLUMNS_PER_ROW + *column) as u32)
.unwrap_or_else(|e| panic!("{}", e));
match self.cells.entry((*column, *row)) {
Entry::Occupied(entry) => {
if *entry.get() != *c {
let maybe_raster = Self::maybe_raster_for_cell(
render_context,
textgrid,
*column as usize,
*row as usize,
*c,
font,
glyphs,
);
if let Some(raster) = maybe_raster {
let value = entry.into_mut();
*value = *c;
layer_group.insert(
order,
Layer {
raster,
clip: None,
style: Style {
fill_rule: FillRule::NonZero,
fill: Fill::Solid(*foreground),
blend_mode: BlendMode::Over,
},
},
);
} else {
entry.remove_entry();
layer_group.remove(order);
}
}
}
Entry::Vacant(entry) => {
let maybe_raster = Self::maybe_raster_for_cell(
render_context,
textgrid,
*column as usize,
*row as usize,
*c,
font,
glyphs,
);
if let Some(raster) = maybe_raster {
entry.insert(*c);
layer_group.insert(
order,
Layer {
raster,
clip: None,
style: Style {
fill_rule: FillRule::NonZero,
fill: Fill::Solid(*foreground),
blend_mode: BlendMode::Over,
},
},
);
}
}
}
}
Ok(())
}
fn handle_message(&mut self, msg: Box<dyn Any>) {
if let Some(_) = msg.downcast_ref::<NextPageMessage>() {
self.current_page = (self.current_page + 1) % self.pages.len();
}
}
fn calculate_size(&self, _available: Size) -> Size {
self.size
}
}
fn load_pages(path: PathBuf) -> Result<Vec<Vec<(u16, u16, char)>>, Error> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut pages = vec![];
let mut cells = vec![];
let mut row_start = 0;
for (row, line) in reader.lines().enumerate() {
if let Ok(line) = line {
for (column, c) in line.chars().enumerate() {
// 12 is form feed (a.k.a. page break).
if c == 12 as char {
pages.push(cells.drain(..).collect());
row_start = row + 1;
break;
} else {
cells.push((column as u16, (row - row_start) as u16, c));
}
}
}
}
if !cells.is_empty() {
pages.push(cells);
}
Ok(pages)
}
struct SceneDetails {
scene: Scene,
textgrid: FacetId,
}
struct TextGridViewAssistant {
filename: String,
background: Color,
foreground: Color,
cell_size: Size,
scene_details: Option<SceneDetails>,
}
impl TextGridViewAssistant {
pub fn new(filename: String, background: Color, foreground: Color, cell_size: Size) -> Self {
Self { filename, background, foreground, cell_size, scene_details: None }
}
}
impl ViewAssistant for TextGridViewAssistant {
fn resize(&mut self, _new_size: &Size) -> Result<(), Error> {
self.scene_details = None;
Ok(())
}
fn render(
&mut self,
render_context: &mut RenderContext,
ready_event: Event,
context: &ViewAssistantContext,
) -> Result<(), Error> {
let mut scene_details = self.scene_details.take().unwrap_or_else(|| {
let font = load_font(PathBuf::from("/pkg/data/fonts/RobotoMono-Regular.ttf"))
.expect("unable to load font data");
let pages = load_pages(Path::new("/pkg/data/static").join(self.filename.clone()))
.expect("unable to load text data");
let mut builder = SceneBuilder::new().background_color(self.background).mutable(false);
let textgrid_facet = TextGridFacet::new(font, self.cell_size, self.foreground, pages);
let textgrid = builder.facet(Box::new(textgrid_facet));
SceneDetails { scene: builder.build(), textgrid }
});
scene_details.scene.render(render_context, ready_event, context)?;
scene_details.scene.send_message(&scene_details.textgrid, Box::new(NextPageMessage {}));
self.scene_details = Some(scene_details);
context.request_render();
Ok(())
}
}
fn main() -> Result<(), Error> {
fuchsia_trace_provider::trace_provider_create_with_fdio();
App::run(make_app_assistant::<TextGridAppAssistant>())
}