use std::borrow::Cow;
use bevy::{prelude::*, reflect::TypePath, render::render_asset::RenderAsset};
use parley::{
FontSettings, FontStyle, FontVariation, Layout, PositionedLayoutItem, RangedBuilder,
StyleProperty,
};
use vello::{
Scene,
kurbo::Affine,
peniko::{Brush, Fill},
};
use super::{VelloFontAxes, VelloTextAnchor, context::LOCAL_FONT_CONTEXT};
use crate::{
integrations::text::context::{LOCAL_LAYOUT_CONTEXT, get_global_font_context},
prelude::{VelloTextAlign, VelloTextStyle},
};
#[derive(Asset, TypePath, Debug, Clone)]
pub struct VelloFont {
pub(crate) family_name: String,
pub bytes: Vec<u8>,
}
impl RenderAsset for VelloFont {
type SourceAsset = VelloFont;
type Param = ();
fn prepare_asset(
source_asset: Self::SourceAsset,
_asset_id: AssetId<Self::SourceAsset>,
_param: &mut bevy::ecs::system::SystemParamItem<Self::Param>,
_previous_asset: Option<&Self>,
) -> Result<Self, bevy::render::render_asset::PrepareAssetError<Self::SourceAsset>> {
Ok(source_asset)
}
}
impl VelloFont {
pub fn new(font_data: Vec<u8>) -> Self {
Self {
bytes: font_data,
family_name: "Fira Mono".to_string(),
}
}
pub fn layout(
&self,
value: &str,
style: &VelloTextStyle,
text_align: VelloTextAlign,
max_advance: Option<f32>,
) -> Layout<Brush> {
LOCAL_FONT_CONTEXT.with_borrow_mut(|font_context| {
if font_context.is_none() {
*font_context = Some(get_global_font_context().clone());
}
let font_context = font_context.as_mut().unwrap();
LOCAL_LAYOUT_CONTEXT.with_borrow_mut(|layout_context| {
let mut builder = layout_context.ranged_builder(font_context, value, 1.0, true);
apply_font_styles(&mut builder, style);
apply_variable_axes(&mut builder, &style.font_axes);
builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(
parley::FontFamily::Named(Cow::Owned(self.family_name.clone())),
)));
let mut layout = builder.build(value);
layout.break_all_lines(max_advance);
layout.align(
max_advance,
text_align.into(),
parley::AlignmentOptions::default(),
);
layout
})
})
}
#[expect(clippy::too_many_arguments, reason = "Common lint in bevy")]
pub(crate) fn render(
&self,
scene: &mut Scene,
mut transform: Affine,
value: &str,
style: &VelloTextStyle,
text_align: VelloTextAlign,
max_advance: Option<f32>,
text_anchor: VelloTextAnchor,
ui_content: Option<Vec2>,
clip: Option<vello::kurbo::Rect>,
) {
let layout = self.layout(value, style, text_align, max_advance);
let text_w = layout.width() as f64;
let text_h = layout.height() as f64;
let (dx, dy) = if let Some(content_size) = ui_content {
let offset = compute_ui_anchor_offset(
text_anchor,
text_w,
text_h,
content_size.x,
content_size.y,
);
let c = transform.as_coeffs();
(
c[0] * offset.0 + c[2] * offset.1,
c[1] * offset.0 + c[3] * offset.1,
)
} else {
compute_world_anchor_offset(text_anchor, text_w, text_h)
};
transform = transform.then_translate(vello::kurbo::Vec2::new(dx, dy));
let cull_y: Option<(f64, f64)> = clip.and_then(|r| {
let inv = transform.inverse();
let c = transform.as_coeffs();
let det = c[0] * c[3] - c[1] * c[2];
if det.abs() < f64::EPSILON {
return None;
}
let corners = [
inv * vello::kurbo::Point::new(r.x0, r.y0),
inv * vello::kurbo::Point::new(r.x1, r.y0),
inv * vello::kurbo::Point::new(r.x0, r.y1),
inv * vello::kurbo::Point::new(r.x1, r.y1),
];
let y_min = corners.iter().map(|p| p.y).fold(f64::INFINITY, f64::min);
let y_max = corners
.iter()
.map(|p| p.y)
.fold(f64::NEG_INFINITY, f64::max);
Some((y_min, y_max))
});
'lines: for line in layout.lines() {
if let Some((y_min, y_max)) = cull_y {
let lm = line.metrics();
if (lm.max_coord as f64) < y_min {
continue;
}
if (lm.min_coord as f64) > y_max {
break 'lines;
}
}
for item in line.items() {
let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
continue;
};
let mut x = glyph_run.offset();
let y = glyph_run.baseline();
let run = glyph_run.run();
let font = run.font();
let font_size = run.font_size();
let synthesis = run.synthesis();
let glyph_xform = synthesis
.skew()
.map(|angle| Affine::skew(angle.to_radians().tan() as f64, 0.0));
scene
.draw_glyphs(font)
.brush(&style.brush)
.hint(true)
.transform(transform)
.glyph_transform(glyph_xform)
.font_size(font_size)
.normalized_coords(run.normalized_coords())
.draw(
Fill::NonZero,
glyph_run.glyphs().map(|glyph| {
let gx = x + glyph.x;
let gy = y - glyph.y;
x += glyph.advance;
vello::Glyph {
id: glyph.id as _,
x: gx,
y: gy,
}
}),
);
}
}
}
}
fn apply_font_styles(builder: &mut RangedBuilder<'_, Brush>, style: &VelloTextStyle) {
builder.push_default(StyleProperty::FontSize(style.font_size));
builder.push_default(StyleProperty::LineHeight(
parley::LineHeight::MetricsRelative(style.line_height),
));
builder.push_default(StyleProperty::WordSpacing(style.word_spacing));
builder.push_default(StyleProperty::LetterSpacing(style.letter_spacing));
}
fn apply_variable_axes(builder: &mut RangedBuilder<'_, Brush>, axes: &VelloFontAxes) {
let mut variable_axes: Vec<FontVariation> = vec![];
if let Some(weight) = axes.weight {
variable_axes.push(parley::swash::Setting {
tag: parley::swash::tag_from_str_lossy("wght"),
value: weight,
});
}
if let Some(width) = axes.width {
variable_axes.push(parley::swash::Setting {
tag: parley::swash::tag_from_str_lossy("wdth"),
value: width,
});
}
if let Some(optical_size) = axes.optical_size {
variable_axes.push(parley::swash::Setting {
tag: parley::swash::tag_from_str_lossy("opsz"),
value: optical_size,
});
}
if let Some(grade) = axes.grade {
variable_axes.push(parley::swash::Setting {
tag: parley::swash::tag_from_str_lossy("GRAD"),
value: grade,
});
}
if let Some(thick_stroke) = axes.thick_stroke {
variable_axes.push(parley::swash::Setting {
tag: parley::swash::tag_from_str_lossy("XOPQ"),
value: thick_stroke,
});
}
if let Some(thin_stroke) = axes.thin_stroke {
variable_axes.push(parley::swash::Setting {
tag: parley::swash::tag_from_str_lossy("YOPQ"),
value: thin_stroke,
});
}
if let Some(counter_width) = axes.counter_width {
variable_axes.push(parley::swash::Setting {
tag: parley::swash::tag_from_str_lossy("XTRA"),
value: counter_width,
});
}
if let Some(uppercase_height) = axes.uppercase_height {
variable_axes.push(parley::swash::Setting {
tag: parley::swash::tag_from_str_lossy("YTUC"),
value: uppercase_height,
});
}
if let Some(lowercase_height) = axes.lowercase_height {
variable_axes.push(parley::swash::Setting {
tag: parley::swash::tag_from_str_lossy("YTLC"),
value: lowercase_height,
});
}
if let Some(ascender_height) = axes.ascender_height {
variable_axes.push(parley::swash::Setting {
tag: parley::swash::tag_from_str_lossy("YTAS"),
value: ascender_height,
});
}
if let Some(descender_depth) = axes.descender_depth {
variable_axes.push(parley::swash::Setting {
tag: parley::swash::tag_from_str_lossy("YTDE"),
value: descender_depth,
});
}
if let Some(figure_height) = axes.figure_height {
variable_axes.push(parley::swash::Setting {
tag: parley::swash::tag_from_str_lossy("YTFI"),
value: figure_height,
});
}
if axes.italic {
builder.push_default(StyleProperty::FontStyle(FontStyle::Italic));
} else if axes.slant.is_some() {
builder.push_default(StyleProperty::FontStyle(FontStyle::Oblique(axes.slant)));
}
builder.push_default(StyleProperty::FontVariations(FontSettings::List(
variable_axes.into(),
)));
}
pub(crate) fn compute_world_anchor_offset(
text_anchor: VelloTextAnchor,
text_w: f64,
text_h: f64,
) -> (f64, f64) {
match text_anchor {
VelloTextAnchor::TopLeft => (0.0, 0.0),
VelloTextAnchor::Left => (0.0, -text_h / 2.0),
VelloTextAnchor::BottomLeft => (0.0, -text_h),
VelloTextAnchor::Top => (-text_w / 2.0, 0.0),
VelloTextAnchor::Center => (-text_w / 2.0, -text_h / 2.0),
VelloTextAnchor::Bottom => (-text_w / 2.0, -text_h),
VelloTextAnchor::TopRight => (-text_w, 0.0),
VelloTextAnchor::Right => (-text_w, -text_h / 2.0),
VelloTextAnchor::BottomRight => (-text_w, -text_h),
}
}
pub(crate) fn compute_ui_anchor_offset(
text_anchor: VelloTextAnchor,
text_w: f64,
text_h: f64,
node_w: f32,
node_h: f32,
) -> (f64, f64) {
let node_w = node_w as f64;
let node_h = node_h as f64;
let top_left_x = -node_w / 2.0;
let top_left_y = -node_h / 2.0;
let (anchor_x, anchor_y) = match text_anchor {
VelloTextAnchor::TopLeft => (0.0, 0.0),
VelloTextAnchor::Top => ((node_w - text_w) / 2.0, 0.0),
VelloTextAnchor::TopRight => (node_w - text_w, 0.0),
VelloTextAnchor::Left => (0.0, (node_h - text_h) / 2.0),
VelloTextAnchor::Center => ((node_w - text_w) / 2.0, (node_h - text_h) / 2.0),
VelloTextAnchor::Right => (node_w - text_w, (node_h - text_h) / 2.0),
VelloTextAnchor::BottomLeft => (0.0, node_h - text_h),
VelloTextAnchor::Bottom => ((node_w - text_w) / 2.0, node_h - text_h),
VelloTextAnchor::BottomRight => (node_w - text_w, node_h - text_h),
};
(top_left_x + anchor_x, top_left_y + anchor_y)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn world_center_offsets_by_half_text_dims() {
let (dx, dy) = compute_world_anchor_offset(VelloTextAnchor::Center, 200.0, 40.0);
assert_eq!((dx, dy), (-100.0, -20.0));
}
#[test]
fn world_top_left_is_zero() {
let (dx, dy) = compute_world_anchor_offset(VelloTextAnchor::TopLeft, 200.0, 40.0);
assert_eq!((dx, dy), (0.0, 0.0));
}
#[test]
fn world_bottom_right_offsets_by_full_text_dims() {
let (dx, dy) = compute_world_anchor_offset(VelloTextAnchor::BottomRight, 200.0, 40.0);
assert_eq!((dx, dy), (-200.0, -40.0));
}
const NODE_W: f32 = 400.0;
const NODE_H: f32 = 200.0;
const TEXT_W: f64 = 200.0;
const TEXT_H: f64 = 40.0;
#[test]
fn ui_top_left_positions_at_node_top_left() {
let (dx, dy) =
compute_ui_anchor_offset(VelloTextAnchor::TopLeft, TEXT_W, TEXT_H, NODE_W, NODE_H);
assert_eq!((dx, dy), (-200.0, -100.0));
}
#[test]
fn ui_center_centers_text_in_node() {
let (dx, dy) =
compute_ui_anchor_offset(VelloTextAnchor::Center, TEXT_W, TEXT_H, NODE_W, NODE_H);
assert_eq!((dx, dy), (-100.0, -20.0));
}
#[test]
fn ui_bottom_right_positions_at_node_bottom_right() {
let (dx, dy) =
compute_ui_anchor_offset(VelloTextAnchor::BottomRight, TEXT_W, TEXT_H, NODE_W, NODE_H);
assert_eq!((dx, dy), (0.0, 60.0));
}
#[test]
fn ui_left_vertically_centers_at_left_edge() {
let (dx, dy) =
compute_ui_anchor_offset(VelloTextAnchor::Left, TEXT_W, TEXT_H, NODE_W, NODE_H);
assert_eq!((dx, dy), (-200.0, -20.0));
}
#[test]
fn ui_top_right_positions_at_node_top_right() {
let (dx, dy) =
compute_ui_anchor_offset(VelloTextAnchor::TopRight, TEXT_W, TEXT_H, NODE_W, NODE_H);
assert_eq!((dx, dy), (0.0, -100.0));
}
#[test]
fn ui_bottom_centers_at_bottom_edge() {
let (dx, dy) =
compute_ui_anchor_offset(VelloTextAnchor::Bottom, TEXT_W, TEXT_H, NODE_W, NODE_H);
assert_eq!((dx, dy), (-100.0, 60.0));
}
#[test]
fn ui_top_centers_at_top_edge() {
let (dx, dy) =
compute_ui_anchor_offset(VelloTextAnchor::Top, TEXT_W, TEXT_H, NODE_W, NODE_H);
assert_eq!((dx, dy), (-100.0, -100.0));
}
#[test]
fn ui_right_vertically_centers_at_right_edge() {
let (dx, dy) =
compute_ui_anchor_offset(VelloTextAnchor::Right, TEXT_W, TEXT_H, NODE_W, NODE_H);
assert_eq!((dx, dy), (0.0, -20.0));
}
#[test]
fn ui_bottom_left_positions_at_node_bottom_left() {
let (dx, dy) =
compute_ui_anchor_offset(VelloTextAnchor::BottomLeft, TEXT_W, TEXT_H, NODE_W, NODE_H);
assert_eq!((dx, dy), (-200.0, 60.0));
}
#[test]
fn ui_anchor_pipeline_correct_at_2x_dpi() {
let scale = 2.0_f64;
let affine = Affine::new([scale, 0.0, 0.0, scale, 500.0, 300.0]);
let offset = compute_ui_anchor_offset(VelloTextAnchor::Center, 100.0, 20.0, 200.0, 100.0);
let c = affine.as_coeffs();
let dx = c[0] * offset.0 + c[2] * offset.1;
let dy = c[1] * offset.0 + c[3] * offset.1;
let result = affine.then_translate(vello::kurbo::Vec2::new(dx, dy));
let c = result.as_coeffs();
assert_eq!((c[4], c[5]), (400.0, 280.0));
}
#[test]
fn ui_anchor_pipeline_correct_under_rotation() {
let scale = 2.0_f64;
let cos90 = 0.0_f64;
let sin90 = 1.0_f64;
let affine = Affine::new([
cos90 * scale,
sin90 * scale,
-sin90 * scale,
cos90 * scale,
500.0,
300.0,
]);
let offset = compute_ui_anchor_offset(VelloTextAnchor::Center, 100.0, 20.0, 200.0, 100.0);
let c = affine.as_coeffs();
let dx = c[0] * offset.0 + c[2] * offset.1;
let dy = c[1] * offset.0 + c[3] * offset.1;
let result = affine.then_translate(vello::kurbo::Vec2::new(dx, dy));
let c = result.as_coeffs();
assert_eq!((c[4], c[5]), (520.0, 200.0));
}
#[test]
fn ui_center_is_origin_when_text_fills_node() {
let (ui_dx, ui_dy) =
compute_ui_anchor_offset(VelloTextAnchor::Center, 400.0, 200.0, 400.0, 200.0);
assert_eq!((ui_dx, ui_dy), (-200.0, -100.0));
let (ui_tl_dx, ui_tl_dy) =
compute_ui_anchor_offset(VelloTextAnchor::TopLeft, 400.0, 200.0, 400.0, 200.0);
assert_eq!((ui_tl_dx, ui_tl_dy), (-200.0, -100.0));
let (w_dx, w_dy) = compute_world_anchor_offset(VelloTextAnchor::TopLeft, 400.0, 200.0);
assert_eq!((w_dx, w_dy), (0.0, 0.0));
}
}