blob: da8b86e4a03779b931248a334d6553deac397c3e [file] [log] [blame]
use std::ops::RangeBounds;
use swash::scale::{ScaleContext, Scaler};
use swash::zeno::{Vector, Verb};
use swash::{FontRef, GlyphId};
use piet::kurbo::{Point, Rect, Size};
use piet::{
Error, FontFamily, HitTestPoint, HitTestPosition, LineMetric, RenderContext, Text,
TextAttribute, TextLayout, TextLayoutBuilder, TextStorage,
};
use crate::encoder::GlyphEncoder;
use crate::render_ctx;
use crate::stages::Transform;
use crate::PietGpuRenderContext;
// This is very much a hack to get things working.
// On Windows, can set this to "c:\\Windows\\Fonts\\seguiemj.ttf" to get color emoji
const FONT_DATA: &[u8] = include_bytes!("../third-party/Roboto-Regular.ttf");
#[derive(Clone)]
pub struct Font {
// Storing the font_ref is ok for static font data, but the better way to do
// this is to store the CacheKey.
font_ref: FontRef<'static>,
}
#[derive(Clone)]
pub struct PietGpuText {
font: Font,
}
#[derive(Clone)]
pub struct PietGpuTextLayout {
font: Font,
size: f64,
glyphs: Vec<Glyph>,
}
pub struct PietGpuTextLayoutBuilder {
font: Font,
text: String,
size: f64,
}
#[derive(Clone, Debug)]
struct Glyph {
glyph_id: GlyphId,
x: f32,
y: f32,
}
struct TextRenderCtx<'a> {
scaler: Scaler<'a>,
}
impl PietGpuText {
pub(crate) fn new(font: Font) -> PietGpuText {
PietGpuText { font }
}
}
impl Text for PietGpuText {
type TextLayout = PietGpuTextLayout;
type TextLayoutBuilder = PietGpuTextLayoutBuilder;
fn load_font(&mut self, _data: &[u8]) -> Result<FontFamily, Error> {
Ok(FontFamily::default())
}
fn new_text_layout(&mut self, text: impl TextStorage) -> Self::TextLayoutBuilder {
PietGpuTextLayoutBuilder::new(&self.font, &text.as_str())
}
fn font_family(&mut self, _family_name: &str) -> Option<FontFamily> {
Some(FontFamily::default())
}
}
impl TextLayout for PietGpuTextLayout {
fn size(&self) -> Size {
Size::ZERO
}
fn image_bounds(&self) -> Rect {
Rect::ZERO
}
fn line_text(&self, _line_number: usize) -> Option<&str> {
None
}
fn line_metric(&self, _line_number: usize) -> Option<LineMetric> {
None
}
fn line_count(&self) -> usize {
0
}
fn hit_test_point(&self, _point: Point) -> HitTestPoint {
HitTestPoint::default()
}
fn hit_test_text_position(&self, _text_position: usize) -> HitTestPosition {
HitTestPosition::default()
}
fn text(&self) -> &str {
""
}
}
impl Font {
pub fn new() -> Font {
let font_ref = FontRef::from_index(FONT_DATA, 0).expect("error parsing font");
Font { font_ref }
}
fn make_path<'a>(&self, glyph_id: GlyphId, tc: &mut TextRenderCtx<'a>) -> GlyphEncoder {
let mut encoder = GlyphEncoder::default();
if tc.scaler.has_color_outlines() {
if let Some(outline) = tc.scaler.scale_color_outline(glyph_id) {
// TODO: be more sophisticated choosing a palette
let palette = self.font_ref.color_palettes().next().unwrap();
let mut i = 0;
while let Some(layer) = outline.get(i) {
if let Some(color_ix) = layer.color_index() {
let color = palette.get(color_ix);
append_outline(&mut encoder, layer.verbs(), layer.points());
encoder.fill_color(*bytemuck::from_bytes(&color));
}
i += 1;
}
return encoder;
}
}
if let Some(outline) = tc.scaler.scale_outline(glyph_id) {
append_outline(&mut encoder, outline.verbs(), outline.points());
}
encoder
}
}
impl PietGpuTextLayout {
pub(crate) fn make_layout(font: &Font, text: &str, size: f64) -> PietGpuTextLayout {
let mut glyphs = Vec::new();
let mut x = 0.0;
let y = 0.0;
for c in text.chars() {
let glyph_id = font.font_ref.charmap().map(c);
let glyph = Glyph { glyph_id, x, y };
glyphs.push(glyph);
let adv = font.font_ref.glyph_metrics(&[]).advance_width(glyph_id);
x += adv;
}
PietGpuTextLayout {
glyphs,
font: font.clone(),
size,
}
}
pub(crate) fn draw_text(&self, ctx: &mut PietGpuRenderContext, pos: Point) {
let mut scale_ctx = ScaleContext::new();
let scaler = scale_ctx.builder(self.font.font_ref).size(2048.).build();
let mut tc = TextRenderCtx { scaler };
// Should we use ppem from font, or let swash scale?
const DEFAULT_UPEM: u16 = 2048;
let scale = self.size as f32 / DEFAULT_UPEM as f32;
ctx.save().unwrap();
// TODO: handle y offsets also
for glyph in &self.glyphs {
let tpos = render_ctx::to_f32_2(pos);
let transform = Transform {
mat: [scale, 0.0, 0.0, -scale],
translate: [tpos[0] + scale * glyph.x, tpos[1]],
};
//println!("{:?}, {:?}", transform.mat, transform.translate);
ctx.encode_transform(transform);
let glyph = self.font.make_path(glyph.glyph_id, &mut tc);
ctx.encode_glyph(&glyph);
if !glyph.is_color() {
ctx.fill_glyph(0xff_ff_ff_ff);
}
}
ctx.restore().unwrap();
}
}
impl PietGpuTextLayoutBuilder {
pub(crate) fn new(font: &Font, text: &str) -> PietGpuTextLayoutBuilder {
PietGpuTextLayoutBuilder {
font: font.clone(),
text: text.to_owned(),
size: 12.0,
}
}
}
impl TextLayoutBuilder for PietGpuTextLayoutBuilder {
type Out = PietGpuTextLayout;
fn max_width(self, _width: f64) -> Self {
self
}
fn alignment(self, _alignment: piet::TextAlignment) -> Self {
self
}
fn default_attribute(mut self, attribute: impl Into<TextAttribute>) -> Self {
let attribute = attribute.into();
match attribute {
TextAttribute::FontSize(size) => self.size = size,
_ => (),
}
self
}
fn range_attribute(
self,
_range: impl RangeBounds<usize>,
_attribute: impl Into<TextAttribute>,
) -> Self {
self
}
fn build(self) -> Result<Self::Out, Error> {
Ok(PietGpuTextLayout::make_layout(
&self.font, &self.text, self.size,
))
}
}
pub(crate) fn append_outline(encoder: &mut GlyphEncoder, verbs: &[Verb], points: &[Vector]) {
let mut path_encoder = encoder.path_encoder();
let mut i = 0;
for verb in verbs {
match verb {
Verb::MoveTo => {
let p = points[i];
path_encoder.move_to(p.x, p.y);
i += 1;
}
Verb::LineTo => {
let p = points[i];
path_encoder.line_to(p.x, p.y);
i += 1;
}
Verb::QuadTo => {
let p1 = points[i];
let p2 = points[i + 1];
path_encoder.quad_to(p1.x, p1.y, p2.x, p2.y);
i += 2;
}
Verb::CurveTo => {
let p1 = points[i];
let p2 = points[i + 1];
let p3 = points[i + 2];
path_encoder.cubic_to(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
i += 3;
}
Verb::Close => path_encoder.close_path(),
}
}
path_encoder.path();
let n_pathseg = path_encoder.n_pathseg();
encoder.finish_path(n_pathseg);
}