layout: Combine layout_2020 and layout_thread_2020 into a crate called layout (#36613)

Now that legacy layout has been removed, the name `layout_2020` doesn't
make much sense any longer, also it's 2025 now for better or worse. The
split between the "layout thread" and "layout" also doesn't make as much
sense since layout doesn't run on it's own thread. There's a possibility
that it will in the future, but that should be something that the user
of the crate controls rather than layout iself.

This is part of the larger layout interface cleanup and optimization
that
@Looriool and I are doing.

Testing: Covered by existing tests as this is just code movement.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-04-19 12:17:03 +02:00 committed by GitHub
parent 3ab5b8c447
commit 7787cab521
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 58 additions and 122 deletions

View file

@ -0,0 +1,762 @@
/* 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;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use servo_arc::Arc;
use style::properties::ComputedValues;
use style::properties::longhands::list_style_position::computed_value::T as ListStylePosition;
use style::selector_parser::PseudoElement;
use style::str::char_is_whitespace;
use super::OutsideMarker;
use super::inline::InlineFormattingContext;
use super::inline::construct::InlineFormattingContextBuilder;
use super::inline::inline_box::InlineBox;
use crate::PropagatedBoxTreeData;
use crate::cell::ArcRefCell;
use crate::context::LayoutContext;
use crate::dom::{BoxSlot, LayoutBox, NodeExt};
use crate::dom_traversal::{
Contents, NodeAndStyleInfo, NonReplacedContents, PseudoElementContentItem, TraversalHandler,
};
use crate::flow::float::FloatBox;
use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox};
use crate::formatting_contexts::IndependentFormattingContext;
use crate::fragment_tree::FragmentFlags;
use crate::layout_box_base::LayoutBoxBase;
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_data: PropagatedBoxTreeData,
is_list_item: bool,
) -> Self
where
Node: NodeExt<'dom>,
{
Self::from_block_container(BlockContainer::construct(
context,
info,
contents,
propagated_data,
is_list_item,
))
}
pub(crate) fn from_block_container(contents: BlockContainer) -> Self {
let contains_floats = contents.contains_floats();
Self {
contents,
contains_floats,
}
}
}
struct BlockLevelJob<'dom, Node> {
info: NodeAndStyleInfo<Node>,
box_slot: BoxSlot<'dom>,
propagated_data: PropagatedBoxTreeData,
kind: BlockLevelCreator,
}
enum BlockLevelCreator {
SameFormattingContextBlock(IntermediateBlockContainer),
Independent {
display_inside: DisplayInside,
contents: Contents,
},
OutOfFlowAbsolutelyPositionedBox {
display_inside: DisplayInside,
contents: Contents,
},
OutOfFlowFloatBox {
display_inside: DisplayInside,
contents: Contents,
},
OutsideMarker {
list_item_style: Arc<ComputedValues>,
contents: Vec<PseudoElementContentItem>,
},
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(BlockContainer),
Deferred {
contents: NonReplacedContents,
propagated_data: PropagatedBoxTreeData,
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>>,
/// 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 data to use for BoxTree construction.
propagated_data: PropagatedBoxTreeData,
inline_formatting_context_builder: InlineFormattingContextBuilder,
/// The [`NodeAndStyleInfo`] to use for anonymous block boxes pushed to the list of
/// block-level boxes, lazily initialized (see `end_ongoing_inline_formatting_context`).
anonymous_box_info: Option<NodeAndStyleInfo<Node>>,
/// 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_data: PropagatedBoxTreeData,
is_list_item: bool,
) -> BlockContainer
where
Node: NodeExt<'dom>,
{
let mut builder = BlockContainerBuilder::new(context, info, propagated_data);
if is_list_item {
if let Some((marker_info, marker_contents)) = crate::lists::make_marker(context, info) {
match marker_info.style.clone_list_style_position() {
ListStylePosition::Inside => {
builder.handle_list_item_marker_inside(&marker_info, info, marker_contents)
},
ListStylePosition::Outside => builder.handle_list_item_marker_outside(
&marker_info,
info,
marker_contents,
info.style.clone(),
),
}
}
}
contents.traverse(context, info, &mut builder);
builder.finish()
}
}
impl<'dom, 'style, Node> BlockContainerBuilder<'dom, 'style, Node>
where
Node: NodeExt<'dom>,
{
pub(crate) fn new(
context: &'style LayoutContext,
info: &'style NodeAndStyleInfo<Node>,
propagated_data: PropagatedBoxTreeData,
) -> Self {
BlockContainerBuilder {
context,
info,
block_level_boxes: Vec::new(),
propagated_data: propagated_data.union(&info.style),
have_already_seen_first_line_for_text_indent: false,
anonymous_box_info: None,
anonymous_table_content: Vec::new(),
inline_formatting_context_builder: InlineFormattingContextBuilder::new(),
}
}
pub(crate) fn finish(mut self) -> BlockContainer {
debug_assert!(
!self
.inline_formatting_context_builder
.currently_processing_inline_box()
);
self.finish_anonymous_table_if_needed();
if let Some(inline_formatting_context) = self.inline_formatting_context_builder.finish(
self.context,
self.propagated_data,
!self.have_already_seen_first_line_for_text_indent,
self.info.is_single_line_text_input(),
self.info.style.writing_mode.to_bidi_level(),
) {
// 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::InlineFormattingContext(inline_formatting_context);
}
self.push_block_level_job_for_inline_formatting_context(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;
}
// From https://drafts.csswg.org/css-tables/#fixup-algorithm:
// > If the boxs 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 propagated_data = match inline_table {
true => self.propagated_data.without_text_decorations(),
false => self.propagated_data,
};
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 (table_info, ifc) =
Table::construct_anonymous(self.context, self.info, contents, propagated_data);
if inline_table {
self.inline_formatting_context_builder.push_atomic(ifc);
} else {
let table_block = ArcRefCell::new(BlockLevelBox::Independent(ifc));
self.end_ongoing_inline_formatting_context();
self.block_level_boxes.push(BlockLevelJob {
info: table_info,
box_slot: BoxSlot::dummy(),
kind: BlockLevelCreator::AnonymousTable { table_block },
propagated_data,
});
}
// 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 => {
self.handle_inline_level_element(info, inside, contents, box_slot)
},
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>, 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() && text.chars().all(char_is_whitespace) {
self.anonymous_table_content
.push(AnonymousTableContent::Text(info.clone(), text));
return;
} else {
self.finish_anonymous_table_if_needed();
}
self.inline_formatting_context_builder.push_text(text, info);
}
}
impl<'dom, Node> BlockContainerBuilder<'dom, '_, Node>
where
Node: NodeExt<'dom>,
{
fn handle_list_item_marker_inside(
&mut self,
marker_info: &NodeAndStyleInfo<Node>,
container_info: &NodeAndStyleInfo<Node>,
contents: Vec<crate::dom_traversal::PseudoElementContentItem>,
) {
// TODO: We do not currently support saving box slots for ::marker pseudo-elements
// that are part nested in ::before and ::after pseudo elements. For now, just
// forget about them once they are built.
let box_slot = match container_info.pseudo_element_type {
Some(_) => BoxSlot::dummy(),
None => marker_info
.node
.pseudo_element_box_slot(PseudoElement::Marker),
};
self.handle_inline_level_element(
marker_info,
DisplayInside::Flow {
is_list_item: false,
},
NonReplacedContents::OfPseudoElement(contents).into(),
box_slot,
);
}
fn handle_list_item_marker_outside(
&mut self,
marker_info: &NodeAndStyleInfo<Node>,
container_info: &NodeAndStyleInfo<Node>,
contents: Vec<crate::dom_traversal::PseudoElementContentItem>,
list_item_style: Arc<ComputedValues>,
) {
// TODO: We do not currently support saving box slots for ::marker pseudo-elements
// that are part nested in ::before and ::after pseudo elements. For now, just
// forget about them once they are built.
let box_slot = match container_info.pseudo_element_type {
Some(_) => BoxSlot::dummy(),
None => marker_info
.node
.pseudo_element_box_slot(PseudoElement::Marker),
};
self.block_level_boxes.push(BlockLevelJob {
info: marker_info.clone(),
box_slot,
kind: BlockLevelCreator::OutsideMarker {
contents,
list_item_style,
},
propagated_data: self.propagated_data.without_text_decorations(),
});
}
fn handle_inline_level_element(
&mut self,
info: &NodeAndStyleInfo<Node>,
display_inside: DisplayInside,
contents: Contents,
box_slot: BoxSlot<'dom>,
) {
let (DisplayInside::Flow { is_list_item }, false) =
(display_inside, contents.is_replaced())
else {
// If this inline element is an atomic, handle it and return.
let atomic = self.inline_formatting_context_builder.push_atomic(
IndependentFormattingContext::construct(
self.context,
info,
display_inside,
contents,
// Text decorations are not propagated to atomic inline-level descendants.
self.propagated_data.without_text_decorations(),
),
);
box_slot.set(LayoutBox::InlineLevel(atomic));
return;
};
// 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.
let inline_item = self
.inline_formatting_context_builder
.start_inline_box(InlineBox::new(info));
if is_list_item {
if let Some((marker_info, 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(&marker_info, info, marker_contents)
}
}
// `unwrap` doesnt panic here because `is_replaced` returned `false`.
NonReplacedContents::try_from(contents)
.unwrap()
.traverse(self.context, info, self);
self.finish_anonymous_table_if_needed();
self.inline_formatting_context_builder.end_inline_box();
box_slot.set(LayoutBox::InlineLevel(inline_item));
}
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.
//
// 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.propagated_data,
!self.have_already_seen_first_line_for_text_indent,
self.info.style.writing_mode.to_bidi_level(),
)
{
self.push_block_level_job_for_inline_formatting_context(inline_formatting_context);
}
let propagated_data = self.propagated_data;
let kind = match contents {
Contents::NonReplaced(contents) => match display_inside {
DisplayInside::Flow { is_list_item }
// Fragment flags are just used to indicate that the element is not replaced, so empty
// flags are okay here.
if !info.style.establishes_block_formatting_context(
FragmentFlags::empty()
) =>
{
BlockLevelCreator::SameFormattingContextBlock(
IntermediateBlockContainer::Deferred {
contents,
propagated_data,
is_list_item,
},
)
},
_ => BlockLevelCreator::Independent {
display_inside,
contents: contents.into(),
},
},
Contents::Replaced(contents) => {
let contents = Contents::Replaced(contents);
BlockLevelCreator::Independent {
display_inside,
contents,
}
},
};
self.block_level_boxes.push(BlockLevelJob {
info: info.clone(),
box_slot,
kind,
propagated_data,
});
// 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(
&mut self,
info: &NodeAndStyleInfo<Node>,
display_inside: DisplayInside,
contents: Contents,
box_slot: BoxSlot<'dom>,
) {
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,
));
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,
propagated_data: self.propagated_data.without_text_decorations(),
});
}
fn handle_float_element(
&mut self,
info: &NodeAndStyleInfo<Node>,
display_inside: DisplayInside,
contents: Contents,
box_slot: BoxSlot<'dom>,
) {
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,
self.propagated_data,
));
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,
propagated_data: self.propagated_data.without_text_decorations(),
});
}
fn end_ongoing_inline_formatting_context(&mut self) {
if let Some(inline_formatting_context) = self.inline_formatting_context_builder.finish(
self.context,
self.propagated_data,
!self.have_already_seen_first_line_for_text_indent,
self.info.is_single_line_text_input(),
self.info.style.writing_mode.to_bidi_level(),
) {
self.push_block_level_job_for_inline_formatting_context(inline_formatting_context);
}
}
fn push_block_level_job_for_inline_formatting_context(
&mut self,
inline_formatting_context: InlineFormattingContext,
) {
let layout_context = self.context;
let info = self
.anonymous_box_info
.get_or_insert_with(|| {
self.info
.pseudo(layout_context, PseudoElement::ServoAnonymousBox)
.expect("Should never fail to create anonymous box")
})
.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(
BlockContainer::InlineFormattingContext(inline_formatting_context),
),
),
propagated_data: self.propagated_data,
});
self.have_already_seen_first_line_for_text_indent = true;
}
}
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: LayoutBoxBase::new(info.into(), info.style.clone()),
contents,
contains_floats,
})
},
BlockLevelCreator::Independent {
display_inside,
contents,
} => {
let context = IndependentFormattingContext::construct(
context,
info,
display_inside,
contents,
self.propagated_data,
);
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,
self.propagated_data,
))),
BlockLevelCreator::OutsideMarker {
contents,
list_item_style,
} => {
let contents = NonReplacedContents::OfPseudoElement(contents);
let block_container = BlockContainer::construct(
context,
info,
contents,
self.propagated_data.without_text_decorations(),
false, /* is_list_item */
);
ArcRefCell::new(BlockLevelBox::OutsideMarker(OutsideMarker {
base: LayoutBoxBase::new(info.into(), info.style.clone()),
block_container,
list_item_style,
}))
},
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_data,
is_list_item,
} => BlockContainer::construct(context, info, contents, propagated_data, is_list_item),
IntermediateBlockContainer::InlineFormattingContext(block_container) => block_container,
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,636 @@
/* 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 icu_segmenter::WordSegmenter;
use servo_arc::Arc;
use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
use style::values::specified::text::TextTransformCase;
use unicode_bidi::Level;
use super::text_run::TextRun;
use super::{InlineBox, InlineBoxIdentifier, InlineBoxes, InlineFormattingContext, InlineItem};
use crate::PropagatedBoxTreeData;
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;
use crate::style_ext::ComputedValuesExt;
#[derive(Default)]
pub(crate) struct InlineFormattingContextBuilder {
/// The collection of text strings that make up this [`InlineFormattingContext`] under
/// construction.
pub text_segments: Vec<String>,
/// The current offset in the final text string of this [`InlineFormattingContext`],
/// used to properly set the text range of new [`InlineItem::TextRun`]s.
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,
/// The current list of [`InlineItem`]s in this [`InlineFormattingContext`] under
/// construction. This is stored in a flat list to make it easy to access the last
/// item.
pub inline_items: Vec<ArcRefCell<InlineItem>>,
/// The current [`InlineBox`] tree of this [`InlineFormattingContext`] under construction.
pub inline_boxes: InlineBoxes,
/// 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`).
///
/// When an inline box ends, it's removed from this stack.
inline_box_stack: Vec<InlineBoxIdentifier>,
/// Whether or not the inline formatting context under construction has any
/// uncollapsible text content.
pub has_uncollapsible_text_content: bool,
}
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()
}
fn push_control_character_string(&mut self, string_to_push: &str) {
self.text_segments.push(string_to_push.to_owned());
self.current_text_offset += string_to_push.len();
}
/// 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_box_is_empty(inline_level_box: &InlineItem) -> bool {
match inline_level_box {
InlineItem::StartInlineBox(_) => false,
InlineItem::EndInlineBox => false,
// Text content is handled by `self.has_uncollapsible_text` content above in order
// to avoid having to iterate through the character once again.
InlineItem::TextRun(_) => true,
InlineItem::OutOfFlowAbsolutelyPositionedBox(..) => false,
InlineItem::OutOfFlowFloatBox(_) => false,
InlineItem::Atomic(..) => false,
}
}
self.inline_items
.iter()
.all(|inline_level_box| inline_level_box_is_empty(&inline_level_box.borrow()))
}
pub(crate) fn push_atomic(
&mut self,
independent_formatting_context: IndependentFormattingContext,
) -> ArcRefCell<InlineItem> {
let inline_level_box = ArcRefCell::new(InlineItem::Atomic(
Arc::new(independent_formatting_context),
self.current_text_offset,
Level::ltr(), /* This will be assigned later if necessary. */
));
self.inline_items.push(inline_level_box.clone());
// Push an object replacement character for this atomic, which will ensure that the line breaker
// inserts a line breaking opportunity here.
self.push_control_character_string("\u{fffc}");
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<InlineItem> {
let absolutely_positioned_box = ArcRefCell::new(absolutely_positioned_box);
let inline_level_box = ArcRefCell::new(InlineItem::OutOfFlowAbsolutelyPositionedBox(
absolutely_positioned_box,
self.current_text_offset,
));
self.inline_items.push(inline_level_box.clone());
inline_level_box
}
pub(crate) fn push_float_box(&mut self, float_box: FloatBox) -> ArcRefCell<InlineItem> {
let inline_level_box = ArcRefCell::new(InlineItem::OutOfFlowFloatBox(Arc::new(float_box)));
self.inline_items.push(inline_level_box.clone());
self.contains_floats = true;
inline_level_box
}
pub(crate) fn start_inline_box(&mut self, inline_box: InlineBox) -> ArcRefCell<InlineItem> {
self.push_control_character_string(inline_box.base.style.bidi_control_chars().0);
let (identifier, inline_box) = self.inline_boxes.start_inline_box(inline_box);
let inline_level_box = ArcRefCell::new(InlineItem::StartInlineBox(inline_box));
self.inline_items.push(inline_level_box.clone());
self.inline_box_stack.push(identifier);
inline_level_box
}
pub(crate) fn end_inline_box(&mut self) -> ArcRefCell<InlineBox> {
let identifier = self.end_inline_box_internal();
let inline_level_box = self.inline_boxes.get(&identifier);
inline_level_box.borrow_mut().is_last_fragment = true;
self.push_control_character_string(
inline_level_box.borrow().base.style.bidi_control_chars().1,
);
inline_level_box
}
fn end_inline_box_internal(&mut self) -> InlineBoxIdentifier {
let identifier = self
.inline_box_stack
.pop()
.expect("Ended non-existent inline box");
self.inline_items
.push(ArcRefCell::new(InlineItem::EndInlineBox));
self.inline_boxes.end_inline_box(identifier);
identifier
}
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,
);
// TODO: Not all text transforms are about case, this logic should stop ignoring
// TextTransform::FULL_WIDTH and TextTransform::FULL_SIZE_KANA.
let text_transform = info.style.clone_text_transform().case();
let capitalized_text: String;
let char_iterator: Box<dyn Iterator<Item = char>> = match text_transform {
TextTransformCase::None => Box::new(collapsed),
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())
},
_ => {
// If `text-transform` is active, wrap the `WhitespaceCollapse` iterator in
// a `TextTransformation` iterator.
Box::new(TextTransformation::new(collapsed, text_transform))
},
};
let white_space_collapse = info.style.clone_white_space_collapse();
let new_text: String = char_iterator
.inspect(|&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);
})
.collect();
if new_text.is_empty() {
return;
}
let selection_range = info.get_selection_range();
let selected_style = info.get_selected_style();
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);
if let Some(inline_item) = self.inline_items.last() {
if let InlineItem::TextRun(text_run) = &mut *inline_item.borrow_mut() {
text_run.borrow_mut().text_range.end = new_range.end;
return;
}
}
self.inline_items
.push(ArcRefCell::new(InlineItem::TextRun(ArcRefCell::new(
TextRun::new(
info.into(),
info.style.clone(),
new_range,
selection_range,
selected_style,
),
))));
}
pub(crate) fn split_around_block_and_finish(
&mut self,
layout_context: &LayoutContext,
propagated_data: PropagatedBoxTreeData,
has_first_formatted_line: bool,
default_bidi_level: Level,
) -> 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 new_builder = InlineFormattingContextBuilder::new();
for identifier in self.inline_box_stack.iter() {
new_builder.start_inline_box(
self.inline_boxes
.get(identifier)
.borrow()
.split_around_block(),
);
}
let mut inline_builder_from_before_split = std::mem::replace(self, new_builder);
// 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_builder_from_before_split.inline_box_stack.is_empty() {
inline_builder_from_before_split.end_inline_box_internal();
}
inline_builder_from_before_split.finish(
layout_context,
propagated_data,
has_first_formatted_line,
/* is_single_line_text_input = */ false,
default_bidi_level,
)
}
/// Finish the current inline formatting context, returning [`None`] if the context was empty.
pub(crate) fn finish(
&mut self,
layout_context: &LayoutContext,
propagated_data: PropagatedBoxTreeData,
has_first_formatted_line: bool,
is_single_line_text_input: bool,
default_bidi_level: Level,
) -> 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,
propagated_data,
has_first_formatted_line,
is_single_line_text_input,
default_bidi_level,
))
}
}
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: TextTransformCase,
/// 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: TextTransformCase) -> 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 {
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.
TextTransformCase::Capitalize => 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 word_segmenter = WordSegmenter::new_auto();
let mut bounds = word_segmenter.segment_str(string).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
}

