1use crate::shaping::Direction;
16use crate::text_run::{
17 FontId, FontOrigin, OwnedTextRun, RunGlyph, TextCluster, TextRunContext, byte_to_utf16,
18};
19use std::fmt;
20use std::ops::Range;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28pub enum ShapingRouteKind {
29 BundledShaper,
31 SystemPlatform,
33 SimulatedPlatform,
35}
36
37impl ShapingRouteKind {
38 #[must_use]
40 pub const fn is_pixel_deterministic(self) -> bool {
41 matches!(self, Self::BundledShaper)
42 }
43}
44
45#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct ShapingRouteCapabilities {
50 pub route_kind: ShapingRouteKind,
52 pub is_pixel_deterministic: bool,
54 pub supports_fallback_fonts: bool,
56 pub supports_color_emoji: bool,
58 pub supports_bidi: bool,
60 pub max_paragraph_bytes: usize,
64}
65
66impl Default for ShapingRouteCapabilities {
67 fn default() -> Self {
68 Self {
69 route_kind: ShapingRouteKind::SimulatedPlatform,
70 is_pixel_deterministic: false,
71 supports_fallback_fonts: true,
72 supports_color_emoji: true,
73 supports_bidi: true,
74 max_paragraph_bytes: 64 * 1024, }
76 }
77}
78
79#[derive(Clone, Debug, PartialEq, Eq, Hash)]
84pub struct FallbackFace {
85 pub font_id: FontId,
87 pub family_name: String,
89 pub postscript_name: String,
91 pub units_per_em: u16,
93 pub is_color_emoji: bool,
95}
96
97#[derive(Clone, Copy, Debug, PartialEq)]
99pub struct PlatformRunGlyph {
100 pub glyph_id: u16,
102 pub font_id: FontId,
104 pub cluster_byte_offset: usize,
106 pub cluster_byte_len: usize,
108 pub x_advance: f32,
110 pub y_advance: f32,
112 pub x_offset: f32,
114 pub y_offset: f32,
116}
117
118#[derive(Clone, Debug, PartialEq)]
120pub struct PlatformShapedOutput {
121 pub logical_text: String,
123 pub direction: Direction,
125 pub font_size: f32,
127 pub glyphs: Vec<PlatformRunGlyph>,
129 pub fallback_faces: Vec<FallbackFace>,
131 pub route_kind: ShapingRouteKind,
133}
134
135#[derive(Clone, Debug, PartialEq)]
137pub struct NativeShapingRequest<'a> {
138 pub text: &'a str,
140 pub primary_font_id: FontId,
142 pub font_size: f32,
144 pub direction: Direction,
146 pub script: [u8; 4],
148 pub language: [u8; 4],
150 pub allow_system_fallback: bool,
153}
154
155#[derive(Clone, Debug, PartialEq, Eq)]
157pub enum NativeShapingError {
158 ContextBudgetExceeded {
160 length: usize,
162 max_allowed: usize,
164 },
165 EmptyText,
167 InvalidByteRange {
169 offset: usize,
171 text_len: usize,
173 },
174 MidScalarBoundary {
176 offset: usize,
178 },
179 FallbackRequired {
181 unshaped_byte_offset: usize,
183 },
184 PlatformUnavailable(String),
186 AdapterError(String),
188}
189
190impl fmt::Display for NativeShapingError {
191 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192 match self {
193 Self::ContextBudgetExceeded {
194 length,
195 max_allowed,
196 } => {
197 write!(
198 f,
199 "text context length ({length} bytes) exceeds maximum budget ({max_allowed} bytes)"
200 )
201 }
202 Self::EmptyText => write!(f, "cannot shape empty text"),
203 Self::InvalidByteRange { offset, text_len } => {
204 write!(
205 f,
206 "byte offset {offset} is out of bounds for text of length {text_len}"
207 )
208 }
209 Self::MidScalarBoundary { offset } => {
210 write!(
211 f,
212 "byte offset {offset} is inside a multi-byte UTF-8 scalar"
213 )
214 }
215 Self::FallbackRequired {
216 unshaped_byte_offset,
217 } => {
218 write!(
219 f,
220 "system fallback required at byte offset {unshaped_byte_offset} but fallback was disabled"
221 )
222 }
223 Self::PlatformUnavailable(msg) => write!(f, "platform shaper unavailable: {msg}"),
224 Self::AdapterError(msg) => write!(f, "platform adapter error: {msg}"),
225 }
226 }
227}
228
229impl std::error::Error for NativeShapingError {}
230
231pub fn assemble_platform_run(
236 primary_context: TextRunContext,
237 output: PlatformShapedOutput,
238) -> Result<OwnedTextRun, NativeShapingError> {
239 if output.logical_text.is_empty() {
240 return Err(NativeShapingError::EmptyText);
241 }
242
243 let text = &output.logical_text;
244 let text_len = text.len();
245 let is_rtl = output.direction == Direction::RightToLeft;
246
247 let mut clusters: Vec<TextCluster> = Vec::new();
248 let mut run_glyphs: Vec<RunGlyph> = Vec::with_capacity(output.glyphs.len());
249
250 let mut cur_x = 0.0f32;
251 let mut i = 0;
252
253 while i < output.glyphs.len() {
254 let first = &output.glyphs[i];
255 let byte_start = first.cluster_byte_offset;
256 let byte_len = first.cluster_byte_len;
257 let byte_end = byte_start.saturating_add(byte_len);
258
259 if byte_end > text_len {
261 return Err(NativeShapingError::InvalidByteRange {
262 offset: byte_end,
263 text_len,
264 });
265 }
266
267 if !text.is_char_boundary(byte_start) {
269 return Err(NativeShapingError::MidScalarBoundary { offset: byte_start });
270 }
271 if !text.is_char_boundary(byte_end) {
272 return Err(NativeShapingError::MidScalarBoundary { offset: byte_end });
273 }
274
275 let cluster_start_glyph = run_glyphs.len();
276 let cluster_x_start = cur_x;
277 let cluster_font_id = first.font_id;
278
279 while i < output.glyphs.len() {
281 let g = &output.glyphs[i];
282 if g.cluster_byte_offset != byte_start || g.cluster_byte_len != byte_len {
283 break;
284 }
285
286 run_glyphs.push(RunGlyph {
287 glyph_id: g.glyph_id,
288 font_id: g.font_id,
289 cluster_index: clusters.len(),
290 x_advance: g.x_advance,
291 y_advance: g.y_advance,
292 x_offset: g.x_offset,
293 y_offset: g.y_offset,
294 });
295
296 cur_x += g.x_advance;
297 i += 1;
298 }
299
300 let cluster_x_end = cur_x;
301
302 let utf16_start = match byte_to_utf16(text, byte_start) {
303 Some(o) => o,
304 None => return Err(NativeShapingError::MidScalarBoundary { offset: byte_start }),
305 };
306 let utf16_end = match byte_to_utf16(text, byte_end) {
307 Some(o) => o,
308 None => return Err(NativeShapingError::MidScalarBoundary { offset: byte_end }),
309 };
310
311 clusters.push(TextCluster {
312 cluster_index: clusters.len(),
313 byte_range: byte_start..byte_end,
314 utf16_range: utf16_start..utf16_end,
315 glyph_range: cluster_start_glyph..run_glyphs.len(),
316 x_start: cluster_x_start,
317 x_end: cluster_x_end,
318 font_id: cluster_font_id,
319 });
320 }
321
322 let total_advance = cur_x;
323
324 if is_rtl {
326 for cluster in &mut clusters {
327 let old_start = cluster.x_start;
328 let old_end = cluster.x_end;
329 cluster.x_start = (total_advance - old_end).max(0.0);
330 cluster.x_end = (total_advance - old_start).max(0.0);
331 }
332 }
333
334 let font_origin = if output.fallback_faces.is_empty() {
336 primary_context.font_origin
337 } else {
338 FontOrigin::SystemFallbackFace
339 };
340
341 let context = TextRunContext {
342 font_id: primary_context.font_id,
343 font_size: output.font_size,
344 script: primary_context.script,
345 language: primary_context.language,
346 direction: output.direction,
347 font_origin,
348 };
349
350 Ok(OwnedTextRun {
351 context,
352 logical_text: output.logical_text,
353 clusters,
354 glyphs: run_glyphs,
355 total_advance,
356 })
357}
358
359pub trait NativeShapingRoute: Send + Sync {
364 fn route_kind(&self) -> ShapingRouteKind;
366
367 fn capabilities(&self) -> ShapingRouteCapabilities;
369
370 fn shape_run(&self, req: &NativeShapingRequest<'_>)
372 -> Result<OwnedTextRun, NativeShapingError>;
373}
374
375pub struct SimulatedNativeRoute {
381 capabilities: ShapingRouteCapabilities,
382 fallback_rules: Vec<SimulatedFallbackRule>,
383}
384
385#[derive(Clone, Debug)]
387pub struct SimulatedFallbackRule {
388 pub range: Range<u32>,
390 pub fallback_face: FallbackFace,
392 pub advance_per_em: f32,
394}
395
396impl SimulatedNativeRoute {
397 #[must_use]
399 pub fn new() -> Self {
400 Self {
401 capabilities: ShapingRouteCapabilities::default(),
402 fallback_rules: Vec::new(),
403 }
404 }
405
406 #[must_use]
408 pub fn with_capabilities(capabilities: ShapingRouteCapabilities) -> Self {
409 Self {
410 capabilities,
411 fallback_rules: Vec::new(),
412 }
413 }
414
415 pub fn register_fallback(&mut self, rule: SimulatedFallbackRule) {
417 self.fallback_rules.push(rule);
418 }
419}
420
421impl Default for SimulatedNativeRoute {
422 fn default() -> Self {
423 Self::new()
424 }
425}
426
427impl NativeShapingRoute for SimulatedNativeRoute {
428 fn route_kind(&self) -> ShapingRouteKind {
429 self.capabilities.route_kind
430 }
431
432 fn capabilities(&self) -> ShapingRouteCapabilities {
433 self.capabilities.clone()
434 }
435
436 fn shape_run(
437 &self,
438 req: &NativeShapingRequest<'_>,
439 ) -> Result<OwnedTextRun, NativeShapingError> {
440 if req.text.is_empty() {
442 return Err(NativeShapingError::EmptyText);
443 }
444
445 if req.text.len() > self.capabilities.max_paragraph_bytes {
447 return Err(NativeShapingError::ContextBudgetExceeded {
448 length: req.text.len(),
449 max_allowed: self.capabilities.max_paragraph_bytes,
450 });
451 }
452
453 let mut platform_glyphs: Vec<PlatformRunGlyph> = Vec::new();
454 let mut used_fallbacks: Vec<FallbackFace> = Vec::new();
455
456 let primary_advance = (req.font_size * 0.5).max(1.0); for (byte_idx, ch) in req.text.char_indices() {
459 let cp = ch as u32;
460 let ch_len = ch.len_utf8();
461
462 let matched_fallback = self.fallback_rules.iter().find(|r| r.range.contains(&cp));
464
465 if let Some(rule) = matched_fallback {
466 if !req.allow_system_fallback {
467 return Err(NativeShapingError::FallbackRequired {
468 unshaped_byte_offset: byte_idx,
469 });
470 }
471
472 if !used_fallbacks
473 .iter()
474 .any(|f| f.font_id == rule.fallback_face.font_id)
475 {
476 used_fallbacks.push(rule.fallback_face.clone());
477 }
478
479 let adv = req.font_size * rule.advance_per_em;
480 let glyph_id = (cp % 1000) as u16;
481
482 platform_glyphs.push(PlatformRunGlyph {
483 glyph_id,
484 font_id: rule.fallback_face.font_id,
485 cluster_byte_offset: byte_idx,
486 cluster_byte_len: ch_len,
487 x_advance: adv,
488 y_advance: 0.0,
489 x_offset: 0.0,
490 y_offset: 0.0,
491 });
492 } else {
493 let glyph_id = (cp % 500) as u16;
495 platform_glyphs.push(PlatformRunGlyph {
496 glyph_id,
497 font_id: req.primary_font_id,
498 cluster_byte_offset: byte_idx,
499 cluster_byte_len: ch_len,
500 x_advance: primary_advance,
501 y_advance: 0.0,
502 x_offset: 0.0,
503 y_offset: 0.0,
504 });
505 }
506 }
507
508 let output = PlatformShapedOutput {
509 logical_text: req.text.to_string(),
510 direction: req.direction,
511 font_size: req.font_size,
512 glyphs: platform_glyphs,
513 fallback_faces: used_fallbacks,
514 route_kind: self.capabilities.route_kind,
515 };
516
517 let primary_context = TextRunContext {
518 font_id: req.primary_font_id,
519 font_size: req.font_size,
520 script: req.script,
521 language: req.language,
522 direction: req.direction,
523 font_origin: FontOrigin::BundledFace,
524 };
525
526 assemble_platform_run(primary_context, output)
527 }
528}