use alloc::vec::Vec;
use parley_engine::shape::{CharCluster, Coverage};
use parley_engine::{
Analysis, AnalysisDataSources, FontInstance, FontInstanceRef, ShapeOptions, Shaper,
};
use smallvec::SmallVec;
use super::layout::Layout;
use super::resolve::{ResolveContext, ResolvedStyle};
use super::style::{Brush, FontFeature};
use crate::inline_box::LayoutInlineBox;
use crate::util::{nearly_eq, nearly_zero};
use crate::{FontContext, FontData, Spacing};
use fontique::{self, Query, QueryFamily, QueryFont};
use parlance::{BidiLevel, GenericFamily, Tag};
const OPTIONAL_LIGATURES_OFF: [FontFeature; 4] = [
FontFeature::new(Tag::new(b"liga"), 0),
FontFeature::new(Tag::new(b"clig"), 0),
FontFeature::new(Tag::new(b"dlig"), 0),
FontFeature::new(Tag::new(b"hlig"), 0),
];
pub(crate) fn shape_text<'a, B: Brush>(
rcx: &'a ResolveContext,
fcx: &'a mut FontContext,
styles: &'a [ResolvedStyle<B>],
inline_boxes: &[LayoutInlineBox],
analysis: &Analysis,
char_style_indices: &[u16],
scx: &mut Shaper,
mut text: &str,
layout: &mut Layout<B>,
analysis_data_sources: &AnalysisDataSources,
) {
if text.is_empty() && inline_boxes.is_empty() {
text = " ";
}
if text.is_empty() || styles.is_empty() {
for box_idx in 0..inline_boxes.len() {
layout.data.push_inline_box(box_idx, BidiLevel::new(0));
}
return;
}
let mut fq = fcx.collection.query(&mut fcx.source_cache);
let mut inline_box_iter = inline_boxes.iter().map(|b| &b.inline_box).peekable();
let style_features: &'_ Vec<SmallVec<[FontFeature; 8]>> = &styles
.iter()
.map(|style| {
let style_features = rcx.features(style.font_features).unwrap_or(&[]);
if !nearly_zero(style.letter_spacing) {
OPTIONAL_LIGATURES_OFF
.into_iter()
.chain(style_features.iter().copied())
.collect()
} else {
style_features.iter().copied().collect()
}
})
.collect();
let items = {
let char_count = char_style_indices.len();
let mut item_start_char = 0_usize;
let mut chars = text.char_indices().enumerate().peekable();
core::iter::from_fn(move || {
if item_start_char == char_count {
return None;
}
let item_style_index = char_style_indices[item_start_char];
let item_style = &styles[usize::from(item_style_index)];
chars.next();
let char_end = loop {
let Some(&(char_index, (byte_index, _))) = chars.peek() else {
break char_count;
};
let mut split = false;
while let Some(inline_box) = inline_box_iter.peek() {
if inline_box.index < byte_index {
inline_box_iter.next();
} else if inline_box.index == byte_index {
inline_box_iter.next();
split = true;
} else {
break;
}
}
if split {
break char_index;
}
let style_index = char_style_indices[char_index];
if style_index != item_style_index {
let style = &styles[usize::from(style_index)];
split = !nearly_eq(style.font_size, item_style.font_size)
|| style.locale != item_style.locale
|| style.font_variations != item_style.font_variations
|| style.font_features != item_style.font_features
|| !nearly_eq(style.letter_spacing, item_style.letter_spacing)
|| !nearly_eq(style.word_spacing, item_style.word_spacing);
}
if split {
break char_index;
}
chars.next();
};
item_start_char = char_end;
Some(parley_engine::itemize::Item {
char_end: char_end as u32,
options: ShapeOptions {
language: item_style.locale,
font_size: item_style.font_size,
features: &style_features[item_style_index as usize],
variations: rcx.variations(item_style.font_variations).unwrap_or(&[]),
},
})
})
};
let font_selector = FontSelector::new(&mut fq, rcx, styles, analysis_data_sources);
scx.shape_text(
text,
analysis,
char_style_indices,
items,
font_selector,
&mut layout.data.shaped_text,
);
let mut inline_box_iter = inline_boxes
.iter()
.map(|b| &b.inline_box)
.enumerate()
.peekable();
for shaped_run_idx in 0..layout.data.shaped_text.runs().len() {
let shaped_run = &layout.data.shaped_text.runs()[shaped_run_idx];
let run_text_byte_start = shaped_run.range.byte_range.start;
let run_style_index = char_style_indices[shaped_run.range.char_range.start];
let run_style = &styles[usize::from(run_style_index)];
let prev_bidi_level = if shaped_run_idx > 0 {
layout.data.shaped_text.runs()[&shaped_run_idx - 1].bidi_level
} else {
BidiLevel::new(0)
};
while let Some((box_idx, inline_box)) = inline_box_iter.peek() {
if inline_box.index <= run_text_byte_start {
layout.data.push_inline_box(*box_idx, prev_bidi_level);
inline_box_iter.next();
} else {
break;
}
}
layout.data.process_shaped_run(
shaped_run_idx,
run_style,
Spacing::new(run_style.word_spacing, run_style.letter_spacing),
char_style_indices,
);
}
let bidi_level = layout
.data
.shaped_text
.runs()
.last()
.map(|r| r.bidi_level)
.unwrap_or(BidiLevel::new(0));
for (box_idx, _inline_box) in inline_box_iter {
layout.data.push_inline_box(box_idx, bidi_level);
}
}
#[derive(Debug)]
enum LastResortFont {
Unresolved,
Resolved(FontInstance),
Unavailable,
}
struct FontSelector<'a, 'b, B: Brush> {
query: &'b mut Query<'a>,
fonts_id: Option<usize>,
rcx: &'a ResolveContext,
styles: &'a [ResolvedStyle<B>],
style_index: u16,
attrs: fontique::Attributes,
query_yields_font: Option<bool>,
last_resort_font: LastResortFont,
analysis_data_sources: &'a AnalysisDataSources,
}
impl<'a, 'b, B: Brush> FontSelector<'a, 'b, B> {
fn new(
query: &'b mut Query<'a>,
rcx: &'a ResolveContext,
styles: &'a [ResolvedStyle<B>],
analysis_data_sources: &'a AnalysisDataSources,
) -> Self {
let attrs = fontique::Attributes::default();
Self {
query,
fonts_id: None,
rcx,
styles,
style_index: 0,
attrs,
query_yields_font: None,
last_resort_font: LastResortFont::Unresolved,
analysis_data_sources,
}
}
}
impl<'a, 'b, B: Brush> parley_engine::FontSelector for FontSelector<'a, 'b, B> {
fn begin_segment(
&mut self,
segment: &parley_engine::itemize::Segment,
options: &ShapeOptions<'_>,
) {
self.query.set_fallbacks(fontique::FallbackKey::new(
segment.script,
options.language.as_ref(),
));
self.query_yields_font = None;
}
fn select_font(
&mut self,
_segment: &parley_engine::itemize::Segment,
_options: &ShapeOptions<'_>,
cluster: &mut CharCluster,
) -> Option<FontInstanceRef<'_>> {
let style_index = cluster.style_index();
let is_emoji = cluster.is_emoji();
if style_index != self.style_index || is_emoji || self.fonts_id.is_none() {
self.style_index = style_index;
let style = &self.styles[style_index as usize];
let fonts_id = style.font_family.id();
let fonts = self.rcx.stack(style.font_family).unwrap_or(&[]);
let fonts = fonts.iter().copied().map(QueryFamily::Id);
if is_emoji {
use core::iter::once;
let emoji_family = QueryFamily::Generic(GenericFamily::Emoji);
self.query.set_families(fonts.chain(once(emoji_family)));
self.fonts_id = None;
self.query_yields_font = None;
} else if self.fonts_id != Some(fonts_id) {
self.query.set_families(fonts);
self.fonts_id = Some(fonts_id);
self.query_yields_font = None;
}
let attrs = fontique::Attributes {
width: style.font_width,
weight: style.font_weight,
style: style.font_style,
};
if self.attrs != attrs {
self.query.set_attributes(attrs);
self.attrs = attrs;
self.query_yields_font = None;
}
}
let query = &mut *self.query;
let query_yields_font = *self.query_yields_font.get_or_insert_with(|| {
let mut yields_font = false;
query.matches_with(|font| {
if font.charmap().is_some() {
yields_font = true;
fontique::QueryStatus::Stop
} else {
fontique::QueryStatus::Continue
}
});
yields_font
});
if !query_yields_font {
if matches!(self.last_resort_font, LastResortFont::Unresolved) {
if let Some(font) = any_font(self.query) {
self.last_resort_font = LastResortFont::Resolved(FontInstance {
font: FontData {
data: font.blob,
index: font.index,
},
synthesis: font.synthesis,
});
} else {
self.last_resort_font = LastResortFont::Unavailable;
}
self.fonts_id = None;
}
return if let LastResortFont::Resolved(ref font) = self.last_resort_font {
Some(font.as_ref())
} else {
None
};
}
let mut selected_font = None;
let mut best_coverage = Coverage::NONE;
self.query.matches_with(|font| {
let Some(charmap) = font.charmap() else {
return fontique::QueryStatus::Continue;
};
let coverage = cluster.calculate_coverage(
|ch| {
charmap
.map(ch)
.map(|g| {
g != 0
})
.unwrap_or_default()
},
self.analysis_data_sources,
);
if coverage > best_coverage {
selected_font = Some(font);
best_coverage = coverage;
if coverage.is_complete() {
fontique::QueryStatus::Stop
} else {
fontique::QueryStatus::Continue
}
} else {
if selected_font.is_none() {
selected_font = Some(font);
}
fontique::QueryStatus::Continue
}
});
selected_font.map(|font| FontInstanceRef {
data: &font.blob,
index: font.index,
synthesis: &font.synthesis,
})
}
}
fn any_font(query: &mut Query<'_>) -> Option<QueryFont> {
let mut found = None;
query.set_families([
QueryFamily::Generic(GenericFamily::SansSerif),
QueryFamily::Generic(GenericFamily::Serif),
QueryFamily::Generic(GenericFamily::Monospace),
]);
query.matches_with(
#[inline]
|font: &QueryFont| {
found = Some(font.clone());
fontique::QueryStatus::Stop
},
);
found
}