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-core/src/text.rs
1//! Advanced text rendering system for StratoUI
2//!
3//! Provides comprehensive text layout, shaping, and rendering capabilities
4//! with support for complex scripts, bidirectional text, and typography features.
5 
6use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8 
9/// Text alignment options
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum TextAlign {
12 Left,
13 Center,
14 Right,
15 Justify,
16 Start,
17 End,
18}
19 
20/// Text direction for bidirectional text support
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum TextDirection {
23 LeftToRight,
24 RightToLeft,
25 Auto,
26}
27 
28/// Text wrapping behavior
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum TextWrap {
31 None,
32 Word,
33 Character,
34 WordCharacter,
35}
36 
37/// Font weight values
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum FontWeight {
40 Thin = 100,
41 ExtraLight = 200,
42 Light = 300,
43 Normal = 400,
44 Medium = 500,
45 SemiBold = 600,
46 Bold = 700,
47 ExtraBold = 800,
48 Black = 900,
49}
50 
51/// Font style variants
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum FontStyle {
54 Normal,
55 Italic,
56 Oblique,
57}
58 
59/// Font stretch values
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum FontStretch {
62 UltraCondensed,
63 ExtraCondensed,
64 Condensed,
65 SemiCondensed,
66 Normal,
67 SemiExpanded,
68 Expanded,
69 ExtraExpanded,
70 UltraExpanded,
71}
72 
73/// Font descriptor for text styling
74#[derive(Debug, Clone)]
75pub struct FontDescriptor {
76 pub family: String,
77 pub size: f32,
78 pub weight: FontWeight,
79 pub style: FontStyle,
80 pub stretch: FontStretch,
81}
82 
83impl Default for FontDescriptor {
84 fn default() -> Self {
85 // Use platform-specific default fonts instead of generic "system-ui"
86 #[cfg(target_os = "windows")]
87 let default_family = "Segoe UI";
88 
89 #[cfg(target_os = "macos")]
90 let default_family = "SF Pro Display";
91 
92 #[cfg(target_os = "linux")]
93 let default_family = "Ubuntu";
94 
95 #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
96 let default_family = "Arial";
97 
98 Self {
99 family: default_family.to_string(),
100 size: 14.0,
101 weight: FontWeight::Normal,
102 style: FontStyle::Normal,
103 stretch: FontStretch::Normal,
104 }
105 }
106}
107 
108/// Text decoration options
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum TextDecoration {
111 None,
112 Underline,
113 Overline,
114 LineThrough,
115 Blink,
116}
117 
118/// Text style configuration
119#[derive(Debug, Clone)]
120pub struct TextStyle {
121 pub font: FontDescriptor,
122 pub color: [f32; 4], // RGBA
123 pub decoration: TextDecoration,
124 pub decoration_color: Option<[f32; 4]>,
125 pub letter_spacing: f32,
126 pub word_spacing: f32,
127 pub line_height: f32,
128 pub text_shadow: Option<TextShadow>,
129}
130 
131impl Default for TextStyle {
132 fn default() -> Self {
133 Self {
134 font: FontDescriptor::default(),
135 color: [0.0, 0.0, 0.0, 1.0], // Black
136 decoration: TextDecoration::None,
137 decoration_color: None,
138 letter_spacing: 0.0,
139 word_spacing: 0.0,
140 line_height: 1.2,
141 text_shadow: None,
142 }
143 }
144}
145 
146/// Text shadow configuration
147#[derive(Debug, Clone)]
148pub struct TextShadow {
149 pub offset_x: f32,
150 pub offset_y: f32,
151 pub blur_radius: f32,
152 pub color: [f32; 4],
153}
154 
155/// Text layout configuration
156#[derive(Debug, Clone)]
157pub struct TextLayout {
158 pub align: TextAlign,
159 pub direction: TextDirection,
160 pub wrap: TextWrap,
161 pub max_width: Option<f32>,
162 pub max_height: Option<f32>,
163 pub line_height: f32,
164 pub paragraph_spacing: f32,
165}
166 
167impl Default for TextLayout {
168 fn default() -> Self {
169 Self {
170 align: TextAlign::Left,
171 direction: TextDirection::Auto,
172 wrap: TextWrap::Word,
173 max_width: None,
174 max_height: None,
175 line_height: 1.2,
176 paragraph_spacing: 0.0,
177 }
178 }
179}
180 
181/// Represents a positioned glyph in text layout
182#[derive(Debug, Clone)]
183pub struct PositionedGlyph {
184 pub glyph_id: u32,
185 pub x: f32,
186 pub y: f32,
187 pub advance_x: f32,
188 pub advance_y: f32,
189 pub font_size: f32,
190}
191 
192/// Text run with consistent styling
193#[derive(Debug, Clone)]
194pub struct TextRun {
195 pub text: String,
196 pub style: TextStyle,
197 pub glyphs: Vec<PositionedGlyph>,
198 pub bounds: TextBounds,
199}
200 
201/// Text bounds information
202#[derive(Debug, Clone, Copy)]
203pub struct TextBounds {
204 pub x: f32,
205 pub y: f32,
206 pub width: f32,
207 pub height: f32,
208}
209 
210/// Line of text with multiple runs
211#[derive(Debug, Clone)]
212pub struct TextLine {
213 pub runs: Vec<TextRun>,
214 pub bounds: TextBounds,
215 pub baseline: f32,
216}
217 
218/// Complete text layout result
219#[derive(Debug, Clone)]
220pub struct LayoutResult {
221 pub lines: Vec<TextLine>,
222 pub bounds: TextBounds,
223 pub line_count: usize,
224}
225 
226/// Font loading and management
227pub struct FontManager {
228 fonts: Arc<Mutex<HashMap<String, FontData>>>,
229 fallback_fonts: Vec<String>,
230}
231 
232#[derive(Debug, Clone)]
233#[allow(dead_code)] // Fields are used for font storage but not in simplified implementation
234pub struct FontData {
235 pub family: String,
236 pub data: Vec<u8>,
237 pub index: u32,
238}
239 
240impl FontManager {
241 pub fn new() -> Self {
242 Self {
243 fonts: Arc::new(Mutex::new(HashMap::new())),
244 fallback_fonts: vec![
245 "system-ui".to_string(),
246 "Arial".to_string(),
247 "Helvetica".to_string(),
248 "sans-serif".to_string(),
249 ],
250 }
251 }
252 
253 /// Load a font from file
254 pub fn load_font_from_file(&mut self, path: &str, family: &str) -> Result<(), TextError> {
255 let data = std::fs::read(path)
256 .map_err(|e| TextError::FontLoadError(format!("Failed to read font file: {}", e)))?;
257 
258 self.load_font_from_data(data, family, 0)
259 }
260 
261 /// Load a font from raw data
262 pub fn load_font_from_data(
263 &mut self,
264 data: Vec<u8>,
265 family: &str,
266 index: u32,
267 ) -> Result<(), TextError> {
268 let font_data = FontData {
269 family: family.to_string(),
270 data,
271 index,
272 };
273 
274 let mut fonts = self.fonts.lock().unwrap();
275 fonts.insert(family.to_string(), font_data);
276 Ok(())
277 }
278 
279 /// Get font data by family name
280 pub fn get_font(&self, family: &str) -> Option<FontData> {
281 let fonts = self.fonts.lock().unwrap();
282 fonts.get(family).cloned()
283 }
284 
285 /// Add fallback font
286 pub fn add_fallback_font(&mut self, family: String) {
287 self.fallback_fonts.push(family);
288 }
289}
290 
291/// Text shaper for complex text layout
292pub struct TextShaper {
293 #[allow(dead_code)]
294 font_manager: Arc<FontManager>,
295}
296 
297impl TextShaper {
298 pub fn new(font_manager: Arc<FontManager>) -> Self {
299 Self { font_manager }
300 }
301 
302 /// Shape text into positioned glyphs
303 pub fn shape_text(
304 &self,
305 text: &str,
306 style: &TextStyle,
307 ) -> Result<Vec<PositionedGlyph>, TextError> {
308 // TODO: Use self.font_manager to get proper font metrics
309 let mut glyphs = Vec::new();
310 let mut x = 0.0;
311 
312 for (_i, ch) in text.char_indices() {
313 let glyph = PositionedGlyph {
314 glyph_id: ch as u32, // Simplified glyph ID
315 x,
316 y: 0.0,
317 advance_x: style.font.size * 0.6, // Simplified advance
318 advance_y: 0.0,
319 font_size: style.font.size,
320 };
321 
322 x += glyph.advance_x + style.letter_spacing;
323 glyphs.push(glyph);
324 }
325 
326 Ok(glyphs)
327 }
328}
329 
330/// Text layout engine
331pub struct TextLayoutEngine {
332 shaper: TextShaper,
333}
334 
335impl TextLayoutEngine {
336 pub fn new(font_manager: Arc<FontManager>) -> Self {
337 Self {
338 shaper: TextShaper::new(font_manager),
339 }
340 }
341 
342 /// Layout text according to the given configuration
343 pub fn layout_text(
344 &self,
345 text: &str,
346 style: &TextStyle,
347 layout: &TextLayout,
348 ) -> Result<LayoutResult, TextError> {
349 let glyphs = self.shaper.shape_text(text, style)?;
350 
351 // Simple line breaking and layout
352 let mut lines = Vec::new();
353 let mut current_line_glyphs = Vec::new();
354 let mut current_x = 0.0;
355 let line_height = style.font.size * layout.line_height;
356 
357 for glyph in glyphs {
358 if let Some(max_width) = layout.max_width {
359 if current_x + glyph.advance_x > max_width && !current_line_glyphs.is_empty() {
360 // Create line from current glyphs
361 let line = self.create_text_line(
362 current_line_glyphs,
363 style,
364 current_x,
365 line_height * lines.len() as f32,
366 );
367 lines.push(line);
368 current_line_glyphs = Vec::new();
369 current_x = 0.0;
370 }
371 }
372 
373 current_line_glyphs.push(glyph.clone());
374 current_x += glyph.advance_x;
375 }
376 
377 // Add remaining glyphs as final line
378 if !current_line_glyphs.is_empty() {
379 let line = self.create_text_line(
380 current_line_glyphs,
381 style,
382 current_x,
383 line_height * lines.len() as f32,
384 );
385 lines.push(line);
386 }
387 
388 // Calculate overall bounds
389 let total_width = lines
390 .iter()
391 .map(|line| line.bounds.width)
392 .fold(0.0, f32::max);
393 let total_height = lines.len() as f32 * line_height;
394 
395 Ok(LayoutResult {
396 lines: lines.clone(),
397 bounds: TextBounds {
398 x: 0.0,
399 y: 0.0,
400 width: total_width,
401 height: total_height,
402 },
403 line_count: lines.len(),
404 })
405 }
406 
407 fn create_text_line(
408 &self,
409 glyphs: Vec<PositionedGlyph>,
410 style: &TextStyle,
411 width: f32,
412 y: f32,
413 ) -> TextLine {
414 let run = TextRun {
415 text: String::new(), // Would be populated in real implementation
416 style: style.clone(),
417 glyphs,
418 bounds: TextBounds {
419 x: 0.0,
420 y,
421 width,
422 height: style.font.size,
423 },
424 };
425 
426 TextLine {
427 runs: vec![run],
428 bounds: TextBounds {
429 x: 0.0,
430 y,
431 width,
432 height: style.font.size,
433 },
434 baseline: y + style.font.size * 0.8, // Simplified baseline calculation
435 }
436 }
437}
438 
439/// Text measurement utilities
440pub struct TextMeasurer {
441 layout_engine: TextLayoutEngine,
442}
443 
444impl TextMeasurer {
445 pub fn new(font_manager: Arc<FontManager>) -> Self {
446 Self {
447 layout_engine: TextLayoutEngine::new(font_manager),
448 }
449 }
450 
451 /// Measure text dimensions
452 pub fn measure_text(
453 &self,
454 text: &str,
455 style: &TextStyle,
456 layout: &TextLayout,
457 ) -> Result<TextBounds, TextError> {
458 let result = self.layout_engine.layout_text(text, style, layout)?;
459 Ok(result.bounds)
460 }
461 
462 /// Get text baseline position
463 pub fn get_baseline(&self, style: &TextStyle) -> f32 {
464 style.font.size * 0.8 // Simplified baseline calculation
465 }
466}
467 
468/// Text rendering errors
469#[derive(Debug, thiserror::Error)]
470pub enum TextError {
471 #[error("Font loading error: {0}")]
472 FontLoadError(String),
473 
474 #[error("Text shaping error: {0}")]
475 ShapingError(String),
476 
477 #[error("Layout error: {0}")]
478 LayoutError(String),
479 
480 #[error("Rendering error: {0}")]
481 RenderingError(String),
482 
483 #[error("Invalid font data")]
484 InvalidFontData,
485 
486 #[error("Unsupported text feature: {0}")]
487 UnsupportedFeature(String),
488}
489 
490/// Global text system instance
491use std::sync::OnceLock;
492static TEXT_SYSTEM: OnceLock<TextSystem> = OnceLock::new();
493 
494/// Main text system
495pub struct TextSystem {
496 font_manager: Arc<FontManager>,
497 layout_engine: TextLayoutEngine,
498 measurer: TextMeasurer,
499}
500 
501impl TextSystem {
502 pub fn new() -> Self {
503 let font_manager = Arc::new(FontManager::new());
504 let layout_engine = TextLayoutEngine::new(font_manager.clone());
505 let measurer = TextMeasurer::new(font_manager.clone());
506 
507 Self {
508 font_manager,
509 layout_engine,
510 measurer,
511 }
512 }
513 
514 pub fn font_manager(&self) -> Arc<FontManager> {
515 self.font_manager.clone()
516 }
517 
518 pub fn layout_engine(&self) -> &TextLayoutEngine {
519 &self.layout_engine
520 }
521 
522 pub fn measurer(&self) -> &TextMeasurer {
523 &self.measurer
524 }
525}
526 
527/// Initialize the global text system
528pub fn init_text_system() -> Result<(), TextError> {
529 TEXT_SYSTEM.get_or_init(|| TextSystem::new());
530 Ok(())
531}
532 
533/// Get the global text system instance
534pub fn text_system() -> &'static TextSystem {
535 TEXT_SYSTEM.get().expect("Text system not initialized")
536}
537 
538#[cfg(test)]
539mod tests {
540 use super::*;
541 
542 #[test]
543 fn test_font_descriptor_default() {
544 let font = FontDescriptor::default();
545 
546 #[cfg(target_os = "windows")]
547 assert_eq!(font.family, "Segoe UI");
548 
549 #[cfg(target_os = "macos")]
550 assert_eq!(font.family, "SF Pro Display");
551 
552 #[cfg(target_os = "linux")]
553 assert_eq!(font.family, "Ubuntu");
554 
555 #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
556 assert_eq!(font.family, "Arial");
557 
558 assert_eq!(font.size, 14.0);
559 assert_eq!(font.weight, FontWeight::Normal);
560 }
561 
562 #[test]
563 fn test_text_style_default() {
564 let style = TextStyle::default();
565 assert_eq!(style.color, [0.0, 0.0, 0.0, 1.0]);
566 assert_eq!(style.decoration, TextDecoration::None);
567 }
568 
569 #[test]
570 fn test_font_manager() {
571 let mut manager = FontManager::new();
572 assert!(manager.get_font("nonexistent").is_none());
573 
574 manager.add_fallback_font("Test Font".to_string());
575 assert!(manager.fallback_fonts.contains(&"Test Font".to_string()));
576 }
577 
578 #[test]
579 fn test_text_system_init() {
580 assert!(init_text_system().is_ok());
581 }
582}
583