diff --git a/components/gfx/font.rs b/components/gfx/font.rs index 77e27158826..5bce57528f5 100644 --- a/components/gfx/font.rs +++ b/components/gfx/font.rs @@ -106,7 +106,7 @@ pub trait FontTableMethods { fn buffer(&self) -> &[u8]; } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct FontMetrics { pub underline_size: Au, pub underline_offset: Au, diff --git a/components/layout_2020/flexbox/construct.rs b/components/layout_2020/flexbox/construct.rs index 22215651395..55e94b35655 100644 --- a/components/layout_2020/flexbox/construct.rs +++ b/components/layout_2020/flexbox/construct.rs @@ -148,12 +148,13 @@ where .into_par_iter() .map(|job| match job { FlexLevelJob::TextRuns(runs) => ArcRefCell::new(FlexLevelBox::FlexItem({ - let runs = runs.into_iter().map(|run| crate::flow::text_run::TextRun { - base_fragment_info: (&run.info).into(), - text: run.text.into(), - parent_style: run.info.style, - has_uncollapsible_content: false, - shaped_text: None, + let runs = runs.into_iter().map(|run| { + crate::flow::text_run::TextRun::new( + (&run.info).into(), + run.info.style, + run.text.into(), + false, /* has_uncollapsible_content */ + ) }); let bfc = BlockFormattingContext::construct_for_text_runs( runs, diff --git a/components/layout_2020/flow/construct.rs b/components/layout_2020/flow/construct.rs index 3155ff6e878..fc49b8beef7 100644 --- a/components/layout_2020/flow/construct.rs +++ b/components/layout_2020/flow/construct.rs @@ -67,6 +67,7 @@ impl BlockFormattingContext { let ifc = InlineFormattingContext { inline_level_boxes, + font_metrics: Vec::new(), text_decoration_line, has_first_formatted_line: true, contains_floats: false, @@ -433,13 +434,12 @@ where _ => {}, } - inlines.push(ArcRefCell::new(InlineLevelBox::TextRun(TextRun { - base_fragment_info: info.into(), - parent_style: Arc::clone(&info.style), - text: output, + inlines.push(ArcRefCell::new(InlineLevelBox::TextRun(TextRun::new( + info.into(), + Arc::clone(&info.style), + output, has_uncollapsible_content, - shaped_text: None, - }))); + )))); } } @@ -606,6 +606,7 @@ where is_first_fragment: true, is_last_fragment: false, children: vec![], + default_font_index: None, }); if is_list_item { @@ -670,6 +671,7 @@ where // are obviously not the last fragment. is_last_fragment: false, children: std::mem::take(&mut ongoing.children), + default_font_index: None, }; ongoing.is_first_fragment = false; fragmented diff --git a/components/layout_2020/flow/inline.rs b/components/layout_2020/flow/inline.rs index 1873843f58f..ca4c796281f 100644 --- a/components/layout_2020/flow/inline.rs +++ b/components/layout_2020/flow/inline.rs @@ -8,7 +8,6 @@ use std::mem; use app_units::Au; use gfx::font::FontMetrics; use gfx::text::glyph::GlyphStore; -use log::warn; use serde::Serialize; use servo_arc::Arc; use style::computed_values::white_space::T as WhiteSpace; @@ -28,7 +27,7 @@ use super::line::{ layout_line_items, AbsolutelyPositionedLineItem, AtomicLineItem, FloatLineItem, InlineBoxLineItem, LineItem, LineItemLayoutState, LineMetrics, TextRunLineItem, }; -use super::text_run::TextRun; +use super::text_run::{add_or_get_font, get_font_for_first_font_for_style, TextRun}; use super::CollapsibleWithParentStartMargin; use crate::cell::ArcRefCell; use crate::context::LayoutContext; @@ -52,11 +51,20 @@ static FONT_SUPERSCRIPT_OFFSET_RATIO: f32 = 0.34; #[derive(Debug, Serialize)] pub(crate) struct InlineFormattingContext { pub(super) inline_level_boxes: Vec>, + + /// A store of font information for all the shaped segments in this formatting + /// context in order to avoid duplicating this information. + pub font_metrics: Vec, + pub(super) text_decoration_line: TextDecorationLine, - // Whether this IFC contains the 1st formatted line of an element - // https://www.w3.org/TR/css-pseudo-4/#first-formatted-line + + /// Whether this IFC contains the 1st formatted line of an element: + /// . pub(super) has_first_formatted_line: bool, + + /// Whether or not this [`InlineFormattingContext`] contains floats. pub(super) contains_floats: bool, + /// Whether this IFC being constructed currently ends with whitespace. This is used to /// implement rule 4 of : /// @@ -67,6 +75,14 @@ pub(crate) struct InlineFormattingContext { pub(super) ends_with_whitespace: bool, } +/// A collection of data used to cache [`FontMetrics`] in the [`InlineFormattingContext`] +#[derive(Debug, Serialize)] +pub(crate) struct FontKeyAndMetrics { + pub key: FontInstanceKey, + pub actual_pt_size: Au, + pub metrics: FontMetrics, +} + #[derive(Debug, Serialize)] pub(crate) enum InlineLevelBox { InlineBox(InlineBox), @@ -84,6 +100,9 @@ pub(crate) struct InlineBox { pub is_first_fragment: bool, pub is_last_fragment: bool, pub children: Vec>, + /// The index of the default font in the [`InlineFormattingContext`]'s font metrics store. + /// This is initialized during IFC shaping. + pub default_font_index: Option, } /// Information about the current line under construction for a particular @@ -508,6 +527,10 @@ pub(super) struct InlineFormattingContextState<'a, 'b> { sequential_layout_state: Option<&'a mut SequentialLayoutState>, layout_context: &'b LayoutContext<'b>, + /// The list of [`FontMetrics`] used by the [`InlineFormattingContext`] that + /// we are laying out. + fonts: &'a Vec, + /// The [`InlineContainerState`] for the container formed by the root of the /// [`InlineFormattingContext`]. This is effectively the "root inline box" described /// by : @@ -619,6 +642,9 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { self.layout_context, self.current_inline_container_state(), inline_box.is_last_fragment, + inline_box + .default_font_index + .map(|index| &self.fonts[index].metrics), ); if inline_box.is_first_fragment { @@ -1121,26 +1147,37 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { pub(super) fn push_glyph_store_to_unbreakable_segment( &mut self, glyph_store: std::sync::Arc, - base_fragment_info: BaseFragmentInfo, - parent_style: &Arc, - font_metrics: &FontMetrics, - font_key: FontInstanceKey, + text_run: &TextRun, + font_index: usize, ) { let inline_advance = Length::from(glyph_store.total_advance()); - let preserve_spaces = parent_style + let preserve_spaces = text_run + .parent_style .get_inherited_text() .white_space .preserve_spaces(); let is_collapsible_whitespace = glyph_store.is_whitespace() && !preserve_spaces; - // Normally, the strut is incorporated into the nested block size. In quirks mode though - // if we find any text that isn't collapsed whitespace, we need to incorporate the strut. - // TODO(mrobinson): This isn't quite right for situations where collapsible white space - // ultimately does not collapse because it is between two other pieces of content. - // TODO(mrobinson): When we have font fallback, this should be calculating the - // block sizes of the fallback font. + // If the metrics of this font don't match the default font, we are likely using a fallback + // font and need to adjust the line size to account for a potentially different font. + // If somehow the metrics match, the line size won't change. + let ifc_font_info = &self.fonts[font_index]; + let font_metrics = ifc_font_info.metrics.clone(); + let using_fallback_font = + self.current_inline_container_state().font_metrics != font_metrics; + let quirks_mode = self.layout_context.style_context.quirks_mode() != QuirksMode::NoQuirks; - let strut_size = if quirks_mode && !is_collapsible_whitespace { + let strut_size = if using_fallback_font { + // TODO(mrobinson): This value should probably be cached somewhere. + let container_state = self.current_inline_container_state(); + let mut block_size = container_state.get_block_size_contribution(&font_metrics); + block_size.adjust_for_baseline_offset(container_state.baseline_offset); + block_size + } else if quirks_mode && !is_collapsible_whitespace { + // Normally, the strut is incorporated into the nested block size. In quirks mode though + // if we find any text that isn't collapsed whitespace, we need to incorporate the strut. + // TODO(mrobinson): This isn't quite right for situations where collapsible white space + // ultimately does not collapse because it is between two other pieces of content. self.current_inline_container_state() .strut_block_sizes .clone() @@ -1154,9 +1191,8 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { ); match self.current_line_segment.line_items.last_mut() { - Some(LineItem::TextRun(text_run)) => { - debug_assert!(font_key == text_run.font_key); - text_run.text.push(glyph_store); + Some(LineItem::TextRun(line_item)) if ifc_font_info.key == line_item.font_key => { + line_item.text.push(glyph_store); return; }, _ => {}, @@ -1164,10 +1200,10 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { self.push_line_item_to_unbreakable_segment(LineItem::TextRun(TextRunLineItem { text: vec![glyph_store], - base_fragment_info, - parent_style: parent_style.clone(), - font_metrics: font_metrics.clone(), - font_key, + base_fragment_info: text_run.base_fragment_info, + parent_style: text_run.parent_style.clone(), + font_metrics, + font_key: ifc_font_info.key, text_decoration_line: self.current_inline_container_state().text_decoration_line, })); } @@ -1293,7 +1329,7 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { ( Some(LineItem::TextRun(last_line_item)), Some(LineItem::TextRun(first_segment_item)), - ) => { + ) if last_line_item.font_key == first_segment_item.font_key => { last_line_item.text.append(&mut first_segment_item.text); 1 }, @@ -1322,6 +1358,7 @@ impl InlineFormattingContext { ) -> InlineFormattingContext { InlineFormattingContext { inline_level_boxes: Default::default(), + font_metrics: Vec::new(), text_decoration_line, has_first_formatted_line, contains_floats: false, @@ -1423,11 +1460,21 @@ impl InlineFormattingContext { }; let style = containing_block.style; + + // It's unfortunate that it isn't possible to get this during IFC text processing, but in + // that situation the style of the containing block is unknown. + let default_font_metrics = + crate::context::with_thread_local_font_context(layout_context, |font_context| { + get_font_for_first_font_for_style(style, font_context) + .map(|font| font.borrow().metrics.clone()) + }); + let mut ifc = InlineFormattingContextState { positioning_context, containing_block, sequential_layout_state, layout_context, + fonts: &self.font_metrics, fragments: Vec::new(), current_line: LineUnderConstruction::new(LogicalVec2 { inline: first_line_inline_start, @@ -1435,9 +1482,9 @@ impl InlineFormattingContext { }), root_nesting_level: InlineContainerState::new( style.to_arc(), - layout_context, None, /* parent_container */ self.text_decoration_line, + default_font_metrics.as_ref(), inline_container_needs_strut(style, layout_context, None), ), inline_box_state_stack: Vec::new(), @@ -1530,26 +1577,41 @@ impl InlineFormattingContext { /// Break and shape text of this InlineFormattingContext's TextRun's, which requires doing /// all font matching and FontMetrics collection. pub(crate) fn break_and_shape_text(&mut self, layout_context: &LayoutContext) { - let mut linebreaker = None; - self.foreach(|iter_item| match iter_item { - InlineFormattingContextIterItem::Item(InlineLevelBox::TextRun(ref mut text_run)) => { - text_run.break_and_shape(layout_context, &mut linebreaker); - }, - _ => {}, + let mut ifc_fonts = Vec::new(); + crate::context::with_thread_local_font_context(layout_context, |font_context| { + let mut linebreaker = None; + self.foreach(|iter_item| match iter_item { + InlineFormattingContextIterItem::Item(InlineLevelBox::TextRun( + ref mut text_run, + )) => { + text_run.break_and_shape(font_context, &mut linebreaker, &mut ifc_fonts); + }, + InlineFormattingContextIterItem::Item(InlineLevelBox::InlineBox(inline_box)) => { + if let Some(font) = + get_font_for_first_font_for_style(&inline_box.style, font_context) + { + inline_box.default_font_index = + Some(add_or_get_font(&font, &mut ifc_fonts)); + } + }, + _ => {}, + }); }); + + self.font_metrics = ifc_fonts; } } impl InlineContainerState { fn new( style: Arc, - layout_context: &LayoutContext, parent_container: Option<&InlineContainerState>, parent_text_decoration_line: TextDecorationLine, + font_metrics: Option<&FontMetrics>, create_strut: bool, ) -> Self { let text_decoration_line = parent_text_decoration_line | style.clone_text_decoration_line(); - let font_metrics = font_metrics_from_style(layout_context, &style); + let font_metrics = font_metrics.cloned().unwrap_or_else(FontMetrics::empty); let line_height = line_height(&style, &font_metrics); let mut baseline_offset = Au::zero(); @@ -1712,6 +1774,7 @@ impl InlineBoxContainerState { layout_context: &LayoutContext, parent_container: &InlineContainerState, is_last_fragment: bool, + font_metrics: Option<&FontMetrics>, ) -> Self { let style = inline_box.style.clone(); let pbm = style.padding_border_margin(containing_block); @@ -1719,9 +1782,9 @@ impl InlineBoxContainerState { Self { base: InlineContainerState::new( style, - layout_context, Some(parent_container), parent_container.text_decoration_line, + font_metrics, create_strut, ), base_fragment_info: inline_box.base_fragment_info, @@ -1978,21 +2041,6 @@ fn line_height(parent_style: &ComputedValues, font_metrics: &FontMetrics) -> Len } } -fn font_metrics_from_style(layout_context: &LayoutContext, style: &ComputedValues) -> FontMetrics { - crate::context::with_thread_local_font_context(layout_context, |font_context| { - let font_group = font_context.font_group(style.clone_font()); - let font = match font_group.borrow_mut().first(font_context) { - Some(font) => font, - None => { - warn!("Could not find find for TextRun."); - return FontMetrics::empty(); - }, - }; - let font = font.borrow(); - font.metrics.clone() - }) -} - fn is_baseline_relative(vertical_align: GenericVerticalAlign) -> bool { match vertical_align { GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) | @@ -2099,41 +2147,42 @@ impl<'a> ContentSizesComputation<'a> { self.add_length(length); }, InlineFormattingContextIterItem::Item(InlineLevelBox::TextRun(text_run)) => { - let result = match text_run.shaped_text { - Some(ref result) => result, - None => return, - }; + for segment in text_run.shaped_text.iter() { + // TODO: This should take account whether or not the first and last character prevent + // linebreaks after atomics as in layout. + if segment.break_at_start { + self.line_break_opportunity() + } - if result.break_at_start { - self.line_break_opportunity() - } - for run in result.runs.iter() { - let advance = Length::from(run.glyph_store.total_advance()); + for run in segment.runs.iter() { + let advance = Length::from(run.glyph_store.total_advance()); - if !run.glyph_store.is_whitespace() { - self.had_non_whitespace_content_yet = true; - self.current_line.min_content += advance.into(); - self.current_line.max_content += (self.pending_whitespace + advance).into(); - self.pending_whitespace = Length::zero(); - } else { - // If this run is a forced line break, we *must* break the line - // and start measuring from the inline origin once more. - if text_run.glyph_run_is_whitespace_ending_with_preserved_newline(run) { + if !run.glyph_store.is_whitespace() { self.had_non_whitespace_content_yet = true; - self.forced_line_break(); - self.current_line = ContentSizes::zero(); - continue; - } + self.current_line.min_content += advance.into(); + self.current_line.max_content += + (self.pending_whitespace + advance).into(); + self.pending_whitespace = Length::zero(); + } else { + // If this run is a forced line break, we *must* break the line + // and start measuring from the inline origin once more. + if text_run.glyph_run_is_whitespace_ending_with_preserved_newline(run) { + self.had_non_whitespace_content_yet = true; + self.forced_line_break(); + self.current_line = ContentSizes::zero(); + continue; + } - // Discard any leading whitespace in the IFC. This will always be trimmed. - if !self.had_non_whitespace_content_yet { - continue; - } + // Discard any leading whitespace in the IFC. This will always be trimmed. + if !self.had_non_whitespace_content_yet { + continue; + } - // Wait to take into account other whitespace until we see more content. - // Whitespace at the end of the IFC will always be trimmed. - self.line_break_opportunity(); - self.pending_whitespace += advance; + // Wait to take into account other whitespace until we see more content. + // Whitespace at the end of the IFC will always be trimmed. + self.line_break_opportunity(); + self.pending_whitespace += advance; + } } } }, diff --git a/components/layout_2020/flow/text_run.rs b/components/layout_2020/flow/text_run.rs index 77bf83bf58d..415317cffe9 100644 --- a/components/layout_2020/flow/text_run.rs +++ b/components/layout_2020/flow/text_run.rs @@ -5,21 +5,29 @@ use std::mem; use app_units::Au; -use gfx::font::FontMetrics; +use gfx::font::{FontRef, ShapingFlags, ShapingOptions}; +use gfx::font_cache_thread::FontCacheThread; +use gfx::font_context::FontContext; use gfx::text::text_run::GlyphRun; +use gfx_traits::ByteIndex; +use log::warn; +use range::Range; use serde::Serialize; use servo_arc::Arc; +use style::computed_values::text_rendering::T as TextRendering; +use style::computed_values::word_break::T as WordBreak; use style::properties::ComputedValues; -use webrender_api::FontInstanceKey; +use unicode_script::Script; use xi_unicode::{linebreak_property, LineBreakLeafIter}; -use super::inline::InlineFormattingContextState; -use crate::context::LayoutContext; +use super::inline::{FontKeyAndMetrics, InlineFormattingContextState}; use crate::fragment_tree::BaseFragmentInfo; // These constants are the xi-unicode line breaking classes that are defined in // `table.rs`. Unfortunately, they are only identified by number. +const XI_LINE_BREAKING_CLASS_CM: u8 = 9; const XI_LINE_BREAKING_CLASS_GL: u8 = 12; +const XI_LINE_BREAKING_CLASS_ZW: u8 = 28; const XI_LINE_BREAKING_CLASS_WJ: u8 = 30; const XI_LINE_BREAKING_CLASS_ZWJ: u8 = 40; @@ -30,30 +38,163 @@ pub(crate) struct TextRun { #[serde(skip_serializing)] pub parent_style: Arc, pub text: String, + + /// The text of this [`TextRun`] with a font selected, broken into unbreakable + /// segments, and shaped. + pub shaped_text: Vec, + + /// Whether or not this [`TextRun`] has uncollapsible content. This is used + /// to determine if an [`super::InlineFormattingContext`] is considered empty or not. pub has_uncollapsible_content: bool, - pub shaped_text: Option, + + /// Whether or not to prevent a soft wrap opportunity at the start of this [`TextRun`]. + /// This depends on the whether the first character in the run prevents a soft wrap + /// opportunity. + prevent_soft_wrap_opportunity_at_start: bool, + + /// Whether or not to prevent a soft wrap opportunity at the end of this [`TextRun`]. + /// This depends on the whether the last character in the run prevents a soft wrap + /// opportunity. + prevent_soft_wrap_opportunity_at_end: bool, +} + +// 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, + Prevent, + FollowLinebreaker, } #[derive(Debug, Serialize)] -pub(crate) struct BreakAndShapeResult { - pub font_metrics: FontMetrics, - pub font_key: FontInstanceKey, - pub runs: Vec, +pub(crate) struct TextRunSegment { + /// The index of this font in the parent [`InlineFormattingContext`]'s collection of font + /// information. + pub font_index: usize, + + /// The [`Script`] of this segment. + #[serde(skip_serializing)] + pub script: Script, + + /// The range of bytes in the [`TextRun`]'s text that this segment covers. + pub range: Range, + + /// 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. + pub runs: Vec, +} + +impl TextRunSegment { + fn new(font_index: usize, script: Script, byte_index: ByteIndex) -> Self { + Self { + script, + font_index, + range: Range::new(byte_index, ByteIndex(0)), + runs: Vec::new(), + break_at_start: false, + } + } + + /// Update this segment if the Font and Script are compatible. The update will only + /// ever make the Script specific. Returns true if the new Font and Script are + /// compatible with this segment or false otherwise. + fn update_if_compatible( + &mut self, + font: &FontRef, + script: Script, + fonts: &[FontKeyAndMetrics], + ) -> bool { + fn is_specific(script: Script) -> bool { + script != Script::Common && script != Script::Inherited + } + + let current_font_key_and_metrics = &fonts[self.font_index]; + let new_font = font.borrow(); + if new_font.font_key != current_font_key_and_metrics.key || + new_font.actual_pt_size != current_font_key_and_metrics.actual_pt_size + { + return false; + } + + if !is_specific(self.script) && is_specific(script) { + self.script = script; + } + script == self.script || !is_specific(script) + } + + fn layout_into_line_items( + &self, + text_run: &TextRun, + mut soft_wrap_policy: SegmentStartSoftWrapPolicy, + ifc: &mut InlineFormattingContextState, + ) { + if self.break_at_start && soft_wrap_policy == SegmentStartSoftWrapPolicy::FollowLinebreaker + { + soft_wrap_policy = SegmentStartSoftWrapPolicy::Force; + } + + for (run_index, run) in self.runs.iter().enumerate() { + ifc.possibly_flush_deferred_forced_line_break(); + + // 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. + if text_run.glyph_run_is_whitespace_ending_with_preserved_newline(run) { + ifc.defer_forced_line_break(); + continue; + } + + // 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.glyph_store.clone(), + text_run, + self.font_index, + ); + } + } } impl TextRun { + pub(crate) fn new( + base_fragment_info: BaseFragmentInfo, + parent_style: Arc, + text: String, + has_uncollapsible_content: bool, + ) -> Self { + Self { + base_fragment_info, + parent_style, + text, + has_uncollapsible_content, + shaped_text: Vec::new(), + prevent_soft_wrap_opportunity_at_start: false, + prevent_soft_wrap_opportunity_at_end: false, + } + } + pub(super) fn break_and_shape( &mut self, - layout_context: &LayoutContext, + font_context: &mut FontContext, linebreaker: &mut Option, + font_cache: &mut Vec, ) { - use gfx::font::ShapingFlags; - use style::computed_values::text_rendering::T as TextRendering; - use style::computed_values::word_break::T as WordBreak; - - let font_style = self.parent_style.clone_font(); - let inherited_text_style = self.parent_style.get_inherited_text(); + let segment_results = self.segment_text(font_context, font_cache); + let inherited_text_style = self.parent_style.get_inherited_text().clone(); let letter_spacing = if inherited_text_style.letter_spacing.0.px() != 0. { Some(app_units::Au::from(inherited_text_style.letter_spacing.0)) } else { @@ -72,50 +213,145 @@ impl TextRun { flags.insert(ShapingFlags::KEEP_ALL_FLAG); } - self.shaped_text = - crate::context::with_thread_local_font_context(layout_context, |font_context| { - let font_group = font_context.font_group(font_style); - let font = match font_group.borrow_mut().first(font_context) { - Some(font) => font, - None => return Err("Could not find find for TextRun."), - }; + let specified_word_spacing = &inherited_text_style.word_spacing; + let style_word_spacing: Option = specified_word_spacing.to_length().map(|l| l.into()); + let segments = segment_results + .into_iter() + .map(|(mut segment, font)| { let mut font = font.borrow_mut(); - - let word_spacing = &inherited_text_style.word_spacing; - let word_spacing = - word_spacing - .to_length() - .map(|l| l.into()) - .unwrap_or_else(|| { - let space_width = font - .glyph_index(' ') - .map(|glyph_id| font.glyph_h_advance(glyph_id)) - .unwrap_or(gfx::font::LAST_RESORT_GLYPH_ADVANCE); - word_spacing.to_used_value(Au::from_f64_px(space_width)) - }); - - let shaping_options = gfx::font::ShapingOptions { + let word_spacing = style_word_spacing.unwrap_or_else(|| { + let space_width = font + .glyph_index(' ') + .map(|glyph_id| font.glyph_h_advance(glyph_id)) + .unwrap_or(gfx::font::LAST_RESORT_GLYPH_ADVANCE); + specified_word_spacing.to_used_value(Au::from_f64_px(space_width)) + }); + let shaping_options = ShapingOptions { letter_spacing, word_spacing, - script: unicode_script::Script::Common, + script: segment.script, flags, }; + (segment.runs, segment.break_at_start) = + gfx::text::text_run::TextRun::break_and_shape( + &mut font, + &self.text + [segment.range.begin().0 as usize..segment.range.end().0 as usize], + &shaping_options, + linebreaker, + ); - let (runs, break_at_start) = gfx::text::text_run::TextRun::break_and_shape( - &mut font, - &self.text, - &shaping_options, - linebreaker, - ); - - Ok(BreakAndShapeResult { - font_metrics: font.metrics.clone(), - font_key: font.font_key, - runs, - break_at_start, - }) + segment }) - .ok(); + .collect(); + + let _ = std::mem::replace(&mut self.shaped_text, segments); + } + + /// 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. Fonts are stored + /// in the `font_cache` which is a cache of all font keys and metrics used in this + /// [`super::InlineFormattingContext`]. + fn segment_text( + &mut self, + font_context: &mut FontContext, + font_cache: &mut Vec, + ) -> Vec<(TextRunSegment, FontRef)> { + let font_group = font_context.font_group(self.parent_style.clone_font()); + let mut current: Option<(TextRunSegment, FontRef)> = None; + let mut results = Vec::new(); + + for (byte_index, character) in self.text.char_indices() { + let prevents_soft_wrap_opportunity = + char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character); + if byte_index == 0 && prevents_soft_wrap_opportunity { + self.prevent_soft_wrap_opportunity_at_start = true; + } + self.prevent_soft_wrap_opportunity_at_end = prevents_soft_wrap_opportunity; + + if char_does_not_change_font(character) { + continue; + } + + let font = match font_group + .borrow_mut() + .find_by_codepoint(font_context, character) + { + Some(font) => font, + None => continue, + }; + + // If the existing segment is compatible with the character, keep going. + let script = Script::from(character); + if let Some(current) = current.as_mut() { + if current.0.update_if_compatible(&font, script, font_cache) { + continue; + } + } + + let font_index = add_or_get_font(&font, font_cache); + + // Add the new segment and finish the existing one, if we had one. If the first + // characters in the run were control characters we may be creating the first + // segment in the middle of the run (ie the start should be 0). + let byte_index = match current { + Some(_) => ByteIndex(byte_index as isize), + None => ByteIndex(0 as isize), + }; + let new = (TextRunSegment::new(font_index, script, byte_index), font); + if let Some(mut finished) = current.replace(new) { + finished.0.range.extend_to(byte_index); + results.push(finished); + } + } + + // Either we have a current segment or we only had control character and whitespace. In both + // of those cases, just use the first font. + if current.is_none() { + current = font_group.borrow_mut().first(font_context).map(|font| { + let font_index = add_or_get_font(&font, font_cache); + ( + TextRunSegment::new(font_index, Script::Common, ByteIndex(0)), + font, + ) + }) + } + + // Extend the last segment to the end of the string and add it to the results. + if let Some(mut last_segment) = current.take() { + last_segment + .0 + .range + .extend_to(ByteIndex(self.text.len() as isize)); + results.push(last_segment); + } + + results + } + + pub(super) fn layout_into_line_items(&self, ifc: &mut InlineFormattingContextState) { + if self.text.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 self.prevent_soft_wrap_opportunity_at_start { + true => SegmentStartSoftWrapPolicy::Prevent, + false if have_deferred_soft_wrap_opportunity => SegmentStartSoftWrapPolicy::Force, + false => SegmentStartSoftWrapPolicy::FollowLinebreaker, + }; + + for segment in self.shaped_text.iter() { + segment.layout_into_line_items(self, soft_wrap_policy, ifc); + soft_wrap_policy = SegmentStartSoftWrapPolicy::FollowLinebreaker; + } + + ifc.prevent_soft_wrap_opportunity_before_next_atomic = + self.prevent_soft_wrap_opportunity_at_end; } pub(super) fn glyph_run_is_whitespace_ending_with_preserved_newline( @@ -137,61 +373,9 @@ impl TextRun { let last_byte = self.text.as_bytes().get(run.range.end().to_usize() - 1); last_byte == Some(&b'\n') } - - pub(super) fn layout_into_line_items(&self, ifc: &mut InlineFormattingContextState) { - let broken = match self.shaped_text.as_ref() { - Some(broken) => broken, - None => return, - }; - - // We either have a soft wrap opportunity if specified by the breaker or if we are - // following replaced content. - let have_deferred_soft_wrap_opportunity = - mem::replace(&mut ifc.have_deferred_soft_wrap_opportunity, false); - let mut break_at_start = broken.break_at_start || have_deferred_soft_wrap_opportunity; - - if have_deferred_soft_wrap_opportunity { - if let Some(first_character) = self.text.chars().nth(0) { - break_at_start = break_at_start && - !char_prevents_soft_wrap_opportunity_when_before_or_after_atomic( - first_character, - ) - } - } - - if let Some(last_character) = self.text.chars().last() { - ifc.prevent_soft_wrap_opportunity_before_next_atomic = - char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(last_character); - } - - for (run_index, run) in broken.runs.iter().enumerate() { - ifc.possibly_flush_deferred_forced_line_break(); - - // 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. - if self.glyph_run_is_whitespace_ending_with_preserved_newline(run) { - ifc.defer_forced_line_break(); - continue; - } - - // Break before each unbrekable run in this TextRun, except the first unless the - // linebreaker was set to break before the first run. - if run_index != 0 || break_at_start { - ifc.process_soft_wrap_opportunity(); - } - - ifc.push_glyph_store_to_unbreakable_segment( - run.glyph_store.clone(), - self.base_fragment_info, - &self.parent_style, - &broken.font_metrics, - broken.font_key, - ); - } - } } +/// Whether or not this character will rpevent a soft wrap opportunity when it /// comes before or after an atomic inline element. /// /// From https://www.w3.org/TR/css-text-3/#line-break-details: @@ -211,3 +395,51 @@ fn char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character: ch class == XI_LINE_BREAKING_CLASS_WJ || class == XI_LINE_BREAKING_CLASS_ZWJ } + +/// 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 char_does_not_change_font(character: char) -> bool { + if character.is_whitespace() || character.is_control() { + return true; + } + if character == '\u{00A0}' { + return true; + } + let class = linebreak_property(character); + class == XI_LINE_BREAKING_CLASS_CM || + class == XI_LINE_BREAKING_CLASS_GL || + class == XI_LINE_BREAKING_CLASS_ZW || + class == XI_LINE_BREAKING_CLASS_WJ || + class == XI_LINE_BREAKING_CLASS_ZWJ +} + +pub(super) fn add_or_get_font(font: &FontRef, ifc_fonts: &mut Vec) -> usize { + let font = font.borrow(); + for (index, ifc_font_info) in ifc_fonts.iter().enumerate() { + if ifc_font_info.key == font.font_key && ifc_font_info.actual_pt_size == font.actual_pt_size + { + return index; + } + } + ifc_fonts.push(FontKeyAndMetrics { + metrics: font.metrics.clone(), + key: font.font_key, + actual_pt_size: font.actual_pt_size, + }); + ifc_fonts.len() - 1 +} + +pub(super) fn get_font_for_first_font_for_style( + style: &ComputedValues, + font_context: &mut FontContext, +) -> Option { + let font = font_context + .font_group(style.clone_font()) + .borrow_mut() + .first(font_context); + if font.is_none() { + warn!("Could not find font for style: {:?}", style.clone_font()); + } + font +} diff --git a/tests/wpt/meta/css/CSS2/fonts/font-family-013.xht.ini b/tests/wpt/meta/css/CSS2/fonts/font-family-013.xht.ini deleted file mode 100644 index 3dae1898579..00000000000 --- a/tests/wpt/meta/css/CSS2/fonts/font-family-013.xht.ini +++ /dev/null @@ -1,2 +0,0 @@ -[font-family-013.xht] - expected: FAIL diff --git a/tests/wpt/meta/css/CSS2/fonts/fonts-013.xht.ini b/tests/wpt/meta/css/CSS2/fonts/fonts-012.xht.ini similarity index 51% rename from tests/wpt/meta/css/CSS2/fonts/fonts-013.xht.ini rename to tests/wpt/meta/css/CSS2/fonts/fonts-012.xht.ini index 15802c89aa7..5e35412a247 100644 --- a/tests/wpt/meta/css/CSS2/fonts/fonts-013.xht.ini +++ b/tests/wpt/meta/css/CSS2/fonts/fonts-012.xht.ini @@ -1,2 +1,2 @@ -[fonts-013.xht] +[fonts-012.xht] expected: FAIL diff --git a/tests/wpt/meta/css/CSS2/text/text-transform-bicameral-009.xht.ini b/tests/wpt/meta/css/CSS2/text/text-transform-bicameral-009.xht.ini new file mode 100644 index 00000000000..c7edfd9f2d1 --- /dev/null +++ b/tests/wpt/meta/css/CSS2/text/text-transform-bicameral-009.xht.ini @@ -0,0 +1,2 @@ +[text-transform-bicameral-009.xht] + expected: FAIL diff --git a/tests/wpt/meta/css/CSS2/text/text-transform-bicameral-010.xht.ini b/tests/wpt/meta/css/CSS2/text/text-transform-bicameral-010.xht.ini new file mode 100644 index 00000000000..434982be585 --- /dev/null +++ b/tests/wpt/meta/css/CSS2/text/text-transform-bicameral-010.xht.ini @@ -0,0 +1,2 @@ +[text-transform-bicameral-010.xht] + expected: FAIL diff --git a/tests/wpt/meta/css/css-fonts/font-variant-ligatures-11.optional.html.ini b/tests/wpt/meta/css/css-fonts/font-variant-ligatures-11.optional.html.ini new file mode 100644 index 00000000000..558ca07056d --- /dev/null +++ b/tests/wpt/meta/css/css-fonts/font-variant-ligatures-11.optional.html.ini @@ -0,0 +1,2 @@ +[font-variant-ligatures-11.optional.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/bidi/bidi-lines-001.html.ini b/tests/wpt/meta/css/css-text/bidi/bidi-lines-001.html.ini new file mode 100644 index 00000000000..aa2029fb7e9 --- /dev/null +++ b/tests/wpt/meta/css/css-text/bidi/bidi-lines-001.html.ini @@ -0,0 +1,2 @@ +[bidi-lines-001.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/boundary-shaping/boundary-shaping-010.html.ini b/tests/wpt/meta/css/css-text/boundary-shaping/boundary-shaping-010.html.ini new file mode 100644 index 00000000000..32fafca5ce1 --- /dev/null +++ b/tests/wpt/meta/css/css-text/boundary-shaping/boundary-shaping-010.html.ini @@ -0,0 +1,2 @@ +[boundary-shaping-010.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/hyphens/hyphens-shaping-001.html.ini b/tests/wpt/meta/css/css-text/hyphens/hyphens-shaping-001.html.ini new file mode 100644 index 00000000000..e1f84085782 --- /dev/null +++ b/tests/wpt/meta/css/css-text/hyphens/hyphens-shaping-001.html.ini @@ -0,0 +1,2 @@ +[hyphens-shaping-001.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/letter-spacing/letter-spacing-ligatures-002.html.ini b/tests/wpt/meta/css/css-text/letter-spacing/letter-spacing-ligatures-002.html.ini new file mode 100644 index 00000000000..fa87a1d43f4 --- /dev/null +++ b/tests/wpt/meta/css/css-text/letter-spacing/letter-spacing-ligatures-002.html.ini @@ -0,0 +1,2 @@ +[letter-spacing-ligatures-002.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/shaping/reference/shaping-000-ref.html.ini b/tests/wpt/meta/css/css-text/shaping/reference/shaping-000-ref.html.ini deleted file mode 100644 index f7796c2277f..00000000000 --- a/tests/wpt/meta/css/css-text/shaping/reference/shaping-000-ref.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[shaping-000-ref.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-text/shaping/reference/shaping-012-ref.html.ini b/tests/wpt/meta/css/css-text/shaping/reference/shaping-012-ref.html.ini deleted file mode 100644 index 9b2604e265d..00000000000 --- a/tests/wpt/meta/css/css-text/shaping/reference/shaping-012-ref.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[shaping-012-ref.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-text/shaping/shaping-000.html.ini b/tests/wpt/meta/css/css-text/shaping/shaping-000.html.ini new file mode 100644 index 00000000000..f1d5f3ca173 --- /dev/null +++ b/tests/wpt/meta/css/css-text/shaping/shaping-000.html.ini @@ -0,0 +1,2 @@ +[shaping-000.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/shaping/shaping-004.html.ini b/tests/wpt/meta/css/css-text/shaping/shaping-004.html.ini new file mode 100644 index 00000000000..a506ceaec88 --- /dev/null +++ b/tests/wpt/meta/css/css-text/shaping/shaping-004.html.ini @@ -0,0 +1,2 @@ +[shaping-004.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/shaping/shaping-005.html.ini b/tests/wpt/meta/css/css-text/shaping/shaping-005.html.ini new file mode 100644 index 00000000000..f7a7877d4e5 --- /dev/null +++ b/tests/wpt/meta/css/css-text/shaping/shaping-005.html.ini @@ -0,0 +1,2 @@ +[shaping-005.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/shaping/shaping-006.html.ini b/tests/wpt/meta/css/css-text/shaping/shaping-006.html.ini new file mode 100644 index 00000000000..2ab7cfdf636 --- /dev/null +++ b/tests/wpt/meta/css/css-text/shaping/shaping-006.html.ini @@ -0,0 +1,2 @@ +[shaping-006.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/shaping/shaping-007.html.ini b/tests/wpt/meta/css/css-text/shaping/shaping-007.html.ini new file mode 100644 index 00000000000..9c93fc1cdab --- /dev/null +++ b/tests/wpt/meta/css/css-text/shaping/shaping-007.html.ini @@ -0,0 +1,2 @@ +[shaping-007.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/shaping/shaping_lig-000.html.ini b/tests/wpt/meta/css/css-text/shaping/shaping_lig-000.html.ini new file mode 100644 index 00000000000..5d8cc6016e3 --- /dev/null +++ b/tests/wpt/meta/css/css-text/shaping/shaping_lig-000.html.ini @@ -0,0 +1,2 @@ +[shaping_lig-000.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/text-encoding/shaping-join-001.html.ini b/tests/wpt/meta/css/css-text/text-encoding/shaping-join-001.html.ini new file mode 100644 index 00000000000..1bce5144069 --- /dev/null +++ b/tests/wpt/meta/css/css-text/text-encoding/shaping-join-001.html.ini @@ -0,0 +1,2 @@ +[shaping-join-001.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/text-encoding/shaping-join-003.html.ini b/tests/wpt/meta/css/css-text/text-encoding/shaping-join-003.html.ini new file mode 100644 index 00000000000..1d1a4727f63 --- /dev/null +++ b/tests/wpt/meta/css/css-text/text-encoding/shaping-join-003.html.ini @@ -0,0 +1,2 @@ +[shaping-join-003.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/text-encoding/shaping-no-join-001.html.ini b/tests/wpt/meta/css/css-text/text-encoding/shaping-no-join-001.html.ini new file mode 100644 index 00000000000..23475b0cd68 --- /dev/null +++ b/tests/wpt/meta/css/css-text/text-encoding/shaping-no-join-001.html.ini @@ -0,0 +1,2 @@ +[shaping-no-join-001.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/text-encoding/shaping-no-join-003.html.ini b/tests/wpt/meta/css/css-text/text-encoding/shaping-no-join-003.html.ini new file mode 100644 index 00000000000..01697530439 --- /dev/null +++ b/tests/wpt/meta/css/css-text/text-encoding/shaping-no-join-003.html.ini @@ -0,0 +1,2 @@ +[shaping-no-join-003.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/text-encoding/shaping-tatweel-001.html.ini b/tests/wpt/meta/css/css-text/text-encoding/shaping-tatweel-001.html.ini new file mode 100644 index 00000000000..320f05d25d3 --- /dev/null +++ b/tests/wpt/meta/css/css-text/text-encoding/shaping-tatweel-001.html.ini @@ -0,0 +1,2 @@ +[shaping-tatweel-001.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/text-encoding/shaping-tatweel-003.html.ini b/tests/wpt/meta/css/css-text/text-encoding/shaping-tatweel-003.html.ini new file mode 100644 index 00000000000..a82c26b1420 --- /dev/null +++ b/tests/wpt/meta/css/css-text/text-encoding/shaping-tatweel-003.html.ini @@ -0,0 +1,2 @@ +[shaping-tatweel-003.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/text-transform/text-transform-upperlower-103.html.ini b/tests/wpt/meta/css/css-text/text-transform/text-transform-upperlower-103.html.ini new file mode 100644 index 00000000000..64bc8708fc5 --- /dev/null +++ b/tests/wpt/meta/css/css-text/text-transform/text-transform-upperlower-103.html.ini @@ -0,0 +1,2 @@ +[text-transform-upperlower-103.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/text-transform/text-transform-upperlower-104.html.ini b/tests/wpt/meta/css/css-text/text-transform/text-transform-upperlower-104.html.ini new file mode 100644 index 00000000000..ab03bd7c646 --- /dev/null +++ b/tests/wpt/meta/css/css-text/text-transform/text-transform-upperlower-104.html.ini @@ -0,0 +1,2 @@ +[text-transform-upperlower-104.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/word-break/word-break-normal-ar-000.html.ini b/tests/wpt/meta/css/css-text/word-break/word-break-normal-ar-000.html.ini deleted file mode 100644 index b79a864a815..00000000000 --- a/tests/wpt/meta/css/css-text/word-break/word-break-normal-ar-000.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[word-break-normal-ar-000.html] - expected: FAIL diff --git a/tests/wpt/mozilla/meta/css/font_fallback_03.html.ini b/tests/wpt/mozilla/meta/css/font_fallback_03.html.ini deleted file mode 100644 index b53d61d8e9f..00000000000 --- a/tests/wpt/mozilla/meta/css/font_fallback_03.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[font_fallback_03.html] - expected: FAIL diff --git a/tests/wpt/mozilla/meta/css/per_glyph_font_fallback_a.html.ini b/tests/wpt/mozilla/meta/css/per_glyph_font_fallback_a.html.ini deleted file mode 100644 index 7aaf105eb33..00000000000 --- a/tests/wpt/mozilla/meta/css/per_glyph_font_fallback_a.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[per_glyph_font_fallback_a.html] - expected: FAIL