diff options
Diffstat (limited to 'components')
-rw-r--r-- | components/gfx/font.rs | 3 | ||||
-rw-r--r-- | components/gfx/text/glyph.rs | 20 | ||||
-rw-r--r-- | components/layout_2020/flexbox/construct.rs | 57 | ||||
-rw-r--r-- | components/layout_2020/flow/construct.rs | 393 | ||||
-rw-r--r-- | components/layout_2020/flow/inline/construct.rs | 610 | ||||
-rw-r--r-- | components/layout_2020/flow/inline/line.rs (renamed from components/layout_2020/flow/line.rs) | 0 | ||||
-rw-r--r-- | components/layout_2020/flow/inline/mod.rs (renamed from components/layout_2020/flow/inline.rs) | 164 | ||||
-rw-r--r-- | components/layout_2020/flow/inline/text_run.rs (renamed from components/layout_2020/flow/text_run.rs) | 502 | ||||
-rw-r--r-- | components/layout_2020/flow/mod.rs | 8 | ||||
-rw-r--r-- | components/layout_2020/tests/text.rs | 2 |
10 files changed, 961 insertions, 798 deletions
diff --git a/components/gfx/font.rs b/components/gfx/font.rs index c418fce06d3..1ea59c73fd5 100644 --- a/components/gfx/font.rs +++ b/components/gfx/font.rs @@ -314,12 +314,15 @@ impl Font { } } + let is_single_preserved_newline = text.len() == 1 && text.chars().next() == Some('\n'); + let start_time = Instant::now(); let mut glyphs = GlyphStore::new( text.len(), options .flags .contains(ShapingFlags::IS_WHITESPACE_SHAPING_FLAG), + is_single_preserved_newline, options.flags.contains(ShapingFlags::RTL_FLAG), ); diff --git a/components/gfx/text/glyph.rs b/components/gfx/text/glyph.rs index 30705ab9d0f..fd608e7ebaa 100644 --- a/components/gfx/text/glyph.rs +++ b/components/gfx/text/glyph.rs @@ -440,7 +440,14 @@ pub struct GlyphStore { /// Used to check if fast path should be used in glyph iteration. has_detailed_glyphs: bool, + + /// Whether or not this glyph store contains only glyphs for whitespace. is_whitespace: bool, + + /// Whether or not this glyph store contains only a single glyph for a single + /// preserved newline. + is_single_preserved_newline: bool, + is_rtl: bool, } @@ -448,7 +455,12 @@ impl<'a> GlyphStore { /// Initializes the glyph store, but doesn't actually shape anything. /// /// Use the `add_*` methods to store glyph data. - pub fn new(length: usize, is_whitespace: bool, is_rtl: bool) -> GlyphStore { + pub fn new( + length: usize, + is_whitespace: bool, + is_single_preserved_newline: bool, + is_rtl: bool, + ) -> GlyphStore { assert!(length > 0); GlyphStore { @@ -458,6 +470,7 @@ impl<'a> GlyphStore { total_word_separators: 0, has_detailed_glyphs: false, is_whitespace, + is_single_preserved_newline, is_rtl, } } @@ -794,4 +807,9 @@ impl GlyphRun { Ordering::Equal } } + + #[inline] + pub fn is_single_preserved_newline(&self) -> bool { + self.glyph_store.is_single_preserved_newline + } } diff --git a/components/layout_2020/flexbox/construct.rs b/components/layout_2020/flexbox/construct.rs index 065e03989d3..1695393065a 100644 --- a/components/layout_2020/flexbox/construct.rs +++ b/components/layout_2020/flexbox/construct.rs @@ -12,7 +12,8 @@ use crate::cell::ArcRefCell; use crate::context::LayoutContext; use crate::dom::{BoxSlot, LayoutBox, NodeExt}; use crate::dom_traversal::{Contents, NodeAndStyleInfo, NonReplacedContents, TraversalHandler}; -use crate::flow::BlockFormattingContext; +use crate::flow::inline::construct::InlineFormattingContextBuilder; +use crate::flow::{BlockContainer, BlockFormattingContext}; use crate::formatting_contexts::{ IndependentFormattingContext, NonReplacedFormattingContext, NonReplacedFormattingContextContents, @@ -47,7 +48,7 @@ struct FlexContainerBuilder<'a, 'dom, Node> { context: &'a LayoutContext<'a>, info: &'a NodeAndStyleInfo<Node>, text_decoration_line: TextDecorationLine, - contiguous_text_runs: Vec<TextRun<'dom, Node>>, + contiguous_text_runs: Vec<FlexTextRun<'dom, Node>>, /// To be run in parallel with rayon in `finish` jobs: Vec<FlexLevelJob<'dom, Node>>, has_text_runs: bool, @@ -61,10 +62,10 @@ enum FlexLevelJob<'dom, Node> { contents: Contents, box_slot: BoxSlot<'dom>, }, - TextRuns(Vec<TextRun<'dom, Node>>), + TextRuns(Vec<FlexTextRun<'dom, Node>>), } -struct TextRun<'dom, Node> { +struct FlexTextRun<'dom, Node> { info: NodeAndStyleInfo<Node>, text: Cow<'dom, str>, } @@ -74,7 +75,7 @@ where Node: NodeExt<'dom>, { fn handle_text(&mut self, info: &NodeAndStyleInfo<Node>, text: Cow<'dom, str>) { - self.contiguous_text_runs.push(TextRun { + self.contiguous_text_runs.push(FlexTextRun { info: info.clone(), text, }) @@ -103,7 +104,7 @@ where } /// <https://drafts.csswg.org/css-text/#white-space> -fn is_only_document_white_space<Node>(run: &TextRun<'_, Node>) -> bool { +fn is_only_document_white_space<Node>(run: &FlexTextRun<'_, Node>) -> bool { // FIXME: is this the right definition? See // https://github.com/w3c/csswg-drafts/issues/5146 // https://github.com/w3c/csswg-drafts/issues/5147 @@ -146,28 +147,40 @@ where let mut children = std::mem::take(&mut self.jobs) .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::new( - (&run.info).into(), - run.info.style, - run.text.into(), - ) - }); - let bfc = BlockFormattingContext::construct_for_text_runs( - runs, + .filter_map(|job| match job { + FlexLevelJob::TextRuns(runs) => { + let mut inline_formatting_context_builder = + InlineFormattingContextBuilder::new(); + for flex_text_run in runs.into_iter() { + inline_formatting_context_builder + .push_text(flex_text_run.text, &flex_text_run.info); + } + + let Some(inline_formatting_context) = inline_formatting_context_builder.finish( self.context, self.text_decoration_line, + true, /* has_first_formatted_line */ + ) else { + return None; + }; + + let block_formatting_context = BlockFormattingContext::from_block_container( + BlockContainer::InlineFormattingContext(inline_formatting_context), ); let info = &self.info.new_anonymous(anonymous_style.clone().unwrap()); - IndependentFormattingContext::NonReplaced(NonReplacedFormattingContext { + let non_replaced = NonReplacedFormattingContext { base_fragment_info: info.into(), style: info.style.clone(), content_sizes: None, - contents: NonReplacedFormattingContextContents::Flow(bfc), - }) - })), + contents: NonReplacedFormattingContextContents::Flow( + block_formatting_context, + ), + }; + + Some(ArcRefCell::new(FlexLevelBox::FlexItem( + IndependentFormattingContext::NonReplaced(non_replaced), + ))) + }, FlexLevelJob::Element { info, display, @@ -200,7 +213,7 @@ where )) }; box_slot.set(LayoutBox::FlexLevel(box_.clone())); - box_ + Some(box_) }, }) .collect::<Vec<_>>(); diff --git a/components/layout_2020/flow/construct.rs b/components/layout_2020/flow/construct.rs index 586a109e918..bba6e2eec1f 100644 --- a/components/layout_2020/flow/construct.rs +++ b/components/layout_2020/flow/construct.rs @@ -13,6 +13,7 @@ use style::selector_parser::PseudoElement; use style::str::char_is_whitespace; use style::values::specified::text::TextDecorationLine; +use super::inline::construct::InlineFormattingContextBuilder; use super::OutsideMarker; use crate::cell::ArcRefCell; use crate::context::LayoutContext; @@ -21,8 +22,7 @@ use crate::dom_traversal::{ Contents, NodeAndStyleInfo, NonReplacedContents, PseudoElementContentItem, TraversalHandler, }; use crate::flow::float::FloatBox; -use crate::flow::inline::{InlineBox, InlineFormattingContext, InlineLevelBox}; -use crate::flow::text_run::TextRun; +use crate::flow::inline::{InlineFormattingContext, InlineLevelBox}; use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox}; use crate::formatting_contexts::IndependentFormattingContext; use crate::positioned::AbsolutelyPositionedBox; @@ -56,28 +56,6 @@ impl BlockFormattingContext { contains_floats, } } - - pub fn construct_for_text_runs( - runs: impl Iterator<Item = TextRun>, - layout_context: &LayoutContext, - text_decoration_line: TextDecorationLine, - ) -> Self { - let inline_level_boxes = runs - .map(|run| ArcRefCell::new(InlineLevelBox::TextRun(run))) - .collect(); - - let ifc = InlineFormattingContext { - inline_level_boxes, - font_metrics: Vec::new(), - text_decoration_line, - has_first_formatted_line: true, - contains_floats: false, - }; - Self { - contents: BlockContainer::construct_inline_formatting_context(layout_context, ifc), - contains_floats: false, - } - } } struct BlockLevelJob<'dom, Node> { @@ -151,29 +129,14 @@ pub(crate) struct BlockContainerBuilder<'dom, 'style, Node> { /// (see `handle_block_level_element`). block_level_boxes: Vec<BlockLevelJob<'dom, Node>>, - /// The ongoing inline formatting context of the builder. - /// - /// Contains all the complete inline level boxes we found traversing the - /// tree so far. If a block-level box is found during traversal, - /// this inline formatting context is pushed as a block level box to - /// the list of block-level boxes of the builder - /// (see `end_ongoing_inline_formatting_context`). - ongoing_inline_formatting_context: InlineFormattingContext, - - /// The ongoing stack of inline boxes stack of the builder. - /// - /// Contains all the currently ongoing inline boxes we entered so far. - /// The traversal is at all times as deep in the tree as this stack is, - /// which is why the code doesn't need to keep track of the actual - /// container root (see `handle_inline_level_element`). - /// - /// Whenever the end of a DOM element that represents an inline box is - /// reached, the inline box at the top of this stack is complete and ready - /// to be pushed to the children of the next last ongoing inline box - /// the ongoing inline formatting context if the stack is now empty, - /// which means we reached the end of a child of the actual - /// container root (see `move_to_next_sibling`). - ongoing_inline_boxes_stack: Vec<InlineBox>, + /// Whether or not this builder has yet produced a block which would be + /// be considered the first line for the purposes of `text-indent`. + have_already_seen_first_line_for_text_indent: bool, + + /// The propagated [`TextDecorationLine`]. + text_decoration_line: TextDecorationLine, + + inline_formatting_context_builder: InlineFormattingContextBuilder, /// The style of the anonymous block boxes pushed to the list of block-level /// boxes, if any (see `end_ongoing_inline_formatting_context`). @@ -215,16 +178,6 @@ impl BlockContainer { contents.traverse(context, info, &mut builder); builder.finish() } - - pub(super) fn construct_inline_formatting_context( - layout_context: &LayoutContext, - mut ifc: InlineFormattingContext, - ) -> Self { - // TODO(mrobinson): Perhaps it would be better to iteratively break and shape the contents - // of the IFC, and not wait until it is completely built. - ifc.break_and_shape_text(layout_context); - BlockContainer::InlineFormattingContext(ifc) - } } impl<'dom, 'style, Node> BlockContainerBuilder<'dom, 'style, Node> @@ -242,29 +195,33 @@ where context, info, block_level_boxes: Vec::new(), - ongoing_inline_formatting_context: InlineFormattingContext::new( - text_decoration_line, - /* has_first_formatted_line = */ true, - ), - ongoing_inline_boxes_stack: Vec::new(), + text_decoration_line, + have_already_seen_first_line_for_text_indent: false, anonymous_style: None, anonymous_table_content: Vec::new(), + inline_formatting_context_builder: InlineFormattingContextBuilder::new(), } } pub(crate) fn finish(mut self) -> BlockContainer { - debug_assert!(self.ongoing_inline_boxes_stack.is_empty()); + debug_assert!(!self + .inline_formatting_context_builder + .currently_processing_inline_box()); self.finish_anonymous_table_if_needed(); - if !self.ongoing_inline_formatting_context.is_empty() { + if let Some(inline_formatting_context) = self.inline_formatting_context_builder.finish( + self.context, + self.text_decoration_line, + !self.have_already_seen_first_line_for_text_indent, + ) { + // There are two options here. This block was composed of both one or more inline formatting contexts + // and child blocks OR this block was a single inline formatting context. In the latter case, we + // just return the inline formatting context as the block itself. if self.block_level_boxes.is_empty() { - return BlockContainer::construct_inline_formatting_context( - self.context, - self.ongoing_inline_formatting_context, - ); + return BlockContainer::InlineFormattingContext(inline_formatting_context); } - self.end_ongoing_inline_formatting_context(); + self.push_block_level_job_for_inline_formatting_context(inline_formatting_context); } let context = self.context; @@ -288,16 +245,26 @@ where return; } + // From https://drafts.csswg.org/css-tables/#fixup-algorithm: + // > If the box’s parent is an inline, run-in, or ruby box (or any box that would perform + // > inlinification of its children), then an inline-table box must be generated; otherwise + // > it must be a table box. + // + // Note that text content in the inline formatting context isn't enough to force the + // creation of an inline table. It requires the parent to be an inline box. + let inline_table = self + .inline_formatting_context_builder + .currently_processing_inline_box(); + // Text decorations are not propagated to atomic inline-level descendants. // From https://drafts.csswg.org/css2/#lining-striking-props: // > Note that text decorations are not propagated to floating and absolutely // > positioned descendants, nor to the contents of atomic inline-level descendants // > such as inline blocks and inline tables. - let inline_table = !self.ongoing_inline_boxes_stack.is_empty(); let propagated_text_decoration_line = if inline_table { TextDecorationLine::NONE } else { - self.ongoing_inline_formatting_context.text_decoration_line + self.text_decoration_line }; let contents: Vec<AnonymousTableContent<'dom, Node>> = @@ -315,12 +282,11 @@ where ); if inline_table { - self.current_inline_level_boxes() - .push(ArcRefCell::new(InlineLevelBox::Atomic(ifc))); + self.inline_formatting_context_builder.push_atomic(ifc); } else { - self.end_ongoing_inline_formatting_context(); let anonymous_info = self.info.new_anonymous(ifc.style().clone()); let table_block = ArcRefCell::new(BlockLevelBox::Independent(ifc)); + self.end_ongoing_inline_formatting_context(); self.block_level_boxes.push(BlockLevelJob { info: anonymous_info, box_slot: BoxSlot::dummy(), @@ -390,39 +356,23 @@ where } } - fn handle_text(&mut self, info: &NodeAndStyleInfo<Node>, input: Cow<'dom, str>) { - if input.is_empty() { + fn handle_text(&mut self, info: &NodeAndStyleInfo<Node>, text: Cow<'dom, str>) { + if text.is_empty() { return; } // If we are building an anonymous table ie this text directly followed internal // table elements that did not have a `<table>` ancestor, then we forward all // whitespace to the table builder. - if !self.anonymous_table_content.is_empty() && input.chars().all(char_is_whitespace) { + if !self.anonymous_table_content.is_empty() && text.chars().all(char_is_whitespace) { self.anonymous_table_content - .push(AnonymousTableContent::Text(info.clone(), input)); + .push(AnonymousTableContent::Text(info.clone(), text)); return; } else { self.finish_anonymous_table_if_needed(); } - // TODO: We can do better here than `push_str` and wait until we are breaking and - // shaping text to allocate space big enough for the final text. It would require - // collecting all Cow strings into a vector and passing them along to text breaking - // and shaping during final InlineFormattingContext construction. - let inlines = self.current_inline_level_boxes(); - if let Some(mut last_box) = inlines.last_mut().map(|last| last.borrow_mut()) { - if let InlineLevelBox::TextRun(ref mut text_run) = *last_box { - text_run.text.push_str(&input); - return; - } - } - - inlines.push(ArcRefCell::new(InlineLevelBox::TextRun(TextRun::new( - info.into(), - Arc::clone(&info.style), - input.into(), - )))); + self.inline_formatting_context_builder.push_text(text, info); } } @@ -471,45 +421,11 @@ where display_inside: DisplayInside, contents: Contents, ) -> ArcRefCell<InlineLevelBox> { - let box_ = if let (DisplayInside::Flow { is_list_item }, false) = + let (DisplayInside::Flow { is_list_item }, false) = (display_inside, contents.is_replaced()) - { - // We found un inline box. - // Whatever happened before, all we need to do before recurring - // is to remember this ongoing inline level box. - self.ongoing_inline_boxes_stack.push(InlineBox { - base_fragment_info: info.into(), - style: info.style.clone(), - is_first_fragment: true, - is_last_fragment: false, - children: vec![], - default_font_index: None, - }); - - if is_list_item { - if let Some(marker_contents) = crate::lists::make_marker(self.context, info) { - // Ignore `list-style-position` here: - // “If the list item is an inline box: this value is equivalent to `inside`.” - // https://drafts.csswg.org/css-lists/#list-style-position-outside - self.handle_list_item_marker_inside(info, marker_contents) - } - } - - // `unwrap` doesn’t panic here because `is_replaced` returned `false`. - NonReplacedContents::try_from(contents) - .unwrap() - .traverse(self.context, info, self); - - self.finish_anonymous_table_if_needed(); - - let mut inline_box = self - .ongoing_inline_boxes_stack - .pop() - .expect("no ongoing inline level box found"); - inline_box.is_last_fragment = true; - ArcRefCell::new(InlineLevelBox::InlineBox(inline_box)) - } else { - ArcRefCell::new(InlineLevelBox::Atomic( + else { + // If this inline element is an atomic, handle it and return. + return self.inline_formatting_context_builder.push_atomic( IndependentFormattingContext::construct( self.context, info, @@ -518,10 +434,32 @@ where // Text decorations are not propagated to atomic inline-level descendants. TextDecorationLine::NONE, ), - )) + ); }; - self.current_inline_level_boxes().push(box_.clone()); - box_ + + // Otherwise, this is just a normal inline box. Whatever happened before, all we need to do + // before recurring is to remember this ongoing inline level box. + self.inline_formatting_context_builder + .start_inline_box(info); + + if is_list_item { + if let Some(marker_contents) = crate::lists::make_marker(self.context, info) { + // Ignore `list-style-position` here: + // “If the list item is an inline box: this value is equivalent to `inside`.” + // https://drafts.csswg.org/css-lists/#list-style-position-outside + self.handle_list_item_marker_inside(info, marker_contents) + } + } + + // `unwrap` doesn’t panic here because `is_replaced` returned `false`. + NonReplacedContents::try_from(contents) + .unwrap() + .traverse(self.context, info, self); + + self.finish_anonymous_table_if_needed(); + + // Finish the inline box in our IFC builder and return it for `display: contents`. + self.inline_formatting_context_builder.end_inline_box() } fn handle_block_level_element( @@ -532,53 +470,23 @@ where box_slot: BoxSlot<'dom>, ) { // We just found a block level element, all ongoing inline level boxes - // need to be split around it. We iterate on the fragmented inline - // level box stack to take their contents and set their first_fragment - // field to false, for the fragmented inline level boxes that will - // come after the block level element. - let mut fragmented_inline_boxes = - self.ongoing_inline_boxes_stack - .iter_mut() - .rev() - .map(|ongoing| { - let fragmented = InlineBox { - base_fragment_info: ongoing.base_fragment_info, - style: ongoing.style.clone(), - is_first_fragment: ongoing.is_first_fragment, - // The fragmented boxes before the block level element - // 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 - }); - - if let Some(last) = fragmented_inline_boxes.next() { - // There were indeed some ongoing inline level boxes before - // the block, we accumulate them as a single inline level box - // to be pushed to the ongoing inline formatting context. - let mut fragmented_inline = InlineLevelBox::InlineBox(last); - for mut fragmented_parent_inline_box in fragmented_inline_boxes { - fragmented_parent_inline_box - .children - .push(ArcRefCell::new(fragmented_inline)); - fragmented_inline = InlineLevelBox::InlineBox(fragmented_parent_inline_box); - } - - self.ongoing_inline_formatting_context - .inline_level_boxes - .push(ArcRefCell::new(fragmented_inline)); + // need to be split around it. + // + // After calling `split_around_block_and_finish`, + // `self.inline_formatting_context_builder` is set up with the state + // that we want to have after we push the block below. + if let Some(inline_formatting_context) = self + .inline_formatting_context_builder + .split_around_block_and_finish( + self.context, + self.text_decoration_line, + !self.have_already_seen_first_line_for_text_indent, + ) + { + self.push_block_level_job_for_inline_formatting_context(inline_formatting_context); } - let propagated_text_decoration_line = - self.ongoing_inline_formatting_context.text_decoration_line; - - // We found a block level element, so the ongoing inline formatting - // context needs to be ended. - self.end_ongoing_inline_formatting_context(); - + let propagated_text_decoration_line = self.text_decoration_line; let kind = match contents.try_into() { Ok(contents) => match display_inside { DisplayInside::Flow { is_list_item } @@ -612,6 +520,10 @@ where box_slot, kind, }); + + // Any block also counts as the first line for the purposes of text indent. Even if + // they don't actually indent. + self.have_already_seen_first_line_for_text_indent = true; } fn handle_absolutely_positioned_element( @@ -621,28 +533,28 @@ where contents: Contents, box_slot: BoxSlot<'dom>, ) { - if !self.has_ongoing_inline_formatting_context() { - let kind = BlockLevelCreator::OutOfFlowAbsolutelyPositionedBox { - contents, - display_inside, - }; - self.block_level_boxes.push(BlockLevelJob { - info: info.clone(), - box_slot, - kind, - }); - } else { - let box_ = ArcRefCell::new(InlineLevelBox::OutOfFlowAbsolutelyPositionedBox( - ArcRefCell::new(AbsolutelyPositionedBox::construct( + if !self.inline_formatting_context_builder.is_empty() { + let inline_level_box = self + .inline_formatting_context_builder + .push_absolutely_positioned_box(AbsolutelyPositionedBox::construct( self.context, info, display_inside, contents, - )), - )); - self.current_inline_level_boxes().push(box_.clone()); - box_slot.set(LayoutBox::InlineLevel(box_)) + )); + box_slot.set(LayoutBox::InlineLevel(inline_level_box)); + return; } + + let kind = BlockLevelCreator::OutOfFlowAbsolutelyPositionedBox { + contents, + display_inside, + }; + self.block_level_boxes.push(BlockLevelJob { + info: info.clone(), + box_slot, + kind, + }); } fn handle_float_element( @@ -652,56 +564,57 @@ where contents: Contents, box_slot: BoxSlot<'dom>, ) { - if !self.has_ongoing_inline_formatting_context() { - let kind = BlockLevelCreator::OutOfFlowFloatBox { - contents, - display_inside, - }; - self.block_level_boxes.push(BlockLevelJob { - info: info.clone(), - box_slot, - kind, - }); - } else { - let box_ = ArcRefCell::new(InlineLevelBox::OutOfFlowFloatBox(FloatBox::construct( - self.context, - info, - display_inside, - contents, - ))); - self.ongoing_inline_formatting_context.contains_floats = true; - self.current_inline_level_boxes().push(box_.clone()); - box_slot.set(LayoutBox::InlineLevel(box_)) + if !self.inline_formatting_context_builder.is_empty() { + let inline_level_box = + self.inline_formatting_context_builder + .push_float_box(FloatBox::construct( + self.context, + info, + display_inside, + contents, + )); + box_slot.set(LayoutBox::InlineLevel(inline_level_box)); + return; } + + let kind = BlockLevelCreator::OutOfFlowFloatBox { + contents, + display_inside, + }; + self.block_level_boxes.push(BlockLevelJob { + info: info.clone(), + box_slot, + kind, + }); } fn end_ongoing_inline_formatting_context(&mut self) { - if self.ongoing_inline_formatting_context.is_empty() { - // There should never be an empty inline formatting context. - self.ongoing_inline_formatting_context - .has_first_formatted_line = false; - return; + if let Some(inline_formatting_context) = self.inline_formatting_context_builder.finish( + self.context, + self.text_decoration_line, + !self.have_already_seen_first_line_for_text_indent, + ) { + self.push_block_level_job_for_inline_formatting_context(inline_formatting_context); } + } - let context = self.context; + fn push_block_level_job_for_inline_formatting_context( + &mut self, + inline_formatting_context: InlineFormattingContext, + ) { let block_container_style = &self.info.style; + let layout_context = self.context; let anonymous_style = self.anonymous_style.get_or_insert_with(|| { - context + layout_context .shared_context() .stylist .style_for_anonymous::<Node::ConcreteElement>( - &context.shared_context().guards, + &layout_context.shared_context().guards, &PseudoElement::ServoAnonymousBox, block_container_style, ) }); - let mut ifc = InlineFormattingContext::new( - self.ongoing_inline_formatting_context.text_decoration_line, - /* has_first_formatted_line = */ false, - ); - std::mem::swap(&mut self.ongoing_inline_formatting_context, &mut ifc); - let info = self.info.new_anonymous(anonymous_style.clone()); self.block_level_boxes.push(BlockLevelJob { info, @@ -709,24 +622,12 @@ where box_slot: BoxSlot::dummy(), kind: BlockLevelCreator::SameFormattingContextBlock( IntermediateBlockContainer::InlineFormattingContext( - BlockContainer::construct_inline_formatting_context(self.context, ifc), + BlockContainer::InlineFormattingContext(inline_formatting_context), ), ), }); - } - - // Retrieves the mutable reference of inline boxes either from the last - // element of a stack or directly from the formatting context, depending on the situation. - fn current_inline_level_boxes(&mut self) -> &mut Vec<ArcRefCell<InlineLevelBox>> { - match self.ongoing_inline_boxes_stack.last_mut() { - Some(last) => &mut last.children, - None => &mut self.ongoing_inline_formatting_context.inline_level_boxes, - } - } - fn has_ongoing_inline_formatting_context(&self) -> bool { - !self.ongoing_inline_formatting_context.is_empty() || - !self.ongoing_inline_boxes_stack.is_empty() + self.have_already_seen_first_line_for_text_indent = true; } } diff --git a/components/layout_2020/flow/inline/construct.rs b/components/layout_2020/flow/inline/construct.rs new file mode 100644 index 00000000000..b88b5014f01 --- /dev/null +++ b/components/layout_2020/flow/inline/construct.rs @@ -0,0 +1,610 @@ +/* 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::borrow::Cow; +use std::char::{ToLowercase, ToUppercase}; + +use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse; +use style::values::computed::{TextDecorationLine, TextTransform}; +use style::values::specified::text::TextTransformCase; +use unicode_segmentation::UnicodeSegmentation; + +use super::text_run::TextRun; +use super::{InlineBox, InlineFormattingContext, InlineLevelBox}; +use crate::cell::ArcRefCell; +use crate::context::LayoutContext; +use crate::dom::NodeExt; +use crate::dom_traversal::NodeAndStyleInfo; +use crate::flow::float::FloatBox; +use crate::formatting_contexts::IndependentFormattingContext; +use crate::positioned::AbsolutelyPositionedBox; + +#[derive(Default)] +pub(crate) struct InlineFormattingContextBuilder { + pub text_segments: Vec<String>, + current_text_offset: usize, + + /// Whether the last processed node ended with whitespace. This is used to + /// implement rule 4 of <https://www.w3.org/TR/css-text-3/#collapse>: + /// + /// > Any collapsible space immediately following another collapsible space—even one + /// > outside the boundary of the inline containing that space, provided both spaces are + /// > within the same inline formatting context—is collapsed to have zero advance width. + /// > (It is invisible, but retains its soft wrap opportunity, if any.) + last_inline_box_ended_with_collapsible_white_space: bool, + + /// Whether or not the current state of the inline formatting context is on a word boundary + /// for the purposes of `text-transform: capitalize`. + on_word_boundary: bool, + + /// Whether or not this inline formatting context will contain floats. + pub contains_floats: bool, + + /// Inline elements are direct descendants of the element that establishes + /// the inline formatting context that this builder builds. + pub root_inline_boxes: Vec<ArcRefCell<InlineLevelBox>>, + + /// Whether or not the inline formatting context under construction has any + /// uncollapsible text content. + pub has_uncollapsible_text_content: bool, + + /// The ongoing stack of inline boxes stack of the builder. + /// + /// Contains all the currently ongoing inline boxes we entered so far. + /// The traversal is at all times as deep in the tree as this stack is, + /// which is why the code doesn't need to keep track of the actual + /// container root (see `handle_inline_level_element`). + /// + /// Whenever the end of a DOM element that represents an inline box is + /// reached, the inline box at the top of this stack is complete and ready + /// to be pushed to the children of the next last ongoing inline box + /// the ongoing inline formatting context if the stack is now empty, + /// which means we reached the end of a child of the actual + /// container root (see `move_to_next_sibling`). + /// + /// When an inline box ends, it's removed from this stack and added to + /// [`Self::root_inline_boxes`]. + inline_box_stack: Vec<InlineBox>, +} + +impl InlineFormattingContextBuilder { + pub(crate) fn new() -> Self { + // For the purposes of `text-transform: capitalize` the start of the IFC is a word boundary. + Self { + on_word_boundary: true, + ..Default::default() + } + } + + pub(crate) fn currently_processing_inline_box(&self) -> bool { + !self.inline_box_stack.is_empty() + } + + /// Return true if this [`InlineFormattingContextBuilder`] is empty for the purposes of ignoring + /// during box tree construction. An IFC is empty if it only contains TextRuns with + /// completely collapsible whitespace. When that happens it can be ignored completely. + pub(crate) fn is_empty(&self) -> bool { + if self.has_uncollapsible_text_content { + return false; + } + + if !self.inline_box_stack.is_empty() { + return false; + } + + fn inline_level_boxes_are_empty(boxes: &[ArcRefCell<InlineLevelBox>]) -> bool { + boxes + .iter() + .all(|inline_level_box| inline_level_box_is_empty(&inline_level_box.borrow())) + } + + fn inline_level_box_is_empty(inline_level_box: &InlineLevelBox) -> bool { + match inline_level_box { + InlineLevelBox::InlineBox(_) => false, + // Text content is handled by `self.has_uncollapsible_text` content above in order + // to avoid having to iterate through the character once again. + InlineLevelBox::TextRun(_) => true, + InlineLevelBox::OutOfFlowAbsolutelyPositionedBox(_) => false, + InlineLevelBox::OutOfFlowFloatBox(_) => false, + InlineLevelBox::Atomic(_) => false, + } + } + + inline_level_boxes_are_empty(&self.root_inline_boxes) + } + + // Retrieves the mutable reference of inline boxes either from the last + // element of a stack or directly from the formatting context, depending on the situation. + fn current_inline_level_boxes(&mut self) -> &mut Vec<ArcRefCell<InlineLevelBox>> { + match self.inline_box_stack.last_mut() { + Some(last) => &mut last.children, + None => &mut self.root_inline_boxes, + } + } + + pub(crate) fn push_atomic( + &mut self, + independent_formatting_context: IndependentFormattingContext, + ) -> ArcRefCell<InlineLevelBox> { + let inline_level_box = + ArcRefCell::new(InlineLevelBox::Atomic(independent_formatting_context)); + self.current_inline_level_boxes() + .push(inline_level_box.clone()); + self.last_inline_box_ended_with_collapsible_white_space = false; + self.on_word_boundary = true; + inline_level_box + } + + pub(crate) fn push_absolutely_positioned_box( + &mut self, + absolutely_positioned_box: AbsolutelyPositionedBox, + ) -> ArcRefCell<InlineLevelBox> { + let absolutely_positioned_box = ArcRefCell::new(absolutely_positioned_box); + let inline_level_box = ArcRefCell::new(InlineLevelBox::OutOfFlowAbsolutelyPositionedBox( + absolutely_positioned_box, + )); + self.current_inline_level_boxes() + .push(inline_level_box.clone()); + inline_level_box + } + + pub(crate) fn push_float_box(&mut self, float_box: FloatBox) -> ArcRefCell<InlineLevelBox> { + let inline_level_box = ArcRefCell::new(InlineLevelBox::OutOfFlowFloatBox(float_box)); + self.current_inline_level_boxes() + .push(inline_level_box.clone()); + self.contains_floats = true; + inline_level_box + } + + pub(crate) fn start_inline_box<'dom, Node: NodeExt<'dom>>( + &mut self, + info: &NodeAndStyleInfo<Node>, + ) { + self.inline_box_stack.push(InlineBox::new(info)) + } + + pub(crate) fn end_inline_box(&mut self) -> ArcRefCell<InlineLevelBox> { + self.end_inline_box_internal(true) + } + + fn end_inline_box_internal(&mut self, is_last_fragment: bool) -> ArcRefCell<InlineLevelBox> { + let mut inline_box = self + .inline_box_stack + .pop() + .expect("no ongoing inline level box found"); + + if is_last_fragment { + inline_box.is_last_fragment = true; + } + + let inline_box = ArcRefCell::new(InlineLevelBox::InlineBox(inline_box)); + self.current_inline_level_boxes().push(inline_box.clone()); + + inline_box + } + + pub(crate) fn push_text<'dom, Node: NodeExt<'dom>>( + &mut self, + text: Cow<'dom, str>, + info: &NodeAndStyleInfo<Node>, + ) { + let white_space_collapse = info.style.clone_white_space_collapse(); + let collapsed = WhitespaceCollapse::new( + text.chars(), + white_space_collapse, + self.last_inline_box_ended_with_collapsible_white_space, + ); + + let text_transform = info.style.clone_text_transform(); + let capitalized_text: String; + let char_iterator: Box<dyn Iterator<Item = char>> = + if text_transform.case_ == TextTransformCase::Capitalize { + // `TextTransformation` doesn't support capitalization, so we must capitalize the whole + // string at once and make a copy. Here `on_word_boundary` indicates whether or not the + // inline formatting context as a whole is on a word boundary. This is different from + // `last_inline_box_ended_with_collapsible_white_space` because the word boundaries are + // between atomic inlines and at the start of the IFC, and because preserved spaces + // are a word boundary. + let collapsed_string: String = collapsed.collect(); + capitalized_text = capitalize_string(&collapsed_string, self.on_word_boundary); + Box::new(capitalized_text.chars()) + } else if !text_transform.is_none() { + // If `text-transform` is active, wrap the `WhitespaceCollapse` iterator in + // a `TextTransformation` iterator. + Box::new(TextTransformation::new(collapsed, text_transform)) + } else { + Box::new(collapsed) + }; + + let white_space_collapse = info.style.clone_white_space_collapse(); + let new_text: String = char_iterator + .map(|character| { + self.has_uncollapsible_text_content |= matches!( + white_space_collapse, + WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces + ) || !character.is_ascii_whitespace() || + (character == '\n' && white_space_collapse != WhiteSpaceCollapse::Collapse); + character + }) + .collect(); + + if new_text.is_empty() { + return; + } + + if let Some(last_character) = new_text.chars().next_back() { + self.on_word_boundary = last_character.is_whitespace(); + self.last_inline_box_ended_with_collapsible_white_space = + self.on_word_boundary && white_space_collapse != WhiteSpaceCollapse::Preserve; + } + + let new_range = self.current_text_offset..self.current_text_offset + new_text.len(); + self.current_text_offset = new_range.end; + self.text_segments.push(new_text); + + let inlines = self.current_inline_level_boxes(); + if let Some(mut last_box) = inlines.last_mut().map(|last| last.borrow_mut()) { + if let InlineLevelBox::TextRun(ref mut text_run) = *last_box { + text_run.text_range.end = new_range.end; + return; + } + } + + inlines.push(ArcRefCell::new(InlineLevelBox::TextRun(TextRun::new( + info.into(), + info.style.clone(), + new_range, + )))); + } + + pub(crate) fn split_around_block_and_finish( + &mut self, + layout_context: &LayoutContext, + text_decoration_line: TextDecorationLine, + has_first_formatted_line: bool, + ) -> Option<InlineFormattingContext> { + if self.is_empty() { + return None; + } + + // Create a new inline builder which will be active after the block splits this inline formatting + // context. It has the same inline box structure as this builder, except the boxes are + // marked as not being the first fragment. No inline content is carried over to this new + // builder. + let mut inline_buidler_from_before_split = std::mem::replace( + self, + InlineFormattingContextBuilder { + on_word_boundary: true, + inline_box_stack: self + .inline_box_stack + .iter() + .map(|inline_box| inline_box.split_around_block()) + .collect(), + ..Default::default() + }, + ); + + // End all ongoing inline boxes in the first builder, but ensure that they are not + // marked as the final fragments, so that they do not get inline end margin, borders, + // and padding. + while !inline_buidler_from_before_split.inline_box_stack.is_empty() { + inline_buidler_from_before_split.end_inline_box_internal(false); + } + + inline_buidler_from_before_split.finish( + layout_context, + text_decoration_line, + has_first_formatted_line, + ) + } + + /// Finish the current inline formatting context, returning [`None`] if the context was empty. + pub(crate) fn finish( + &mut self, + layout_context: &LayoutContext, + text_decoration_line: TextDecorationLine, + has_first_formatted_line: bool, + ) -> Option<InlineFormattingContext> { + if self.is_empty() { + return None; + } + + let old_builder = std::mem::replace(self, InlineFormattingContextBuilder::new()); + assert!(old_builder.inline_box_stack.is_empty()); + + Some(InlineFormattingContext::new_with_builder( + old_builder, + layout_context, + text_decoration_line, + has_first_formatted_line, + )) + } +} + +fn preserve_segment_break() -> bool { + true +} + +pub struct WhitespaceCollapse<InputIterator> { + char_iterator: InputIterator, + white_space_collapse: WhiteSpaceCollapse, + + /// Whether or not we should collapse white space completely at the start of the string. + /// This is true when the last character handled in our owning [`super::InlineFormattingContext`] + /// was collapsible white space. + remove_collapsible_white_space_at_start: bool, + + /// Whether or not the last character produced was newline. There is special behavior + /// we do after each newline. + following_newline: bool, + + /// Whether or not we have seen any non-white space characters, indicating that we are not + /// in a collapsible white space section at the beginning of the string. + have_seen_non_white_space_characters: bool, + + /// Whether the last character that we processed was a non-newline white space character. When + /// collapsing white space we need to wait until the next non-white space character or the end + /// of the string to push a single white space. + inside_white_space: bool, + + /// When we enter a collapsible white space region, we may need to wait to produce a single + /// white space character as soon as we encounter a non-white space character. When that + /// happens we queue up the non-white space character for the next iterator call. + character_pending_to_return: Option<char>, +} + +impl<InputIterator> WhitespaceCollapse<InputIterator> { + pub fn new( + char_iterator: InputIterator, + white_space_collapse: WhiteSpaceCollapse, + trim_beginning_white_space: bool, + ) -> Self { + Self { + char_iterator, + white_space_collapse, + remove_collapsible_white_space_at_start: trim_beginning_white_space, + inside_white_space: false, + following_newline: false, + have_seen_non_white_space_characters: false, + character_pending_to_return: None, + } + } + + fn is_leading_trimmed_white_space(&self) -> bool { + !self.have_seen_non_white_space_characters && self.remove_collapsible_white_space_at_start + } + + /// Whether or not we need to produce a space character if the next character is not a newline + /// and not white space. This happens when we are exiting a section of white space and we + /// waited to produce a single space character for the entire section of white space (but + /// not following or preceding a newline). + fn need_to_produce_space_character_after_white_space(&self) -> bool { + self.inside_white_space && !self.following_newline && !self.is_leading_trimmed_white_space() + } +} + +impl<InputIterator> Iterator for WhitespaceCollapse<InputIterator> +where + InputIterator: Iterator<Item = char>, +{ + type Item = char; + + fn next(&mut self) -> Option<Self::Item> { + // Point 4.1.1 first bullet: + // > If white-space is set to normal, nowrap, or pre-line, whitespace + // > characters are considered collapsible + // If whitespace is not considered collapsible, it is preserved entirely, which + // means that we can simply return the input string exactly. + if self.white_space_collapse == WhiteSpaceCollapse::Preserve || + self.white_space_collapse == WhiteSpaceCollapse::BreakSpaces + { + // From <https://drafts.csswg.org/css-text-3/#white-space-processing>: + // > Carriage returns (U+000D) are treated identically to spaces (U+0020) in all respects. + // + // In the non-preserved case these are converted to space below. + return match self.char_iterator.next() { + Some('\r') => Some(' '), + next => next, + }; + } + + if let Some(character) = self.character_pending_to_return.take() { + self.inside_white_space = false; + self.have_seen_non_white_space_characters = true; + self.following_newline = false; + return Some(character); + } + + while let Some(character) = self.char_iterator.next() { + // Don't push non-newline whitespace immediately. Instead wait to push it until we + // know that it isn't followed by a newline. See `push_pending_whitespace_if_needed` + // above. + if character.is_ascii_whitespace() && character != '\n' { + self.inside_white_space = true; + continue; + } + + // Point 4.1.1: + // > 2. Collapsible segment breaks are transformed for rendering according to the + // > segment break transformation rules. + if character == '\n' { + // From <https://drafts.csswg.org/css-text-3/#line-break-transform> + // (4.1.3 -- the segment break transformation rules): + // + // > When white-space is pre, pre-wrap, or pre-line, segment breaks are not + // > collapsible and are instead transformed into a preserved line feed" + if self.white_space_collapse != WhiteSpaceCollapse::Collapse { + self.inside_white_space = false; + self.following_newline = true; + return Some(character); + + // Point 4.1.3: + // > 1. First, any collapsible segment break immediately following another + // > collapsible segment break is removed. + // > 2. Then any remaining segment break is either transformed into a space (U+0020) + // > or removed depending on the context before and after the break. + } else if !self.following_newline && + preserve_segment_break() && + !self.is_leading_trimmed_white_space() + { + self.inside_white_space = false; + self.following_newline = true; + return Some(' '); + } else { + self.following_newline = true; + continue; + } + } + + // Point 4.1.1: + // > 2. Any sequence of collapsible spaces and tabs immediately preceding or + // > following a segment break is removed. + // > 3. Every collapsible tab is converted to a collapsible space (U+0020). + // > 4. Any collapsible space immediately following another collapsible space—even + // > one outside the boundary of the inline containing that space, provided both + // > spaces are within the same inline formatting context—is collapsed to have zero + // > advance width. + if self.need_to_produce_space_character_after_white_space() { + self.inside_white_space = false; + self.character_pending_to_return = Some(character); + return Some(' '); + } + + self.inside_white_space = false; + self.have_seen_non_white_space_characters = true; + self.following_newline = false; + return Some(character); + } + + if self.need_to_produce_space_character_after_white_space() { + self.inside_white_space = false; + return Some(' '); + } + + None + } + + fn size_hint(&self) -> (usize, Option<usize>) { + self.char_iterator.size_hint() + } + + fn count(self) -> usize + where + Self: Sized, + { + self.char_iterator.count() + } +} + +enum PendingCaseConversionResult { + Uppercase(ToUppercase), + Lowercase(ToLowercase), +} + +impl PendingCaseConversionResult { + fn next(&mut self) -> Option<char> { + match self { + PendingCaseConversionResult::Uppercase(to_uppercase) => to_uppercase.next(), + PendingCaseConversionResult::Lowercase(to_lowercase) => to_lowercase.next(), + } + } +} + +/// This is an interator that consumes a char iterator and produces character transformed +/// by the given CSS `text-transform` value. It currently does not support +/// `text-transform: capitalize` because Unicode segmentation libraries do not support +/// streaming input one character at a time. +pub struct TextTransformation<InputIterator> { + /// The input character iterator. + char_iterator: InputIterator, + /// The `text-transform` value to use. + text_transform: TextTransform, + /// If an uppercasing or lowercasing produces more than one character, this + /// caches them so that they can be returned in subsequent iterator calls. + pending_case_conversion_result: Option<PendingCaseConversionResult>, +} + +impl<InputIterator> TextTransformation<InputIterator> { + pub fn new(char_iterator: InputIterator, text_transform: TextTransform) -> Self { + Self { + char_iterator, + text_transform, + pending_case_conversion_result: None, + } + } +} + +impl<InputIterator> Iterator for TextTransformation<InputIterator> +where + InputIterator: Iterator<Item = char>, +{ + type Item = char; + + fn next(&mut self) -> Option<Self::Item> { + if let Some(character) = self + .pending_case_conversion_result + .as_mut() + .and_then(|result| result.next()) + { + return Some(character); + } + self.pending_case_conversion_result = None; + + for character in self.char_iterator.by_ref() { + match self.text_transform.case_ { + TextTransformCase::None => return Some(character), + TextTransformCase::Uppercase => { + let mut pending_result = + PendingCaseConversionResult::Uppercase(character.to_uppercase()); + if let Some(character) = pending_result.next() { + self.pending_case_conversion_result = Some(pending_result); + return Some(character); + } + }, + TextTransformCase::Lowercase => { + let mut pending_result = + PendingCaseConversionResult::Lowercase(character.to_lowercase()); + if let Some(character) = pending_result.next() { + self.pending_case_conversion_result = Some(pending_result); + return Some(character); + } + }, + // `text-transform: capitalize` currently cannot work on a per-character basis, + // so must be handled outside of this iterator. + // TODO: Add support for `full-width` and `full-size-kana`. + _ => return Some(character), + } + } + None + } +} + +/// Given a string and whether the start of the string represents a word boundary, create a copy of +/// the string with letters after word boundaries capitalized. +fn capitalize_string(string: &str, allow_word_at_start: bool) -> String { + let mut output_string = String::new(); + output_string.reserve(string.len()); + + let mut bounds = string.unicode_word_indices().peekable(); + let mut byte_index = 0; + for character in string.chars() { + let current_byte_index = byte_index; + byte_index += character.len_utf8(); + + if let Some((next_index, _)) = bounds.peek() { + if *next_index == current_byte_index { + bounds.next(); + + if current_byte_index != 0 || allow_word_at_start { + output_string.extend(character.to_uppercase()); + continue; + } + } + } + + output_string.push(character); + } + + output_string +} diff --git a/components/layout_2020/flow/line.rs b/components/layout_2020/flow/inline/line.rs index 77e428aa90e..77e428aa90e 100644 --- a/components/layout_2020/flow/line.rs +++ b/components/layout_2020/flow/inline/line.rs diff --git a/components/layout_2020/flow/inline.rs b/components/layout_2020/flow/inline/mod.rs index 77b6db41db3..4856e86c4bc 100644 --- a/components/layout_2020/flow/inline.rs +++ b/components/layout_2020/flow/inline/mod.rs @@ -68,13 +68,22 @@ //! The code for this phase, can mainly be found in `line.rs`. //! +pub mod construct; +pub mod line; +pub mod text_run; + use std::cell::OnceCell; use std::mem; use app_units::Au; use bitflags::bitflags; +use construct::InlineFormattingContextBuilder; use gfx::font::FontMetrics; use gfx::text::glyph::GlyphStore; +use line::{ + layout_line_items, AbsolutelyPositionedLineItem, AtomicLineItem, FloatLineItem, + InlineBoxLineItem, LineItem, LineItemLayoutState, LineMetrics, TextRunLineItem, +}; use serde::Serialize; use servo_arc::Arc; use style::computed_values::text_wrap_mode::T as TextWrapMode; @@ -91,19 +100,16 @@ use style::values::specified::box_::BaselineSource; use style::values::specified::text::{TextAlignKeyword, TextDecorationLine}; use style::values::specified::{TextAlignLast, TextJustify}; use style::Zero; +use text_run::{add_or_get_font, get_font_for_first_font_for_style, TextRun}; use webrender_api::FontInstanceKey; use super::float::PlacementAmongFloats; -use super::line::{ - layout_line_items, AbsolutelyPositionedLineItem, AtomicLineItem, FloatLineItem, - InlineBoxLineItem, LineItem, LineItemLayoutState, LineMetrics, TextRunLineItem, -}; -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; +use crate::dom::NodeExt; +use crate::dom_traversal::NodeAndStyleInfo; use crate::flow::float::{FloatBox, SequentialLayoutState}; -use crate::flow::FlowLayout; +use crate::flow::{CollapsibleWithParentStartMargin, FlowLayout}; use crate::formatting_contexts::{ Baselines, IndependentFormattingContext, NonReplacedFormattingContextContents, }; @@ -125,6 +131,9 @@ static FONT_SUPERSCRIPT_OFFSET_RATIO: f32 = 0.34; pub(crate) struct InlineFormattingContext { pub(super) inline_level_boxes: Vec<ArcRefCell<InlineLevelBox>>, + /// The text content of this inline formatting context. + pub(super) text_content: String, + /// 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<FontKeyAndMetrics>, @@ -169,6 +178,30 @@ pub(crate) struct InlineBox { pub default_font_index: Option<usize>, } +impl InlineBox { + pub(crate) fn new<'dom, Node: NodeExt<'dom>>(info: &NodeAndStyleInfo<Node>) -> Self { + Self { + base_fragment_info: info.into(), + style: info.style.clone(), + is_first_fragment: true, + is_last_fragment: false, + children: vec![], + default_font_index: None, + } + } + + pub(crate) fn split_around_block(&self) -> Self { + Self { + base_fragment_info: self.base_fragment_info, + style: self.style.clone(), + is_first_fragment: false, + is_last_fragment: false, + children: vec![], + default_font_index: None, + } + } +} + /// Information about the current line under construction for a particular /// [`InlineFormattingContextState`]. This tracks position and size information while /// [`LineItem`]s are collected and is used as input when those [`LineItem`]s are @@ -1541,17 +1574,51 @@ enum InlineFormattingContextIterItem<'a> { } impl InlineFormattingContext { - pub(super) fn new( + pub(super) fn new_with_builder( + builder: InlineFormattingContextBuilder, + layout_context: &LayoutContext, text_decoration_line: TextDecorationLine, has_first_formatted_line: bool, - ) -> InlineFormattingContext { - InlineFormattingContext { - inline_level_boxes: Default::default(), + ) -> Self { + let mut inline_formatting_context = InlineFormattingContext { + text_content: String::new(), + inline_level_boxes: builder.root_inline_boxes, font_metrics: Vec::new(), text_decoration_line, has_first_formatted_line, - contains_floats: false, - } + contains_floats: builder.contains_floats, + }; + + // This is to prevent a double borrow. + let text_content: String = builder.text_segments.into_iter().collect(); + let mut font_metrics = Vec::new(); + + let mut linebreaker = None; + inline_formatting_context.foreach(|iter_item| match iter_item { + InlineFormattingContextIterItem::Item(InlineLevelBox::TextRun(ref mut text_run)) => { + text_run.break_and_shape( + &text_content[text_run.text_range.clone()], + &layout_context.font_context, + &mut linebreaker, + &mut font_metrics, + ); + }, + InlineFormattingContextIterItem::Item(InlineLevelBox::InlineBox(inline_box)) => { + if let Some(font) = get_font_for_first_font_for_style( + &inline_box.style, + &layout_context.font_context, + ) { + inline_box.default_font_index = Some(add_or_get_font(&font, &mut font_metrics)); + } + }, + InlineFormattingContextIterItem::Item(InlineLevelBox::Atomic(_)) => {}, + _ => {}, + }); + + inline_formatting_context.text_content = text_content; + inline_formatting_context.font_metrics = font_metrics; + + inline_formatting_context } fn foreach(&self, mut func: impl FnMut(InlineFormattingContextIterItem)) { @@ -1740,75 +1807,6 @@ impl InlineFormattingContext { baselines: ifc.baselines, } } - - /// Return true if this [InlineFormattingContext] is empty for the purposes of ignoring - /// during box tree construction. An IFC is empty if it only contains TextRuns with - /// completely collapsible whitespace. When that happens it can be ignored completely. - pub fn is_empty(&self) -> bool { - fn inline_level_boxes_are_empty(boxes: &[ArcRefCell<InlineLevelBox>]) -> bool { - boxes - .iter() - .all(|inline_level_box| inline_level_box_is_empty(&inline_level_box.borrow())) - } - - fn inline_level_box_is_empty(inline_level_box: &InlineLevelBox) -> bool { - match inline_level_box { - InlineLevelBox::InlineBox(_) => false, - InlineLevelBox::TextRun(text_run) => !text_run.has_uncollapsible_content(), - InlineLevelBox::OutOfFlowAbsolutelyPositionedBox(_) => false, - InlineLevelBox::OutOfFlowFloatBox(_) => false, - InlineLevelBox::Atomic(_) => false, - } - } - - inline_level_boxes_are_empty(&self.inline_level_boxes) - } - - /// 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 ifc_fonts = Vec::new(); - - // Whether the last processed node ended with whitespace. This is used to - // implement rule 4 of <https://www.w3.org/TR/css-text-3/#collapse>: - // - // > Any collapsible space immediately following another collapsible space—even one - // > outside the boundary of the inline containing that space, provided both spaces are - // > within the same inline formatting context—is collapsed to have zero advance width. - // > (It is invisible, but retains its soft wrap opportunity, if any.) - let mut last_inline_box_ended_with_white_space = false; - - // For the purposes of `text-transform: capitalize` the start of the IFC is a word boundary. - let mut on_word_boundary = true; - - 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.font_context, - &mut linebreaker, - &mut ifc_fonts, - &mut last_inline_box_ended_with_white_space, - &mut on_word_boundary, - ); - }, - InlineFormattingContextIterItem::Item(InlineLevelBox::InlineBox(inline_box)) => { - if let Some(font) = get_font_for_first_font_for_style( - &inline_box.style, - &layout_context.font_context, - ) { - inline_box.default_font_index = Some(add_or_get_font(&font, &mut ifc_fonts)); - } - }, - InlineFormattingContextIterItem::Item(InlineLevelBox::Atomic(_)) => { - last_inline_box_ended_with_white_space = false; - on_word_boundary = true; - }, - _ => {}, - }); - - self.font_metrics = ifc_fonts; - } } impl InlineContainerState { @@ -2427,7 +2425,7 @@ impl<'a> ContentSizesComputation<'a> { if run.glyph_store.is_whitespace() { // 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_preserved_newline(segment, run) { + if run.is_single_preserved_newline() { self.forced_line_break(); self.current_line = ContentSizes::zero(); continue; diff --git a/components/layout_2020/flow/text_run.rs b/components/layout_2020/flow/inline/text_run.rs index 215ec89f6b3..e916be287c5 100644 --- a/components/layout_2020/flow/text_run.rs +++ b/components/layout_2020/flow/inline/text_run.rs @@ -2,8 +2,8 @@ * 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::char::{ToLowercase, ToUppercase}; use std::mem; +use std::ops::Range; use app_units::Au; use gfx::font::{FontRef, ShapingFlags, ShapingOptions}; @@ -12,7 +12,7 @@ use gfx::font_context::FontContext; use gfx::text::glyph::GlyphRun; use gfx_traits::ByteIndex; use log::warn; -use range::Range; +use range::Range as ServoRange; use serde::Serialize; use servo_arc::Arc; use style::computed_values::text_rendering::T as TextRendering; @@ -22,13 +22,10 @@ use style::properties::style_structs::InheritedText; use style::properties::ComputedValues; use style::str::char_is_whitespace; use style::values::computed::OverflowWrap; -use style::values::specified::text::TextTransformCase; -use style::values::specified::TextTransform; use unicode_script::Script; -use unicode_segmentation::UnicodeSegmentation; use xi_unicode::{linebreak_property, LineBreakLeafIter}; -use super::inline::{FontKeyAndMetrics, InlineFormattingContextState}; +use super::{FontKeyAndMetrics, InlineFormattingContextState}; use crate::fragment_tree::BaseFragmentInfo; // These constants are the xi-unicode line breaking classes that are defined in @@ -45,7 +42,7 @@ pub(crate) struct TextRun { pub base_fragment_info: BaseFragmentInfo, #[serde(skip_serializing)] pub parent_style: Arc<ComputedValues>, - pub text: String, + pub text_range: Range<usize>, /// The text of this [`TextRun`] with a font selected, broken into unbreakable /// segments, and shaped. @@ -88,7 +85,7 @@ pub(crate) struct TextRunSegment { pub script: Script, /// The range of bytes in the [`TextRun`]'s text that this segment covers. - pub range: Range<ByteIndex>, + pub range: ServoRange<ByteIndex>, /// Whether or not the linebreaker said that we should allow a line break at the start of this /// segment. @@ -103,7 +100,7 @@ impl TextRunSegment { Self { script, font_index, - range: Range::new(byte_index, ByteIndex(0)), + range: ServoRange::new(byte_index, ByteIndex(0)), runs: Vec::new(), break_at_start: false, } @@ -152,7 +149,7 @@ impl TextRunSegment { // 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_preserved_newline(self, run) { + if run.is_single_preserved_newline() { ifc.defer_forced_line_break(); continue; } @@ -176,52 +173,26 @@ impl TextRun { pub(crate) fn new( base_fragment_info: BaseFragmentInfo, parent_style: Arc<ComputedValues>, - text: String, + text_range: Range<usize>, ) -> Self { Self { base_fragment_info, parent_style, - text, + text_range, shaped_text: Vec::new(), prevent_soft_wrap_opportunity_at_start: false, prevent_soft_wrap_opportunity_at_end: false, } } - /// Whether or not this [`TextRun`] has uncollapsible content. This is used - /// to determine if an [`super::InlineFormattingContext`] is considered empty or not. - pub(super) fn has_uncollapsible_content(&self) -> bool { - let white_space_collapse = self.parent_style.clone_white_space_collapse(); - if white_space_collapse == WhiteSpaceCollapse::Preserve && !self.text.is_empty() { - return true; - } - - for character in self.text.chars() { - if !character.is_ascii_whitespace() { - return true; - } - if character == '\n' && white_space_collapse != WhiteSpaceCollapse::Collapse { - return true; - } - } - - false - } - pub(super) fn break_and_shape( &mut self, + text_content: &str, font_context: &FontContext<FontCacheThread>, linebreaker: &mut Option<LineBreakLeafIter>, font_cache: &mut Vec<FontKeyAndMetrics>, - last_inline_box_ended_with_collapsible_white_space: &mut bool, - on_word_boundary: &mut bool, ) { - let segment_results = self.segment_text( - font_context, - font_cache, - last_inline_box_ended_with_collapsible_white_space, - on_word_boundary, - ); + let segment_results = self.segment_text(text_content, 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)) @@ -261,7 +232,7 @@ impl TextRun { }; (segment.runs, segment.break_at_start) = break_and_shape( font, - &self.text[segment.range.begin().0 as usize..segment.range.end().0 as usize], + &text_content[segment.range.begin().0 as usize..segment.range.end().0 as usize], &inherited_text_style, &shaping_options, linebreaker, @@ -280,107 +251,65 @@ impl TextRun { /// [`super::InlineFormattingContext`]. fn segment_text( &mut self, + text_content: &str, font_context: &FontContext<FontCacheThread>, font_cache: &mut Vec<FontKeyAndMetrics>, - last_inline_box_ended_with_collapsible_white_space: &mut bool, - on_word_boundary: &mut bool, ) -> 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(); - // TODO: Eventually the text should come directly from the Cow strings of the DOM nodes. - let text = std::mem::take(&mut self.text); - let white_space_collapse = self.parent_style.clone_white_space_collapse(); - let collapsed = WhitespaceCollapse::new( - text.as_str().chars(), - white_space_collapse, - *last_inline_box_ended_with_collapsible_white_space, - ); - - let text_transform = self.parent_style.clone_text_transform(); - let collected_text: String; - let char_iterator: Box<dyn Iterator<Item = char>> = - if text_transform.case_ == TextTransformCase::Capitalize { - // `TextTransformation` doesn't support capitalization, so we must capitalize the whole - // string at once and make a copy. Here `on_word_boundary` indicates whether or not the - // inline formatting context as a whole is on a word boundary. This is different from - // `last_inline_box_ended_with_collapsible_white_space` because the word boundaries are - // between atomic inlines and at the start of the IFC, and because preserved spaces - // are a word boundary. - let collapsed_string: String = collapsed.collect(); - collected_text = capitalize_string(&collapsed_string, *on_word_boundary); - Box::new(collected_text.chars()) - } else if !text_transform.is_none() { - // If `text-transform` is active, wrap the `WhitespaceCollapse` iterator in - // a `TextTransformation` iterator. - Box::new(TextTransformation::new(collapsed, text_transform)) - } else { - Box::new(collapsed) - }; - let char_iterator = TwoCharsAtATimeIterator::new(char_iterator); - + let char_iterator = TwoCharsAtATimeIterator::new(text_content.chars()); let mut next_byte_index = 0; - let text = char_iterator - .map(|(character, next_character)| { - let current_byte_index = next_byte_index; - next_byte_index += character.len_utf8(); - - *on_word_boundary = character.is_whitespace(); - *last_inline_box_ended_with_collapsible_white_space = - *on_word_boundary && white_space_collapse != WhiteSpaceCollapse::Preserve; - - let prevents_soft_wrap_opportunity = - char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character); - if current_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) { - return character; - } - - let font = match font_group.write().find_by_codepoint( - font_context, - character, - next_character, - ) { - Some(font) => font, - None => return character, - }; + for (character, next_character) in char_iterator { + let current_byte_index = next_byte_index; + next_byte_index += character.len_utf8(); + + let prevents_soft_wrap_opportunity = + char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character); + if current_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 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) { - return character; - } - } + if char_does_not_change_font(character) { + continue; + } - let font_index = add_or_get_font(&font, font_cache); + let Some(font) = + font_group + .write() + .find_by_codepoint(font_context, character, next_character) + else { + continue; + }; - // 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 start_byte_index = match current { - Some(_) => ByteIndex(current_byte_index as isize), - None => ByteIndex(0_isize), - }; - let new = ( - TextRunSegment::new(font_index, script, start_byte_index), - font, - ); - if let Some(mut finished) = current.replace(new) { - finished.0.range.extend_to(start_byte_index); - results.push(finished); + // 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; } + } - character - }) - .collect(); + let font_index = add_or_get_font(&font, font_cache); - let _ = std::mem::replace(&mut self.text, text); + // 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 start_byte_index = match current { + Some(_) => ByteIndex(current_byte_index as isize), + None => ByteIndex(0_isize), + }; + let new = ( + TextRunSegment::new(font_index, script, start_byte_index), + font, + ); + if let Some(mut finished) = current.replace(new) { + finished.0.range.extend_to(start_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. @@ -399,7 +328,7 @@ impl TextRun { last_segment .0 .range - .extend_to(ByteIndex(self.text.len() as isize)); + .extend_to(ByteIndex(text_content.len() as isize)); results.push(last_segment); } @@ -407,7 +336,7 @@ impl TextRun { } pub(super) fn layout_into_line_items(&self, ifc: &mut InlineFormattingContextState) { - if self.text.is_empty() { + if self.text_range.is_empty() { return; } @@ -430,25 +359,6 @@ impl TextRun { ifc.prevent_soft_wrap_opportunity_before_next_atomic = self.prevent_soft_wrap_opportunity_at_end; } - - pub(super) fn glyph_run_is_preserved_newline( - &self, - text_run_segment: &TextRunSegment, - run: &GlyphRun, - ) -> bool { - if !run.glyph_store.is_whitespace() || run.range.length() != ByteIndex(1) { - return false; - } - if self.parent_style.get_inherited_text().white_space_collapse == - WhiteSpaceCollapse::Collapse - { - return false; - } - - let byte_offset = (text_run_segment.range.begin() + run.range.begin()).to_usize(); - let byte = self.text.as_bytes().get(byte_offset); - byte == Some(&b'\n') - } } /// Whether or not this character will rpevent a soft wrap opportunity when it @@ -517,295 +427,7 @@ pub(super) fn get_font_for_first_font_for_style( } font } - -fn preserve_segment_break() -> bool { - true -} - -pub struct WhitespaceCollapse<InputIterator> { - char_iterator: InputIterator, - white_space_collapse: WhiteSpaceCollapse, - - /// Whether or not we should collapse white space completely at the start of the string. - /// This is true when the last character handled in our owning [`super::InlineFormattingContext`] - /// was collapsible white space. - remove_collapsible_white_space_at_start: bool, - - /// Whether or not the last character produced was newline. There is special behavior - /// we do after each newline. - following_newline: bool, - - /// Whether or not we have seen any non-white space characters, indicating that we are not - /// in a collapsible white space section at the beginning of the string. - have_seen_non_white_space_characters: bool, - - /// Whether the last character that we processed was a non-newline white space character. When - /// collapsing white space we need to wait until the next non-white space character or the end - /// of the string to push a single white space. - inside_white_space: bool, - - /// When we enter a collapsible white space region, we may need to wait to produce a single - /// white space character as soon as we encounter a non-white space character. When that - /// happens we queue up the non-white space character for the next iterator call. - character_pending_to_return: Option<char>, -} - -impl<InputIterator> WhitespaceCollapse<InputIterator> { - pub fn new( - char_iterator: InputIterator, - white_space_collapse: WhiteSpaceCollapse, - trim_beginning_white_space: bool, - ) -> Self { - Self { - char_iterator, - white_space_collapse, - remove_collapsible_white_space_at_start: trim_beginning_white_space, - inside_white_space: false, - following_newline: false, - have_seen_non_white_space_characters: false, - character_pending_to_return: None, - } - } - - fn is_leading_trimmed_white_space(&self) -> bool { - !self.have_seen_non_white_space_characters && self.remove_collapsible_white_space_at_start - } - - /// Whether or not we need to produce a space character if the next character is not a newline - /// and not white space. This happens when we are exiting a section of white space and we - /// waited to produce a single space character for the entire section of white space (but - /// not following or preceding a newline). - fn need_to_produce_space_character_after_white_space(&self) -> bool { - self.inside_white_space && !self.following_newline && !self.is_leading_trimmed_white_space() - } -} - -impl<InputIterator> Iterator for WhitespaceCollapse<InputIterator> -where - InputIterator: Iterator<Item = char>, -{ - type Item = char; - - fn next(&mut self) -> Option<Self::Item> { - // Point 4.1.1 first bullet: - // > If white-space is set to normal, nowrap, or pre-line, whitespace - // > characters are considered collapsible - // If whitespace is not considered collapsible, it is preserved entirely, which - // means that we can simply return the input string exactly. - if self.white_space_collapse == WhiteSpaceCollapse::Preserve || - self.white_space_collapse == WhiteSpaceCollapse::BreakSpaces - { - // From <https://drafts.csswg.org/css-text-3/#white-space-processing>: - // > Carriage returns (U+000D) are treated identically to spaces (U+0020) in all respects. - // - // In the non-preserved case these are converted to space below. - return match self.char_iterator.next() { - Some('\r') => Some(' '), - next => next, - }; - } - - if let Some(character) = self.character_pending_to_return.take() { - self.inside_white_space = false; - self.have_seen_non_white_space_characters = true; - self.following_newline = false; - return Some(character); - } - - while let Some(character) = self.char_iterator.next() { - // Don't push non-newline whitespace immediately. Instead wait to push it until we - // know that it isn't followed by a newline. See `push_pending_whitespace_if_needed` - // above. - if character.is_ascii_whitespace() && character != '\n' { - self.inside_white_space = true; - continue; - } - - // Point 4.1.1: - // > 2. Collapsible segment breaks are transformed for rendering according to the - // > segment break transformation rules. - if character == '\n' { - // From <https://drafts.csswg.org/css-text-3/#line-break-transform> - // (4.1.3 -- the segment break transformation rules): - // - // > When white-space is pre, pre-wrap, or pre-line, segment breaks are not - // > collapsible and are instead transformed into a preserved line feed" - if self.white_space_collapse != WhiteSpaceCollapse::Collapse { - self.inside_white_space = false; - self.following_newline = true; - return Some(character); - - // Point 4.1.3: - // > 1. First, any collapsible segment break immediately following another - // > collapsible segment break is removed. - // > 2. Then any remaining segment break is either transformed into a space (U+0020) - // > or removed depending on the context before and after the break. - } else if !self.following_newline && - preserve_segment_break() && - !self.is_leading_trimmed_white_space() - { - self.inside_white_space = false; - self.following_newline = true; - return Some(' '); - } else { - self.following_newline = true; - continue; - } - } - - // Point 4.1.1: - // > 2. Any sequence of collapsible spaces and tabs immediately preceding or - // > following a segment break is removed. - // > 3. Every collapsible tab is converted to a collapsible space (U+0020). - // > 4. Any collapsible space immediately following another collapsible space—even - // > one outside the boundary of the inline containing that space, provided both - // > spaces are within the same inline formatting context—is collapsed to have zero - // > advance width. - if self.need_to_produce_space_character_after_white_space() { - self.inside_white_space = false; - self.character_pending_to_return = Some(character); - return Some(' '); - } - - self.inside_white_space = false; - self.have_seen_non_white_space_characters = true; - self.following_newline = false; - return Some(character); - } - - if self.need_to_produce_space_character_after_white_space() { - self.inside_white_space = false; - return Some(' '); - } - - None - } - - fn size_hint(&self) -> (usize, Option<usize>) { - self.char_iterator.size_hint() - } - - fn count(self) -> usize - where - Self: Sized, - { - self.char_iterator.count() - } -} - -enum PendingCaseConversionResult { - Uppercase(ToUppercase), - Lowercase(ToLowercase), -} - -impl PendingCaseConversionResult { - fn next(&mut self) -> Option<char> { - match self { - PendingCaseConversionResult::Uppercase(to_uppercase) => to_uppercase.next(), - PendingCaseConversionResult::Lowercase(to_lowercase) => to_lowercase.next(), - } - } -} - -/// This is an interator that consumes a char iterator and produces character transformed -/// by the given CSS `text-transform` value. It currently does not support -/// `text-transform: capitalize` because Unicode segmentation libraries do not support -/// streaming input one character at a time. -pub struct TextTransformation<InputIterator> { - /// The input character iterator. - char_iterator: InputIterator, - /// The `text-transform` value to use. - text_transform: TextTransform, - /// If an uppercasing or lowercasing produces more than one character, this - /// caches them so that they can be returned in subsequent iterator calls. - pending_case_conversion_result: Option<PendingCaseConversionResult>, -} - -impl<InputIterator> TextTransformation<InputIterator> { - pub fn new(char_iterator: InputIterator, text_transform: TextTransform) -> Self { - Self { - char_iterator, - text_transform, - pending_case_conversion_result: None, - } - } -} - -impl<InputIterator> Iterator for TextTransformation<InputIterator> -where - InputIterator: Iterator<Item = char>, -{ - type Item = char; - - fn next(&mut self) -> Option<Self::Item> { - if let Some(character) = self - .pending_case_conversion_result - .as_mut() - .and_then(|result| result.next()) - { - return Some(character); - } - self.pending_case_conversion_result = None; - - for character in self.char_iterator.by_ref() { - match self.text_transform.case_ { - TextTransformCase::None => return Some(character), - TextTransformCase::Uppercase => { - let mut pending_result = - PendingCaseConversionResult::Uppercase(character.to_uppercase()); - if let Some(character) = pending_result.next() { - self.pending_case_conversion_result = Some(pending_result); - return Some(character); - } - }, - TextTransformCase::Lowercase => { - let mut pending_result = - PendingCaseConversionResult::Lowercase(character.to_lowercase()); - if let Some(character) = pending_result.next() { - self.pending_case_conversion_result = Some(pending_result); - return Some(character); - } - }, - // `text-transform: capitalize` currently cannot work on a per-character basis, - // so must be handled outside of this iterator. - // TODO: Add support for `full-width` and `full-size-kana`. - _ => return Some(character), - } - } - None - } -} - -/// Given a string and whether the start of the string represents a word boundary, create a copy of -/// the string with letters after word boundaries capitalized. -fn capitalize_string(string: &str, allow_word_at_start: bool) -> String { - let mut output_string = String::new(); - output_string.reserve(string.len()); - - let mut bounds = string.unicode_word_indices().peekable(); - let mut byte_index = 0; - for character in string.chars() { - let current_byte_index = byte_index; - byte_index += character.len_utf8(); - - if let Some((next_index, _)) = bounds.peek() { - if *next_index == current_byte_index { - bounds.next(); - - if current_byte_index != 0 || allow_word_at_start { - output_string.extend(character.to_uppercase()); - continue; - } - } - } - - output_string.push(character); - } - - output_string -} - -pub struct TwoCharsAtATimeIterator<InputIterator> { +pub(crate) struct TwoCharsAtATimeIterator<InputIterator> { /// The input character iterator. iterator: InputIterator, /// The first character to produce in the next run of the iterator. @@ -860,10 +482,10 @@ pub fn break_and_shape( let breaker = breaker.as_mut().unwrap(); - let mut push_range = |range: &std::ops::Range<usize>, options: &ShapingOptions| { + let mut push_range = |range: &Range<usize>, options: &ShapingOptions| { glyphs.push(GlyphRun { glyph_store: font.shape_text(&text[range.clone()], options), - range: Range::new( + range: ServoRange::new( ByteIndex(range.start as isize), ByteIndex(range.len() as isize), ), diff --git a/components/layout_2020/flow/mod.rs b/components/layout_2020/flow/mod.rs index 05a56c6f05f..6a8249201dd 100644 --- a/components/layout_2020/flow/mod.rs +++ b/components/layout_2020/flow/mod.rs @@ -6,6 +6,7 @@ //! Flow layout, also known as block-and-inline layout. use app_units::Au; +use inline::InlineFormattingContext; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use serde::Serialize; use servo_arc::Arc; @@ -22,7 +23,6 @@ use crate::context::LayoutContext; use crate::flow::float::{ ContainingBlockPositionInfo, FloatBox, PlacementAmongFloats, SequentialLayoutState, }; -use crate::flow::inline::InlineFormattingContext; use crate::formatting_contexts::{ Baselines, IndependentFormattingContext, IndependentLayout, NonReplacedFormattingContext, }; @@ -39,9 +39,7 @@ use crate::ContainingBlock; mod construct; pub mod float; pub mod inline; -mod line; mod root; -pub mod text_run; pub(crate) use construct::BlockContainerBuilder; pub use root::{BoxTree, CanvasBackground}; @@ -194,7 +192,7 @@ impl BlockLevelBox { } } -struct FlowLayout { +pub(crate) struct FlowLayout { pub fragments: Vec<Fragment>, pub content_block_size: Length, pub collapsible_margins_in_children: CollapsedBlockMargins, @@ -205,7 +203,7 @@ struct FlowLayout { } #[derive(Clone, Copy)] -struct CollapsibleWithParentStartMargin(bool); +pub(crate) struct CollapsibleWithParentStartMargin(bool); /// The contentes of a BlockContainer created to render a list marker /// for a list that has `list-style-position: outside`. diff --git a/components/layout_2020/tests/text.rs b/components/layout_2020/tests/text.rs index da44c1eec30..74ed9c6d263 100644 --- a/components/layout_2020/tests/text.rs +++ b/components/layout_2020/tests/text.rs @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ mod text { - use layout_2020::flow::text_run::WhitespaceCollapse; + use layout_2020::flow::inline::construct::WhitespaceCollapse; use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse; #[test] |