mirror of
https://github.com/servo/servo
synced 2026-04-25 17:15:48 +02:00
666 lines
25 KiB
Rust
666 lines
25 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||
|
||
use std::mem;
|
||
use std::ops::Range;
|
||
use std::sync::Arc;
|
||
|
||
use app_units::Au;
|
||
use fonts::{FontContext, FontRef, GlyphStore, 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::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::OverflowWrap;
|
||
use unicode_bidi::{BidiInfo, Level};
|
||
use unicode_script::Script;
|
||
|
||
use super::line_breaker::LineBreaker;
|
||
use super::{InlineFormattingContextLayout, SharedInlineStyles};
|
||
use crate::context::LayoutContext;
|
||
use crate::dom::WeakLayoutBox;
|
||
use crate::flow::inline::line::TextRunOffsets;
|
||
use crate::fragment_tree::BaseFragmentInfo;
|
||
|
||
// There are two reasons why we might want to break at the start:
|
||
//
|
||
// 1. The line breaker told us that a break was necessary between two separate
|
||
// instances of sending text to it.
|
||
// 2. We are following replaced content ie `have_deferred_soft_wrap_opportunity`.
|
||
//
|
||
// In both cases, we don't want to do this if the first character prevents a
|
||
// soft wrap opportunity.
|
||
#[derive(PartialEq)]
|
||
enum SegmentStartSoftWrapPolicy {
|
||
Force,
|
||
FollowLinebreaker,
|
||
}
|
||
|
||
/// A data structure which contains information used when shaping a [`TextRunSegment`].
|
||
#[derive(Clone, Debug, MallocSizeOf)]
|
||
pub(crate) struct FontAndScriptInfo {
|
||
/// The font used when shaping a [`TextRunSegment`].
|
||
pub font: FontRef,
|
||
/// The script used when shaping a [`TextRunSegment`].
|
||
pub script: Script,
|
||
/// The BiDi [`Level`] used when shaping a [`TextRunSegment`].
|
||
pub bidi_level: Level,
|
||
/// The [`Language`] used when shaping a [`TextRunSegment`].
|
||
pub language: Language,
|
||
/// Spacing to add between each letter. Corresponds to the CSS 2.1 `letter-spacing` property.
|
||
/// NB: You will probably want to set the `IGNORE_LIGATURES_SHAPING_FLAG` if this is non-null.
|
||
///
|
||
/// Letter spacing is not applied to all characters. Use [Self::letter_spacing_for_character] to
|
||
/// determine the amount of spacing to apply.
|
||
pub letter_spacing: Option<Au>,
|
||
/// Spacing to add between each word. Corresponds to the CSS 2.1 `word-spacing` property.
|
||
pub word_spacing: Option<Au>,
|
||
/// The [`TextRendering`] value from the original style.
|
||
pub text_rendering: TextRendering,
|
||
}
|
||
|
||
impl FontAndScriptInfo {
|
||
/// Creates a minimal [`FontAndScriptInfo`] for a single font, with generic language settings
|
||
/// and the default shaping configuration. This is only used to generate placeholders for
|
||
/// text carets on otherwise empty lines.
|
||
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,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<&FontAndScriptInfo> for ShapingOptions {
|
||
fn from(info: &FontAndScriptInfo) -> Self {
|
||
let mut flags = ShapingFlags::empty();
|
||
if info.bidi_level.is_rtl() {
|
||
flags.insert(ShapingFlags::RTL_FLAG);
|
||
}
|
||
|
||
// From https://www.w3.org/TR/css-text-3/#cursive-script:
|
||
// Cursive scripts do not admit gaps between their letters for either
|
||
// justification or letter-spacing.
|
||
let letter_spacing = info
|
||
.letter_spacing
|
||
.filter(|_| !is_cursive_script(info.script));
|
||
if letter_spacing.is_some() {
|
||
flags.insert(ShapingFlags::IGNORE_LIGATURES_SHAPING_FLAG);
|
||
};
|
||
if info.text_rendering == TextRendering::Optimizespeed {
|
||
flags.insert(ShapingFlags::IGNORE_LIGATURES_SHAPING_FLAG);
|
||
flags.insert(ShapingFlags::DISABLE_KERNING_SHAPING_FLAG)
|
||
}
|
||
Self {
|
||
letter_spacing,
|
||
word_spacing: info.word_spacing,
|
||
script: info.script,
|
||
language: info.language,
|
||
flags,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, MallocSizeOf)]
|
||
pub(crate) struct TextRunSegment {
|
||
/// Information about the font and language used in this text run. This is produced by
|
||
/// segmenting the inline formatting context's text content by font, script, and bidi level.
|
||
#[conditional_malloc_size_of]
|
||
pub info: Arc<FontAndScriptInfo>,
|
||
|
||
/// The range of bytes in the parent [`super::InlineFormattingContext`]'s text content.
|
||
pub byte_range: Range<usize>,
|
||
|
||
/// The range of characters in the parent [`super::InlineFormattingContext`]'s text content.
|
||
pub character_range: Range<usize>,
|
||
|
||
/// Whether or not the linebreaker said that we should allow a line break at the start of this
|
||
/// segment.
|
||
pub break_at_start: bool,
|
||
|
||
/// The shaped runs within this segment.
|
||
#[conditional_malloc_size_of]
|
||
pub runs: Vec<Arc<GlyphStore>>,
|
||
}
|
||
|
||
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,
|
||
}
|
||
}
|
||
|
||
/// Returns true if the new `Font`, `Script` and BiDi `Level` are compatible with this segment
|
||
/// or false otherwise.
|
||
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
|
||
}
|
||
|
||
/// Update this segment to end at the given byte and character index. The update will only ever
|
||
/// make the Script specific and will not change it otherwise.
|
||
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() {
|
||
ifc.possibly_flush_deferred_forced_line_break();
|
||
|
||
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,
|
||
});
|
||
|
||
// Break before each unbreakable run in this TextRun, except the first unless the
|
||
// linebreaker was set to break before the first run.
|
||
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_and_push_range(
|
||
&mut self,
|
||
range: &Range<usize>,
|
||
formatting_context_text: &str,
|
||
options: &ShapingOptions,
|
||
) {
|
||
self.runs.push(
|
||
self.info
|
||
.font
|
||
.shape_text(&formatting_context_text[range.clone()], options),
|
||
);
|
||
}
|
||
|
||
/// Shape the text of this [`TextRunSegment`], first finding "words" for the shaper by processing
|
||
/// the linebreaks found in the owning [`super::InlineFormattingContext`]. Linebreaks are filtered,
|
||
/// based on the style of the parent inline box.
|
||
fn shape_text(
|
||
&mut self,
|
||
parent_style: &ComputedValues,
|
||
formatting_context_text: &str,
|
||
linebreaker: &mut LineBreaker,
|
||
) {
|
||
let options: ShapingOptions = (&*self.info).into();
|
||
|
||
// Gather the linebreaks that apply to this segment from the inline formatting context's collection
|
||
// of line breaks. Also add a simulated break at the end of the segment in order to ensure the final
|
||
// piece of text is processed.
|
||
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));
|
||
|
||
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 {
|
||
let mut options = options;
|
||
if *break_index == self.byte_range.start {
|
||
self.break_at_start = true;
|
||
continue;
|
||
}
|
||
|
||
// Extend the slice to the next UAX#14 line break opportunity.
|
||
let mut slice = last_slice.end..*break_index;
|
||
let word = &formatting_context_text[slice.clone()];
|
||
|
||
// Split off any trailing whitespace into a separate glyph run.
|
||
let mut whitespace = slice.end..slice.end;
|
||
let rev_char_indices = word.char_indices().rev().peekable();
|
||
|
||
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 line breaking for a piece of text that has `white-space-collapse:
|
||
// break-spaces` there is a line break opportunity *after* every preserved space,
|
||
// but not before. This means that we should not split off the first whitespace.
|
||
//
|
||
// An exception to this is if the style tells us that we can break in the middle of words.
|
||
if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces &&
|
||
!can_break_anywhere
|
||
{
|
||
whitespace.start += first_white_space_character.len_utf8();
|
||
options
|
||
.flags
|
||
.insert(ShapingFlags::ENDS_WITH_WHITESPACE_SHAPING_FLAG);
|
||
}
|
||
|
||
slice.end = whitespace.start;
|
||
}
|
||
|
||
// If there's no whitespace and `word-break` is set to `keep-all`, try increasing the slice.
|
||
// TODO: This should only happen for CJK text.
|
||
if !ends_with_whitespace &&
|
||
*break_index != self.byte_range.end &&
|
||
text_style.word_break == WordBreak::KeepAll &&
|
||
!can_break_anywhere
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// Only advance the last slice if we are not going to try to expand the slice.
|
||
last_slice = slice.start..*break_index;
|
||
|
||
// Push the non-whitespace part of the range.
|
||
if !slice.is_empty() {
|
||
self.shape_and_push_range(&slice, formatting_context_text, &options);
|
||
}
|
||
|
||
if whitespace.is_empty() {
|
||
continue;
|
||
}
|
||
|
||
options.flags.insert(
|
||
ShapingFlags::IS_WHITESPACE_SHAPING_FLAG |
|
||
ShapingFlags::ENDS_WITH_WHITESPACE_SHAPING_FLAG,
|
||
);
|
||
|
||
// If `white-space-collapse: break-spaces` is active, insert a line breaking opportunity
|
||
// between each white space character in the white space that we trimmed off.
|
||
if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces {
|
||
let start_index = whitespace.start;
|
||
for (index, character) in formatting_context_text[whitespace].char_indices() {
|
||
let index = start_index + index;
|
||
self.shape_and_push_range(
|
||
&(index..index + character.len_utf8()),
|
||
formatting_context_text,
|
||
&options,
|
||
);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
self.shape_and_push_range(&whitespace, formatting_context_text, &options);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A single item in a [`TextRun`].
|
||
#[derive(Debug, MallocSizeOf)]
|
||
pub(crate) enum TextRunItem {
|
||
/// A hard line break i.e. a "\n" as other types line breaks are normalized to "\n".
|
||
LineBreak { character_index: usize },
|
||
/// Any other text for which a font can be matched. We store a `Box` here as [`TextRunSegment`]
|
||
/// is quite a bit larger than the other enum variants.
|
||
TextSegment(Box<TextRunSegment>),
|
||
}
|
||
|
||
/// A single [`TextRun`] for the box tree. These are all descendants of
|
||
/// [`super::InlineBox`] or the root of the [`super::InlineFormattingContext`]. During
|
||
/// box tree construction, text is split into [`TextRun`]s based on their font, script,
|
||
/// etc. When these are created text is already shaped.
|
||
///
|
||
/// <https://www.w3.org/TR/css-display-3/#css-text-run>
|
||
#[derive(Debug, MallocSizeOf)]
|
||
pub(crate) struct TextRun {
|
||
/// The [`BaseFragmentInfo`] for this [`TextRun`]. Usually this comes from the
|
||
/// original text node in the DOM for the text.
|
||
pub base_fragment_info: BaseFragmentInfo,
|
||
|
||
/// A weak reference to the parent of this layout box. This becomes valid as soon
|
||
/// as the *parent* of this box is added to the tree.
|
||
pub parent_box: Option<WeakLayoutBox>,
|
||
|
||
/// The [`crate::SharedStyle`] from this [`TextRun`]s parent element. This is
|
||
/// shared so that incremental layout can simply update the parent element and
|
||
/// this [`TextRun`] will be updated automatically.
|
||
pub inline_styles: SharedInlineStyles,
|
||
|
||
/// The range of text in [`super::InlineFormattingContext::text_content`] of the
|
||
/// [`super::InlineFormattingContext`] that owns this [`TextRun`]. These are UTF-8 offsets.
|
||
pub text_range: Range<usize>,
|
||
|
||
/// The range of characters in this text in [`super::InlineFormattingContext::text_content`]
|
||
/// of the [`super::InlineFormattingContext`] that owns this [`TextRun`]. These are *not*
|
||
/// UTF-8 offsets.
|
||
pub character_range: Range<usize>,
|
||
|
||
/// The [`TextRunItem`]s of this text run. This is produced by segmenting the incoming text
|
||
/// by things such as font and script as well as separating out hard line breaks.
|
||
/// segments, and shaped.
|
||
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>,
|
||
) -> Self {
|
||
Self {
|
||
base_fragment_info,
|
||
parent_box: None,
|
||
inline_styles,
|
||
text_range,
|
||
character_range,
|
||
items: Vec::new(),
|
||
}
|
||
}
|
||
|
||
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();
|
||
self.items = self.segment_text_by_font(
|
||
layout_context,
|
||
formatting_context_text,
|
||
bidi_info,
|
||
&parent_style,
|
||
);
|
||
for item in self.items.iter_mut() {
|
||
if let TextRunItem::TextSegment(text_segment) = item {
|
||
text_segment.shape_text(&parent_style, formatting_context_text, linebreaker)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Take the [`TextRun`]'s text and turn it into [`TextRunSegment`]s. Each segment has a matched
|
||
/// font and script. Fonts may differ when glyphs are found in fallback fonts.
|
||
/// [`super::InlineFormattingContext`].
|
||
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 font_group = layout_context.font_context.font_group(font_style);
|
||
let inherited_text_style = parent_style.get_inherited_text().clone();
|
||
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());
|
||
// The next bytes index of the character within the entire inline formatting context's text.
|
||
let mut next_byte_index = self.text_range.start;
|
||
for (relative_character_index, (character, next_character)) in char_iterator.enumerate() {
|
||
// The current character index within the entire inline formatting context's text.
|
||
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;
|
||
}
|
||
|
||
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 the existing segment is compatible with the character, just merge the character into it.
|
||
if let Some(current) = current.as_mut() {
|
||
if 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,
|
||
};
|
||
|
||
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;
|
||
}
|
||
|
||
// If we are following replaced content, we should have a soft wrap opportunity, unless the
|
||
// first character of this `TextRun` prevents that soft wrap opportunity. If we see such a
|
||
// character it should also override the LineBreaker's indication to break at the start.
|
||
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() {
|
||
match item {
|
||
// If this whitespace forces a line break, queue up a hard line break the next time we
|
||
// see any content. We don't line break immediately, because we'd like to finish processing
|
||
// any ongoing inline boxes before ending the line.
|
||
TextRunItem::LineBreak { character_index } => {
|
||
ifc.possibly_flush_deferred_forced_line_break();
|
||
ifc.defer_forced_line_break_at_character_offset(*character_index);
|
||
},
|
||
TextRunItem::TextSegment(segment) => {
|
||
segment.layout_into_line_items(self, soft_wrap_policy, ifc)
|
||
},
|
||
}
|
||
soft_wrap_policy = SegmentStartSoftWrapPolicy::FollowLinebreaker;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// From <https://www.w3.org/TR/css-text-3/#cursive-script>:
|
||
/// Cursive scripts do not admit gaps between their letters for either justification
|
||
/// or letter-spacing. The following Unicode scripts are included: Arabic, Hanifi
|
||
/// Rohingya, Mandaic, Mongolian, N’Ko, Phags Pa, Syriac
|
||
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
|
||
)
|
||
}
|
||
|
||
/// Whether or not this character should be able to change the font during segmentation. Certain
|
||
/// character are not rendered at all, so it doesn't matter what font we use to render them. They
|
||
/// should just be added to the current segment.
|
||
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> {
|
||
/// The input character iterator.
|
||
iterator: InputIterator,
|
||
/// The first character to produce in the next run of the iterator.
|
||
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 the iterator isn't initialized do that now.
|
||
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
|
||
}
|