Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-renderer/src/font_system.rs
StratoSDK / crates / strato-renderer / src / font_system.rs
1//! Advanced font management system for StratoUI
2//!
3//! This module provides a comprehensive font management system with:
4//! - Font registration and identification via FontId
5//! - Glyph atlas for efficient rendering
6//! - Font fallback chains for multi-language support
7//! - High-performance text rendering with caching
8 
9use cosmic_text::{
10 Attrs, Buffer, Family, FontSystem as CosmicFontSystem, Metrics, Shaping, SwashCache, Wrap,
11};
12use dashmap::DashMap;
13use parking_lot::RwLock;
14use std::collections::HashMap;
15use std::sync::Arc;
16use strato_core::layout::Size;
17use strato_core::types::{Color, Point};
18use wgpu::{Device, Sampler, Texture, TextureView};
19 
20/// Unique identifier for registered fonts
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct FontId(u32);
23 
24impl FontId {
25 pub const DEFAULT: FontId = FontId(0);
26 pub const SYSTEM: FontId = FontId(1);
27 pub const MONOSPACE: FontId = FontId(2);
28}
29 
30/// Comprehensive text style configuration
31#[derive(Debug, Clone)]
32pub struct TextStyle {
33 pub font: FontId,
34 pub size: f32,
35 pub color: Color,
36 pub weight: FontWeight,
37 pub style: FontStyle,
38 pub line_height: f32,
39 pub letter_spacing: f32,
40}
41 
42impl Default for TextStyle {
43 fn default() -> Self {
44 Self {
45 font: FontId::DEFAULT,
46 size: 16.0,
47 color: Color::BLACK,
48 weight: FontWeight::Normal,
49 style: FontStyle::Normal,
50 line_height: 1.2,
51 letter_spacing: 0.0,
52 }
53 }
54}
55 
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum FontWeight {
58 Thin = 100,
59 ExtraLight = 200,
60 Light = 300,
61 Normal = 400,
62 Medium = 500,
63 SemiBold = 600,
64 Bold = 700,
65 ExtraBold = 800,
66 Black = 900,
67}
68 
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum FontStyle {
71 Normal,
72 Italic,
73 Oblique,
74}
75 
76/// Font registration and metadata
77#[derive(Debug, Clone)]
78pub struct FontInfo {
79 pub id: FontId,
80 pub family: String,
81 pub fallback_chain: Vec<String>,
82 pub supports_cjk: bool,
83 pub supports_emoji: bool,
84}
85 
86/// Glyph atlas for efficient GPU rendering
87pub struct GlyphAtlas {
88 _texture: Texture,
89 texture_view: TextureView,
90 sampler: Sampler,
91 _size: (u32, u32),
92 current_x: u32,
93 current_y: u32,
94 row_height: u32,
95 glyph_cache: DashMap<u64, GlyphInfo>,
96 needs_update: bool,
97}
98 
99#[derive(Debug, Clone)]
100struct GlyphInfo {
101 _texture_coords: (f32, f32, f32, f32), // (u1, v1, u2, v2)
102 _size: (u32, u32),
103 _offset: (i32, i32),
104}
105 
106impl GlyphAtlas {
107 pub fn new(device: &Device, size: (u32, u32)) -> Self {
108 let texture = device.create_texture(&wgpu::TextureDescriptor {
109 label: Some("Glyph Atlas"),
110 size: wgpu::Extent3d {
111 width: size.0,
112 height: size.1,
113 depth_or_array_layers: 1,
114 },
115 mip_level_count: 1,
116 sample_count: 1,
117 dimension: wgpu::TextureDimension::D2,
118 format: wgpu::TextureFormat::R8Unorm,
119 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
120 view_formats: &[],
121 });
122 
123 let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
124 
125 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
126 label: Some("Glyph Atlas Sampler"),
127 address_mode_u: wgpu::AddressMode::ClampToEdge,
128 address_mode_v: wgpu::AddressMode::ClampToEdge,
129 address_mode_w: wgpu::AddressMode::ClampToEdge,
130 mag_filter: wgpu::FilterMode::Linear,
131 min_filter: wgpu::FilterMode::Linear,
132 mipmap_filter: wgpu::FilterMode::Nearest,
133 ..Default::default()
134 });
135 
136 Self {
137 _texture: texture,
138 texture_view,
139 sampler,
140 _size: size,
141 current_x: 0,
142 current_y: 0,
143 row_height: 0,
144 glyph_cache: DashMap::new(),
145 needs_update: false,
146 }
147 }
148 
149 pub fn texture_view(&self) -> &TextureView {
150 &self.texture_view
151 }
152 
153 pub fn sampler(&self) -> &Sampler {
154 &self.sampler
155 }
156 
157 pub fn needs_update(&self) -> bool {
158 self.needs_update
159 }
160 
161 pub fn mark_updated(&mut self) {
162 self.needs_update = false;
163 }
164 
165 #[allow(dead_code)] // Method is used for glyph atlas management but not in simplified implementation
166 fn allocate_space(&mut self, width: u32, height: u32) -> Option<(u32, u32)> {
167 // Simple row-based allocation
168 if self.current_x + width > self._size.0 {
169 // Move to next row
170 self.current_x = 0;
171 self.current_y += self.row_height;
172 self.row_height = 0;
173 }
174 
175 if self.current_y + height > self._size.1 {
176 // Atlas is full
177 return None;
178 }
179 
180 let pos = (self.current_x, self.current_y);
181 self.current_x += width;
182 self.row_height = self.row_height.max(height);
183 
184 Some(pos)
185 }
186}
187 
188/// Advanced font management system
189/// Creates a safe font system that loads only specific fonts to avoid problematic system fonts
190/// This function has been moved to font_config.rs for centralized configuration
191///
192/// FontSystem implementation with advanced text rendering capabilities
193 
194pub struct FontSystem {
195 font_system: Arc<RwLock<CosmicFontSystem>>,
196 #[allow(dead_code)] // Field is used for glyph caching but not in simplified implementation
197 swash_cache: Arc<RwLock<SwashCache>>,
198 glyph_atlas: Arc<RwLock<GlyphAtlas>>,
199 fonts: HashMap<FontId, FontInfo>,
200 next_font_id: u32,
201 text_buffers: DashMap<u64, Buffer>,
202}
203 
204impl FontSystem {
205 pub fn new(device: &Device) -> Self {
206 let font_system = crate::font_config::create_safe_font_system();
207 
208 // Register default fonts
209 let mut fonts = HashMap::new();
210 
211 // Default system font with platform-specific fallbacks
212 #[cfg(target_os = "windows")]
213 let default_font_chain = vec![
214 "Segoe UI".to_string(),
215 "Segoe UI Emoji".to_string(),
216 "Tahoma".to_string(),
217 "Arial".to_string(),
218 "sans-serif".to_string(),
219 ];
220 
221 #[cfg(target_os = "macos")]
222 let default_font_chain = vec![
223 "SF Pro Display".to_string(),
224 "Apple Color Emoji".to_string(),
225 "Helvetica Neue".to_string(),
226 "Arial".to_string(),
227 "sans-serif".to_string(),
228 ];
229 
230 #[cfg(target_os = "linux")]
231 let default_font_chain = vec![
232 "Ubuntu".to_string(),
233 "Noto Color Emoji".to_string(),
234 "DejaVu Sans".to_string(),
235 "Liberation Sans".to_string(),
236 "Arial".to_string(),
237 "sans-serif".to_string(),
238 ];
239 
240 #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
241 let default_font_chain = vec!["Arial".to_string(), "sans-serif".to_string()];
242 
243 fonts.insert(
244 FontId::DEFAULT,
245 FontInfo {
246 id: FontId::DEFAULT,
247 family: default_font_chain[0].clone(),
248 fallback_chain: default_font_chain.clone(),
249 supports_cjk: false,
250 supports_emoji: true,
251 },
252 );
253 
254 // System font with CJK support
255 #[cfg(target_os = "windows")]
256 let system_font_chain = vec![
257 "Segoe UI".to_string(),
258 "Segoe UI Emoji".to_string(),
259 "Microsoft YaHei".to_string(),
260 "SimSun".to_string(),
261 "Tahoma".to_string(),
262 "Arial".to_string(),
263 "sans-serif".to_string(),
264 ];
265 
266 #[cfg(target_os = "macos")]
267 let system_font_chain = vec![
268 "SF Pro Display".to_string(),
269 "Apple Color Emoji".to_string(),
270 "Hiragino Sans".to_string(),
271 "PingFang SC".to_string(),
272 "Helvetica Neue".to_string(),
273 "Arial".to_string(),
274 "sans-serif".to_string(),
275 ];
276 
277 #[cfg(target_os = "linux")]
278 let system_font_chain = vec![
279 "Ubuntu".to_string(),
280 "Noto Color Emoji".to_string(),
281 "Noto Sans CJK".to_string(),
282 "DejaVu Sans".to_string(),
283 "Liberation Sans".to_string(),
284 "Arial".to_string(),
285 "sans-serif".to_string(),
286 ];
287 
288 #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
289 let system_font_chain = vec!["Arial".to_string(), "sans-serif".to_string()];
290 
291 fonts.insert(
292 FontId::SYSTEM,
293 FontInfo {
294 id: FontId::SYSTEM,
295 family: system_font_chain[0].clone(),
296 fallback_chain: system_font_chain,
297 supports_cjk: true,
298 supports_emoji: true,
299 },
300 );
301 
302 // Monospace font
303 fonts.insert(
304 FontId::MONOSPACE,
305 FontInfo {
306 id: FontId::MONOSPACE,
307 family: "monospace".to_string(),
308 fallback_chain: vec![
309 "Consolas".to_string(),
310 "Monaco".to_string(),
311 "Courier New".to_string(),
312 "monospace".to_string(),
313 ],
314 supports_cjk: false,
315 supports_emoji: false,
316 },
317 );
318 
319 Self {
320 font_system: Arc::new(RwLock::new(font_system)),
321 swash_cache: Arc::new(RwLock::new(SwashCache::new())),
322 glyph_atlas: Arc::new(RwLock::new(GlyphAtlas::new(device, (1024, 1024)))),
323 fonts,
324 next_font_id: 3,
325 text_buffers: DashMap::new(),
326 }
327 }
328 
329 /// Register a new font family
330 pub fn register_font(&mut self, family: String, fallback_chain: Vec<String>) -> FontId {
331 let id = FontId(self.next_font_id);
332 self.next_font_id += 1;
333 
334 let font_info = FontInfo {
335 id,
336 family,
337 fallback_chain,
338 supports_cjk: false, // Could be detected automatically
339 supports_emoji: false,
340 };
341 
342 self.fonts.insert(id, font_info);
343 id
344 }
345 
346 /// Get font information
347 pub fn get_font_info(&self, font_id: FontId) -> Option<&FontInfo> {
348 self.fonts.get(&font_id)
349 }
350 
351 /// Create text attributes for rendering
352 fn create_attrs<'a>(&'a self, style: &'a TextStyle) -> Attrs<'a> {
353 let font_info = self
354 .fonts
355 .get(&style.font)
356 .unwrap_or_else(|| &self.fonts[&FontId::DEFAULT]);
357 
358 Attrs::new()
359 .family(Family::Name(&font_info.family))
360 .weight(cosmic_text::Weight(style.weight as u16))
361 .style(match style.style {
362 FontStyle::Normal => cosmic_text::Style::Normal,
363 FontStyle::Italic => cosmic_text::Style::Italic,
364 FontStyle::Oblique => cosmic_text::Style::Oblique,
365 })
366 }
367 
368 /// Measure text dimensions
369 pub fn measure_text(&self, text: &str, style: &TextStyle, max_width: Option<f32>) -> Size {
370 let mut font_system = self.font_system.write();
371 
372 let metrics = Metrics::new(style.size, style.size * style.line_height);
373 let mut buffer = Buffer::new(&mut font_system, metrics);
374 
375 let attrs = self.create_attrs(style);
376 buffer.set_text(&mut font_system, text, attrs, Shaping::Advanced);
377 
378 if let Some(width) = max_width {
379 buffer.set_wrap(&mut font_system, Wrap::Word);
380 buffer.set_size(&mut font_system, Some(width), Some(f32::MAX));
381 }
382 
383 buffer.shape_until_scroll(&mut font_system, false);
384 
385 let mut max_width: f32 = 0.0;
386 let mut total_height = 0.0;
387 
388 for run in buffer.layout_runs() {
389 let line_width = run.glyphs.iter().map(|g| g.w).sum::<f32>();
390 max_width = max_width.max(line_width);
391 total_height += run.line_height;
392 }
393 
394 Size::new(max_width, total_height)
395 }
396 
397 /// Render text and return vertex data
398 pub fn render_text(
399 &self,
400 text: &str,
401 style: &TextStyle,
402 position: Point,
403 max_width: Option<f32>,
404 ) -> Vec<TextVertex> {
405 let mut vertices = Vec::new();
406 let mut font_system = self.font_system.write();
407 
408 let metrics = Metrics::new(style.size, style.size * style.line_height);
409 let mut buffer = Buffer::new(&mut font_system, metrics);
410 
411 let attrs = self.create_attrs(style);
412 buffer.set_text(&mut font_system, text, attrs, Shaping::Advanced);
413 
414 if let Some(width) = max_width {
415 buffer.set_wrap(&mut font_system, Wrap::Word);
416 buffer.set_size(&mut font_system, Some(width), Some(f32::MAX));
417 }
418 
419 buffer.shape_until_scroll(&mut font_system, false);
420 
421 // Process glyphs and add to atlas if needed
422 for run in buffer.layout_runs() {
423 for glyph in run.glyphs {
424 let physical_glyph = glyph.physical((position.x, position.y), 1.0);
425 // Add glyph to atlas and create vertex
426 let tex_coords = self.ensure_glyph_in_atlas(&physical_glyph);
427 
428 vertices.push(TextVertex {
429 position: [physical_glyph.x as f32, physical_glyph.y as f32],
430 color: [style.color.r, style.color.g, style.color.b, style.color.a],
431 tex_coords,
432 });
433 }
434 }
435 
436 vertices
437 }
438 
439 fn ensure_glyph_in_atlas(&self, _glyph: &cosmic_text::PhysicalGlyph) -> [f32; 2] {
440 [0.0, 0.0] // Placeholder texture coordinates
441 }
442 
443 /// Clear all caches
444 pub fn clear_cache(&self) {
445 self.text_buffers.clear();
446 let mut atlas = self.glyph_atlas.write();
447 atlas.glyph_cache.clear();
448 atlas.current_x = 0;
449 atlas.current_y = 0;
450 atlas.row_height = 0;
451 atlas.needs_update = true;
452 }
453}
454 
455/// Vertex data for text rendering
456#[repr(C)]
457#[derive(Debug, Clone, Copy)]
458pub struct TextVertex {
459 pub position: [f32; 2],
460 pub color: [f32; 4],
461 pub tex_coords: [f32; 2],
462}
463 
464unsafe impl bytemuck::Pod for TextVertex {}
465unsafe impl bytemuck::Zeroable for TextVertex {}
466 
467/// High-level text renderer interface
468pub struct TextRenderer {
469 font_system: FontSystem,
470}
471 
472impl TextRenderer {
473 pub fn new(device: &Device) -> Self {
474 Self {
475 font_system: FontSystem::new(device),
476 }
477 }
478 
479 /// Draw text with the specified style
480 pub fn draw_text(&self, text: &str, style: &TextStyle, position: Point) -> Vec<TextVertex> {
481 self.font_system.render_text(text, style, position, None)
482 }
483 
484 /// Measure text dimensions
485 pub fn measure_text(&self, text: &str, style: &TextStyle, max_width: Option<f32>) -> Size {
486 self.font_system.measure_text(text, style, max_width)
487 }
488 
489 /// Register a new font
490 pub fn register_font(&mut self, family: String, fallback_chain: Vec<String>) -> FontId {
491 self.font_system.register_font(family, fallback_chain)
492 }
493 
494 /// Get the glyph atlas for GPU binding
495 pub fn glyph_atlas(&self) -> &Arc<RwLock<GlyphAtlas>> {
496 &self.font_system.glyph_atlas
497 }
498}
499