1use crate::*;
2use lyon::math::Point;
3use lyon::path::Path;
4use lyon::tessellation::*;
5use std::collections::HashMap;
6use swash::zeno::{Command, PathData};
7use swash::{scale::ScaleContext, shape::ShapeContext, FontRef, GlyphId};
8
9#[derive(Debug, Clone, Copy)]
13pub struct TextLayoutOptions {
14 pub line_height: f32,
19}
20
21impl Default for TextLayoutOptions {
22 fn default() -> Self {
23 Self { line_height: 1.2 }
24 }
25}
26
27pub struct TextGenerator<'a> {
31 map: HashMap<GlyphId, CpuMesh>,
32 font: FontRef<'a>,
33 max_height: f32,
34 size: f32,
35}
36
37impl<'a> TextGenerator<'a> {
38 pub fn new(font_bytes: &'a [u8], font_index: u32, size: f32) -> Result<Self, RendererError> {
43 let font = FontRef::from_index(font_bytes, font_index as usize)
44 .ok_or(RendererError::MissingFont(font_index))?;
45 let mut context = ScaleContext::new();
46 let mut scaler = context.builder(font).size(size).build();
47 let mut map = HashMap::new();
48 let mut max_height: f32 = 0.0;
49 font.charmap().enumerate(|_, id| {
50 if let Some(outline) = scaler.scale_outline(id) {
51 let mut builder = Path::builder();
52 for command in outline.path().commands() {
53 match command {
54 Command::MoveTo(p) => {
55 builder.begin(Point::new(p.x, p.y));
56 }
57 Command::LineTo(p) => {
58 builder.line_to(Point::new(p.x, p.y));
59 }
60 Command::CurveTo(p1, p2, p3) => {
61 builder.cubic_bezier_to(
62 Point::new(p1.x, p1.y),
63 Point::new(p2.x, p2.y),
64 Point::new(p3.x, p3.y),
65 );
66 }
67 Command::QuadTo(p1, p2) => {
68 builder.quadratic_bezier_to(
69 Point::new(p1.x, p1.y),
70 Point::new(p2.x, p2.y),
71 );
72 }
73 Command::Close => builder.close(),
74 }
75 }
76 let path = builder.build();
77
78 let mut tessellator = FillTessellator::new();
79 let mut geometry: VertexBuffers<Vec3, u32> = VertexBuffers::new();
80 let options = FillOptions::default();
81 if tessellator
82 .tessellate_path(
83 &path,
84 &options,
85 &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| {
86 vec3(vertex.position().x, vertex.position().y, 0.0)
87 }),
88 )
89 .is_ok()
90 {
91 let mesh = CpuMesh {
92 positions: Positions::F32(geometry.vertices),
93 indices: Indices::U32(geometry.indices),
94 ..Default::default()
95 };
96 max_height = max_height.max(mesh.compute_aabb().size().y);
97 map.insert(id, mesh);
98 }
99 }
100 });
101 Ok(Self {
102 map,
103 font,
104 max_height,
105 size,
106 })
107 }
108
109 pub fn generate(&self, text: &str, options: TextLayoutOptions) -> CpuMesh {
113 let mut shape_context = ShapeContext::new();
114 let mut shaper = shape_context.builder(self.font).size(self.size).build();
115 let mut positions = Vec::new();
116 let mut indices = Vec::new();
117 let mut position = vec2(0.0, 0.0);
118
119 shaper.add_str(text);
120 shaper.shape_with(|cluster| {
121 let t = text.get(cluster.source.to_range());
122 if matches!(t, Some("\n")) {
123 position.y -= self.max_height * options.line_height;
125 position.x = 0.0;
126 }
127 for glyph in cluster.glyphs {
128 let mesh = self.map.get(&glyph.id).unwrap();
129
130 let index_offset = positions.len() as u32;
131 let Indices::U32(mesh_indices) = &mesh.indices else {
132 unreachable!()
133 };
134 indices.extend(mesh_indices.iter().map(|i| i + index_offset));
135
136 let position_offset = (position + vec2(glyph.x, glyph.y)).extend(0.0);
137 let Positions::F32(mesh_positions) = &mesh.positions else {
138 unreachable!()
139 };
140 positions.extend(mesh_positions.iter().map(|p| p + position_offset));
141 }
142 position.x += cluster.advance();
143 });
144
145 CpuMesh {
146 positions: Positions::F32(positions),
147 indices: Indices::U32(indices),
148 ..Default::default()
149 }
150 }
151}