use polyfont_core::PolyfontEngine;
use thiserror::Error;
use tracing::info;
#[derive(Debug, Error)]
pub enum RenderError {
#[error("no rendering backend available")]
NoBackend,
#[error("font atlas error: {0}")]
FontAtlas(String),
#[error("shaping error: {0}")]
Shaping(String),
#[error("compositing error: {0}")]
Compositing(String),
#[error("GPU initialization failed: {0}")]
GpuInit(String),
#[error("window creation failed: {0}")]
Window(String),
}
#[derive(Debug, Clone)]
pub struct GlyphPosition {
pub font_family: String,
pub x: f32,
pub y: f32,
pub glyph_id: u32,
pub advance: f32,
}
#[derive(Debug, Clone)]
pub struct RenderedLine {
pub glyphs: Vec<GlyphPosition>,
pub width: f32,
pub height: f32,
}
#[derive(Debug, Clone)]
pub struct RenderConfig {
pub width: u32,
pub height: u32,
pub font_size: f32,
pub line_height: f32,
}
impl Default for RenderConfig {
fn default() -> Self {
Self {
width: 800,
height: 600,
font_size: 14.0,
line_height: 1.4,
}
}
}
pub struct FontAtlas {
families: Vec<String>,
}
impl FontAtlas {
#[must_use]
pub fn new() -> Self {
info!("initializing font atlas");
Self {
families: Vec::new(),
}
}
pub fn register_family(&mut self, family: &str) {
if !self.families.contains(&family.to_string()) {
info!(family, "registering font family in atlas");
self.families.push(family.to_string());
}
}
#[must_use]
pub fn registered_families(&self) -> &[String] {
&self.families
}
pub fn clear(&mut self) {
self.families.clear();
}
}
impl Default for FontAtlas {
fn default() -> Self {
Self::new()
}
}
pub struct ScopeAnnotator {
engine: polyfont_core::ScopeMatchEngine,
}
impl ScopeAnnotator {
#[must_use]
pub fn new(engine: polyfont_core::ScopeMatchEngine) -> Self {
Self { engine }
}
#[must_use]
pub fn annotate(&self, token: &polyfont_core::TokenInfo) -> Option<String> {
self.engine
.resolve_token(token)
.map(|a| a.font.family.clone())
}
}
pub struct RenderEngine {
atlas: FontAtlas,
config: RenderConfig,
}
impl RenderEngine {
#[must_use]
pub fn new(config: RenderConfig) -> Self {
info!(
width = config.width,
height = config.height,
font_size = config.font_size,
"initializing render engine"
);
Self {
atlas: FontAtlas::new(),
config,
}
}
pub fn register_font(&mut self, family: &str) {
self.atlas.register_family(family);
}
#[must_use]
pub fn config(&self) -> &RenderConfig {
&self.config
}
pub fn render_tokens(
&self,
tokens: &[polyfont_core::TokenInfo],
annotator: &ScopeAnnotator,
) -> Result<Vec<RenderedLine>, RenderError> {
let mut lines: Vec<RenderedLine> = Vec::new();
let mut current_glyphs: Vec<GlyphPosition> = Vec::new();
let mut current_x = 0.0_f32;
let mut current_line = 0_u32;
let line_height = self.config.font_size * self.config.line_height;
for token in tokens {
if token.range.start.line != current_line && !current_glyphs.is_empty() {
let width = current_x;
lines.push(RenderedLine {
glyphs: std::mem::take(&mut current_glyphs),
width,
height: line_height,
});
current_x = 0.0;
current_line = token.range.start.line;
}
let family = annotator
.annotate(token)
.unwrap_or_else(|| "monospace".to_string());
let char_count = token.text.chars().count() as f32;
let advance = char_count * self.config.font_size * 0.6;
current_glyphs.push(GlyphPosition {
font_family: family,
x: current_x,
y: (token.range.start.line as f32 + 1.0) * line_height,
glyph_id: 0,
advance,
});
current_x += advance;
}
if !current_glyphs.is_empty() {
lines.push(RenderedLine {
glyphs: current_glyphs,
width: current_x,
height: line_height,
});
}
Ok(lines)
}
}
#[cfg(test)]
mod tests {
use super::*;
use polyfont_core::{FontRule, FontSpec, Position, Range, TokenInfo};
#[test]
fn test_font_atlas_register() {
let mut atlas = FontAtlas::new();
atlas.register_family("Test");
atlas.register_family("Test"); assert_eq!(atlas.registered_families().len(), 1);
}
#[test]
fn test_scope_annotator() {
let engine = polyfont_core::ScopeMatchEngine::from_rules(vec![FontRule {
scope: "keyword".to_string(),
font: FontSpec::default_font("Maple Mono"),
}]);
let annotator = ScopeAnnotator::new(engine);
let token = TokenInfo {
text: "fn".to_string(),
range: Range {
start: Position { line: 0, column: 0 },
end: Position { line: 0, column: 2 },
},
scope: "keyword".to_string(),
modifiers: vec![],
};
assert_eq!(annotator.annotate(&token), Some("Maple Mono".to_string()));
}
#[test]
fn test_render_engine_tokens() {
let engine = polyfont_core::ScopeMatchEngine::from_rules(vec![FontRule {
scope: "*".to_string(),
font: FontSpec::default_font("Mono"),
}]);
let annotator = ScopeAnnotator::new(engine);
let render = RenderEngine::new(RenderConfig::default());
let tokens = vec![
TokenInfo {
text: "fn".to_string(),
range: Range {
start: Position { line: 0, column: 0 },
end: Position { line: 0, column: 2 },
},
scope: "keyword".to_string(),
modifiers: vec![],
},
TokenInfo {
text: "main".to_string(),
range: Range {
start: Position { line: 0, column: 3 },
end: Position { line: 0, column: 7 },
},
scope: "entity.name.function".to_string(),
modifiers: vec![],
},
];
let lines = render.render_tokens(&tokens, &annotator).unwrap();
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].glyphs.len(), 2);
}
#[test]
fn test_render_config_default() {
let config = RenderConfig::default();
assert_eq!(config.width, 800);
assert_eq!(config.height, 600);
assert!((config.font_size - 14.0).abs() < f32::EPSILON);
}
}