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/text.rs
1//! Text rendering with cosmic-text
2 
3use crate::glyph_atlas::GlyphAtlasManager;
4use crate::vertex::{TextVertex, Vertex};
5use cosmic_text::{
6 Attrs, Buffer, CacheKey, Family, FontSystem, Metrics, Shaping, SwashCache, Weight, Wrap,
7};
8use dashmap::DashMap;
9use image::{DynamicImage, ImageBuffer, Rgba};
10use parking_lot::RwLock;
11use std::collections::HashMap;
12use std::sync::{Arc, Mutex};
13use strato_core::types::{Color, Point, Size};
14 
15/// Font wrapper
16pub struct Font {
17 family: Family<'static>,
18 pub size: f32,
19 weight: u16,
20 italic: bool,
21}
22 
23impl Font {
24 /// Create a new font
25 pub fn new(family: &str, size: f32) -> Self {
26 Self {
27 // Family::Name requires a 'static str, so leak the provided family name.
28 // This is acceptable for long-lived font definitions.
29 family: Family::Name(Box::leak(family.to_string().into_boxed_str())),
30 size,
31 weight: 400,
32 italic: false,
33 }
34 }
35 
36 /// Set font weight
37 pub fn with_weight(mut self, weight: u16) -> Self {
38 self.weight = weight;
39 self
40 }
41 
42 /// Set italic style
43 pub fn with_italic(mut self, italic: bool) -> Self {
44 self.italic = italic;
45 self
46 }
47 
48 /// Convert to cosmic-text attributes
49 pub fn to_attrs(&self) -> Attrs<'static> {
50 Attrs::new()
51 .family(self.family.clone())
52 .weight(cosmic_text::Weight(self.weight))
53 .style(if self.italic {
54 cosmic_text::Style::Italic
55 } else {
56 cosmic_text::Style::Normal
57 })
58 }
59}
60 
61impl Default for Font {
62 fn default() -> Self {
63 // Use platform-specific default fonts
64 #[cfg(target_os = "windows")]
65 let default_family = "Segoe UI";
66 
67 #[cfg(target_os = "macos")]
68 let default_family = "San Francisco";
69 
70 #[cfg(target_os = "linux")]
71 let default_family = "Ubuntu";
72 
73 #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
74 let default_family = "sans-serif";
75 
76 Self::new(default_family, 16.0)
77 }
78}
79 
80/// Glyph cache for efficient text rendering
81pub struct GlyphCache {
82 #[allow(dead_code)] // Field is used for glyph caching but not in simplified implementation
83 cache: SwashCache,
84 glyphs: DashMap<u64, CachedGlyph>,
85}
86 
87/// Cached glyph data
88#[allow(dead_code)] // Fields are used for glyph rendering but not in simplified implementation
89struct CachedGlyph {
90 texture_coords: (f32, f32, f32, f32),
91 size: (u32, u32),
92 offset: (i32, i32),
93}
94 
95impl GlyphCache {
96 /// Create a new glyph cache
97 pub fn new() -> Self {
98 Self {
99 cache: SwashCache::new(),
100 glyphs: DashMap::new(),
101 }
102 }
103 
104 /// Clear the cache
105 pub fn clear(&mut self) {
106 self.glyphs.clear();
107 }
108}
109 
110/// Text renderer
111pub struct TextRenderer {
112 font_system: Arc<RwLock<FontSystem>>,
113 glyph_cache: Arc<RwLock<GlyphCache>>,
114 glyph_atlas_manager: Arc<RwLock<GlyphAtlasManager>>,
115 buffers: DashMap<u64, Buffer>,
116}
117 
118impl TextRenderer {
119 /// Create a new text renderer
120 pub fn new() -> Self {
121 let font_system = crate::font_config::create_safe_font_system();
122 
123 Self {
124 font_system: Arc::new(RwLock::new(font_system)),
125 glyph_cache: Arc::new(RwLock::new(GlyphCache::new())),
126 glyph_atlas_manager: Arc::new(RwLock::new(GlyphAtlasManager::new((1024, 1024)))),
127 buffers: DashMap::new(),
128 }
129 }
130 
131 /// Render text to vertices for GPU rendering
132 pub fn render_text(
133 &self,
134 text: &str,
135 font: &Font,
136 position: Point,
137 color: Color,
138 max_width: Option<f32>,
139 ) -> Vec<TextVertex> {
140 let mut vertices = Vec::new();
141 let mut font_system = self.font_system.write();
142 let mut glyph_cache = self.glyph_cache.write();
143 let mut glyph_atlas = self.glyph_atlas_manager.write();
144 
145 // Create buffer for layout
146 let metrics = Metrics::new(font.size, font.size * 1.2);
147 let mut buffer = Buffer::new(&mut font_system, metrics);
148 buffer.set_text(&mut font_system, text, font.to_attrs(), Shaping::Advanced);
149 
150 if let Some(width) = max_width {
151 buffer.set_wrap(&mut font_system, Wrap::Word);
152 buffer.set_size(&mut font_system, Some(width), Some(f32::MAX));
153 } else {
154 buffer.set_size(&mut font_system, None, Some(f32::MAX));
155 }
156 
157 buffer.shape_until_scroll(&mut font_system, false);
158 
159 let start_x = position.x;
160 let start_y = position.y;
161 
162 // Iterate over layout runs
163 for run in buffer.layout_runs() {
164 for glyph in run.glyphs.iter() {
165 let physical_glyph = glyph.physical((start_x, start_y + run.line_y), 1.0);
166 
167 // Get texture coordinates from atlas
168 if let Some((_atlas_index, glyph_info)) = glyph_atlas.get_or_create_glyph(
169 &mut font_system,
170 &mut glyph_cache.cache,
171 physical_glyph.cache_key,
172 ) {
173 let glyph_x = physical_glyph.x as f32;
174 let glyph_y = physical_glyph.y as f32;
175 let glyph_w = glyph_info.size.0 as f32;
176 let glyph_h = glyph_info.size.1 as f32;
177 
178 let (u0, v0, u1, v1) = glyph_info.uv_rect;
179 
180 // Create quad vertices for this glyph
181 vertices.extend_from_slice(&[
182 TextVertex::new(
183 [glyph_x, glyph_y, 0.0],
184 [u0, v0],
185 [color.r, color.g, color.b, color.a],
186 0,
187 ),
188 TextVertex::new(
189 [glyph_x + glyph_w, glyph_y, 0.0],
190 [u1, v0],
191 [color.r, color.g, color.b, color.a],
192 0,
193 ),
194 TextVertex::new(
195 [glyph_x + glyph_w, glyph_y + glyph_h, 0.0],
196 [u1, v1],
197 [color.r, color.g, color.b, color.a],
198 0,
199 ),
200 TextVertex::new(
201 [glyph_x, glyph_y + glyph_h, 0.0],
202 [u0, v1],
203 [color.r, color.g, color.b, color.a],
204 0,
205 ),
206 ]);
207 }
208 }
209 }
210 
211 vertices
212 }
213 
214 /// Measure text dimensions
215 pub fn measure_text(&self, text: &str, font: &Font, max_width: Option<f32>) -> Size {
216 let mut font_system = self.font_system.write();
217 
218 // Create buffer for measurement
219 let metrics = Metrics::new(font.size, font.size * 1.2);
220 let mut buffer = Buffer::new(&mut font_system, metrics);
221 buffer.set_text(&mut font_system, text, font.to_attrs(), Shaping::Advanced);
222 
223 if let Some(width) = max_width {
224 buffer.set_wrap(&mut font_system, Wrap::Word);
225 buffer.set_size(&mut font_system, Some(width), Some(f32::MAX));
226 }
227 
228 buffer.shape_until_scroll(&mut font_system, false);
229 
230 let mut max_width: f32 = 0.0;
231 let mut total_height = 0.0;
232 
233 for run in buffer.layout_runs() {
234 let line_width = run.glyphs.iter().map(|g| g.w).sum::<f32>();
235 max_width = max_width.max(line_width);
236 total_height += run.line_height;
237 }
238 
239 Size::new(max_width, total_height)
240 }
241 
242 /// Hash text and font for caching
243 #[allow(dead_code)] // Used for text caching but not in simplified implementation
244 fn hash_text(text: &str, font: &Font) -> u64 {
245 use std::collections::hash_map::DefaultHasher;
246 use std::hash::{Hash, Hasher};
247 
248 let mut hasher = DefaultHasher::new();
249 text.hash(&mut hasher);
250 font.size.to_bits().hash(&mut hasher);
251 font.weight.hash(&mut hasher);
252 font.italic.hash(&mut hasher);
253 hasher.finish()
254 }
255 
256 /// Clear all caches
257 pub fn clear_cache(&self) {
258 self.buffers.clear();
259 self.glyph_cache.write().clear();
260 }
261 
262 /// Rasterize text to an image using cosmic-text
263 pub fn rasterize_text_image(
264 &self,
265 text: &str,
266 font: &Font,
267 max_width: Option<f32>,
268 ) -> DynamicImage {
269 let mut font_system = self.font_system.write();
270 let mut swash_cache = SwashCache::new();
271 
272 // Create buffer for text layout
273 let metrics = Metrics::new(font.size, font.size * 1.2);
274 let mut buffer = Buffer::new(&mut font_system, metrics);
275 buffer.set_text(&mut font_system, text, font.to_attrs(), Shaping::Advanced);
276 
277 if let Some(width) = max_width {
278 buffer.set_wrap(&mut font_system, cosmic_text::Wrap::Word);
279 buffer.set_size(&mut font_system, Some(width), Some(f32::MAX));
280 }
281 
282 buffer.shape_until_scroll(&mut font_system, false);
283 
284 // Calculate dimensions
285 let size = self.measure_text(text, font, max_width);
286 let width = size.width.ceil().max(1.0) as u32;
287 let height = size.height.ceil().max(1.0) as u32;
288 
289 // Create pixel buffer
290 let mut img = ImageBuffer::<Rgba<u8>, Vec<u8>>::new(width, height);
291 
292 // Render each glyph
293 for run in buffer.layout_runs() {
294 for glyph in run.glyphs {
295 let physical_glyph = glyph.physical((0.0, 0.0), 1.0);
296 
297 swash_cache.with_pixels(
298 &mut font_system,
299 physical_glyph.cache_key,
300 cosmic_text::Color::rgba(255, 255, 255, 255),
301 |x, y, color| {
302 let px = (physical_glyph.x as i32 + x) as u32;
303 let py = (physical_glyph.y as i32 + y) as u32;
304 
305 if px < width && py < height {
306 let pixel = img.get_pixel_mut(px, py);
307 pixel[0] = color.r();
308 pixel[1] = color.g();
309 pixel[2] = color.b();
310 pixel[3] = color.a();
311 }
312 },
313 );
314 }
315 }
316 
317 DynamicImage::ImageRgba8(img)
318 }
319}
320 
321impl Default for TextRenderer {
322 fn default() -> Self {
323 Self::new()
324 }
325}
326 
327/// Text vertex for GPU rendering
328#[repr(C)]
329#[derive(Debug, Clone)]
330pub struct LocalTextVertex {
331 pub position: [f32; 2],
332 pub color: [f32; 4],
333 pub tex_coords: [f32; 2],
334}
335 
336impl LocalTextVertex {
337 pub fn new(position: [f32; 2], color: Color, tex_coords: [f32; 2]) -> Self {
338 Self {
339 position,
340 color: color.to_array(),
341 tex_coords,
342 }
343 }
344}
345 
346/// Text layout options
347#[derive(Debug, Clone)]
348pub struct TextLayout {
349 pub align: TextAlign,
350 pub wrap: TextWrap,
351 pub line_spacing: f32,
352 pub letter_spacing: f32,
353}
354 
355/// Text alignment
356#[derive(Debug, Clone, Copy, PartialEq, Eq)]
357pub enum TextAlign {
358 Left,
359 Center,
360 Right,
361 Justify,
362}
363 
364/// Text wrapping mode
365#[derive(Debug, Clone, Copy, PartialEq, Eq)]
366pub enum TextWrap {
367 None,
368 Word,
369 Character,
370}
371 
372impl Default for TextLayout {
373 fn default() -> Self {
374 Self {
375 align: TextAlign::Left,
376 wrap: TextWrap::Word,
377 line_spacing: 1.2,
378 letter_spacing: 0.0,
379 }
380 }
381}
382