StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 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 | |
| 6 | use std::collections::HashMap; |
| 7 | use std::sync::{Arc, Mutex}; |
| 8 | |
| 9 | /// Text alignment options |
| 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 11 | pub 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)] |
| 22 | pub enum TextDirection { |
| 23 | LeftToRight, |
| 24 | RightToLeft, |
| 25 | Auto, |
| 26 | } |
| 27 | |
| 28 | /// Text wrapping behavior |
| 29 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 30 | pub enum TextWrap { |
| 31 | None, |
| 32 | Word, |
| 33 | Character, |
| 34 | WordCharacter, |
| 35 | } |
| 36 | |
| 37 | /// Font weight values |
| 38 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 39 | pub 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)] |
| 53 | pub enum FontStyle { |
| 54 | Normal, |
| 55 | Italic, |
| 56 | Oblique, |
| 57 | } |
| 58 | |
| 59 | /// Font stretch values |
| 60 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 61 | pub 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)] |
| 75 | pub struct FontDescriptor { |
| 76 | pub family: String, |
| 77 | pub size: f32, |
| 78 | pub weight: FontWeight, |
| 79 | pub style: FontStyle, |
| 80 | pub stretch: FontStretch, |
| 81 | } |
| 82 | |
| 83 | impl 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)] |
| 110 | pub enum TextDecoration { |
| 111 | None, |
| 112 | Underline, |
| 113 | Overline, |
| 114 | LineThrough, |
| 115 | Blink, |
| 116 | } |
| 117 | |
| 118 | /// Text style configuration |
| 119 | #[derive(Debug, Clone)] |
| 120 | pub 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 | |
| 131 | impl 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)] |
| 148 | pub 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)] |
| 157 | pub 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 | |
| 167 | impl 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)] |
| 183 | pub 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)] |
| 194 | pub 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)] |
| 203 | pub 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)] |
| 212 | pub 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)] |
| 220 | pub struct LayoutResult { |
| 221 | pub lines: Vec<TextLine>, |
| 222 | pub bounds: TextBounds, |
| 223 | pub line_count: usize, |
| 224 | } |
| 225 | |
| 226 | /// Font loading and management |
| 227 | pub 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 |
| 234 | pub struct FontData { |
| 235 | pub family: String, |
| 236 | pub data: Vec<u8>, |
| 237 | pub index: u32, |
| 238 | } |
| 239 | |
| 240 | impl 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 |
| 292 | pub struct TextShaper { |
| 293 | #[allow(dead_code)] |
| 294 | font_manager: Arc<FontManager>, |
| 295 | } |
| 296 | |
| 297 | impl 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 |
| 331 | pub struct TextLayoutEngine { |
| 332 | shaper: TextShaper, |
| 333 | } |
| 334 | |
| 335 | impl 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 |
| 440 | pub struct TextMeasurer { |
| 441 | layout_engine: TextLayoutEngine, |
| 442 | } |
| 443 | |
| 444 | impl 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)] |
| 470 | pub 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 |
| 491 | use std::sync::OnceLock; |
| 492 | static TEXT_SYSTEM: OnceLock<TextSystem> = OnceLock::new(); |
| 493 | |
| 494 | /// Main text system |
| 495 | pub struct TextSystem { |
| 496 | font_manager: Arc<FontManager>, |
| 497 | layout_engine: TextLayoutEngine, |
| 498 | measurer: TextMeasurer, |
| 499 | } |
| 500 | |
| 501 | impl 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 |
| 528 | pub 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 |
| 534 | pub fn text_system() -> &'static TextSystem { |
| 535 | TEXT_SYSTEM.get().expect("Text system not initialized") |
| 536 | } |
| 537 | |
| 538 | #[cfg(test)] |
| 539 | mod 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 |