use std::mem;
use std::ops::Range;
use std::sync::Arc;
use app_units::Au;
use fonts::{
FontContext, FontRef, ShapedTextSlice, ShapedTextSlicer, ShapingFlags, ShapingOptions,
};
use icu_locid::subtags::Language;
use icu_properties::{self, LineBreak};
use log::warn;
use malloc_size_of_derive::MallocSizeOf;
use servo_arc::Arc as ServoArc;
use servo_base::text::is_bidi_control;
use style::Zero;
use style::computed_values::font_kerning::T as FontKerning;
use style::computed_values::font_variant_position::T as FontVariantPosition;
use style::computed_values::text_rendering::T as TextRendering;
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::{
FontFeatureSettings, FontVariantEastAsian, FontVariantLigatures, FontVariantNumeric,
OverflowWrap,
};
use unicode_bidi::{BidiInfo, Level};
use unicode_script::Script;
use super::line_breaker::LineBreaker;
use super::{InlineFormattingContextLayout, SharedInlineStyles};
use crate::ArcRefCell;
use crate::context::LayoutContext;
use crate::dom::WeakLayoutBox;
use crate::flow::inline::line::TextRunOffsets;
use crate::flow::inline::{LineBlockSizes, LineItem, SegmentContentFlags};
use crate::fragment_tree::BaseFragmentInfo;
#[derive(PartialEq)]
enum SegmentStartSoftWrapPolicy {
Force,
FollowLinebreaker,
}
#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
pub(crate) struct FontAndScriptInfo {
pub font: FontRef,
pub script: Script,
pub bidi_level: Level,
pub language: Language,
pub letter_spacing: Option<Au>,
pub word_spacing: Option<Au>,
pub text_rendering: TextRendering,
pub kerning: FontKerning,
pub ligatures: FontVariantLigatures,
pub numeric: FontVariantNumeric,
pub east_asian: FontVariantEastAsian,
pub feature_settings: FontFeatureSettings,
pub position: FontVariantPosition,
}
impl FontAndScriptInfo {
pub(crate) fn simple_for_font(font: FontRef) -> Self {
Self {
font,
script: Script::Common,
bidi_level: Level::ltr(),
language: Language::UND,
letter_spacing: None,
word_spacing: None,
text_rendering: TextRendering::Auto,
kerning: FontKerning::Auto,
ligatures: FontVariantLigatures::NORMAL,
numeric: FontVariantNumeric::NORMAL,
east_asian: FontVariantEastAsian::NORMAL,
feature_settings: FontFeatureSettings::normal(),
position: FontVariantPosition::Normal,
}
}
}
impl From<&FontAndScriptInfo> for ShapingOptions {
fn from(info: &FontAndScriptInfo) -> Self {
let mut ligatures = info.ligatures;
let mut flags = ShapingFlags::empty();
if info.bidi_level.is_rtl() {
flags.insert(ShapingFlags::RTL_FLAG);
}
let letter_spacing = info
.letter_spacing
.filter(|_| !is_cursive_script(info.script));
if letter_spacing.is_some() {
ligatures = FontVariantLigatures::NONE;
};
if info.text_rendering == TextRendering::Optimizespeed {
ligatures = FontVariantLigatures::NONE;
flags.insert(ShapingFlags::DISABLE_KERNING_SHAPING_FLAG)
}
if info.kerning == FontKerning::None {
flags.insert(ShapingFlags::DISABLE_KERNING_SHAPING_FLAG);
}
Self {
letter_spacing,
word_spacing: info.word_spacing,
script: info.script,
language: info.language,
ligatures,
numeric: info.numeric,
east_asian: info.east_asian,
feature_settings: info.feature_settings.clone(),
position: info.position,
flags,
}
}
}
#[derive(Clone, Debug, MallocSizeOf)]
pub(crate) struct TextRunSegment {
#[conditional_malloc_size_of]
pub info: Arc<FontAndScriptInfo>,
pub byte_range: Range<usize>,
pub character_range: Range<usize>,
pub break_at_start: bool,
#[conditional_malloc_size_of]
pub runs: Vec<Arc<ShapedTextSlice>>,
}
impl TextRunSegment {
fn new(
info: Arc<FontAndScriptInfo>,
byte_range: Range<usize>,
character_range: Range<usize>,
) -> Self {
Self {
info,
byte_range,
character_range,
runs: Vec::new(),
break_at_start: false,
}
}
fn is_compatible(
&self,
new_font: &Option<FontRef>,
new_script: Script,
new_bidi_level: Level,
) -> bool {
if self.info.bidi_level != new_bidi_level {
return false;
}
if new_font
.as_ref()
.is_some_and(|new_font| !Arc::ptr_eq(&self.info.font, new_font))
{
return false;
}
!script_is_specific(self.info.script) ||
!script_is_specific(new_script) ||
self.info.script == new_script
}
fn update(&mut self, next_byte_index: usize, next_character_index: usize, new_script: Script) {
if !script_is_specific(self.info.script) && script_is_specific(new_script) {
self.info = Arc::new(FontAndScriptInfo {
script: new_script,
..(*self.info).clone()
});
}
self.character_range.end = next_character_index;
self.byte_range.end = next_byte_index;
}
fn layout_into_line_items(
&self,
text_run: &TextRun,
mut soft_wrap_policy: SegmentStartSoftWrapPolicy,
ifc: &mut InlineFormattingContextLayout,
) {
if self.break_at_start && soft_wrap_policy == SegmentStartSoftWrapPolicy::FollowLinebreaker
{
soft_wrap_policy = SegmentStartSoftWrapPolicy::Force;
}
let mut character_range_start = self.character_range.start;
for (run_index, run) in self.runs.iter().enumerate() {
let new_character_range_end = character_range_start + run.character_count();
let offsets = ifc
.ifc
.shared_selection
.clone()
.map(|shared_selection| TextRunOffsets {
shared_selection,
character_range: character_range_start..new_character_range_end,
});
if run_index != 0 || soft_wrap_policy == SegmentStartSoftWrapPolicy::Force {
ifc.process_soft_wrap_opportunity();
}
ifc.push_glyph_store_to_unbreakable_segment(run.clone(), text_run, &self.info, offsets);
character_range_start = new_character_range_end;
}
}
fn shape_text(
&mut self,
parent_style: &ComputedValues,
formatting_context_text: &str,
linebreaker: &mut LineBreaker,
old_text_run_item: Option<TextRunItem>,
) {
let range = self.byte_range.clone();
let linebreaks = linebreaker.advance_to_linebreaks_in_range(self.byte_range.clone());
let linebreak_iter = linebreaks.iter().chain(std::iter::once(&range.end));
let options: ShapingOptions = (&*self.info).into();
let shaped_text = old_text_run_item
.and_then(|old_text_run_item| {
let TextRunItem::TextSegment(old_text_segment) = old_text_run_item else {
return None;
};
if !self.is_compatible_with_old_shaping_result(&old_text_segment) {
return None;
}
Some(old_text_segment.runs.first()?.shaped_text())
})
.unwrap_or_else(|| {
self.info
.font
.shape_text(&formatting_context_text[range.clone()], &options)
});
let mut shaped_text_slicer = ShapedTextSlicer::new(shaped_text);
self.runs.clear();
self.runs.reserve(linebreaks.len());
self.break_at_start = false;
let text_style = parent_style.get_inherited_text().clone();
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 = self.byte_range.start..self.byte_range.start;
for break_index in linebreak_iter {
if *break_index == self.byte_range.start {
self.break_at_start = true;
continue;
}
let mut slice = last_slice.end..*break_index;
let word = &formatting_context_text[slice.clone()];
let mut whitespace = slice.end..slice.end;
let rev_char_indices = word.char_indices().rev().peekable();
let mut non_whitespace_slice_ends_with_whitespace = false;
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();
non_whitespace_slice_ends_with_whitespace = true;
}
slice.end = whitespace.start;
}
if !ends_with_whitespace &&
*break_index != self.byte_range.end &&
text_style.word_break == WordBreak::KeepAll &&
!can_break_anywhere
{
continue;
}
last_slice = slice.start..*break_index;
if !slice.is_empty() {
let character_count = formatting_context_text[slice].chars().count();
self.runs.push(shaped_text_slicer.slice_for_character_count(
character_count,
false,
non_whitespace_slice_ends_with_whitespace,
));
}
if whitespace.is_empty() {
continue;
}
if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces {
for _ in formatting_context_text[whitespace].chars() {
self.runs.push(shaped_text_slicer.slice_for_character_count(
1, true,
true,
));
}
continue;
}
let character_count = formatting_context_text[whitespace].chars().count();
self.runs.push(shaped_text_slicer.slice_for_character_count(
character_count,
true,
true,
));
}
}
fn is_compatible_with_old_shaping_result(&self, old_segment: &Self) -> bool {
*old_segment.info == *self.info && self.byte_range == old_segment.byte_range
}
}
#[derive(Debug, MallocSizeOf)]
pub(crate) enum TextRunItem {
LineBreak { character_index: usize },
Tab { bidi_level: Level },
TextSegment(Box<TextRunSegment>),
}
#[derive(Debug, MallocSizeOf)]
pub(crate) struct TextRun {
pub base_fragment_info: BaseFragmentInfo,
pub parent_box: Option<WeakLayoutBox>,
pub inline_styles: SharedInlineStyles,
pub text_range: Range<usize>,
pub character_range: Range<usize>,
pub items: Vec<TextRunItem>,
}
impl TextRun {
pub(crate) fn new(
base_fragment_info: BaseFragmentInfo,
inline_styles: SharedInlineStyles,
text_range: Range<usize>,
character_range: Range<usize>,
old_text_run: Option<ArcRefCell<TextRun>>,
) -> Self {
let items = old_text_run
.map(|old_text_run| std::mem::take(&mut old_text_run.borrow_mut().items))
.unwrap_or_default();
Self {
base_fragment_info,
parent_box: None,
inline_styles,
text_range,
character_range,
items,
}
}
pub(super) fn segment_and_shape(
&mut self,
formatting_context_text: &str,
layout_context: &LayoutContext,
linebreaker: &mut LineBreaker,
bidi_info: &BidiInfo,
) {
let parent_style = self.inline_styles.style.borrow().clone();
let items = self.segment_text_by_font(
layout_context,
formatting_context_text,
bidi_info,
&parent_style,
);
let mut old_text_run_items = std::mem::replace(&mut self.items, items).into_iter();
for item in self.items.iter_mut() {
let old_text_run_item = old_text_run_items.next();
if let TextRunItem::TextSegment(text_segment) = item {
text_segment.shape_text(
&parent_style,
formatting_context_text,
linebreaker,
old_text_run_item,
);
}
}
}
fn segment_text_by_font(
&mut self,
layout_context: &LayoutContext,
formatting_context_text: &str,
bidi_info: &BidiInfo,
parent_style: &ServoArc<ComputedValues>,
) -> Vec<TextRunItem> {
let font_style = parent_style.clone_font();
let language = font_style._x_lang.0.parse().unwrap_or(Language::UND);
let font_size = font_style.font_size.computed_size().into();
let kerning = font_style.font_kerning;
let ligatures = font_style.font_variant_ligatures;
let numeric = font_style.font_variant_numeric;
let east_asian = font_style.font_variant_east_asian;
let feature_settings = font_style.font_feature_settings.clone();
let position = font_style.font_variant_position;
let font_group = layout_context.font_context.font_group(font_style);
let inherited_text_style = parent_style.get_inherited_text();
let word_spacing = Some(inherited_text_style.word_spacing.to_used_value(font_size));
let letter_spacing = inherited_text_style
.letter_spacing
.0
.to_used_value(font_size);
let letter_spacing = if !letter_spacing.is_zero() {
Some(letter_spacing)
} else {
None
};
let text_rendering = inherited_text_style.text_rendering;
let mut current: Option<TextRunSegment> = None;
let mut results = Vec::new();
let finish_current_segment =
|current: &mut Option<TextRunSegment>, results: &mut Vec<TextRunItem>| {
if let Some(current) = current.take() {
results.push(TextRunItem::TextSegment(Box::new(current)));
}
};
let text_run_text = &formatting_context_text[self.text_range.clone()];
let char_iterator = TwoCharsAtATimeIterator::new(text_run_text.chars());
let mut next_byte_index = self.text_range.start;
for (relative_character_index, (character, next_character)) in char_iterator.enumerate() {
let current_character_index = self.character_range.start + relative_character_index;
let current_byte_index = next_byte_index;
next_byte_index += character.len_utf8();
if character == '\n' {
finish_current_segment(&mut current, &mut results);
results.push(TextRunItem::LineBreak {
character_index: current_character_index,
});
continue;
}
if character == '\t' {
finish_current_segment(&mut current, &mut results);
results.push(TextRunItem::Tab {
bidi_level: bidi_info.levels[current_byte_index],
});
continue;
}
let (font, script, bidi_level) = if character_cannot_change_font(character) {
(None, Script::Common, bidi_info.levels[current_byte_index])
} else {
(
font_group.find_by_codepoint(
&layout_context.font_context,
character,
next_character,
language,
),
Script::from(character),
bidi_info.levels[current_byte_index],
)
};
if let Some(current) = current.as_mut() &&
current.is_compatible(&font, script, bidi_level)
{
current.update(next_byte_index, current_character_index + 1, script);
continue;
}
let Some(font) = font.or_else(|| font_group.first(&layout_context.font_context)) else {
continue;
};
let info = FontAndScriptInfo {
font,
script,
bidi_level,
language,
word_spacing,
letter_spacing,
text_rendering,
kerning,
ligatures,
numeric,
east_asian,
feature_settings: feature_settings.clone(),
position,
};
finish_current_segment(&mut current, &mut results);
assert!(current.is_none());
current = Some(TextRunSegment::new(
Arc::new(info),
current_byte_index..next_byte_index,
current_character_index..current_character_index + 1,
));
}
finish_current_segment(&mut current, &mut results);
results
}
pub(super) fn layout_into_line_items(&self, ifc: &mut InlineFormattingContextLayout) {
if self.text_range.is_empty() {
return;
}
let have_deferred_soft_wrap_opportunity =
mem::replace(&mut ifc.have_deferred_soft_wrap_opportunity, false);
let mut soft_wrap_policy = match have_deferred_soft_wrap_opportunity {
true => SegmentStartSoftWrapPolicy::Force,
false => SegmentStartSoftWrapPolicy::FollowLinebreaker,
};
for item in self.items.iter() {
ifc.possibly_flush_deferred_forced_line_break();
match item {
TextRunItem::LineBreak { character_index } => {
ifc.defer_forced_line_break_at_character_offset(*character_index);
},
TextRunItem::Tab { bidi_level } => self.process_preserved_tab(ifc, *bidi_level),
TextRunItem::TextSegment(segment) => {
segment.layout_into_line_items(self, soft_wrap_policy, ifc)
},
}
soft_wrap_policy = SegmentStartSoftWrapPolicy::FollowLinebreaker;
}
}
fn process_preserved_tab(
&self,
ifc_layout: &mut InlineFormattingContextLayout,
bidi_level: Level,
) {
let advance = ifc_layout
.ifc
.next_tab_stop_after_inline_advance(ifc_layout.potential_line_size().inline);
if advance.is_zero() {
return;
}
ifc_layout.update_unbreakable_segment_for_new_content(
&LineBlockSizes::zero(),
advance,
SegmentContentFlags::empty(),
);
ifc_layout.push_line_item_to_unbreakable_segment(LineItem::Tab {
inline_box_identifier: ifc_layout.current_inline_box_identifier(),
advance,
bidi_level,
});
if ifc_layout
.current_inline_container_state()
.style
.get_inherited_text()
.white_space_collapse ==
WhiteSpaceCollapse::BreakSpaces
{
ifc_layout.process_soft_wrap_opportunity();
}
}
}
fn is_cursive_script(script: Script) -> bool {
matches!(
script,
Script::Arabic |
Script::Hanifi_Rohingya |
Script::Mandaic |
Script::Mongolian |
Script::Nko |
Script::Phags_Pa |
Script::Syriac
)
}
fn character_cannot_change_font(character: char) -> bool {
if character.is_control() {
return true;
}
if character == '\u{00A0}' {
return true;
}
if is_bidi_control(character) {
return false;
}
matches!(
icu_properties::maps::line_break().get(character),
LineBreak::CombiningMark |
LineBreak::Glue |
LineBreak::ZWSpace |
LineBreak::WordJoiner |
LineBreak::ZWJ
)
}
pub(super) fn get_font_for_first_font_for_style(
style: &ComputedValues,
font_context: &FontContext,
) -> Option<FontRef> {
let font = font_context
.font_group(style.clone_font())
.first(font_context);
if font.is_none() {
warn!("Could not find font for style: {:?}", style.clone_font());
}
font
}
pub(crate) struct TwoCharsAtATimeIterator<InputIterator> {
iterator: InputIterator,
next_character: Option<char>,
}
impl<InputIterator> TwoCharsAtATimeIterator<InputIterator> {
fn new(iterator: InputIterator) -> Self {
Self {
iterator,
next_character: None,
}
}
}
impl<InputIterator> Iterator for TwoCharsAtATimeIterator<InputIterator>
where
InputIterator: Iterator<Item = char>,
{
type Item = (char, Option<char>);
fn next(&mut self) -> Option<Self::Item> {
if self.next_character.is_none() {
self.next_character = self.iterator.next();
}
let character = self.next_character?;
self.next_character = self.iterator.next();
Some((character, self.next_character))
}
}
fn script_is_specific(script: Script) -> bool {
script != Script::Common && script != Script::Inherited
}