1use polyfont_core::PolyfontEngine;
17use thiserror::Error;
18use tracing::info;
19
20#[derive(Debug, Error)]
22pub enum RenderError {
23 #[error("no rendering backend available")]
24 NoBackend,
25
26 #[error("font atlas error: {0}")]
27 FontAtlas(String),
28
29 #[error("shaping error: {0}")]
30 Shaping(String),
31
32 #[error("compositing error: {0}")]
33 Compositing(String),
34
35 #[error("GPU initialization failed: {0}")]
36 GpuInit(String),
37
38 #[error("window creation failed: {0}")]
39 Window(String),
40}
41
42#[derive(Debug, Clone)]
44pub struct GlyphPosition {
45 pub font_family: String,
47 pub x: f32,
49 pub y: f32,
51 pub glyph_id: u32,
53 pub advance: f32,
55}
56
57#[derive(Debug, Clone)]
59pub struct RenderedLine {
60 pub glyphs: Vec<GlyphPosition>,
62 pub width: f32,
64 pub height: f32,
66}
67
68#[derive(Debug, Clone)]
70pub struct RenderConfig {
71 pub width: u32,
73 pub height: u32,
75 pub font_size: f32,
77 pub line_height: f32,
79}
80
81impl Default for RenderConfig {
82 fn default() -> Self {
83 Self {
84 width: 800,
85 height: 600,
86 font_size: 14.0,
87 line_height: 1.4,
88 }
89 }
90}
91
92pub struct FontAtlas {
98 families: Vec<String>,
99}
100
101impl FontAtlas {
102 #[must_use]
104 pub fn new() -> Self {
105 info!("initializing font atlas");
106 Self {
107 families: Vec::new(),
108 }
109 }
110
111 pub fn register_family(&mut self, family: &str) {
113 if !self.families.contains(&family.to_string()) {
114 info!(family, "registering font family in atlas");
115 self.families.push(family.to_string());
116 }
117 }
118
119 #[must_use]
121 pub fn registered_families(&self) -> &[String] {
122 &self.families
123 }
124
125 pub fn clear(&mut self) {
127 self.families.clear();
128 }
129}
130
131impl Default for FontAtlas {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137pub struct ScopeAnnotator {
141 engine: polyfont_core::ScopeMatchEngine,
142}
143
144impl ScopeAnnotator {
145 #[must_use]
147 pub fn new(engine: polyfont_core::ScopeMatchEngine) -> Self {
148 Self { engine }
149 }
150
151 #[must_use]
153 pub fn annotate(&self, token: &polyfont_core::TokenInfo) -> Option<String> {
154 self.engine
155 .resolve_token(token)
156 .map(|a| a.font.family.clone())
157 }
158}
159
160pub struct RenderEngine {
164 atlas: FontAtlas,
165 config: RenderConfig,
166}
167
168impl RenderEngine {
169 #[must_use]
171 pub fn new(config: RenderConfig) -> Self {
172 info!(
173 width = config.width,
174 height = config.height,
175 font_size = config.font_size,
176 "initializing render engine"
177 );
178 Self {
179 atlas: FontAtlas::new(),
180 config,
181 }
182 }
183
184 pub fn register_font(&mut self, family: &str) {
186 self.atlas.register_family(family);
187 }
188
189 #[must_use]
191 pub fn config(&self) -> &RenderConfig {
192 &self.config
193 }
194
195 pub fn render_tokens(
199 &self,
200 tokens: &[polyfont_core::TokenInfo],
201 annotator: &ScopeAnnotator,
202 ) -> Result<Vec<RenderedLine>, RenderError> {
203 let mut lines: Vec<RenderedLine> = Vec::new();
204 let mut current_glyphs: Vec<GlyphPosition> = Vec::new();
205 let mut current_x = 0.0_f32;
206 let mut current_line = 0_u32;
207
208 let line_height = self.config.font_size * self.config.line_height;
209
210 for token in tokens {
211 if token.range.start.line != current_line && !current_glyphs.is_empty() {
212 let width = current_x;
213 lines.push(RenderedLine {
214 glyphs: std::mem::take(&mut current_glyphs),
215 width,
216 height: line_height,
217 });
218 current_x = 0.0;
219 current_line = token.range.start.line;
220 }
221
222 let family = annotator
223 .annotate(token)
224 .unwrap_or_else(|| "monospace".to_string());
225
226 let char_count = token.text.chars().count() as f32;
229 let advance = char_count * self.config.font_size * 0.6;
230
231 current_glyphs.push(GlyphPosition {
232 font_family: family,
233 x: current_x,
234 y: (token.range.start.line as f32 + 1.0) * line_height,
235 glyph_id: 0,
236 advance,
237 });
238 current_x += advance;
239 }
240
241 if !current_glyphs.is_empty() {
242 lines.push(RenderedLine {
243 glyphs: current_glyphs,
244 width: current_x,
245 height: line_height,
246 });
247 }
248
249 Ok(lines)
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use polyfont_core::{FontRule, FontSpec, Position, Range, TokenInfo};
257
258 #[test]
259 fn test_font_atlas_register() {
260 let mut atlas = FontAtlas::new();
261 atlas.register_family("Test");
262 atlas.register_family("Test"); assert_eq!(atlas.registered_families().len(), 1);
264 }
265
266 #[test]
267 fn test_scope_annotator() {
268 let engine = polyfont_core::ScopeMatchEngine::from_rules(vec![FontRule {
269 scope: "keyword".to_string(),
270 font: FontSpec::default_font("Maple Mono"),
271 }]);
272 let annotator = ScopeAnnotator::new(engine);
273 let token = TokenInfo {
274 text: "fn".to_string(),
275 range: Range {
276 start: Position { line: 0, column: 0 },
277 end: Position { line: 0, column: 2 },
278 },
279 scope: "keyword".to_string(),
280 modifiers: vec![],
281 };
282 assert_eq!(annotator.annotate(&token), Some("Maple Mono".to_string()));
283 }
284
285 #[test]
286 fn test_render_engine_tokens() {
287 let engine = polyfont_core::ScopeMatchEngine::from_rules(vec![FontRule {
288 scope: "*".to_string(),
289 font: FontSpec::default_font("Mono"),
290 }]);
291 let annotator = ScopeAnnotator::new(engine);
292 let render = RenderEngine::new(RenderConfig::default());
293
294 let tokens = vec![
295 TokenInfo {
296 text: "fn".to_string(),
297 range: Range {
298 start: Position { line: 0, column: 0 },
299 end: Position { line: 0, column: 2 },
300 },
301 scope: "keyword".to_string(),
302 modifiers: vec![],
303 },
304 TokenInfo {
305 text: "main".to_string(),
306 range: Range {
307 start: Position { line: 0, column: 3 },
308 end: Position { line: 0, column: 7 },
309 },
310 scope: "entity.name.function".to_string(),
311 modifiers: vec![],
312 },
313 ];
314
315 let lines = render.render_tokens(&tokens, &annotator).unwrap();
316 assert_eq!(lines.len(), 1);
317 assert_eq!(lines[0].glyphs.len(), 2);
318 }
319
320 #[test]
321 fn test_render_config_default() {
322 let config = RenderConfig::default();
323 assert_eq!(config.width, 800);
324 assert_eq!(config.height, 600);
325 assert!((config.font_size - 14.0).abs() < f32::EPSILON);
326 }
327}