use alloc::vec::Vec;
use core::mem;
use harfrust::ShapeOptions as HarfShapeOptions;
use linebender_resource_handle::{Blob, FontData};
use parlance::{FontFeature, FontVariation, Language};
use crate::{
Analysis, CharInfo, ShapedText,
itemize::{Item, Segment, TextRange},
lru_cache::LruCache,
shape::{CharCluster, cache},
};
#[derive(Debug)]
pub struct ShapeOptions<'a> {
pub font_size: f32,
pub language: Option<Language>,
pub features: &'a [FontFeature],
pub variations: &'a [FontVariation],
}
#[derive(Clone, Debug, PartialEq)]
pub struct FontInstance {
pub font: FontData,
pub synthesis: fontique::Synthesis,
}
impl FontInstance {
#[inline]
pub fn as_ref(&self) -> FontInstanceRef<'_> {
FontInstanceRef {
data: &self.font.data,
index: self.font.index,
synthesis: &self.synthesis,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FontInstanceRef<'a> {
pub data: &'a Blob<u8>,
pub index: u32,
pub synthesis: &'a fontique::Synthesis,
}
impl From<FontInstanceRef<'_>> for FontInstance {
#[inline]
fn from(font: FontInstanceRef<'_>) -> Self {
Self {
font: FontData {
data: font.data.clone(),
index: font.index,
},
synthesis: *font.synthesis,
}
}
}
pub struct Shaper {
shape_data_cache: LruCache<cache::ShapeDataKey, harfrust::ShaperData>,
shape_instance_cache: LruCache<cache::ShapeInstanceId, harfrust::ShaperInstance>,
shape_plan_cache: LruCache<cache::ShapePlanId, harfrust::ShapePlan>,
unicode_buffer: Option<harfrust::UnicodeBuffer>,
features: Vec<harfrust::Feature>,
char_cluster: CharCluster,
}
impl Default for Shaper {
fn default() -> Self {
const MAX_ENTRIES: usize = 16;
Self {
shape_data_cache: LruCache::new(MAX_ENTRIES),
shape_instance_cache: LruCache::new(MAX_ENTRIES),
shape_plan_cache: LruCache::new(MAX_ENTRIES),
unicode_buffer: Some(harfrust::UnicodeBuffer::new()),
features: Vec::new(),
char_cluster: CharCluster::default(),
}
}
}
impl core::fmt::Debug for Shaper {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Shaper").finish_non_exhaustive()
}
}
impl Shaper {
pub fn shape_text<'options>(
&mut self,
text: &str,
analysis: &Analysis,
char_style_indices: &[u16],
items: impl IntoIterator<Item = Item<'options>>,
mut select_font: impl FontSelector,
shaped_text: &mut ShapedText,
) {
shaped_text.clear();
shaped_text.reserve(text.len());
let char_count = analysis.char_info().len();
debug_assert_eq!(
char_style_indices.len(),
char_count,
"The number of character style indices must be equal to the character count"
);
let mut previous_item_end = 0;
let mut itemizer = analysis.itemize(text);
for item in items {
assert!(
item.char_end > previous_item_end,
"item ends must be strictly increasing"
);
assert!(
item.char_end as usize <= char_count,
"items must not span past the text"
);
loop {
let segment = itemizer
.next(
#[inline(always)]
|text_range| text_range.char_range.end == item.char_end as usize,
)
.expect("A segment must be yielded, given items tile the full text exactly");
if shape_segment(
self,
text,
&segment,
&item.options,
&mut select_font,
analysis.char_info(),
char_style_indices,
shaped_text,
)
.is_err()
{
return;
};
debug_assert!(
segment.range.char_range.end <= item.char_end as usize,
"Segments must not span past the item."
);
if segment.range.char_range.end == item.char_end as usize {
break;
}
}
previous_item_end = item.char_end;
}
assert_eq!(
previous_item_end as usize, char_count,
"`items` does not cover the entire source text"
);
debug_assert_eq!(
shaped_text.characters().len(),
char_count,
"All characters must have been processed"
);
}
}
fn shape_segment(
scx: &mut Shaper,
text: &str,
segment: &Segment,
options: &ShapeOptions<'_>,
select_font: &mut impl FontSelector,
char_info: &[CharInfo],
char_style_indices: &[u16],
shaped_text: &mut ShapedText,
) -> Result<(), ()> {
select_font.begin_segment(segment, options);
let text_range = &segment.range.byte_range;
let char_range = &segment.range.char_range;
let segment_text = &text[text_range.clone()];
let segment_char_info = &char_info[char_range.start..char_range.end];
let segment_char_style_indices = &char_style_indices[char_range.start..char_range.end];
if segment_text.is_empty() {
return Ok(()); }
let char_cluster = &mut scx.char_cluster;
let direction = if segment.bidi_level.is_rtl() {
harfrust::Direction::RightToLeft
} else {
harfrust::Direction::LeftToRight
};
let hb_script = script_to_harfrust(segment.script);
let language = options
.language
.as_ref()
.and_then(|lang| lang.as_str().parse::<harfrust::Language>().ok());
scx.features.clear();
scx.features.extend(options.features.iter().map(|feature| {
harfrust::Feature::new(
harfrust::Tag::new(&feature.tag.to_bytes()),
feature.value as u32,
..,
)
}));
let mut shape_run = |font: &FontInstance, range: TextRange| {
let font_ref =
harfrust::FontRef::from_index(font.font.data.as_ref(), font.font.index).unwrap();
let shaper_data = scx.shape_data_cache.entry(
cache::ShapeDataKey::new(font.font.data.id(), font.font.index),
|| harfrust::ShaperData::new(&font_ref),
);
let instance = scx.shape_instance_cache.entry(
cache::ShapeInstanceKey::new(
font.font.data.id(),
font.font.index,
&font.synthesis,
Some(options.variations),
),
|| {
harfrust::ShaperInstance::from_variations(
&font_ref,
variations_iter(&font.synthesis, options.variations),
)
},
);
let harf_shaper = shaper_data
.shaper(&font_ref)
.instance(Some(instance))
.build();
let shaper_plan = scx.shape_plan_cache.entry(
cache::ShapePlanKey::new(
font.font.data.id(),
font.font.index,
&font.synthesis,
direction,
hb_script,
language.clone(),
&scx.features,
Some(options.variations),
),
|| {
harfrust::ShapePlan::new(
&harf_shaper,
direction,
Some(hb_script),
language.as_ref(),
&scx.features,
)
},
);
let mut buffer = mem::take(&mut scx.unicode_buffer).unwrap();
buffer.clear();
buffer.set_cluster_level(harfrust::BufferClusterLevel::MonotoneCharacters);
let run_text = &text[range.byte_range.clone()];
buffer.reserve(run_text.len());
#[expect(clippy::cast_possible_truncation, reason = "Deferred")]
for (i, ch) in run_text.chars().enumerate() {
buffer.add(ch, i as u32);
}
buffer.set_pre_context(&text[..range.byte_range.start]);
buffer.set_post_context(&text[range.byte_range.end..]);
buffer.set_direction(direction);
buffer.set_script(hb_script);
if let Some(lang) = &language {
buffer.set_language(lang.clone());
}
let glyph_buffer = harf_shaper.shape(
buffer,
HarfShapeOptions::new()
.plan(Some(shaper_plan))
.features(&scx.features)
.point_size(Some(options.font_size)),
);
shaped_text.push_run(
text,
range,
segment,
options,
char_info,
char_style_indices,
font,
&glyph_buffer,
harf_shaper.coords(),
);
scx.unicode_buffer = Some(glyph_buffer.clear());
};
let mut run: Option<(FontInstance, TextRange)> = None;
let mut chars = segment_text
.chars()
.zip(segment_char_info)
.zip(segment_char_style_indices)
.map(|((ch, &info), &style_index)| (ch, info, style_index));
let grapheme_lengths = segment_char_info
.chunk_by(|_, info| !info.is_grapheme_start())
.map(<[_]>::len);
let mut byte_end = text_range.start;
let mut char_end = char_range.start;
for grapheme_length in grapheme_lengths {
let (byte_start, char_start) = (byte_end, char_end);
char_cluster.fill(chars.by_ref().take(grapheme_length), &mut byte_end);
char_end += grapheme_length;
let grapheme = TextRange {
byte_range: byte_start..byte_end,
char_range: char_start..char_end,
};
let font = select_font
.select_font(segment, options, char_cluster)
.ok_or(())?;
if let Some((run_font, run_range)) = &mut run
&& run_font.as_ref() == font
{
run_range.byte_range.end = byte_end;
run_range.char_range.end = char_end;
} else {
if let Some((run_font, run_range)) = run.take() {
shape_run(&run_font, run_range);
}
run = Some((font.into(), grapheme));
}
}
if let Some((run_font, run_range)) = run {
shape_run(&run_font, run_range);
}
Ok(())
}
#[inline]
fn variations_iter<'a>(
synthesis: &'a fontique::Synthesis,
item: &'a [FontVariation],
) -> impl Iterator<Item = harfrust::Variation> + 'a {
synthesis
.variation_settings()
.iter()
.map(|(tag, value)| harfrust::Variation {
tag: *tag,
value: *value,
})
.chain(item.iter().map(|variation| harfrust::Variation {
tag: harfrust::Tag::new(&variation.tag.to_bytes()),
value: variation.value,
}))
}
pub(crate) fn script_to_harfrust(script: fontique::Script) -> harfrust::Script {
harfrust::Script::from_iso15924_tag(harfrust::Tag::new(&script.to_bytes()))
.unwrap_or(harfrust::script::UNKNOWN)
}
pub trait FontSelector {
fn begin_segment(&mut self, segment: &Segment, options: &ShapeOptions<'_>) {
let _ = (segment, options);
}
fn select_font(
&mut self,
segment: &Segment,
options: &ShapeOptions<'_>,
cluster: &mut CharCluster,
) -> Option<FontInstanceRef<'_>>;
}