Skip to main content

polyfont_render/
lib.rs

1//! Custom multi-font rendering engine for polyfont.
2//!
3//! Composites text from multiple font families into a single output.
4//! Supports GPU-accelerated rendering via `wgpu`/`glyphon`/`cosmic-text`
5//! or software rendering via `tiny-skia`/`softbuffer`.
6//!
7//! # Features
8//!
9//! - `gpu` -- Enable GPU-accelerated rendering (requires Vulkan/Metal/DX12)
10//! - `software` -- Enable CPU-based software rendering
11//!
12//! # Status
13//!
14//! This is a skeleton crate. The rendering pipeline is under development.
15
16use polyfont_core::PolyfontEngine;
17use thiserror::Error;
18use tracing::info;
19
20/// Rendering errors.
21#[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/// A glyph position in the rendered output.
43#[derive(Debug, Clone)]
44pub struct GlyphPosition {
45    /// Font family this glyph belongs to.
46    pub font_family: String,
47    /// X position in pixels.
48    pub x: f32,
49    /// Y position (baseline) in pixels.
50    pub y: f32,
51    /// Glyph ID in the font.
52    pub glyph_id: u32,
53    /// Advance width.
54    pub advance: f32,
55}
56
57/// A rendered line of multi-font text.
58#[derive(Debug, Clone)]
59pub struct RenderedLine {
60    /// Glyphs in this line, each potentially from a different font.
61    pub glyphs: Vec<GlyphPosition>,
62    /// Total line width in pixels.
63    pub width: f32,
64    /// Line height in pixels.
65    pub height: f32,
66}
67
68/// Configuration for the rendering engine.
69#[derive(Debug, Clone)]
70pub struct RenderConfig {
71    /// Viewport width in pixels.
72    pub width: u32,
73    /// Viewport height in pixels.
74    pub height: u32,
75    /// Font size in points.
76    pub font_size: f32,
77    /// Line height multiplier (1.0 = tight, 1.5 = comfortable).
78    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
92/// Font atlas management.
93///
94/// Maintains a cache of rasterized glyphs per font family.
95/// In GPU mode, these are uploaded as textures. In software mode,
96/// they are kept as alpha masks.
97pub struct FontAtlas {
98    families: Vec<String>,
99}
100
101impl FontAtlas {
102    /// Create a new empty font atlas.
103    #[must_use]
104    pub fn new() -> Self {
105        info!("initializing font atlas");
106        Self {
107            families: Vec::new(),
108        }
109    }
110
111    /// Register a font family for use in rendering.
112    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    /// List all registered font families.
120    #[must_use]
121    pub fn registered_families(&self) -> &[String] {
122        &self.families
123    }
124
125    /// Clear all cached glyphs and registered families.
126    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
137/// Scope-to-font annotator.
138///
139/// Maps token scopes to font families using the polyfont engine.
140pub struct ScopeAnnotator {
141    engine: polyfont_core::ScopeMatchEngine,
142}
143
144impl ScopeAnnotator {
145    /// Create a new annotator from font rules.
146    #[must_use]
147    pub fn new(engine: polyfont_core::ScopeMatchEngine) -> Self {
148        Self { engine }
149    }
150
151    /// Annotate a token with its assigned font family.
152    #[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
160/// The main rendering engine.
161///
162/// Coordinates font atlas management, text shaping, and compositing.
163pub struct RenderEngine {
164    atlas: FontAtlas,
165    config: RenderConfig,
166}
167
168impl RenderEngine {
169    /// Create a new rendering engine with the given configuration.
170    #[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    /// Register a font family for rendering.
185    pub fn register_font(&mut self, family: &str) {
186        self.atlas.register_family(family);
187    }
188
189    /// Get the current configuration.
190    #[must_use]
191    pub fn config(&self) -> &RenderConfig {
192        &self.config
193    }
194
195    /// Render a sequence of tokens into positioned glyphs.
196    ///
197    /// Returns `Err(RenderError::NoBackend)` until a rendering backend is initialized.
198    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            // Approximate glyph positions using character count.
227            // Real implementation would use cosmic-text or harfbuzz shaping.
228            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"); // duplicate ignored
263        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}