aboutsummaryrefslogtreecommitdiffstats
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-rw-r--r--components/gfx/font.rs3
-rw-r--r--components/gfx/text/glyph.rs20
-rw-r--r--components/layout_2020/flexbox/construct.rs57
-rw-r--r--components/layout_2020/flow/construct.rs393
-rw-r--r--components/layout_2020/flow/inline/construct.rs610
-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.rs8
-rw-r--r--components/layout_2020/tests/text.rs2
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]