View file

@ -0,0 +1,257 @@
/* 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::vec::IntoIter;
use app_units::Au;
use fonts::FontMetrics;
use malloc_size_of_derive::MallocSizeOf;
use super::{InlineContainerState, InlineContainerStateFlags, inline_container_needs_strut};
use crate::ContainingBlock;
use crate::cell::ArcRefCell;
use crate::context::LayoutContext;
use crate::dom::NodeExt;
use crate::dom_traversal::NodeAndStyleInfo;
use crate::fragment_tree::BaseFragmentInfo;
use crate::layout_box_base::LayoutBoxBase;
use crate::style_ext::{LayoutStyle, PaddingBorderMargin};
#[derive(Debug, MallocSizeOf)]
pub(crate) struct InlineBox {
pub base: LayoutBoxBase,
/// The identifier of this inline box in the containing [`super::InlineFormattingContext`].
pub(super) identifier: InlineBoxIdentifier,
pub is_first_fragment: bool,
pub is_last_fragment: bool,
/// The index of the default font in the [`super::InlineFormattingContext`]'s font metrics store.
/// This is initialized during IFC shaping.
pub default_font_index: Option<usize>,
}
impl InlineBox {
pub(crate) fn new<'dom, Node: NodeExt<'dom>>(info: &NodeAndStyleInfo<Node>) -> Self {
Self {
base: LayoutBoxBase::new(info.into(), info.style.clone()),
// This will be assigned later, when the box is actually added to the IFC.
identifier: InlineBoxIdentifier::default(),
is_first_fragment: true,
is_last_fragment: false,
default_font_index: None,
}
}
pub(crate) fn split_around_block(&self) -> Self {
Self {
base: LayoutBoxBase::new(self.base.base_fragment_info, self.base.style.clone()),
is_first_fragment: false,
is_last_fragment: false,
..*self
}
}
#[inline]
pub(crate) fn layout_style(&self) -> LayoutStyle {
LayoutStyle::Default(&self.base.style)
}
}
#[derive(Debug, Default, MallocSizeOf)]
pub(crate) struct InlineBoxes {
/// A collection of all inline boxes in a particular [`super::InlineFormattingContext`].
inline_boxes: Vec<ArcRefCell<InlineBox>>,
/// A list of tokens that represent the actual tree of inline boxes, while allowing
/// easy traversal forward and backwards through the tree. This structure is also
/// stored in the [`super::InlineFormattingContext::inline_items`], but this version is
/// faster to iterate.
inline_box_tree: Vec<InlineBoxTreePathToken>,
}
impl InlineBoxes {
pub(super) fn len(&self) -> usize {
self.inline_boxes.len()
}
pub(super) fn iter(&self) -> impl Iterator<Item = &ArcRefCell<InlineBox>> {
self.inline_boxes.iter()
}
pub(super) fn get(&self, identifier: &InlineBoxIdentifier) -> ArcRefCell<InlineBox> {
self.inline_boxes[identifier.index_in_inline_boxes as usize].clone()
}
pub(super) fn end_inline_box(&mut self, identifier: InlineBoxIdentifier) {
self.inline_box_tree
.push(InlineBoxTreePathToken::End(identifier));
}
pub(super) fn start_inline_box(
&mut self,
mut inline_box: InlineBox,
) -> (InlineBoxIdentifier, ArcRefCell<InlineBox>) {
assert!(self.inline_boxes.len() <= u32::MAX as usize);
assert!(self.inline_box_tree.len() <= u32::MAX as usize);
let index_in_inline_boxes = self.inline_boxes.len() as u32;
let index_of_start_in_tree = self.inline_box_tree.len() as u32;
let identifier = InlineBoxIdentifier {
index_of_start_in_tree,
index_in_inline_boxes,
};
inline_box.identifier = identifier;
let inline_box = ArcRefCell::new(inline_box);
self.inline_boxes.push(inline_box.clone());
self.inline_box_tree
.push(InlineBoxTreePathToken::Start(identifier));
(identifier, inline_box)
}
pub(super) fn get_path(
&self,
from: Option<InlineBoxIdentifier>,
to: InlineBoxIdentifier,
) -> IntoIter<InlineBoxTreePathToken> {
if from == Some(to) {
return Vec::new().into_iter();
}
let mut from_index = match from {
Some(InlineBoxIdentifier {
index_of_start_in_tree,
..
}) => index_of_start_in_tree as usize,
None => 0,
};
let mut to_index = to.index_of_start_in_tree as usize;
let is_reversed = to_index < from_index;
// Do not include the first or final token, depending on direction. These can be equal
// if we are starting or going to the the root of the inline formatting context, in which
// case we don't want to adjust.
if to_index > from_index && from.is_some() {
from_index += 1;
} else if to_index < from_index {
to_index += 1;
}
let mut path = Vec::with_capacity(from_index.abs_diff(to_index));
let min = from_index.min(to_index);
let max = from_index.max(to_index);
for token in &self.inline_box_tree[min..=max] {
// Skip useless recursion into inline boxes; we are looking for a direct path.
if Some(&token.reverse()) == path.last() {
path.pop();
} else {
path.push(*token);
}
}
if is_reversed {
path.reverse();
for token in path.iter_mut() {
*token = token.reverse();
}
}
path.into_iter()
}
}
#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq)]
pub(super) enum InlineBoxTreePathToken {
Start(InlineBoxIdentifier),
End(InlineBoxIdentifier),
}
impl InlineBoxTreePathToken {
fn reverse(&self) -> Self {
match self {
Self::Start(index) => Self::End(*index),
Self::End(index) => Self::Start(*index),
}
}
}
/// An identifier for a particular [`InlineBox`] to be used to fetch it from an [`InlineBoxes`]
/// store of inline boxes.
///
/// [`u32`] is used for the index, in order to save space. The value refers to the token
/// in the start tree data structure which can be fetched to find the actual index of
/// of the [`InlineBox`] in [`InlineBoxes::inline_boxes`].
#[derive(Clone, Copy, Debug, Default, Eq, Hash, MallocSizeOf, PartialEq)]
pub(crate) struct InlineBoxIdentifier {
pub index_of_start_in_tree: u32,
pub index_in_inline_boxes: u32,
}
pub(super) struct InlineBoxContainerState {
/// The container state common to both [`InlineBox`] and the root of the
/// [`super::InlineFormattingContext`].
pub base: InlineContainerState,
/// The [`InlineBoxIdentifier`] of this inline container state. If this is the root
/// the identifier is [`None`].
pub identifier: InlineBoxIdentifier,
/// The [`BaseFragmentInfo`] of the [`InlineBox`] that this state tracks.
pub base_fragment_info: BaseFragmentInfo,
/// The [`PaddingBorderMargin`] of the [`InlineBox`] that this state tracks.
pub pbm: PaddingBorderMargin,
/// Whether this is the last fragment of this InlineBox. This may not be the case if
/// the InlineBox is split due to an block-in-inline-split and this is not the last of
/// that split.
pub is_last_fragment: bool,
}
impl InlineBoxContainerState {
pub(super) fn new(
inline_box: &InlineBox,
containing_block: &ContainingBlock,
layout_context: &LayoutContext,
parent_container: &InlineContainerState,
is_last_fragment: bool,
font_metrics: Option<&FontMetrics>,
) -> Self {
let style = inline_box.base.style.clone();
let pbm = inline_box
.layout_style()
.padding_border_margin(containing_block);
let mut flags = InlineContainerStateFlags::empty();
if inline_container_needs_strut(&style, layout_context, Some(&pbm)) {
flags.insert(InlineContainerStateFlags::CREATE_STRUT);
}
Self {
base: InlineContainerState::new(
style,
flags,
Some(parent_container),
parent_container.text_decoration_line,
font_metrics,
),
identifier: inline_box.identifier,
base_fragment_info: inline_box.base.base_fragment_info,
pbm,
is_last_fragment,
}
}
pub(super) fn calculate_space_above_baseline(&self) -> Au {
let (ascent, descent, line_gap) = (
self.base.font_metrics.ascent,
self.base.font_metrics.descent,
self.base.font_metrics.line_gap,
);
let leading = line_gap - (ascent + descent);
leading.scale_by(0.5) + ascent
}
}

View file

@ -0,0 +1,911 @@
/* 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 app_units::Au;
use bitflags::bitflags;
use fonts::{ByteIndex, FontMetrics, GlyphStore};
use itertools::Either;
use range::Range;
use servo_arc::Arc;
use style::Zero;
use style::computed_values::position::T as Position;
use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
use style::properties::ComputedValues;
use style::values::generics::box_::{GenericVerticalAlign, VerticalAlignKeyword};
use style::values::generics::font::LineHeight;
use style::values::specified::align::AlignFlags;
use style::values::specified::box_::DisplayOutside;
use style::values::specified::text::TextDecorationLine;
use unicode_bidi::{BidiInfo, Level};
use webrender_api::FontInstanceKey;
use super::inline_box::{InlineBoxContainerState, InlineBoxIdentifier, InlineBoxTreePathToken};
use super::{InlineFormattingContextLayout, LineBlockSizes};
use crate::cell::ArcRefCell;
use crate::fragment_tree::{BaseFragmentInfo, BoxFragment, Fragment, TextFragment};
use crate::geom::{LogicalRect, LogicalVec2, PhysicalRect, ToLogical};
use crate::positioned::{
AbsolutelyPositionedBox, PositioningContext, PositioningContextLength, relative_adjustement,
};
use crate::{ContainingBlock, ContainingBlockSize};
pub(super) struct LineMetrics {
/// The block offset of the line start in the containing
/// [`crate::flow::InlineFormattingContext`].
pub block_offset: Au,
/// The block size of this line.
pub block_size: Au,
/// The block offset of this line's baseline from [`Self::block_offset`].
pub baseline_block_offset: Au,
}
bitflags! {
struct LineLayoutInlineContainerFlags: u8 {
/// Whether or not any line items were processed for this inline box, this includes
/// any child inline boxes.
const HAD_ANY_LINE_ITEMS = 1 << 0;
/// Whether or not the starting inline border, padding, or margin of the inline box
/// was encountered.
const HAD_INLINE_START_PBM = 1 << 2;
/// Whether or not the ending inline border, padding, or margin of the inline box
/// was encountered.
const HAD_INLINE_END_PBM = 1 << 3;
/// Whether or not any floats were encountered while laying out this inline box.
const HAD_ANY_FLOATS = 1 << 4;
}
}
/// The state used when laying out a collection of [`LineItem`]s into a line. This state is stored
/// per-inline container. For instance, when laying out the conents of a `<span>` a fresh
/// [`LineItemLayoutInlineContainerState`] is pushed onto [`LineItemLayout`]'s stack of states.
pub(super) struct LineItemLayoutInlineContainerState {
/// If this inline container is not the root inline container, the identifier of the [`super::InlineBox`]
/// that is currently being laid out.
pub identifier: Option<InlineBoxIdentifier>,
/// The fragments and their logical rectangle relative within the current inline box (or
/// line). These logical rectangles will be converted into physical ones and the Fragment's
/// `content_rect` will be updated once the inline box's final size is known in
/// [`LineItemLayout::end_inline_box`].
pub fragments: Vec<(Fragment, LogicalRect<Au>)>,
/// The current inline advance of the layout in the coordinates of this inline box.
pub inline_advance: Au,
/// Flags which track various features during layout.
flags: LineLayoutInlineContainerFlags,
/// The offset of the parent, relative to the start position of the line, not including
/// any inline start and end borders which are only processed when the inline box is
/// finished.
pub parent_offset: LogicalVec2<Au>,
/// The block offset of the parent's baseline relative to the block start of the line. This
/// is often the same as [`Self::parent_offset`], but can be different for the root
/// element.
pub baseline_offset: Au,
/// If this inline box establishes a containing block for positioned elements, this
/// is a fresh positioning context to contain them. Otherwise, this holds the starting
/// offset in the *parent* positioning context so that static positions can be updated
/// at the end of layout.
pub positioning_context_or_start_offset_in_parent:
Either<PositioningContext, PositioningContextLength>,
}
impl LineItemLayoutInlineContainerState {
fn new(
identifier: Option<InlineBoxIdentifier>,
parent_offset: LogicalVec2<Au>,
baseline_offset: Au,
positioning_context_or_start_offset_in_parent: Either<
PositioningContext,
PositioningContextLength,
>,
) -> Self {
Self {
identifier,
fragments: Vec::new(),
inline_advance: Au::zero(),
flags: LineLayoutInlineContainerFlags::empty(),
parent_offset,
baseline_offset,
positioning_context_or_start_offset_in_parent,
}
}
fn root(starting_inline_advance: Au, baseline_offset: Au) -> Self {
let mut state = Self::new(
None,
LogicalVec2::zero(),
baseline_offset,
Either::Right(PositioningContextLength::zero()),
);
state.inline_advance = starting_inline_advance;
state
}
}
/// The second phase of [`super::InlineFormattingContext`] layout: once items are gathered
/// for a line, we must lay them out and create fragments for them, properly positioning them
/// according to their baselines and also handling absolutely positioned children.
pub(super) struct LineItemLayout<'layout_data, 'layout> {
/// The state of the overall [`super::InlineFormattingContext`] layout.
layout: &'layout mut InlineFormattingContextLayout<'layout_data>,
/// The set of [`LineItemLayoutInlineContainerState`] created while laying out items
/// on this line. This does not include the current level of recursion.
pub state_stack: Vec<LineItemLayoutInlineContainerState>,
/// The current [`LineItemLayoutInlineContainerState`].
pub current_state: LineItemLayoutInlineContainerState,
/// The metrics of this line, which should remain constant throughout the
/// layout process.
pub line_metrics: LineMetrics,
/// The amount of space to add to each justification opportunity in order to implement
/// `text-align: justify`.
pub justification_adjustment: Au,
}
impl LineItemLayout<'_, '_> {
pub(super) fn layout_line_items(
layout: &mut InlineFormattingContextLayout,
line_items: Vec<LineItem>,
start_position: LogicalVec2<Au>,
effective_block_advance: &LineBlockSizes,
justification_adjustment: Au,
) -> Vec<Fragment> {
let baseline_offset = effective_block_advance.find_baseline_offset();
LineItemLayout {
layout,
state_stack: Vec::new(),
current_state: LineItemLayoutInlineContainerState::root(
start_position.inline,
baseline_offset,
),
line_metrics: LineMetrics {
block_offset: start_position.block,
block_size: effective_block_advance.resolve(),
baseline_block_offset: baseline_offset,
},
justification_adjustment,
}
.layout(line_items)
}
/// Start and end inline boxes in tree order, so that it reflects the given inline box.
fn prepare_layout_for_inline_box(&mut self, new_inline_box: Option<InlineBoxIdentifier>) {
// Optimize the case where we are moving to the root of the inline box stack.
let Some(new_inline_box) = new_inline_box else {
while !self.state_stack.is_empty() {
self.end_inline_box();
}
return;
};
// Otherwise, follow the path given to us by our collection of inline boxes, so we know which
// inline boxes to start and end.
let path = self
.layout
.ifc
.inline_boxes
.get_path(self.current_state.identifier, new_inline_box);
for token in path {
match token {
InlineBoxTreePathToken::Start(ref identifier) => self.start_inline_box(identifier),
InlineBoxTreePathToken::End(_) => self.end_inline_box(),
}
}
}
pub(super) fn layout(&mut self, mut line_items: Vec<LineItem>) -> Vec<Fragment> {
let mut last_level = Level::ltr();
let levels: Vec<_> = line_items
.iter()
.map(|item| {
let level = match item {
LineItem::TextRun(_, text_run) => text_run.bidi_level,
// TODO: This level needs either to be last_level, or if there were
// unicode characters inserted for the inline box, we need to get the
// level from them.
LineItem::InlineStartBoxPaddingBorderMargin(_) => last_level,
LineItem::InlineEndBoxPaddingBorderMargin(_) => last_level,
LineItem::Atomic(_, atomic) => atomic.bidi_level,
LineItem::AbsolutelyPositioned(..) => last_level,
LineItem::Float(..) => {
// At this point the float is already positioned, so it doesn't really matter what
// position it's fragment has in the order of line items.
last_level
},
};
last_level = level;
level
})
.collect();
if self.layout.ifc.has_right_to_left_content {
sort_by_indices_in_place(&mut line_items, BidiInfo::reorder_visual(&levels));
}
// `BidiInfo::reorder_visual` will reorder the contents of the line so that they
// are in the correct order as if one was looking at the line from left-to-right.
// During this layout we do not lay out from left to right. Instead we lay out
// from inline-start to inline-end. If the overall line contents have been flipped
// for BiDi, flip them again so that they are in line start-to-end order rather
// than left-to-right order.
let line_item_iterator = if self
.layout
.containing_block
.style
.writing_mode
.is_bidi_ltr()
{
Either::Left(line_items.into_iter())
} else {
Either::Right(line_items.into_iter().rev())
};
for item in line_item_iterator.into_iter().by_ref() {
// When preparing to lay out a new line item, start and end inline boxes, so that the current
// inline box state reflects the item's parent. Items in the line are not necessarily in tree
// order due to BiDi and other reordering so the inline box of the item could potentially be
// any in the inline formatting context.
self.prepare_layout_for_inline_box(item.inline_box_identifier());
self.current_state
.flags
.insert(LineLayoutInlineContainerFlags::HAD_ANY_LINE_ITEMS);
match item {
LineItem::InlineStartBoxPaddingBorderMargin(_) => {
self.current_state
.flags
.insert(LineLayoutInlineContainerFlags::HAD_INLINE_START_PBM);
},
LineItem::InlineEndBoxPaddingBorderMargin(_) => {
self.current_state
.flags
.insert(LineLayoutInlineContainerFlags::HAD_INLINE_END_PBM);
},
LineItem::TextRun(_, text_run) => self.layout_text_run(text_run),
LineItem::Atomic(_, atomic) => self.layout_atomic(atomic),
LineItem::AbsolutelyPositioned(_, absolute) => self.layout_absolute(absolute),
LineItem::Float(_, float) => self.layout_float(float),
}
}
// Move back to the root of the inline box tree, so that all boxes are ended.
self.prepare_layout_for_inline_box(None);
let fragments_and_rectangles = std::mem::take(&mut self.current_state.fragments);
fragments_and_rectangles
.into_iter()
.map(|(mut fragment, logical_rect)| {
if matches!(fragment, Fragment::Float(_)) {
return fragment;
}
// We do not know the actual physical position of a logically laid out inline element, until
// we know the width of the containing inline block. This step converts the logical rectangle
// into a physical one based on the inline formatting context width.
fragment.mutate_content_rect(|content_rect| {
*content_rect = logical_rect.as_physical(Some(self.layout.containing_block))
});
fragment
})
.collect()
}
fn current_positioning_context_mut(&mut self) -> &mut PositioningContext {
if let Either::Left(ref mut positioning_context) = self
.current_state
.positioning_context_or_start_offset_in_parent
{
return positioning_context;
}
self.state_stack
.iter_mut()
.rev()
.find_map(
|state| match state.positioning_context_or_start_offset_in_parent {
Either::Left(ref mut positioning_context) => Some(positioning_context),
Either::Right(_) => None,
},
)
.unwrap_or(self.layout.positioning_context)
}
fn start_inline_box(&mut self, identifier: &InlineBoxIdentifier) {
let inline_box_state =
&*self.layout.inline_box_states[identifier.index_in_inline_boxes as usize];
let inline_box = self.layout.ifc.inline_boxes.get(identifier);
let inline_box = &*(inline_box.borrow());
let style = &inline_box.base.style;
let space_above_baseline = inline_box_state.calculate_space_above_baseline();
let block_start_offset =
self.calculate_inline_box_block_start(inline_box_state, space_above_baseline);
let positioning_context_or_start_offset_in_parent =
match PositioningContext::new_for_style(style) {
Some(positioning_context) => Either::Left(positioning_context),
None => Either::Right(self.current_positioning_context_mut().len()),
};
let parent_offset = LogicalVec2 {
inline: self.current_state.inline_advance + self.current_state.parent_offset.inline,
block: block_start_offset,
};
let outer_state = std::mem::replace(
&mut self.current_state,
LineItemLayoutInlineContainerState::new(
Some(*identifier),
parent_offset,
block_start_offset + space_above_baseline,
positioning_context_or_start_offset_in_parent,
),
);
self.state_stack.push(outer_state);
}
fn end_inline_box(&mut self) {
let outer_state = self.state_stack.pop().expect("Ended unknown inline box");
let inner_state = std::mem::replace(&mut self.current_state, outer_state);
let identifier = inner_state.identifier.expect("Ended unknown inline box");
let inline_box_state =
&*self.layout.inline_box_states[identifier.index_in_inline_boxes as usize];
let inline_box = self.layout.ifc.inline_boxes.get(&identifier);
let inline_box = &*(inline_box.borrow());
let mut padding = inline_box_state.pbm.padding;
let mut border = inline_box_state.pbm.border;
let mut margin = inline_box_state.pbm.margin.auto_is(Au::zero);
let mut had_start = inner_state
.flags
.contains(LineLayoutInlineContainerFlags::HAD_INLINE_START_PBM);
let mut had_end = inner_state
.flags
.contains(LineLayoutInlineContainerFlags::HAD_INLINE_END_PBM);
let containing_block_writing_mode = self.layout.containing_block.style.writing_mode;
if containing_block_writing_mode.is_bidi_ltr() !=
inline_box.base.style.writing_mode.is_bidi_ltr()
{
std::mem::swap(&mut had_start, &mut had_end)
}
if !had_start {
padding.inline_start = Au::zero();
border.inline_start = Au::zero();
margin.inline_start = Au::zero();
}
if !had_end {
padding.inline_end = Au::zero();
border.inline_end = Au::zero();
margin.inline_end = Au::zero();
}
// If the inline box didn't have any content at all and it isn't the first fragment for
// an element (needed for layout queries currently) and it didn't have any padding, border,
// or margin do not make a fragment for it.
//
// Note: This is an optimization, but also has side effects. Any fragments on a line will
// force the baseline to advance in the parent IFC.
let pbm_sums = padding + border + margin;
if inner_state.fragments.is_empty() && !had_start && pbm_sums.inline_sum().is_zero() {
return;
}
// Make `content_rect` relative to the parent Fragment.
let mut content_rect = LogicalRect {
start_corner: LogicalVec2 {
inline: self.current_state.inline_advance + pbm_sums.inline_start,
block: inner_state.parent_offset.block - self.current_state.parent_offset.block,
},
size: LogicalVec2 {
inline: inner_state.inline_advance,
block: inline_box_state.base.font_metrics.line_gap,
},
};
// Relative adjustment should not affect the rest of line layout, so we can
// do it right before creating the Fragment.
let style = &inline_box.base.style;
if style.get_box().position == Position::Relative {
content_rect.start_corner += relative_adjustement(style, self.layout.containing_block);
}
let ifc_writing_mode = self.layout.containing_block.style.writing_mode;
let inline_box_containing_block = ContainingBlock {
size: ContainingBlockSize {
inline: content_rect.size.inline,
block: Default::default(),
},
style: self.layout.containing_block.style,
};
let fragments = inner_state
.fragments
.into_iter()
.map(|(mut fragment, logical_rect)| {
let is_float = matches!(fragment, Fragment::Float(_));
fragment.mutate_content_rect(|content_rect| {
if is_float {
content_rect.origin -=
pbm_sums.start_offset().to_physical_size(ifc_writing_mode);
} else {
// We do not know the actual physical position of a logically laid out inline element, until
// we know the width of the containing inline block. This step converts the logical rectangle
// into a physical one now that we've computed inline size of the containing inline block above.
*content_rect = logical_rect.as_physical(Some(&inline_box_containing_block))
}
});
fragment
})
.collect();
// Previously all the fragment's children were positioned relative to the linebox,
// but they need to be made relative to this fragment.
let physical_content_rect = content_rect.as_physical(Some(self.layout.containing_block));
let mut fragment = BoxFragment::new(
inline_box.base.base_fragment_info,
style.clone(),
fragments,
physical_content_rect,
padding.to_physical(ifc_writing_mode),
border.to_physical(ifc_writing_mode),
margin.to_physical(ifc_writing_mode),
None, /* clearance */
);
let offset_from_parent_ifc = LogicalVec2 {
inline: pbm_sums.inline_start + self.current_state.inline_advance,
block: content_rect.start_corner.block,
}
.to_physical_vector(self.layout.containing_block.style.writing_mode);
match inner_state.positioning_context_or_start_offset_in_parent {
Either::Left(mut positioning_context) => {
positioning_context
.layout_collected_children(self.layout.layout_context, &mut fragment);
positioning_context.adjust_static_position_of_hoisted_fragments_with_offset(
&offset_from_parent_ifc,
PositioningContextLength::zero(),
);
self.current_positioning_context_mut()
.append(positioning_context);
},
Either::Right(start_offset) => {
self.current_positioning_context_mut()
.adjust_static_position_of_hoisted_fragments_with_offset(
&offset_from_parent_ifc,
start_offset,
);
},
}
self.current_state.inline_advance += inner_state.inline_advance + pbm_sums.inline_sum();
let fragment = Fragment::Box(ArcRefCell::new(fragment));
inline_box.base.add_fragment(fragment.clone());
self.current_state.fragments.push((fragment, content_rect));
}
fn calculate_inline_box_block_start(
&self,
inline_box_state: &InlineBoxContainerState,
space_above_baseline: Au,
) -> Au {
let font_metrics = &inline_box_state.base.font_metrics;
let style = &inline_box_state.base.style;
let line_gap = font_metrics.line_gap;
// The baseline offset that we have in `Self::baseline_offset` is relative to the line
// baseline, so we need to make it relative to the line block start.
match inline_box_state.base.style.clone_vertical_align() {
GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) => {
let line_height: Au = line_height(style, font_metrics);
(line_height - line_gap).scale_by(0.5)
},
GenericVerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => {
let line_height: Au = line_height(style, font_metrics);
let half_leading = (line_height - line_gap).scale_by(0.5);
self.line_metrics.block_size - line_height + half_leading
},
_ => {
self.line_metrics.baseline_block_offset + inline_box_state.base.baseline_offset -
space_above_baseline
},
}
}
fn layout_text_run(&mut self, text_item: TextRunLineItem) {
if text_item.text.is_empty() {
return;
}
let mut number_of_justification_opportunities = 0;
let mut inline_advance = text_item
.text
.iter()
.map(|glyph_store| {
number_of_justification_opportunities += glyph_store.total_word_separators();
glyph_store.total_advance()
})
.sum();
if !self.justification_adjustment.is_zero() {
inline_advance += self
.justification_adjustment
.scale_by(number_of_justification_opportunities as f32);
}
// The block start of the TextRun is often zero (meaning it has the same font metrics as the
// inline box's strut), but for children of the inline formatting context root or for
// fallback fonts that use baseline relative alignment, it might be different.
let start_corner = LogicalVec2 {
inline: self.current_state.inline_advance,
block: self.current_state.baseline_offset -
text_item.font_metrics.ascent -
self.current_state.parent_offset.block,
};
let content_rect = LogicalRect {
start_corner,
size: LogicalVec2 {
block: text_item.font_metrics.line_gap,
inline: inline_advance,
},
};
self.current_state.inline_advance += inline_advance;
self.current_state.fragments.push((
Fragment::Text(ArcRefCell::new(TextFragment {
base: text_item.base_fragment_info.into(),
parent_style: text_item.parent_style,
rect: PhysicalRect::zero(),
font_metrics: text_item.font_metrics,
font_key: text_item.font_key,
glyphs: text_item.text,
text_decoration_line: text_item.text_decoration_line,
justification_adjustment: self.justification_adjustment,
selection_range: text_item.selection_range,
selected_style: text_item.selected_style,
})),
content_rect,
));
}
fn layout_atomic(&mut self, atomic: AtomicLineItem) {
// The initial `start_corner` of the Fragment is only the PaddingBorderMargin sum start
// offset, which is the sum of the start component of the padding, border, and margin.
// This needs to be added to the calculated block and inline positions.
// Make the final result relative to the parent box.
let ifc_writing_mode = self.layout.containing_block.style.writing_mode;
let content_rect = {
let block_start = atomic.calculate_block_start(&self.line_metrics);
let atomic_fragment = atomic.fragment.borrow_mut();
let padding_border_margin_sides = atomic_fragment
.padding_border_margin()
.to_logical(ifc_writing_mode);
let mut atomic_offset = LogicalVec2 {
inline: self.current_state.inline_advance +
padding_border_margin_sides.inline_start,
block: block_start - self.current_state.parent_offset.block +
padding_border_margin_sides.block_start,
};
if atomic_fragment.style.get_box().position == Position::Relative {
atomic_offset +=
relative_adjustement(&atomic_fragment.style, self.layout.containing_block);
}
// Reconstruct a logical rectangle relative to the inline box container that will be used
// after the inline box is procesed to find a final physical rectangle.
LogicalRect {
start_corner: atomic_offset,
size: atomic_fragment
.content_rect
.size
.to_logical(ifc_writing_mode),
}
};
if let Some(mut positioning_context) = atomic.positioning_context {
let physical_rect_as_if_in_root =
content_rect.as_physical(Some(self.layout.containing_block));
positioning_context.adjust_static_position_of_hoisted_fragments_with_offset(
&physical_rect_as_if_in_root.origin.to_vector(),
PositioningContextLength::zero(),
);
self.current_positioning_context_mut()
.append(positioning_context);
}
self.current_state.inline_advance += atomic.size.inline;
self.current_state
.fragments
.push((Fragment::Box(atomic.fragment), content_rect));
}
fn layout_absolute(&mut self, absolute: AbsolutelyPositionedLineItem) {
let absolutely_positioned_box = (*absolute.absolutely_positioned_box).borrow();
let style = absolutely_positioned_box.context.style();
// From https://drafts.csswg.org/css2/#abs-non-replaced-width
// > The static-position containing block is the containing block of a
// > hypothetical box that would have been the first box of the element if its
// > specified position value had been static and its specified float had been
// > none. (Note that due to the rules in section 9.7 this hypothetical
// > calculation might require also assuming a different computed value for
// > display.)
//
// This box is different based on the original `display` value of the
// absolutely positioned element. If it's `inline` it would be placed inline
// at the top of the line, but if it's block it would be placed in a new
// block position after the linebox established by this line.
let initial_start_corner =
if style.get_box().original_display.outside() == DisplayOutside::Inline {
// Top of the line at the current inline position.
LogicalVec2 {
inline: self.current_state.inline_advance,
block: -self.current_state.parent_offset.block,
}
} else {
// After the bottom of the line at the start of the inline formatting context.
LogicalVec2 {
inline: -self.current_state.parent_offset.inline,
block: self.line_metrics.block_size - self.current_state.parent_offset.block,
}
};
// Since alignment of absolutes in inlines is currently always `start`, the size of
// of the static position rectangle does not matter.
let static_position_rect = LogicalRect {
start_corner: initial_start_corner,
size: LogicalVec2::zero(),
}
.as_physical(Some(self.layout.containing_block));
let hoisted_box = AbsolutelyPositionedBox::to_hoisted(
absolute.absolutely_positioned_box.clone(),
static_position_rect,
LogicalVec2 {
inline: AlignFlags::START,
block: AlignFlags::START,
},
self.layout.containing_block.style.writing_mode,
);
let hoisted_fragment = hoisted_box.fragment.clone();
self.current_positioning_context_mut().push(hoisted_box);
self.current_state.fragments.push((
Fragment::AbsoluteOrFixedPositioned(hoisted_fragment),
LogicalRect::zero(),
));
}
fn layout_float(&mut self, float: FloatLineItem) {
self.current_state
.flags
.insert(LineLayoutInlineContainerFlags::HAD_ANY_FLOATS);
// The `BoxFragment` for this float is positioned relative to the IFC, so we need
// to move it to be positioned relative to our parent InlineBox line item. Float
// fragments are children of these InlineBoxes and not children of the inline
// formatting context, so that they are parented properly for StackingContext
// properties such as opacity & filters.
let distance_from_parent_to_ifc = LogicalVec2 {
inline: self.current_state.parent_offset.inline,
block: self.line_metrics.block_offset + self.current_state.parent_offset.block,
};
float.fragment.borrow_mut().content_rect.origin -= distance_from_parent_to_ifc
.to_physical_size(self.layout.containing_block.style.writing_mode);
self.current_state
.fragments
.push((Fragment::Float(float.fragment), LogicalRect::zero()));
}
}
pub(super) enum LineItem {
InlineStartBoxPaddingBorderMargin(InlineBoxIdentifier),
InlineEndBoxPaddingBorderMargin(InlineBoxIdentifier),
TextRun(Option<InlineBoxIdentifier>, TextRunLineItem),
Atomic(Option<InlineBoxIdentifier>, AtomicLineItem),
AbsolutelyPositioned(Option<InlineBoxIdentifier>, AbsolutelyPositionedLineItem),
Float(Option<InlineBoxIdentifier>, FloatLineItem),
}
impl LineItem {
fn inline_box_identifier(&self) -> Option<InlineBoxIdentifier> {
match self {
LineItem::InlineStartBoxPaddingBorderMargin(identifier) => Some(*identifier),
LineItem::InlineEndBoxPaddingBorderMargin(identifier) => Some(*identifier),
LineItem::TextRun(identifier, _) => *identifier,
LineItem::Atomic(identifier, _) => *identifier,
LineItem::AbsolutelyPositioned(identifier, _) => *identifier,
LineItem::Float(identifier, _) => *identifier,
}
}
pub(super) fn trim_whitespace_at_end(&mut self, whitespace_trimmed: &mut Au) -> bool {
match self {
LineItem::InlineStartBoxPaddingBorderMargin(_) => true,
LineItem::InlineEndBoxPaddingBorderMargin(_) => true,
LineItem::TextRun(_, item) => item.trim_whitespace_at_end(whitespace_trimmed),
LineItem::Atomic(..) => false,
LineItem::AbsolutelyPositioned(..) => true,
LineItem::Float(..) => true,
}
}
pub(super) fn trim_whitespace_at_start(&mut self, whitespace_trimmed: &mut Au) -> bool {
match self {
LineItem::InlineStartBoxPaddingBorderMargin(_) => true,
LineItem::InlineEndBoxPaddingBorderMargin(_) => true,
LineItem::TextRun(_, item) => item.trim_whitespace_at_start(whitespace_trimmed),
LineItem::Atomic(..) => false,
LineItem::AbsolutelyPositioned(..) => true,
LineItem::Float(..) => true,
}
}
}
pub(super) struct TextRunLineItem {
pub base_fragment_info: BaseFragmentInfo,
pub parent_style: Arc<ComputedValues>,
pub text: Vec<std::sync::Arc<GlyphStore>>,
pub font_metrics: FontMetrics,
pub font_key: FontInstanceKey,
pub text_decoration_line: TextDecorationLine,
/// The BiDi level of this [`TextRunLineItem`] to enable reordering.
pub bidi_level: Level,
pub selection_range: Option<Range<ByteIndex>>,
pub selected_style: Arc<ComputedValues>,
}
impl TextRunLineItem {
fn trim_whitespace_at_end(&mut self, whitespace_trimmed: &mut Au) -> bool {
if matches!(
self.parent_style.get_inherited_text().white_space_collapse,
WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces
) {
return false;
}
let index_of_last_non_whitespace = self
.text
.iter()
.rev()
.position(|glyph| !glyph.is_whitespace())
.map(|offset_from_end| self.text.len() - offset_from_end);
let first_whitespace_index = index_of_last_non_whitespace.unwrap_or(0);
*whitespace_trimmed += self
.text
.drain(first_whitespace_index..)
.map(|glyph| glyph.total_advance())
.sum();
// Only keep going if we only encountered whitespace.
index_of_last_non_whitespace.is_none()
}
fn trim_whitespace_at_start(&mut self, whitespace_trimmed: &mut Au) -> bool {
if matches!(
self.parent_style.get_inherited_text().white_space_collapse,
WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces
) {
return false;
}
let index_of_first_non_whitespace = self
.text
.iter()
.position(|glyph| !glyph.is_whitespace())
.unwrap_or(self.text.len());
*whitespace_trimmed += self
.text
.drain(0..index_of_first_non_whitespace)
.map(|glyph| glyph.total_advance())
.sum();
// Only keep going if we only encountered whitespace.
self.text.is_empty()
}
pub(crate) fn can_merge(&self, font_key: FontInstanceKey, bidi_level: Level) -> bool {
self.font_key == font_key && self.bidi_level == bidi_level
}
}
pub(super) struct AtomicLineItem {
pub fragment: ArcRefCell<BoxFragment>,
pub size: LogicalVec2<Au>,
pub positioning_context: Option<PositioningContext>,
/// The block offset of this items' baseline relative to the baseline of the line.
/// This will be zero for boxes with `vertical-align: top` and `vertical-align:
/// bottom` since their baselines are calculated late in layout.
pub baseline_offset_in_parent: Au,
/// The offset of the baseline inside this item.
pub baseline_offset_in_item: Au,
/// The BiDi level of this [`AtomicLineItem`] to enable reordering.
pub bidi_level: Level,
}
impl AtomicLineItem {
/// Given the metrics for a line, our vertical alignment, and our block size, find a block start
/// position relative to the top of the line.
fn calculate_block_start(&self, line_metrics: &LineMetrics) -> Au {
match self.fragment.borrow().style.clone_vertical_align() {
GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) => Au::zero(),
GenericVerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => {
line_metrics.block_size - self.size.block
},
// This covers all baseline-relative vertical alignment.
_ => {
let baseline = line_metrics.baseline_block_offset + self.baseline_offset_in_parent;
baseline - self.baseline_offset_in_item
},
}
}
}
pub(super) struct AbsolutelyPositionedLineItem {
pub absolutely_positioned_box: ArcRefCell<AbsolutelyPositionedBox>,
}
pub(super) struct FloatLineItem {
pub fragment: ArcRefCell<BoxFragment>,
/// Whether or not this float Fragment has been placed yet. Fragments that
/// do not fit on a line need to be placed after the hypothetical block start
/// of the next line.
pub needs_placement: bool,
}
fn line_height(parent_style: &ComputedValues, font_metrics: &FontMetrics) -> Au {
let font = parent_style.get_font();
let font_size = font.font_size.computed_size();
match font.line_height {
LineHeight::Normal => font_metrics.line_gap,
LineHeight::Number(number) => (font_size * number.0).into(),
LineHeight::Length(length) => length.0.into(),
}
}
/// Sort a mutable slice by the the given indices array in place, reording the slice so that final
/// value of `slice[x]` is `slice[indices[x]]`.
fn sort_by_indices_in_place<T>(data: &mut [T], mut indices: Vec<usize>) {
for idx in 0..data.len() {
if indices[idx] == idx {
continue;
}
let mut current_idx = idx;
loop {
let target_idx = indices[current_idx];
indices[current_idx] = current_idx;
if indices[target_idx] == target_idx {
break;
}
data.swap(current_idx, target_idx);
current_idx = target_idx;
}
}
}

