use crate::shaping::Direction;
use crate::text_run::{
FontId, FontOrigin, OwnedTextRun, RunGlyph, TextCluster, TextRunContext, byte_to_utf16,
};
use std::fmt;
use std::ops::Range;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ShapingRouteKind {
BundledShaper,
SystemPlatform,
SimulatedPlatform,
}
impl ShapingRouteKind {
#[must_use]
pub const fn is_pixel_deterministic(self) -> bool {
matches!(self, Self::BundledShaper)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ShapingRouteCapabilities {
pub route_kind: ShapingRouteKind,
pub is_pixel_deterministic: bool,
pub supports_fallback_fonts: bool,
pub supports_color_emoji: bool,
pub supports_bidi: bool,
pub max_paragraph_bytes: usize,
}
impl Default for ShapingRouteCapabilities {
fn default() -> Self {
Self {
route_kind: ShapingRouteKind::SimulatedPlatform,
is_pixel_deterministic: false,
supports_fallback_fonts: true,
supports_color_emoji: true,
supports_bidi: true,
max_paragraph_bytes: 64 * 1024, }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct FallbackFace {
pub font_id: FontId,
pub family_name: String,
pub postscript_name: String,
pub units_per_em: u16,
pub is_color_emoji: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PlatformRunGlyph {
pub glyph_id: u16,
pub font_id: FontId,
pub cluster_byte_offset: usize,
pub cluster_byte_len: usize,
pub x_advance: f32,
pub y_advance: f32,
pub x_offset: f32,
pub y_offset: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PlatformShapedOutput {
pub logical_text: String,
pub direction: Direction,
pub font_size: f32,
pub glyphs: Vec<PlatformRunGlyph>,
pub fallback_faces: Vec<FallbackFace>,
pub route_kind: ShapingRouteKind,
}
#[derive(Clone, Debug, PartialEq)]
pub struct NativeShapingRequest<'a> {
pub text: &'a str,
pub primary_font_id: FontId,
pub font_size: f32,
pub direction: Direction,
pub script: [u8; 4],
pub language: [u8; 4],
pub allow_system_fallback: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NativeShapingError {
ContextBudgetExceeded {
length: usize,
max_allowed: usize,
},
EmptyText,
InvalidByteRange {
offset: usize,
text_len: usize,
},
MidScalarBoundary {
offset: usize,
},
FallbackRequired {
unshaped_byte_offset: usize,
},
PlatformUnavailable(String),
AdapterError(String),
}
impl fmt::Display for NativeShapingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ContextBudgetExceeded {
length,
max_allowed,
} => {
write!(
f,
"text context length ({length} bytes) exceeds maximum budget ({max_allowed} bytes)"
)
}
Self::EmptyText => write!(f, "cannot shape empty text"),
Self::InvalidByteRange { offset, text_len } => {
write!(
f,
"byte offset {offset} is out of bounds for text of length {text_len}"
)
}
Self::MidScalarBoundary { offset } => {
write!(
f,
"byte offset {offset} is inside a multi-byte UTF-8 scalar"
)
}
Self::FallbackRequired {
unshaped_byte_offset,
} => {
write!(
f,
"system fallback required at byte offset {unshaped_byte_offset} but fallback was disabled"
)
}
Self::PlatformUnavailable(msg) => write!(f, "platform shaper unavailable: {msg}"),
Self::AdapterError(msg) => write!(f, "platform adapter error: {msg}"),
}
}
}
impl std::error::Error for NativeShapingError {}
pub fn assemble_platform_run(
primary_context: TextRunContext,
output: PlatformShapedOutput,
) -> Result<OwnedTextRun, NativeShapingError> {
if output.logical_text.is_empty() {
return Err(NativeShapingError::EmptyText);
}
let text = &output.logical_text;
let text_len = text.len();
let is_rtl = output.direction == Direction::RightToLeft;
let mut clusters: Vec<TextCluster> = Vec::new();
let mut run_glyphs: Vec<RunGlyph> = Vec::with_capacity(output.glyphs.len());
let mut cur_x = 0.0f32;
let mut i = 0;
while i < output.glyphs.len() {
let first = &output.glyphs[i];
let byte_start = first.cluster_byte_offset;
let byte_len = first.cluster_byte_len;
let byte_end = byte_start.saturating_add(byte_len);
if byte_end > text_len {
return Err(NativeShapingError::InvalidByteRange {
offset: byte_end,
text_len,
});
}
if !text.is_char_boundary(byte_start) {
return Err(NativeShapingError::MidScalarBoundary { offset: byte_start });
}
if !text.is_char_boundary(byte_end) {
return Err(NativeShapingError::MidScalarBoundary { offset: byte_end });
}
let cluster_start_glyph = run_glyphs.len();
let cluster_x_start = cur_x;
let cluster_font_id = first.font_id;
while i < output.glyphs.len() {
let g = &output.glyphs[i];
if g.cluster_byte_offset != byte_start || g.cluster_byte_len != byte_len {
break;
}
run_glyphs.push(RunGlyph {
glyph_id: g.glyph_id,
font_id: g.font_id,
cluster_index: clusters.len(),
x_advance: g.x_advance,
y_advance: g.y_advance,
x_offset: g.x_offset,
y_offset: g.y_offset,
});
cur_x += g.x_advance;
i += 1;
}
let cluster_x_end = cur_x;
let utf16_start = match byte_to_utf16(text, byte_start) {
Some(o) => o,
None => return Err(NativeShapingError::MidScalarBoundary { offset: byte_start }),
};
let utf16_end = match byte_to_utf16(text, byte_end) {
Some(o) => o,
None => return Err(NativeShapingError::MidScalarBoundary { offset: byte_end }),
};
clusters.push(TextCluster {
cluster_index: clusters.len(),
byte_range: byte_start..byte_end,
utf16_range: utf16_start..utf16_end,
glyph_range: cluster_start_glyph..run_glyphs.len(),
x_start: cluster_x_start,
x_end: cluster_x_end,
font_id: cluster_font_id,
});
}
let total_advance = cur_x;
if is_rtl {
for cluster in &mut clusters {
let old_start = cluster.x_start;
let old_end = cluster.x_end;
cluster.x_start = (total_advance - old_end).max(0.0);
cluster.x_end = (total_advance - old_start).max(0.0);
}
}
let font_origin = if output.fallback_faces.is_empty() {
primary_context.font_origin
} else {
FontOrigin::SystemFallbackFace
};
let context = TextRunContext {
font_id: primary_context.font_id,
font_size: output.font_size,
script: primary_context.script,
language: primary_context.language,
direction: output.direction,
font_origin,
};
Ok(OwnedTextRun {
context,
logical_text: output.logical_text,
clusters,
glyphs: run_glyphs,
total_advance,
})
}
pub trait NativeShapingRoute: Send + Sync {
fn route_kind(&self) -> ShapingRouteKind;
fn capabilities(&self) -> ShapingRouteCapabilities;
fn shape_run(&self, req: &NativeShapingRequest<'_>)
-> Result<OwnedTextRun, NativeShapingError>;
}
pub struct SimulatedNativeRoute {
capabilities: ShapingRouteCapabilities,
fallback_rules: Vec<SimulatedFallbackRule>,
}
#[derive(Clone, Debug)]
pub struct SimulatedFallbackRule {
pub range: Range<u32>,
pub fallback_face: FallbackFace,
pub advance_per_em: f32,
}
impl SimulatedNativeRoute {
#[must_use]
pub fn new() -> Self {
Self {
capabilities: ShapingRouteCapabilities::default(),
fallback_rules: Vec::new(),
}
}
#[must_use]
pub fn with_capabilities(capabilities: ShapingRouteCapabilities) -> Self {
Self {
capabilities,
fallback_rules: Vec::new(),
}
}
pub fn register_fallback(&mut self, rule: SimulatedFallbackRule) {
self.fallback_rules.push(rule);
}
}
impl Default for SimulatedNativeRoute {
fn default() -> Self {
Self::new()
}
}
impl NativeShapingRoute for SimulatedNativeRoute {
fn route_kind(&self) -> ShapingRouteKind {
self.capabilities.route_kind
}
fn capabilities(&self) -> ShapingRouteCapabilities {
self.capabilities.clone()
}
fn shape_run(
&self,
req: &NativeShapingRequest<'_>,
) -> Result<OwnedTextRun, NativeShapingError> {
if req.text.is_empty() {
return Err(NativeShapingError::EmptyText);
}
if req.text.len() > self.capabilities.max_paragraph_bytes {
return Err(NativeShapingError::ContextBudgetExceeded {
length: req.text.len(),
max_allowed: self.capabilities.max_paragraph_bytes,
});
}
let mut platform_glyphs: Vec<PlatformRunGlyph> = Vec::new();
let mut used_fallbacks: Vec<FallbackFace> = Vec::new();
let primary_advance = (req.font_size * 0.5).max(1.0);
for (byte_idx, ch) in req.text.char_indices() {
let cp = ch as u32;
let ch_len = ch.len_utf8();
let matched_fallback = self.fallback_rules.iter().find(|r| r.range.contains(&cp));
if let Some(rule) = matched_fallback {
if !req.allow_system_fallback {
return Err(NativeShapingError::FallbackRequired {
unshaped_byte_offset: byte_idx,
});
}
if !used_fallbacks
.iter()
.any(|f| f.font_id == rule.fallback_face.font_id)
{
used_fallbacks.push(rule.fallback_face.clone());
}
let adv = req.font_size * rule.advance_per_em;
let glyph_id = (cp % 1000) as u16;
platform_glyphs.push(PlatformRunGlyph {
glyph_id,
font_id: rule.fallback_face.font_id,
cluster_byte_offset: byte_idx,
cluster_byte_len: ch_len,
x_advance: adv,
y_advance: 0.0,
x_offset: 0.0,
y_offset: 0.0,
});
} else {
let glyph_id = (cp % 500) as u16;
platform_glyphs.push(PlatformRunGlyph {
glyph_id,
font_id: req.primary_font_id,
cluster_byte_offset: byte_idx,
cluster_byte_len: ch_len,
x_advance: primary_advance,
y_advance: 0.0,
x_offset: 0.0,
y_offset: 0.0,
});
}
}
let output = PlatformShapedOutput {
logical_text: req.text.to_string(),
direction: req.direction,
font_size: req.font_size,
glyphs: platform_glyphs,
fallback_faces: used_fallbacks,
route_kind: self.capabilities.route_kind,
};
let primary_context = TextRunContext {
font_id: req.primary_font_id,
font_size: req.font_size,
script: req.script,
language: req.language,
direction: req.direction,
font_origin: FontOrigin::BundledFace,
};
assemble_platform_run(primary_context, output)
}
}