use std::ops::Range;
use std::sync::Arc;
use fonts::{ShapedText, ShapedTextSlice, ShapedTextSliceType, ShapedTextSlicer, ShapingOptions};
use icu_segmenter::LineBreakOptions;
use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
use style::computed_values::word_break::T as WordBreak;
use style::properties::ComputedValues;
use style::str::char_is_whitespace;
use style::values::computed::OverflowWrap;
use unicode_script::Script;
use crate::ArcRefCell;
use crate::flow::inline::line_breaker::LineBreaker;
use crate::flow::inline::text_run::{FontAndScriptInfo, TextRun, TextRunItem, script_is_specific};
pub(crate) struct ShapingQueueText {
info: FontAndScriptInfo,
byte_range: Range<usize>,
character_range: Range<usize>,
text_run: ArcRefCell<TextRun>,
index_in_text_run: usize,
old_shaped_text: Option<Arc<ShapedText>>,
}
pub(crate) enum ShapingQueueEntry {
PreservedTabOrNewline,
Text(ShapingQueueText),
}
impl ShapingQueueEntry {
pub(crate) fn new(
text_run: ArcRefCell<TextRun>,
text_run_item: &TextRunItem,
index_in_text_run: usize,
old_text_run_line_item: Option<TextRunItem>,
) -> Self {
let text_segment = match text_run_item {
TextRunItem::LineBreak { .. } | TextRunItem::Tab { .. } => {
return Self::PreservedTabOrNewline;
},
TextRunItem::TextSegment(text_run_segment) => text_run_segment,
};
let old_shaped_text = old_text_run_line_item.and_then(|old_text_run_line_item| {
let TextRunItem::TextSegment(old_text_segment) = old_text_run_line_item else {
return None;
};
if !text_segment.is_compatible_with_old_shaping_result(&old_text_segment) {
return None;
}
old_text_segment.shaped_text
});
Self::Text(ShapingQueueText {
info: text_segment.info.clone(),
byte_range: text_segment.byte_range.clone(),
character_range: text_segment.character_range.clone(),
text_run,
index_in_text_run,
old_shaped_text,
})
}
}
struct BatchSlicer<'a> {
slicer: ShapedTextSlicer,
text: &'a str,
line_breaker: &'a mut LineBreaker,
character_offset_origin: usize,
}
impl BatchSlicer<'_> {
fn slice_shaped_text_at_line_break_opportunities(
&mut self,
segment: &ShapingQueueText,
parent_style: &ComputedValues,
) -> (Vec<Arc<ShapedTextSlice>>, bool) {
let range = segment.byte_range.clone();
let linebreaks = self
.line_breaker
.advance_to_linebreaks_in_range(segment.byte_range.clone());
let linebreak_iter = linebreaks.iter().chain(std::iter::once(&range.end));
let mut break_at_start = false;
let text_style = parent_style.get_inherited_text();
let can_break_anywhere = text_style.word_break == WordBreak::BreakAll ||
text_style.overflow_wrap == OverflowWrap::Anywhere ||
text_style.overflow_wrap == OverflowWrap::BreakWord;
let mut last_slice = segment.byte_range.start..segment.byte_range.start;
let mut current_character_offset =
segment.character_range.start - self.character_offset_origin;
let mut runs = Vec::with_capacity(linebreaks.len());
let mut maybe_push_run = |run: Option<Arc<ShapedTextSlice>>| {
if let Some(run) = run {
runs.push(run);
}
};
for break_index in linebreak_iter {
if *break_index == segment.byte_range.start {
break_at_start = true;
continue;
}
let mut slice = last_slice.end..*break_index;
let word = &self.text[slice.clone()];
let mut whitespace = slice.end..slice.end;
let rev_char_indices = word.char_indices().rev().peekable();
let mut slice_type = ShapedTextSliceType::Word;
let mut ends_with_whitespace = false;
if let Some((first_white_space_index, first_white_space_character)) = rev_char_indices
.take_while(|&(_, character)| char_is_whitespace(character))
.last()
{
ends_with_whitespace = true;
whitespace.start = slice.start + first_white_space_index;
if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces &&
!can_break_anywhere
{
whitespace.start += first_white_space_character.len_utf8();
slice_type = ShapedTextSliceType::WordAndWhiteSpace;
}
slice.end = whitespace.start;
}
if !ends_with_whitespace &&
*break_index != segment.byte_range.end &&
text_style.word_break == WordBreak::KeepAll &&
!can_break_anywhere
{
continue;
}
last_slice = slice.start..*break_index;
if !slice.is_empty() {
current_character_offset += self.text[slice].chars().count();
maybe_push_run(
self.slicer
.slice_until_character_offset(current_character_offset, slice_type),
);
}
if whitespace.is_empty() {
continue;
}
if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces {
for _ in self.text[whitespace].chars() {
current_character_offset += 1;
maybe_push_run(self.slicer.slice_until_character_offset(
current_character_offset,
ShapedTextSliceType::WhiteSpace,
));
}
continue;
}
current_character_offset += self.text[whitespace].chars().count();
maybe_push_run(self.slicer.slice_until_character_offset(
current_character_offset,
ShapedTextSliceType::WhiteSpace,
));
}
(runs, break_at_start)
}
}
pub(crate) struct ShapingQueue<'a> {
queue: Vec<ShapingQueueText>,
text: &'a str,
line_breaker: LineBreaker,
byte_range: Range<usize>,
character_range: Range<usize>,
resolved_script: Option<Script>,
}
impl<'a> ShapingQueue<'a> {
pub(crate) fn new(text: &'a str, line_break_options: LineBreakOptions) -> Self {
Self {
queue: Default::default(),
text,
line_breaker: LineBreaker::new(text, line_break_options),
byte_range: Default::default(),
character_range: Default::default(),
resolved_script: None,
}
}
fn compatible_old_shaping_result(&self, character_count: usize) -> Option<Arc<ShapedText>> {
let old_shaped_text = self.queue.first()?.old_shaped_text.as_ref()?;
if old_shaped_text.character_count() != character_count {
return None;
}
if !self.queue.iter().all(|entry| {
entry
.old_shaped_text
.as_ref()
.is_some_and(|entry_old_shaped_text| {
Arc::ptr_eq(old_shaped_text, entry_old_shaped_text)
})
}) {
return None;
}
Some(old_shaped_text.clone())
}
fn shape_batch(&self) -> Option<Arc<ShapedText>> {
let first = self.queue.first()?;
let character_count = self.character_range.end - self.character_range.start;
if let Some(old_shaping_result) = self.compatible_old_shaping_result(character_count) {
return Some(old_shaping_result);
};
let mut options: ShapingOptions = (&first.info).into();
options.script = self.resolved_script.unwrap_or(first.info.script);
let font = &first.info.font_info.font;
Some(font.shape_text(&self.text[self.byte_range.clone()], &options))
}
pub(crate) fn flush(&mut self) {
let Some(shaped_text) = self.shape_batch() else {
return;
};
let mut slicer = BatchSlicer {
slicer: ShapedTextSlicer::new(shaped_text.clone()),
text: self.text,
line_breaker: &mut self.line_breaker,
character_offset_origin: self.character_range.start,
};
for entry in self.queue.drain(..) {
let mut text_run = entry.text_run.borrow_mut();
let style = text_run.inline_styles.style.borrow().clone();
let (runs, break_at_start) =
slicer.slice_shaped_text_at_line_break_opportunities(&entry, &style);
if let TextRunItem::TextSegment(text_segment) =
&mut text_run.items[entry.index_in_text_run]
{
text_segment.shaped_text = Some(shaped_text.clone());
text_segment.runs = runs;
text_segment.break_at_start = break_at_start;
}
}
}
fn compatible_with_batch(&self, text: &ShapingQueueText) -> bool {
let Some(last) = self.queue.last() else {
return true;
};
if last.character_range.end != text.character_range.start ||
last.byte_range.end != text.byte_range.start
{
return false;
}
if !Arc::ptr_eq(&last.info.font_info, &text.info.font_info) &&
*last.info.font_info != *text.info.font_info
{
return false;
}
!script_is_specific(text.info.script) ||
self.resolved_script
.is_none_or(|resolved_script| resolved_script == text.info.script)
}
fn push_text(&mut self, text: ShapingQueueText) {
if !self.compatible_with_batch(&text) {
self.flush();
}
if self.queue.is_empty() {
self.character_range = text.character_range.clone();
self.byte_range = text.byte_range.clone();
self.resolved_script = None;
} else {
self.character_range.end = text.character_range.end;
self.byte_range.end = text.byte_range.end;
}
if self.resolved_script.is_none() && script_is_specific(text.info.script) {
self.resolved_script = Some(text.info.script);
}
self.queue.push(text);
}
pub(crate) fn push(&mut self, entry: ShapingQueueEntry) {
match entry {
ShapingQueueEntry::PreservedTabOrNewline => self.flush(),
ShapingQueueEntry::Text(shaping_queue_text) => self.push_text(shaping_queue_text),
}
}
}