View file

@ -0,0 +1,120 @@
/* 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::ops::Range;
use icu_segmenter::LineSegmenter;
pub(crate) struct LineBreaker {
linebreaks: Vec<usize>,
current_offset: usize,
}
impl LineBreaker {
pub(crate) fn new(string: &str) -> Self {
let line_segmenter = LineSegmenter::new_auto();
Self {
// From https://docs.rs/icu_segmenter/1.5.0/icu_segmenter/struct.LineSegmenter.html
// > For consistency with the grapheme, word, and sentence segmenters, there is always a
// > breakpoint returned at index 0, but this breakpoint is not a meaningful line break
// > opportunity.
//
// Skip this first line break opportunity, as it isn't interesting to us.
linebreaks: line_segmenter.segment_str(string).skip(1).collect(),
current_offset: 0,
}
}
pub(crate) fn advance_to_linebreaks_in_range(&mut self, text_range: Range<usize>) -> &[usize] {
let linebreaks_in_range = self.linebreaks_in_range_after_current_offset(text_range);
self.current_offset = linebreaks_in_range.end;
&self.linebreaks[linebreaks_in_range]
}
fn linebreaks_in_range_after_current_offset(&self, text_range: Range<usize>) -> Range<usize> {
assert!(text_range.start <= text_range.end);
let mut linebreaks_range = self.current_offset..self.linebreaks.len();
while self.linebreaks[linebreaks_range.start] < text_range.start &&
linebreaks_range.len() > 1
{
linebreaks_range.start += 1;
}
let mut ending_linebreak_index = linebreaks_range.start;
while self.linebreaks[ending_linebreak_index] < text_range.end &&
ending_linebreak_index < self.linebreaks.len() - 1
{
ending_linebreak_index += 1;
}
linebreaks_range.end = ending_linebreak_index;
linebreaks_range
}
}
#[test]
fn test_linebreaker_ranges() {
let linebreaker = LineBreaker::new("abc def");
assert_eq!(linebreaker.linebreaks, [4, 7]);
assert_eq!(
linebreaker.linebreaks_in_range_after_current_offset(0..5),
0..1
);
// The last linebreak should not be included for the text range we are interested in.
assert_eq!(
linebreaker.linebreaks_in_range_after_current_offset(0..7),
0..1
);
let linebreaker = LineBreaker::new("abc d def");
assert_eq!(linebreaker.linebreaks, [4, 6, 9]);
assert_eq!(
linebreaker.linebreaks_in_range_after_current_offset(0..5),
0..1
);
assert_eq!(
linebreaker.linebreaks_in_range_after_current_offset(0..7),
0..2
);
assert_eq!(
linebreaker.linebreaks_in_range_after_current_offset(0..9),
0..2
);
assert_eq!(
linebreaker.linebreaks_in_range_after_current_offset(4..9),
0..2
);
std::panic::catch_unwind(|| {
let linebreaker = LineBreaker::new("abc def");
linebreaker.linebreaks_in_range_after_current_offset(5..2);
})
.expect_err("Reversed range should cause an assertion failure.");
}
#[test]
fn test_linebreaker_stateful_advance() {
let mut linebreaker = LineBreaker::new("abc d def");
assert_eq!(linebreaker.linebreaks, [4, 6, 9]);
assert!(linebreaker.advance_to_linebreaks_in_range(0..7) == &[4, 6]);
assert!(linebreaker.advance_to_linebreaks_in_range(8..9).is_empty());
// We've already advanced, so a range from the beginning shouldn't affect things.
assert!(linebreaker.advance_to_linebreaks_in_range(0..9).is_empty());
linebreaker.current_offset = 0;
// Sending a value out of range shoudn't break things.
assert!(linebreaker.advance_to_linebreaks_in_range(0..999) == &[4, 6]);
linebreaker.current_offset = 0;
std::panic::catch_unwind(|| {
let mut linebreaker = LineBreaker::new("abc d def");
linebreaker.advance_to_linebreaks_in_range(2..0);
})
.expect_err("Reversed range should cause an assertion failure.");
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,640 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::mem;
use std::ops::Range;
use app_units::Au;
use base::text::is_bidi_control;
use fonts::{
FontContext, FontRef, GlyphRun, LAST_RESORT_GLYPH_ADVANCE, ShapingFlags, ShapingOptions,
};
use fonts_traits::ByteIndex;
use log::warn;
use malloc_size_of_derive::MallocSizeOf;
use range::Range as ServoRange;
use servo_arc::Arc;
use style::computed_values::text_rendering::T as TextRendering;
use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
use style::computed_values::word_break::T as WordBreak;
use style::properties::ComputedValues;
use style::str::char_is_whitespace;
use style::values::computed::OverflowWrap;
use unicode_bidi::{BidiInfo, Level};
use unicode_script::Script;
use xi_unicode::linebreak_property;
use super::line_breaker::LineBreaker;
use super::{FontKeyAndMetrics, InlineFormattingContextLayout};
use crate::fragment_tree::BaseFragmentInfo;
// These constants are the xi-unicode line breaking classes that are defined in
// `table.rs`. Unfortunately, they are only identified by number.
pub(crate) const XI_LINE_BREAKING_CLASS_CM: u8 = 9;
pub(crate) const XI_LINE_BREAKING_CLASS_GL: u8 = 12;
pub(crate) const XI_LINE_BREAKING_CLASS_ZW: u8 = 28;
pub(crate) const XI_LINE_BREAKING_CLASS_WJ: u8 = 30;
pub(crate) const XI_LINE_BREAKING_CLASS_ZWJ: u8 = 42;
/// <https://www.w3.org/TR/css-display-3/#css-text-run>
#[derive(Debug, MallocSizeOf)]
pub(crate) struct TextRun {
pub base_fragment_info: BaseFragmentInfo,
#[conditional_malloc_size_of]
pub parent_style: Arc<ComputedValues>,
pub text_range: Range<usize>,
/// The text of this [`TextRun`] with a font selected, broken into unbreakable
/// segments, and shaped.
pub shaped_text: Vec<TextRunSegment>,
pub selection_range: Option<ServoRange<ByteIndex>>,
#[conditional_malloc_size_of]
pub selected_style: Arc<ComputedValues>,
}
// There are two reasons why we might want to break at the start:
//
// 1. The line breaker told us that a break was necessary between two separate
// instances of sending text to it.
// 2. We are following replaced content ie `have_deferred_soft_wrap_opportunity`.
//
// In both cases, we don't want to do this if the first character prevents a
// soft wrap opportunity.
#[derive(PartialEq)]
enum SegmentStartSoftWrapPolicy {
Force,
FollowLinebreaker,
}
#[derive(Debug, MallocSizeOf)]
pub(crate) struct TextRunSegment {
/// The index of this font in the parent [`super::InlineFormattingContext`]'s collection of font
/// information.
pub font_index: usize,
/// The [`Script`] of this segment.
pub script: Script,
/// The bidi Level of this segment.
pub bidi_level: Level,
/// The range of bytes in the parent [`super::InlineFormattingContext`]'s text content.
pub range: Range<usize>,
/// Whether or not the linebreaker said that we should allow a line break at the start of this
/// segment.
pub break_at_start: bool,
/// The shaped runs within this segment.
pub runs: Vec<GlyphRun>,
}
impl TextRunSegment {
fn new(font_index: usize, script: Script, bidi_level: Level, start_offset: usize) -> Self {
Self {
font_index,
script,
bidi_level,
range: start_offset..start_offset,
runs: Vec::new(),
break_at_start: false,
}
}
/// Update this segment if the Font and Script are compatible. The update will only
/// ever make the Script specific. Returns true if the new Font and Script are
/// compatible with this segment or false otherwise.
fn update_if_compatible(
&mut self,
new_font: &FontRef,
script: Script,
bidi_level: Level,
fonts: &[FontKeyAndMetrics],
font_context: &FontContext,
) -> bool {
fn is_specific(script: Script) -> bool {
script != Script::Common && script != Script::Inherited
}
if bidi_level != self.bidi_level {
return false;
}
let current_font_key_and_metrics = &fonts[self.font_index];
if new_font.key(font_context) != current_font_key_and_metrics.key ||
new_font.descriptor.pt_size != current_font_key_and_metrics.pt_size
{
return false;
}
if !is_specific(self.script) && is_specific(script) {
self.script = script;
}
script == self.script || !is_specific(script)
}
fn layout_into_line_items(
&self,
text_run: &TextRun,
mut soft_wrap_policy: SegmentStartSoftWrapPolicy,
ifc: &mut InlineFormattingContextLayout,
) {
if self.break_at_start && soft_wrap_policy == SegmentStartSoftWrapPolicy::FollowLinebreaker
{
soft_wrap_policy = SegmentStartSoftWrapPolicy::Force;
}
let mut byte_processed = ByteIndex(0);
for (run_index, run) in self.runs.iter().enumerate() {
ifc.possibly_flush_deferred_forced_line_break();
// If this whitespace forces a line break, queue up a hard line break the next time we
// see any content. We don't line break immediately, because we'd like to finish processing
// any ongoing inline boxes before ending the line.
if run.is_single_preserved_newline() {
byte_processed = byte_processed + run.range.length();
ifc.defer_forced_line_break();
continue;
}
// Break before each unbreakable run in this TextRun, except the first unless the
// linebreaker was set to break before the first run.
if run_index != 0 || soft_wrap_policy == SegmentStartSoftWrapPolicy::Force {
ifc.process_soft_wrap_opportunity();
}
ifc.push_glyph_store_to_unbreakable_segment(
run.glyph_store.clone(),
text_run,
self.font_index,
self.bidi_level,
ServoRange::<ByteIndex>::new(
byte_processed + ByteIndex(self.range.start as isize),
ByteIndex(self.range.len() as isize) - byte_processed,
),
);
byte_processed = byte_processed + run.range.length();
}
}
fn shape_and_push_range(
&mut self,
range: &Range<usize>,
formatting_context_text: &str,
segment_font: &FontRef,
options: &ShapingOptions,
) {
self.runs.push(GlyphRun {
glyph_store: segment_font.shape_text(&formatting_context_text[range.clone()], options),
range: ServoRange::new(
ByteIndex(range.start as isize),
ByteIndex(range.len() as isize),
),
});
}
/// Shape the text of this [`TextRunSegment`], first finding "words" for the shaper by processing
/// the linebreaks found in the owning [`super::InlineFormattingContext`]. Linebreaks are filtered,
/// based on the style of the parent inline box.
fn shape_text(
&mut self,
parent_style: &ComputedValues,
formatting_context_text: &str,
linebreaker: &mut LineBreaker,
shaping_options: &ShapingOptions,
font: FontRef,
) {
// Gather the linebreaks that apply to this segment from the inline formatting context's collection
// of line breaks. Also add a simulated break at the end of the segment in order to ensure the final
// piece of text is processed.
let range = self.range.clone();
let linebreaks = linebreaker.advance_to_linebreaks_in_range(self.range.clone());
let linebreak_iter = linebreaks.iter().chain(std::iter::once(&range.end));
self.runs.clear();
self.runs.reserve(linebreaks.len());
self.break_at_start = false;
let text_style = parent_style.get_inherited_text().clone();
let can_break_anywhere = text_style.word_break == WordBreak::BreakAll ||
text_style.overflow_wrap == OverflowWrap::Anywhere ||
text_style.overflow_wrap == OverflowWrap::BreakWord;
let mut last_slice = self.range.start..self.range.start;
for break_index in linebreak_iter {
if *break_index == self.range.start {
self.break_at_start = true;
continue;
}
let mut options = *shaping_options;
// Extend the slice to the next UAX#14 line break opportunity.
let mut slice = last_slice.end..*break_index;
let word = &formatting_context_text[slice.clone()];
// Split off any trailing whitespace into a separate glyph run.
let mut whitespace = slice.end..slice.end;
let mut rev_char_indices = word.char_indices().rev().peekable();
let mut ends_with_whitespace = false;
let ends_with_newline = rev_char_indices
.peek()
.is_some_and(|&(_, character)| character == '\n');
if let Some((first_white_space_index, first_white_space_character)) = rev_char_indices
.take_while(|&(_, character)| char_is_whitespace(character))
.last()
{
ends_with_whitespace = true;
whitespace.start = slice.start + first_white_space_index;
// If line breaking for a piece of text that has `white-space-collapse: break-spaces` there
// is a line break opportunity *after* every preserved space, but not before. This means
// that we should not split off the first whitespace, unless that white-space is a preserved
// newline.
//
// An exception to this is if the style tells us that we can break in the middle of words.
if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces &&
first_white_space_character != '\n' &&
!can_break_anywhere
{
whitespace.start += first_white_space_character.len_utf8();
options
.flags
.insert(ShapingFlags::ENDS_WITH_WHITESPACE_SHAPING_FLAG);
}
slice.end = whitespace.start;
}
// If there's no whitespace and `word-break` is set to `keep-all`, try increasing the slice.
// TODO: This should only happen for CJK text.
if !ends_with_whitespace &&
*break_index != self.range.end &&
text_style.word_break == WordBreak::KeepAll &&
!can_break_anywhere
{
continue;
}
// Only advance the last slice if we are not going to try to expand the slice.
last_slice = slice.start..*break_index;
// Push the non-whitespace part of the range.
if !slice.is_empty() {
self.shape_and_push_range(&slice, formatting_context_text, &font, &options);
}
if whitespace.is_empty() {
continue;
}
options.flags.insert(
ShapingFlags::IS_WHITESPACE_SHAPING_FLAG |
ShapingFlags::ENDS_WITH_WHITESPACE_SHAPING_FLAG,
);
// If `white-space-collapse: break-spaces` is active, insert a line breaking opportunity
// between each white space character in the white space that we trimmed off.
if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces {
let start_index = whitespace.start;
for (index, character) in formatting_context_text[whitespace].char_indices() {
let index = start_index + index;
self.shape_and_push_range(
&(index..index + character.len_utf8()),
formatting_context_text,
&font,
&options,
);
}
continue;
}
// The breaker breaks after every newline, so either there is none,
// or there is exactly one at the very end. In the latter case,
// split it into a different run. That's because shaping considers
// a newline to have the same advance as a space, but during layout
// we want to treat the newline as having no advance.
if ends_with_newline && whitespace.len() > 1 {
self.shape_and_push_range(
&(whitespace.start..whitespace.end - 1),
formatting_context_text,
&font,
&options,
);
self.shape_and_push_range(
&(whitespace.end - 1..whitespace.end),
formatting_context_text,
&font,
&options,
);
} else {
self.shape_and_push_range(&whitespace, formatting_context_text, &font, &options);
}
}
}
}
impl TextRun {
pub(crate) fn new(
base_fragment_info: BaseFragmentInfo,
parent_style: Arc<ComputedValues>,
text_range: Range<usize>,
selection_range: Option<ServoRange<ByteIndex>>,
selected_style: Arc<ComputedValues>,
) -> Self {
Self {
base_fragment_info,
parent_style,
text_range,
shaped_text: Vec::new(),
selection_range,
selected_style,
}
}
pub(super) fn segment_and_shape(
&mut self,
formatting_context_text: &str,
font_context: &FontContext,
linebreaker: &mut LineBreaker,
font_cache: &mut Vec<FontKeyAndMetrics>,
bidi_info: &BidiInfo,
) {
let inherited_text_style = self.parent_style.get_inherited_text().clone();
let letter_spacing = inherited_text_style
.letter_spacing
.0
.resolve(self.parent_style.clone_font().font_size.computed_size());
let letter_spacing = if letter_spacing.px() != 0. {
Some(app_units::Au::from(letter_spacing))
} else {
None
};
let mut flags = ShapingFlags::empty();
if letter_spacing.is_some() {
flags.insert(ShapingFlags::IGNORE_LIGATURES_SHAPING_FLAG);
}
if inherited_text_style.text_rendering == TextRendering::Optimizespeed {
flags.insert(ShapingFlags::IGNORE_LIGATURES_SHAPING_FLAG);
flags.insert(ShapingFlags::DISABLE_KERNING_SHAPING_FLAG)
}
let specified_word_spacing = &inherited_text_style.word_spacing;
let style_word_spacing: Option<Au> = specified_word_spacing.to_length().map(|l| l.into());
let segments = self
.segment_text_by_font(formatting_context_text, font_context, font_cache, bidi_info)
.into_iter()
.map(|(mut segment, font)| {
let word_spacing = style_word_spacing.unwrap_or_else(|| {
let space_width = font
.glyph_index(' ')
.map(|glyph_id| font.glyph_h_advance(glyph_id))
.unwrap_or(LAST_RESORT_GLYPH_ADVANCE);
specified_word_spacing.to_used_value(Au::from_f64_px(space_width))
});
let mut flags = flags;
if segment.bidi_level.is_rtl() {
flags.insert(ShapingFlags::RTL_FLAG);
}
let shaping_options = ShapingOptions {
letter_spacing,
word_spacing,
script: segment.script,
flags,
};
segment.shape_text(
&self.parent_style,
formatting_context_text,
linebreaker,
&shaping_options,
font,
);
segment
})
.collect();
let _ = std::mem::replace(&mut self.shaped_text, segments);
}
/// Take the [`TextRun`]'s text and turn it into [`TextRunSegment`]s. Each segment has a matched
/// font and script. Fonts may differ when glyphs are found in fallback fonts. Fonts are stored
/// in the `font_cache` which is a cache of all font keys and metrics used in this
/// [`super::InlineFormattingContext`].
fn segment_text_by_font(
&mut self,
formatting_context_text: &str,
font_context: &FontContext,
font_cache: &mut Vec<FontKeyAndMetrics>,
bidi_info: &BidiInfo,
) -> 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();
let text_run_text = &formatting_context_text[self.text_range.clone()];
let char_iterator = TwoCharsAtATimeIterator::new(text_run_text.chars());
let mut next_byte_index = self.text_range.start;
for (character, next_character) in char_iterator {
let current_byte_index = next_byte_index;
next_byte_index += character.len_utf8();
if char_does_not_change_font(character) {
continue;
}
// If the script and BiDi level do not change, use the current font as the first fallback. This
// can potentially speed up fallback on long font lists or with uncommon scripts which might be
// at the bottom of the list.
let script = Script::from(character);
let bidi_level = bidi_info.levels[current_byte_index];
let current_font = current.as_ref().and_then(|(text_run_segment, font)| {
if text_run_segment.bidi_level == bidi_level && text_run_segment.script == script {
Some(font.clone())
} else {
None
}
});
let Some(font) = font_group.write().find_by_codepoint(
font_context,
character,
next_character,
current_font,
) else {
continue;
};
// If the existing segment is compatible with the character, keep going.
if let Some(current) = current.as_mut() {
if current.0.update_if_compatible(
&font,
script,
bidi_level,
font_cache,
font_context,
) {
continue;
}
}
let font_index = add_or_get_font(&font, font_cache, font_context);
// 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 the start of this
// text run's text).
let start_byte_index = match current {
Some(_) => current_byte_index,
None => self.text_range.start,
};
let new = (
TextRunSegment::new(font_index, script, bidi_level, start_byte_index),
font,
);
if let Some(mut finished) = current.replace(new) {
// The end of the previous segment is the start of the next one.
finished.0.range.end = current_byte_index;
results.push(finished);
}
}
// Either we have a current segment or we only had control character and whitespace. In both
// of those cases, just use the first font.
if current.is_none() {
current = font_group.write().first(font_context).map(|font| {
let font_index = add_or_get_font(&font, font_cache, font_context);
(
TextRunSegment::new(
font_index,
Script::Common,
Level::ltr(),
self.text_range.start,
),
font,
)
})
}
// Extend the last segment to the end of the string and add it to the results.
if let Some(mut last_segment) = current.take() {
last_segment.0.range.end = self.text_range.end;
results.push(last_segment);
}
results
}
pub(super) fn layout_into_line_items(&self, ifc: &mut InlineFormattingContextLayout) {
if self.text_range.is_empty() {
return;
}
// If we are following replaced content, we should have a soft wrap opportunity, unless the
// first character of this `TextRun` prevents that soft wrap opportunity. If we see such a
// character it should also override the LineBreaker's indication to break at the start.
let have_deferred_soft_wrap_opportunity =
mem::replace(&mut ifc.have_deferred_soft_wrap_opportunity, false);
let mut soft_wrap_policy = match have_deferred_soft_wrap_opportunity {
true => SegmentStartSoftWrapPolicy::Force,
false => SegmentStartSoftWrapPolicy::FollowLinebreaker,
};
for segment in self.shaped_text.iter() {
segment.layout_into_line_items(self, soft_wrap_policy, ifc);
soft_wrap_policy = SegmentStartSoftWrapPolicy::FollowLinebreaker;
}
}
}
/// Whether or not this character should be able to change the font during segmentation. Certain
/// character are not rendered at all, so it doesn't matter what font we use to render them. They
/// should just be added to the current segment.
fn char_does_not_change_font(character: char) -> bool {
if character.is_control() {
return true;
}
if character == '\u{00A0}' {
return true;
}
if is_bidi_control(character) {
return false;
}
let class = linebreak_property(character);
class == XI_LINE_BREAKING_CLASS_CM ||
class == XI_LINE_BREAKING_CLASS_GL ||
class == XI_LINE_BREAKING_CLASS_ZW ||
class == XI_LINE_BREAKING_CLASS_WJ ||
class == XI_LINE_BREAKING_CLASS_ZWJ
}
pub(super) fn add_or_get_font(
font: &FontRef,
ifc_fonts: &mut Vec<FontKeyAndMetrics>,
font_context: &FontContext,
) -> usize {
let font_instance_key = font.key(font_context);
for (index, ifc_font_info) in ifc_fonts.iter().enumerate() {
if ifc_font_info.key == font_instance_key &&
ifc_font_info.pt_size == font.descriptor.pt_size
{
return index;
}
}
ifc_fonts.push(FontKeyAndMetrics {
metrics: font.metrics.clone(),
key: font_instance_key,
pt_size: font.descriptor.pt_size,
});
ifc_fonts.len() - 1
}
pub(super) fn get_font_for_first_font_for_style(
style: &ComputedValues,
font_context: &FontContext,
) -> Option<FontRef> {
let font = font_context
.font_group(style.clone_font())
.write()
.first(font_context);
if font.is_none() {
warn!("Could not find font for style: {:?}", style.clone_font());
}
font
}
pub(crate) struct TwoCharsAtATimeIterator<InputIterator> {
/// The input character iterator.
iterator: InputIterator,
/// The first character to produce in the next run of the iterator.
next_character: Option<char>,
}
impl<InputIterator> TwoCharsAtATimeIterator<InputIterator> {
fn new(iterator: InputIterator) -> Self {
Self {
iterator,
next_character: None,
}
}
}
impl<InputIterator> Iterator for TwoCharsAtATimeIterator<InputIterator>
where
InputIterator: Iterator<Item = char>,
{
type Item = (char, Option<char>);
fn next(&mut self) -> Option<Self::Item> {
// If the iterator isn't initialized do that now.
if self.next_character.is_none() {
self.next_character = self.iterator.next();
}
let character = self.next_character?;
self.next_character = self.iterator.next();
Some((character, self.next_character))
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,500 @@
/* 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 app_units::Au;
use atomic_refcell::AtomicRef;
use compositing_traits::display_list::AxesScrollSensitivity;
use malloc_size_of_derive::MallocSizeOf;
use script_layout_interface::wrapper_traits::{
LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode,
};
use script_layout_interface::{LayoutElementType, LayoutNodeType};
use servo_arc::Arc;
use style::dom::OpaqueNode;
use style::properties::ComputedValues;
use style::values::computed::Overflow;
use style_traits::CSSPixel;
use crate::cell::ArcRefCell;
use crate::context::LayoutContext;
use crate::dom::{LayoutBox, NodeExt};
use crate::dom_traversal::{Contents, NodeAndStyleInfo, NonReplacedContents, iter_child_nodes};
use crate::flexbox::FlexLevelBox;
use crate::flow::float::FloatBox;
use crate::flow::inline::InlineItem;
use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox};
use crate::formatting_contexts::IndependentFormattingContext;
use crate::fragment_tree::FragmentTree;
use crate::geom::{LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSize};
use crate::positioned::{AbsolutelyPositionedBox, PositioningContext};
use crate::replaced::ReplacedContents;
use crate::style_ext::{ComputedValuesExt, Display, DisplayGeneratingBox, DisplayInside};
use crate::taffy::{TaffyItemBox, TaffyItemBoxInner};
use crate::{DefiniteContainingBlock, PropagatedBoxTreeData};
#[derive(MallocSizeOf)]
pub struct BoxTree {
/// Contains typically exactly one block-level box, which was generated by the root element.
/// There may be zero if that element has `display: none`.
root: BlockFormattingContext,
/// <https://drafts.csswg.org/css-backgrounds/#special-backgrounds>
canvas_background: CanvasBackground,
/// Whether or not the viewport should be sensitive to scrolling input events in two axes
viewport_scroll_sensitivity: AxesScrollSensitivity,
}
impl BoxTree {
pub fn construct<'dom, Node>(context: &LayoutContext, root_element: Node) -> Self
where
Node: 'dom + Copy + LayoutNode<'dom> + Send + Sync,
{
let boxes = construct_for_root_element(context, root_element);
// Zero box for `:root { display: none }`, one for the root element otherwise.
assert!(boxes.len() <= 1);
// From https://www.w3.org/TR/css-overflow-3/#overflow-propagation:
// > UAs must apply the overflow-* values set on the root element to the viewport when the
// > root elements display value is not none. However, when the root element is an [HTML]
// > html element (including XML syntax for HTML) whose overflow value is visible (in both
// > axes), and that element has as a child a body element whose display value is also not
// > none, user agents must instead apply the overflow-* values of the first such child
// > element to the viewport. The element from which the value is propagated must then have a
// > used overflow value of visible.
let root_style = root_element.style(context);
let mut viewport_overflow_x = root_style.clone_overflow_x();
let mut viewport_overflow_y = root_style.clone_overflow_y();
if viewport_overflow_x == Overflow::Visible &&
viewport_overflow_y == Overflow::Visible &&
!root_style.get_box().display.is_none()
{
for child in iter_child_nodes(root_element) {
if !child
.to_threadsafe()
.as_element()
.is_some_and(|element| element.is_body_element_of_html_element_root())
{
continue;
}
let style = child.style(context);
if !style.get_box().display.is_none() {
viewport_overflow_x = style.clone_overflow_x();
viewport_overflow_y = style.clone_overflow_y();
break;
}
}
}
let contents = BlockContainer::BlockLevelBoxes(boxes);
let contains_floats = contents.contains_floats();
Self {
root: BlockFormattingContext {
contents,
contains_floats,
},
canvas_background: CanvasBackground::for_root_element(context, root_element),
// From https://www.w3.org/TR/css-overflow-3/#overflow-propagation:
// > If visible is applied to the viewport, it must be interpreted as auto.
// > If clip is applied to the viewport, it must be interpreted as hidden.
viewport_scroll_sensitivity: AxesScrollSensitivity {
x: viewport_overflow_x.to_scrollable().into(),
y: viewport_overflow_y.to_scrollable().into(),
},
}
}
/// This method attempts to incrementally update the box tree from an
/// arbitrary node that is not necessarily the document's root element.
///
/// If the node is not a valid candidate for incremental update, the method
/// loops over its parent. The only valid candidates for now are absolutely
/// positioned boxes which don't change their outside display mode (i.e. it
/// will not attempt to update from an absolutely positioned inline element
/// which became an absolutely positioned block element). The value `true`
/// is returned if an incremental update could be done, and `false`
/// otherwise.
///
/// There are various pain points that need to be taken care of to extend
/// the set of valid candidates:
/// * it is not obvious how to incrementally check whether a block
/// formatting context still contains floats or not;
/// * the propagation of text decorations towards node descendants is
/// hard to do incrementally with our current representation of boxes
/// * how intrinsic content sizes are computed eagerly makes it hard
/// to update those sizes for ancestors of the node from which we
/// made an incremental update.
pub fn update<'dom, Node>(context: &LayoutContext, mut dirty_node: Node) -> bool
where
Node: 'dom + Copy + LayoutNode<'dom> + Send + Sync,
{
#[allow(clippy::enum_variant_names)]
enum UpdatePoint {
AbsolutelyPositionedBlockLevelBox(ArcRefCell<BlockLevelBox>),
AbsolutelyPositionedInlineLevelBox(ArcRefCell<InlineItem>, usize),
AbsolutelyPositionedFlexLevelBox(ArcRefCell<FlexLevelBox>),
AbsolutelyPositionedTaffyLevelBox(ArcRefCell<TaffyItemBox>),
}
fn update_point<'dom, Node>(
node: Node,
) -> Option<(Arc<ComputedValues>, DisplayInside, UpdatePoint)>
where
Node: NodeExt<'dom>,
{
if !node.is_element() {
return None;
}
if node.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLBodyElement) {
// This can require changes to the canvas background.
return None;
}
// Don't update unstyled nodes or nodes that have pseudo-elements.
let element_data = node.style_data()?.element_data.borrow();
if !element_data.styles.pseudos.is_empty() {
return None;
}
let layout_data = node.layout_data()?;
if layout_data.pseudo_before_box.borrow().is_some() {
return None;
}
if layout_data.pseudo_after_box.borrow().is_some() {
return None;
}
let primary_style = element_data.styles.primary();
let box_style = primary_style.get_box();
if !box_style.position.is_absolutely_positioned() {
return None;
}
let display_inside = match Display::from(box_style.display) {
Display::GeneratingBox(DisplayGeneratingBox::OutsideInside { inside, .. }) => {
inside
},
_ => return None,
};
let update_point =
match &*AtomicRef::filter_map(layout_data.self_box.borrow(), Option::as_ref)? {
LayoutBox::DisplayContents => return None,
LayoutBox::BlockLevel(block_level_box) => match &*block_level_box.borrow() {
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(_)
if box_style.position.is_absolutely_positioned() =>
{
UpdatePoint::AbsolutelyPositionedBlockLevelBox(block_level_box.clone())
},
_ => return None,
},
LayoutBox::InlineLevel(inline_level_box) => match &*inline_level_box.borrow() {
InlineItem::OutOfFlowAbsolutelyPositionedBox(_, text_offset_index)
if box_style.position.is_absolutely_positioned() =>
{
UpdatePoint::AbsolutelyPositionedInlineLevelBox(
inline_level_box.clone(),
*text_offset_index,
)
},
_ => return None,
},
LayoutBox::FlexLevel(flex_level_box) => match &*flex_level_box.borrow() {
FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(_)
if box_style.position.is_absolutely_positioned() =>
{
UpdatePoint::AbsolutelyPositionedFlexLevelBox(flex_level_box.clone())
},
_ => return None,
},
LayoutBox::TableLevelBox(..) => return None,
LayoutBox::TaffyItemBox(taffy_level_box) => match &taffy_level_box
.borrow()
.taffy_level_box
{
TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(_)
if box_style.position.is_absolutely_positioned() =>
{
UpdatePoint::AbsolutelyPositionedTaffyLevelBox(taffy_level_box.clone())
},
_ => return None,
},
};
Some((primary_style.clone(), display_inside, update_point))
}
loop {
let Some((primary_style, display_inside, update_point)) = update_point(dirty_node)
else {
dirty_node = match dirty_node.parent_node() {
Some(parent) => parent,
None => return false,
};
continue;
};
let contents = ReplacedContents::for_element(dirty_node, context)
.map_or_else(|| NonReplacedContents::OfElement.into(), Contents::Replaced);
let info = NodeAndStyleInfo::new(dirty_node, Arc::clone(&primary_style));
let out_of_flow_absolutely_positioned_box = ArcRefCell::new(
AbsolutelyPositionedBox::construct(context, &info, display_inside, contents),
);
match update_point {
UpdatePoint::AbsolutelyPositionedBlockLevelBox(block_level_box) => {
*block_level_box.borrow_mut() = BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(
out_of_flow_absolutely_positioned_box,
);
},
UpdatePoint::AbsolutelyPositionedInlineLevelBox(
inline_level_box,
text_offset_index,
) => {
*inline_level_box.borrow_mut() = InlineItem::OutOfFlowAbsolutelyPositionedBox(
out_of_flow_absolutely_positioned_box,
text_offset_index,
);
},
UpdatePoint::AbsolutelyPositionedFlexLevelBox(flex_level_box) => {
*flex_level_box.borrow_mut() = FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(
out_of_flow_absolutely_positioned_box,
);
},
UpdatePoint::AbsolutelyPositionedTaffyLevelBox(taffy_level_box) => {
taffy_level_box.borrow_mut().taffy_level_box =
TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(
out_of_flow_absolutely_positioned_box,
);
},
}
break;
}
// We are going to rebuild the box tree from the update point downward, but this update
// point is an absolute, which means that it needs to be laid out again in the containing
// block for absolutes, which is established by one of its ancestors. In addition,
// absolutes, when laid out, can produce more absolutes (either fixed or absolutely
// positioned) elements, so there may be yet more layout that has to happen in this
// ancestor.
//
// We do not know which ancestor is the one that established the containing block for this
// update point, so just invalidate the fragment cache of all ancestors, meaning that even
// though the box tree is preserved, the fragment tree from the root to the update point and
// all of its descendants will need to be rebuilt. This isn't as bad as it seems, because
// siblings and siblings of ancestors of this path through the tree will still have cached
// fragments.
//
// TODO: Do better. This is still a very crude way to do incremental layout.
while let Some(parent_node) = dirty_node.parent_node() {
parent_node.invalidate_cached_fragment();
dirty_node = parent_node;
}
true
}
}
fn construct_for_root_element<'dom>(
context: &LayoutContext,
root_element: impl NodeExt<'dom>,
) -> Vec<ArcRefCell<BlockLevelBox>> {
let info = NodeAndStyleInfo::new(root_element, root_element.style(context));
let box_style = info.style.get_box();
let display_inside = match Display::from(box_style.display) {
Display::None => {
root_element.unset_all_boxes();
return Vec::new();
},
Display::Contents => {
// Unreachable because the style crate adjusts the computed values:
// https://drafts.csswg.org/css-display-3/#transformations
// “'display' of 'contents' computes to 'block' on the root element”
unreachable!()
},
// The root element is blockified, ignore DisplayOutside
Display::GeneratingBox(display_generating_box) => display_generating_box.display_inside(),
};
let contents = ReplacedContents::for_element(root_element, context)
.map_or_else(|| NonReplacedContents::OfElement.into(), Contents::Replaced);
let propagated_data = PropagatedBoxTreeData::default().union(&info.style);
let root_box = if box_style.position.is_absolutely_positioned() {
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(ArcRefCell::new(
AbsolutelyPositionedBox::construct(context, &info, display_inside, contents),
))
} else if box_style.float.is_floating() {
BlockLevelBox::OutOfFlowFloatBox(FloatBox::construct(
context,
&info,
display_inside,
contents,
propagated_data,
))
} else {
BlockLevelBox::Independent(IndependentFormattingContext::construct(
context,
&info,
display_inside,
contents,
propagated_data,
))
};
let root_box = ArcRefCell::new(root_box);
root_element
.element_box_slot()
.set(LayoutBox::BlockLevel(root_box.clone()));
vec![root_box]
}
impl BoxTree {
pub fn layout(
&self,
layout_context: &LayoutContext,
viewport: euclid::Size2D<f32, CSSPixel>,
) -> FragmentTree {
let style = layout_context
.style_context
.stylist
.device()
.default_computed_values();
// FIXME: use the documents mode:
// https://drafts.csswg.org/css-writing-modes/#principal-flow
let physical_containing_block = PhysicalRect::new(
PhysicalPoint::zero(),
PhysicalSize::new(
Au::from_f32_px(viewport.width),
Au::from_f32_px(viewport.height),
),
);
let initial_containing_block = DefiniteContainingBlock {
size: LogicalVec2 {
inline: physical_containing_block.size.width,
block: physical_containing_block.size.height,
},
style,
};
let mut positioning_context =
PositioningContext::new_for_containing_block_for_all_descendants();
let independent_layout = self.root.layout(
layout_context,
&mut positioning_context,
&(&initial_containing_block).into(),
false, /* depends_on_block_constraints */
);
let mut root_fragments = independent_layout.fragments.into_iter().collect::<Vec<_>>();
// Zero box for `:root { display: none }`, one for the root element otherwise.
assert!(root_fragments.len() <= 1);
// There may be more fragments at the top-level
// (for positioned boxes whose containing is the initial containing block)
// but only if there was one fragment for the root element.
positioning_context.layout_initial_containing_block_children(
layout_context,
&initial_containing_block,
&mut root_fragments,
);
let scrollable_overflow = root_fragments
.iter()
.fold(PhysicalRect::zero(), |acc, child| {
let child_overflow = child.scrollable_overflow();
// https://drafts.csswg.org/css-overflow/#scrolling-direction
// We want to clip scrollable overflow on box-start and inline-start
// sides of the scroll container.
//
// FIXME(mrobinson, bug 25564): This should take into account writing
// mode.
let child_overflow = PhysicalRect::new(
euclid::Point2D::zero(),
euclid::Size2D::new(
child_overflow.size.width + child_overflow.origin.x,
child_overflow.size.height + child_overflow.origin.y,
),
);
acc.union(&child_overflow)
});
FragmentTree {
root_fragments,
scrollable_overflow,
initial_containing_block: physical_containing_block,
canvas_background: self.canvas_background.clone(),
viewport_scroll_sensitivity: self.viewport_scroll_sensitivity,
}
}
}
/// <https://drafts.csswg.org/css-backgrounds/#root-background>
#[derive(Clone, MallocSizeOf)]
pub struct CanvasBackground {
/// DOM node for the root element
pub root_element: OpaqueNode,
/// The element whose style the canvas takes background properties from (see next field).
/// This can be the root element (same as the previous field), or the HTML `<body>` element.
/// See <https://drafts.csswg.org/css-backgrounds/#body-background>
pub from_element: OpaqueNode,
/// The computed styles to take background properties from.
#[conditional_malloc_size_of]
pub style: Option<Arc<ComputedValues>>,
}
impl CanvasBackground {
fn for_root_element<'dom>(context: &LayoutContext, root_element: impl NodeExt<'dom>) -> Self {
let root_style = root_element.style(context);
let mut style = root_style;
let mut from_element = root_element;
// https://drafts.csswg.org/css-backgrounds/#body-background
// “if the computed value of background-image on the root element is none
// and its background-color is transparent”
if style.background_is_transparent() &&
// “For documents whose root element is an HTML `HTML` element
// or an XHTML `html` element”
root_element.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLHtmlElement) &&
// Dont try to access styles for an unstyled subtree
!matches!(style.clone_display().into(), Display::None)
{
// “that elements first HTML `BODY` or XHTML `body` child element”
if let Some(body) = iter_child_nodes(root_element).find(|child| {
child.is_element() &&
child.type_id() ==
LayoutNodeType::Element(LayoutElementType::HTMLBodyElement)
}) {
style = body.style(context);
from_element = body;
}
}
Self {
root_element: root_element.opaque(),
from_element: from_element.opaque(),
// “However, if no boxes are generated for the element
// whose background would be used for the canvas
// (for example, if the root element has display: none),
// then the canvas background is transparent.”
style: if let Display::GeneratingBox(_) = style.clone_display().into() {
Some(style)
} else {
None
},
}
}
}