blob: de70a53c21e4c658a544836431d43c5298958fab [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Text rendering example scene.
//!
//! Closely follows the parley example at `parley/examples/common/src/lib.rs`
//! and `parley/examples/vello_hybrid_render/src/main.rs`.
use core::any::Any;
use core::fmt;
use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontWeight, GenericFamily, GlyphRun,
Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
};
use parley_draw::Glyph;
use vello_common::kurbo::{Affine, Rect, Vec2};
use vello_common::peniko::Color;
use crate::{ExampleScene, RenderingContext};
const PADDING: u32 = 20;
const MAX_ADVANCE: f32 = 200.0;
const FONT_SIZE: f32 = 16.0;
const SIMPLE_TEXT: &str = "Some text here. Let's make it a bit longer so that \
line wrapping kicks in easily. This demonstrates basic glyph caching with \
plain Latin text and common punctuation???";
const RICH_TEXT: &str = "Some text here. Let's make it a bit longer so that \
line wrapping kicks in. Bitmap emoji 😊 and COLR emoji 🎉.\n\
And also some اللغة العربية arabic text.\n\
This is underlining pq and strikethrough text.";
/// Minimal brush carrying only a solid color, matching the parley examples.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ColorBrush {
/// The solid color for this brush.
pub color: Color,
}
impl Default for ColorBrush {
fn default() -> Self {
Self {
color: Color::WHITE,
}
}
}
#[cfg(target_arch = "wasm32")]
const ROBOTO_FONT: &[u8] = include_bytes!("../../../examples/assets/roboto/Roboto-Regular.ttf");
/// State for the text example.
pub struct TextScene {
layout: Layout<ColorBrush>,
/// Type-erased glyph caches, lazily initialized per backend.
glyph_caches: Option<Box<dyn Any>>,
}
impl fmt::Debug for TextScene {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "TextScene")
}
}
impl ExampleScene for TextScene {
fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) {
if self.glyph_caches.is_none() {
self.glyph_caches = Some(ctx.create_glyph_caches());
}
let content_transform =
root_transform * Affine::translate(Vec2::new(PADDING as f64, PADDING as f64));
ctx.set_transform(content_transform);
let glyph_caches = self
.glyph_caches
.as_mut()
.expect("glyph caches not initialized");
for line in self.layout.lines() {
for item in line.items() {
if let PositionedLayoutItem::GlyphRun(glyph_run) = item {
render_glyph_run(ctx, &glyph_run, glyph_caches.as_mut());
}
}
}
}
}
impl TextScene {
/// Create a new `TextScene` with the given text using the simple layout.
pub fn new(text: &str) -> Self {
let mut font_cx = FontContext::new();
let mut layout_cx = LayoutContext::new();
#[cfg(target_arch = "wasm32")]
font_cx
.collection
.register_fonts(ROBOTO_FONT.to_vec().into(), None);
let layout = build_simple_layout(&mut font_cx, &mut layout_cx, text);
Self {
layout,
glyph_caches: None,
}
}
/// Create a new `TextScene` with the rich layout (emoji, underline, etc.).
pub fn rich() -> Self {
let mut font_cx = FontContext::new();
let mut layout_cx = LayoutContext::new();
#[cfg(target_arch = "wasm32")]
font_cx
.collection
.register_fonts(ROBOTO_FONT.to_vec().into(), None);
let layout = build_rich_layout(&mut font_cx, &mut layout_cx, RICH_TEXT);
Self {
layout,
glyph_caches: None,
}
}
}
impl Default for TextScene {
fn default() -> Self {
Self::new(SIMPLE_TEXT)
}
}
fn build_simple_layout(
font_cx: &mut FontContext,
layout_cx: &mut LayoutContext<ColorBrush>,
text: &str,
) -> Layout<ColorBrush> {
let mut builder = layout_cx.ranged_builder(font_cx, text, 1.0, true);
let foreground = ColorBrush {
color: Color::WHITE,
};
builder.push_default(StyleProperty::Brush(foreground));
builder.push_default(GenericFamily::SystemUi);
builder.push_default(LineHeight::FontSizeRelative(1.3));
builder.push_default(StyleProperty::FontSize(FONT_SIZE));
let bold = FontWeight::new(600.0);
builder.push(StyleProperty::FontWeight(bold), 0..4);
if let Some((start, matched)) = text.match_indices("here").next() {
let purple = ColorBrush {
color: Color::from_rgb8(200, 130, 255),
};
builder.push(StyleProperty::Brush(purple), start..start + matched.len());
}
let mut layout = builder.build(text);
let max_advance = Some(MAX_ADVANCE);
layout.break_all_lines(max_advance);
layout.align(max_advance, Alignment::Start, AlignmentOptions::default());
layout
}
fn build_rich_layout(
font_cx: &mut FontContext,
layout_cx: &mut LayoutContext<ColorBrush>,
text: &str,
) -> Layout<ColorBrush> {
let mut builder = layout_cx.ranged_builder(font_cx, text, 1.0, true);
let foreground = ColorBrush {
color: Color::WHITE,
};
builder.push_default(StyleProperty::Brush(foreground));
builder.push_default(GenericFamily::SystemUi);
builder.push_default(LineHeight::FontSizeRelative(1.3));
builder.push_default(StyleProperty::FontSize(FONT_SIZE));
let bold = FontWeight::new(600.0);
builder.push(StyleProperty::FontWeight(bold), 0..4);
if let Some((start, matched)) = text.match_indices("here").next() {
let purple = ColorBrush {
color: Color::from_rgb8(200, 130, 255),
};
builder.push(StyleProperty::Brush(purple), start..start + matched.len());
}
if let Some((start, matched)) = text.match_indices("underlining pq").next() {
builder.push(StyleProperty::Underline(true), start..start + matched.len());
}
if let Some((start, matched)) = text.match_indices("strikethrough").next() {
builder.push(
StyleProperty::Strikethrough(true),
start..start + matched.len(),
);
}
if let Some((start, matched)) = text.match_indices("🎉").next() {
builder.push(
FontFamily::parse("Noto Color Emoji").unwrap(),
start..start + matched.len(),
);
}
let mut layout = builder.build(text);
let max_advance = Some(MAX_ADVANCE);
layout.break_all_lines(max_advance);
layout.align(max_advance, Alignment::Start, AlignmentOptions::default());
layout
}
fn render_glyph_run(
ctx: &mut impl RenderingContext,
glyph_run: &GlyphRun<'_, ColorBrush>,
glyph_caches: &mut dyn Any,
) {
let style = glyph_run.style();
ctx.set_paint(style.brush.color);
let run = glyph_run.run();
let font = run.font();
let font_size = run.font_size();
let normalized_coords = run.normalized_coords();
let glyphs: Vec<Glyph> = glyph_run
.positioned_glyphs()
.map(|g| Glyph {
id: u32::from(g.id),
x: g.x,
y: g.y,
})
.collect();
ctx.fill_glyphs(
font,
font_size,
true,
normalized_coords,
glyphs.into_iter(),
glyph_caches,
);
if let Some(decoration) = &style.underline {
let offset = decoration.offset.unwrap_or(run.metrics().underline_offset);
let size = decoration.size.unwrap_or(run.metrics().underline_size);
ctx.set_paint(decoration.brush.color);
render_decoration(ctx, glyph_run, offset, size);
}
if let Some(decoration) = &style.strikethrough {
let offset = decoration
.offset
.unwrap_or(run.metrics().strikethrough_offset);
let size = decoration.size.unwrap_or(run.metrics().strikethrough_size);
ctx.set_paint(decoration.brush.color);
render_strikethrough(ctx, glyph_run, offset, size);
}
}
fn render_decoration(
ctx: &mut impl RenderingContext,
glyph_run: &GlyphRun<'_, ColorBrush>,
offset: f32,
size: f32,
) {
let y = glyph_run.baseline() - offset;
let x = glyph_run.offset();
let x1 = x + glyph_run.advance();
let y1 = y + size;
ctx.fill_rect(&Rect::new(x as f64, y as f64, x1 as f64, y1 as f64));
}
fn render_strikethrough(
ctx: &mut impl RenderingContext,
glyph_run: &GlyphRun<'_, ColorBrush>,
offset: f32,
size: f32,
) {
let y = glyph_run.baseline() - offset;
let x = glyph_run.offset();
let x1 = x + glyph_run.advance();
let y1 = y + size;
ctx.fill_rect(&Rect::new(x as f64, y as f64, x1 as f64, y1 as f64));
}