servo/components/layout_2020/flow/construct.rs
Martin Robinson 094f7845b1
layout: Shape text only once (#31146)
Shape text during InlineFormattingContext construction rather than doing
it twice during fragment tree construction. This is a step on the way
toward proper font fallback.

This also moves all `TextRun` related code into `text_run.rs` to try to
trim down the size of `inline.rs`.
<!-- Please describe your changes on the following line: -->


---
<!-- Thank you for contributing to Servo! Please replace each `[ ]` by
`[X]` when the step is complete, and replace `___` with appropriate
data: -->
- [x] `./mach build -d` does not report any errors
- [x] `./mach test-tidy` does not report any errors
- [x] These changes do not require tests because this should only have
performance impacts.

<!-- Also, please make sure that "Allow edits from maintainers" checkbox
is checked, so that we can help you if you get stuck somewhere along the
way.-->

<!-- Pull requests that do not address these steps are welcome, but they
will require additional verification as part of the review process. -->
2024-01-25 14:33:47 +00:00

985 lines
37 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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::convert::{TryFrom, TryInto};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use servo_arc::Arc;
use style::computed_values::white_space::T as WhiteSpace;
use style::properties::longhands::list_style_position::computed_value::T as ListStylePosition;
use style::properties::ComputedValues;
use style::selector_parser::PseudoElement;
use style::str::char_is_whitespace;
use style::values::specified::text::TextDecorationLine;
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::float::FloatBox;
use crate::flow::inline::{InlineBox, InlineFormattingContext, InlineLevelBox};
use crate::flow::text_run::TextRun;
use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox};
use crate::formatting_contexts::IndependentFormattingContext;
use crate::positioned::AbsolutelyPositionedBox;
use crate::style_ext::{ComputedValuesExt, DisplayGeneratingBox, DisplayInside, DisplayOutside};
use crate::table::{AnonymousTableContent, Table};
impl BlockFormattingContext {
pub(crate) fn construct<'dom, Node>(
context: &LayoutContext,
info: &NodeAndStyleInfo<Node>,
contents: NonReplacedContents,
propagated_text_decoration_line: TextDecorationLine,
is_list_item: bool,
) -> Self
where
Node: NodeExt<'dom>,
{
Self::from_block_container(BlockContainer::construct(
context,
info,
contents,
propagated_text_decoration_line,
is_list_item,
))
}
pub(crate) fn from_block_container(contents: BlockContainer) -> Self {
let contains_floats = contents.contains_floats();
Self {
contents,
contains_floats,
}
}
pub fn construct_for_text_runs<'dom>(
runs: impl Iterator<Item = TextRun>,
layout_context: &LayoutContext,
text_decoration_line: TextDecorationLine,
) -> Self {
// FIXME: do white space collapsing
let inline_level_boxes = runs
.map(|run| ArcRefCell::new(InlineLevelBox::TextRun(run)))
.collect();
let ifc = InlineFormattingContext {
inline_level_boxes,
text_decoration_line,
has_first_formatted_line: true,
contains_floats: false,
ends_with_whitespace: false,
};
Self {
contents: BlockContainer::construct_inline_formatting_context(layout_context, ifc),
contains_floats: false,
}
}
}
struct BlockLevelJob<'dom, Node> {
info: NodeAndStyleInfo<Node>,
box_slot: BoxSlot<'dom>,
kind: BlockLevelCreator,
}
enum BlockLevelCreator {
SameFormattingContextBlock(IntermediateBlockContainer),
Independent {
display_inside: DisplayInside,
contents: Contents,
propagated_text_decoration_line: TextDecorationLine,
},
OutOfFlowAbsolutelyPositionedBox {
display_inside: DisplayInside,
contents: Contents,
},
OutOfFlowFloatBox {
display_inside: DisplayInside,
contents: Contents,
},
AnonymousTable {
table_block: ArcRefCell<BlockLevelBox>,
},
}
/// A block container that may still have to be constructed.
///
/// Represents either the inline formatting context of an anonymous block
/// box or the yet-to-be-computed block container generated from the children
/// of a given element.
///
/// Deferring allows using rayons `into_par_iter`.
enum IntermediateBlockContainer {
InlineFormattingContext(InlineFormattingContext),
Deferred {
contents: NonReplacedContents,
propagated_text_decoration_line: TextDecorationLine,
is_list_item: bool,
},
}
/// A builder for a block container.
///
/// This builder starts from the first child of a given DOM node
/// and does a preorder traversal of all of its inclusive siblings.
pub(crate) struct BlockContainerBuilder<'dom, 'style, Node> {
context: &'style LayoutContext<'style>,
/// This NodeAndStyleInfo contains the root node, the corresponding pseudo
/// content designator, and the block container style.
info: &'style NodeAndStyleInfo<Node>,
/// The list of block-level boxes to be built for the final block container.
///
/// Contains all the block-level jobs we found traversing the tree
/// so far, if this is empty at the end of the traversal and the ongoing
/// inline formatting context is not empty, the block container establishes
/// an inline formatting context (see end of `build`).
///
/// DOM nodes which represent block-level boxes are immediately pushed
/// to this list with their style without ever being traversed at this
/// point, instead we just move to their next sibling. If the DOM node
/// doesn't have a next sibling, we either reached the end of the container
/// root or there are ongoing inline-level boxes
/// (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>,
/// The style of the anonymous block boxes pushed to the list of block-level
/// boxes, if any (see `end_ongoing_inline_formatting_context`).
anonymous_style: Option<Arc<ComputedValues>>,
/// A collection of content that is being added to an anonymous table. This is
/// composed of any sequence of internal table elements or table captions that
/// are found outside of a table.
anonymous_table_content: Vec<AnonymousTableContent<'dom, Node>>,
}
impl BlockContainer {
pub fn construct<'dom, Node>(
context: &LayoutContext,
info: &NodeAndStyleInfo<Node>,
contents: NonReplacedContents,
propagated_text_decoration_line: TextDecorationLine,
is_list_item: bool,
) -> BlockContainer
where
Node: NodeExt<'dom>,
{
let mut builder =
BlockContainerBuilder::new(context, info, propagated_text_decoration_line);
if is_list_item {
if let Some(marker_contents) = crate::lists::make_marker(context, info) {
let _position = info.style.clone_list_style_position();
// FIXME: implement support for `outside` and remove this:
let position = ListStylePosition::Inside;
match position {
ListStylePosition::Inside => {
builder.handle_list_item_marker_inside(info, marker_contents)
},
ListStylePosition::Outside => {
// FIXME: implement layout for this case
// https://github.com/servo/servo/issues/27383
// and enable `list-style-position` and the `list-style` shorthand in Stylo.
},
}
}
}
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>
where
Node: NodeExt<'dom>,
{
pub(crate) fn new(
context: &'style LayoutContext,
info: &'style NodeAndStyleInfo<Node>,
propagated_text_decoration_line: TextDecorationLine,
) -> Self {
let text_decoration_line =
propagated_text_decoration_line | info.style.clone_text_decoration_line();
BlockContainerBuilder {
context,
info,
block_level_boxes: Vec::new(),
ongoing_inline_formatting_context: InlineFormattingContext::new(
text_decoration_line,
/* has_first_formatted_line = */ true,
/* ends_with_whitespace */ false,
),
ongoing_inline_boxes_stack: Vec::new(),
anonymous_style: None,
anonymous_table_content: Vec::new(),
}
}
pub(crate) fn finish(mut self) -> BlockContainer {
debug_assert!(self.ongoing_inline_boxes_stack.is_empty());
self.finish_anonymous_table_if_needed();
if !self.ongoing_inline_formatting_context.is_empty() {
if self.block_level_boxes.is_empty() {
return BlockContainer::construct_inline_formatting_context(
self.context,
self.ongoing_inline_formatting_context,
);
}
self.end_ongoing_inline_formatting_context();
}
let context = self.context;
let block_level_boxes = if self.context.use_rayon {
self.block_level_boxes
.into_par_iter()
.map(|block_level_job| block_level_job.finish(context))
.collect()
} else {
self.block_level_boxes
.into_iter()
.map(|block_level_job| block_level_job.finish(context))
.collect()
};
BlockContainer::BlockLevelBoxes(block_level_boxes)
}
fn finish_anonymous_table_if_needed(&mut self) {
if self.anonymous_table_content.is_empty() {
return;
}
// 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
};
let contents: Vec<AnonymousTableContent<'dom, Node>> =
self.anonymous_table_content.drain(..).collect();
let last_text = match contents.last() {
Some(AnonymousTableContent::Text(info, text)) => Some((info.clone(), text.clone())),
_ => None,
};
let ifc = Table::construct_anonymous(
self.context,
self.info,
contents,
propagated_text_decoration_line,
);
if inline_table {
self.ongoing_inline_formatting_context.ends_with_whitespace = false;
self.current_inline_level_boxes()
.push(ArcRefCell::new(InlineLevelBox::Atomic(ifc)));
} else {
let anonymous_info = self.info.new_replacing_style(ifc.style().clone());
let table_block = ArcRefCell::new(BlockLevelBox::Independent(ifc));
self.block_level_boxes.push(BlockLevelJob {
info: anonymous_info,
box_slot: BoxSlot::dummy(),
kind: BlockLevelCreator::AnonymousTable { table_block },
});
}
// If the last element in the anonymous table content is whitespace, that
// whitespace doesn't actually belong to the table. It should be processed outside
// ie become a space between the anonymous table and the rest of the block
// content. Anonymous tables are really only constructed around internal table
// elements and the whitespace between them, so this trailing whitespace should
// not be included.
//
// See https://drafts.csswg.org/css-tables/#fixup-algorithm sections "Remove
// irrelevant boxes" and "Generate missing parents."
if let Some((info, text)) = last_text {
self.handle_text(&info, text);
}
}
}
impl<'dom, Node> TraversalHandler<'dom, Node> for BlockContainerBuilder<'dom, '_, Node>
where
Node: NodeExt<'dom>,
{
fn handle_element(
&mut self,
info: &NodeAndStyleInfo<Node>,
display: DisplayGeneratingBox,
contents: Contents,
box_slot: BoxSlot<'dom>,
) {
match display {
DisplayGeneratingBox::OutsideInside { outside, inside } => {
self.finish_anonymous_table_if_needed();
match outside {
DisplayOutside::Inline => box_slot.set(LayoutBox::InlineLevel(
self.handle_inline_level_element(info, inside, contents),
)),
DisplayOutside::Block => {
let box_style = info.style.get_box();
// Floats and abspos cause blockification, so they only happen in this case.
// https://drafts.csswg.org/css2/visuren.html#dis-pos-flo
if box_style.position.is_absolutely_positioned() {
self.handle_absolutely_positioned_element(
info, inside, contents, box_slot,
)
} else if box_style.float.is_floating() {
self.handle_float_element(info, inside, contents, box_slot)
} else {
self.handle_block_level_element(info, inside, contents, box_slot)
}
},
};
},
DisplayGeneratingBox::LayoutInternal(_) => {
self.anonymous_table_content
.push(AnonymousTableContent::Element {
info: info.clone(),
display,
contents,
box_slot,
});
},
}
}
fn handle_text(&mut self, info: &NodeAndStyleInfo<Node>, input: Cow<'dom, str>) {
if input.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) {
self.anonymous_table_content
.push(AnonymousTableContent::Text(info.clone(), input));
return;
} else {
self.finish_anonymous_table_if_needed();
}
let (output, has_uncollapsible_content) = collapse_and_transform_whitespace(
&input,
info.style.get_inherited_text().white_space,
self.ongoing_inline_formatting_context.ends_with_whitespace,
);
if output.is_empty() {
return;
}
self.ongoing_inline_formatting_context.ends_with_whitespace =
output.chars().last().unwrap().is_ascii_whitespace();
let inlines = self.current_inline_level_boxes();
match inlines.last_mut().map(|last| last.borrow_mut()) {
Some(mut last_box) => match *last_box {
InlineLevelBox::TextRun(ref mut text_run) => {
text_run.text.push_str(&output);
text_run.has_uncollapsible_content |= has_uncollapsible_content;
return;
},
_ => {},
},
_ => {},
}
inlines.push(ArcRefCell::new(InlineLevelBox::TextRun(TextRun {
base_fragment_info: info.into(),
parent_style: Arc::clone(&info.style),
text: output,
has_uncollapsible_content,
shaped_text: None,
})));
}
}
fn preserve_segment_break() -> bool {
true
}
/// Collapse and transform whitespace in the given input according to the rules in
/// <https://drafts.csswg.org/css-text-3/#white-space-phase-1>. This method doesn't
/// follow the steps exactly since they are defined in a multi-pass appraoach, but it
/// tries to be effectively the same transformation.
///
/// Returns the transformed text as a [String] and also whether or not the input had
/// any uncollapsible content.
fn collapse_and_transform_whitespace(
input: &str,
white_space: WhiteSpace,
trim_beginning_white_space: bool,
) -> (String, bool) {
// 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 white_space.preserve_spaces() {
return (input.to_owned(), true);
}
let mut output = String::with_capacity(input.len());
let mut has_uncollapsible_content = false;
let mut had_whitespace = false;
let mut following_newline = false;
let mut in_whitespace_at_beginning = true;
let is_leading_trimmed_whitespace =
|in_whitespace_at_beginning: bool| in_whitespace_at_beginning && trim_beginning_white_space;
// 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.
let push_pending_whitespace_if_needed =
|output: &mut String,
had_whitespace: bool,
following_newline: bool,
in_whitespace_at_beginning: bool| {
if had_whitespace &&
!following_newline &&
!is_leading_trimmed_whitespace(in_whitespace_at_beginning)
{
output.push(' ');
}
};
for character in input.chars() {
// 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' {
had_whitespace = 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 white_space == WhiteSpace::PreLine {
has_uncollapsible_content = true;
had_whitespace = false;
output.push('\n');
// 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 !following_newline &&
preserve_segment_break() &&
!is_leading_trimmed_whitespace(in_whitespace_at_beginning)
{
had_whitespace = false;
output.push(' ');
}
following_newline = true;
continue;
}
push_pending_whitespace_if_needed(
&mut output,
had_whitespace,
following_newline,
in_whitespace_at_beginning,
);
has_uncollapsible_content = true;
had_whitespace = false;
in_whitespace_at_beginning = false;
following_newline = false;
output.push(character);
}
push_pending_whitespace_if_needed(
&mut output,
had_whitespace,
following_newline,
in_whitespace_at_beginning,
);
(output, has_uncollapsible_content)
}
impl<'dom, Node> BlockContainerBuilder<'dom, '_, Node>
where
Node: NodeExt<'dom>,
{
fn handle_list_item_marker_inside(
&mut self,
info: &NodeAndStyleInfo<Node>,
contents: Vec<crate::dom_traversal::PseudoElementContentItem>,
) {
let marker_style = self
.context
.shared_context()
.stylist
.style_for_anonymous::<Node::ConcreteElement>(
&self.context.shared_context().guards,
&PseudoElement::ServoLegacyText, // FIMXE: use `PseudoElement::Marker` when we add it
&info.style,
);
self.handle_inline_level_element(
&info.new_replacing_style(marker_style),
DisplayInside::Flow {
is_list_item: false,
},
Contents::OfPseudoElement(contents),
);
}
fn handle_inline_level_element(
&mut self,
info: &NodeAndStyleInfo<Node>,
display_inside: DisplayInside,
contents: Contents,
) -> ArcRefCell<InlineLevelBox> {
let box_ = if 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![],
});
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` doesnt panic here because `is_replaced` returned `false`.
NonReplacedContents::try_from(contents)
.unwrap()
.traverse(self.context, info, self);
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 {
self.ongoing_inline_formatting_context.ends_with_whitespace = false;
ArcRefCell::new(InlineLevelBox::Atomic(
IndependentFormattingContext::construct(
self.context,
info,
display_inside,
contents,
// Text decorations are not propagated to atomic inline-level descendants.
TextDecorationLine::NONE,
),
))
};
self.current_inline_level_boxes().push(box_.clone());
box_
}
fn handle_block_level_element(
&mut self,
info: &NodeAndStyleInfo<Node>,
display_inside: DisplayInside,
contents: Contents,
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),
};
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));
}
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 kind = match contents.try_into() {
Ok(contents) => match display_inside {
DisplayInside::Flow { is_list_item }
if !info.style.establishes_block_formatting_context() =>
{
BlockLevelCreator::SameFormattingContextBlock(
IntermediateBlockContainer::Deferred {
contents,
propagated_text_decoration_line,
is_list_item,
},
)
},
_ => BlockLevelCreator::Independent {
display_inside,
contents: contents.into(),
propagated_text_decoration_line,
},
},
Err(contents) => {
let contents = Contents::Replaced(contents);
BlockLevelCreator::Independent {
display_inside,
contents,
propagated_text_decoration_line,
}
},
};
self.block_level_boxes.push(BlockLevelJob {
info: info.clone(),
box_slot,
kind,
});
}
fn handle_absolutely_positioned_element(
&mut self,
info: &NodeAndStyleInfo<Node>,
display_inside: DisplayInside,
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(
self.context,
info,
display_inside,
contents,
)),
));
self.current_inline_level_boxes().push(box_.clone());
box_slot.set(LayoutBox::InlineLevel(box_))
}
}
fn handle_float_element(
&mut self,
info: &NodeAndStyleInfo<Node>,
display_inside: DisplayInside,
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_))
}
}
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;
self.ongoing_inline_formatting_context.ends_with_whitespace = false;
return;
}
let context = self.context;
let block_container_style = &self.info.style;
let anonymous_style = self.anonymous_style.get_or_insert_with(|| {
context
.shared_context()
.stylist
.style_for_anonymous::<Node::ConcreteElement>(
&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,
/* ends_with_whitespace */ false,
);
std::mem::swap(&mut self.ongoing_inline_formatting_context, &mut ifc);
let info = self.info.new_replacing_style(anonymous_style.clone());
self.block_level_boxes.push(BlockLevelJob {
info,
// FIXME(nox): We should be storing this somewhere.
box_slot: BoxSlot::dummy(),
kind: BlockLevelCreator::SameFormattingContextBlock(
IntermediateBlockContainer::InlineFormattingContext(ifc),
),
});
}
// 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()
}
}
impl<'dom, Node> BlockLevelJob<'dom, Node>
where
Node: NodeExt<'dom>,
{
fn finish(self, context: &LayoutContext) -> ArcRefCell<BlockLevelBox> {
let info = &self.info;
let block_level_box = match self.kind {
BlockLevelCreator::SameFormattingContextBlock(intermediate_block_container) => {
let contents = intermediate_block_container.finish(context, info);
let contains_floats = contents.contains_floats();
ArcRefCell::new(BlockLevelBox::SameFormattingContextBlock {
base_fragment_info: info.into(),
contents,
style: Arc::clone(&info.style),
contains_floats,
})
},
BlockLevelCreator::Independent {
display_inside,
contents,
propagated_text_decoration_line,
} => {
let context = IndependentFormattingContext::construct(
context,
info,
display_inside,
contents,
propagated_text_decoration_line,
);
ArcRefCell::new(BlockLevelBox::Independent(context))
},
BlockLevelCreator::OutOfFlowAbsolutelyPositionedBox {
display_inside,
contents,
} => ArcRefCell::new(BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(
ArcRefCell::new(AbsolutelyPositionedBox::construct(
context,
info,
display_inside,
contents,
)),
)),
BlockLevelCreator::OutOfFlowFloatBox {
display_inside,
contents,
} => ArcRefCell::new(BlockLevelBox::OutOfFlowFloatBox(FloatBox::construct(
context,
info,
display_inside,
contents,
))),
BlockLevelCreator::AnonymousTable { table_block } => table_block,
};
self.box_slot
.set(LayoutBox::BlockLevel(block_level_box.clone()));
block_level_box
}
}
impl IntermediateBlockContainer {
fn finish<'dom, Node>(
self,
context: &LayoutContext,
info: &NodeAndStyleInfo<Node>,
) -> BlockContainer
where
Node: NodeExt<'dom>,
{
match self {
IntermediateBlockContainer::Deferred {
contents,
propagated_text_decoration_line,
is_list_item,
} => BlockContainer::construct(
context,
info,
contents,
propagated_text_decoration_line,
is_list_item,
),
IntermediateBlockContainer::InlineFormattingContext(ifc) => {
BlockContainer::construct_inline_formatting_context(context, ifc)
},
}
}
}
#[test]
fn test_collapase_and_transform_whitespace() {
let output = collapse_and_transform_whitespace("H ", WhiteSpace::Normal, false);
assert_eq!(output.0, "H ");
assert!(output.1);
let output = collapse_and_transform_whitespace(" W", WhiteSpace::Normal, true);
assert_eq!(output.0, "W");
assert!(output.1);
let output = collapse_and_transform_whitespace(" W", WhiteSpace::Normal, false);
assert_eq!(output.0, " W");
assert!(output.1);
let output = collapse_and_transform_whitespace(" H W", WhiteSpace::Normal, false);
assert_eq!(output.0, " H W");
assert!(output.1);
let output = collapse_and_transform_whitespace("\n H \n \t W", WhiteSpace::Normal, false);
assert_eq!(output.0, " H W");
let output = collapse_and_transform_whitespace("\n H \n \t W \n", WhiteSpace::Pre, false);
assert_eq!(output.0, "\n H \n \t W \n");
assert!(output.1);
let output =
collapse_and_transform_whitespace("\n H \n \t W \n ", WhiteSpace::PreLine, false);
assert_eq!(output.0, "\nH\nW\n");
assert!(output.1);
let output = collapse_and_transform_whitespace(" ", WhiteSpace::Normal, true);
assert_eq!(output.0, "");
assert!(!output.1);
let output = collapse_and_transform_whitespace(" ", WhiteSpace::Normal, false);
assert_eq!(output.0, " ");
assert!(!output.1);
let output = collapse_and_transform_whitespace("\n ", WhiteSpace::Normal, true);
assert_eq!(output.0, "");
assert!(!output.1);
let output = collapse_and_transform_whitespace("\n ", WhiteSpace::Normal, false);
assert_eq!(output.0, " ");
assert!(!output.1);
}