mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
Add initial support for css-text-3 whitespace handling (#29828)
* Add initial support for css-text-3 whitespace handling This adds initial support for whitespace handling from the CSS specification for Layout 2020. In general, the basics are covered. Since test output is very sensitive to whitespace handling, this change incorporates several fixes: 1. Whitespace is collapsed according to the Phase 1 rules of the specification, though language-specific unbreaking rules are not handled properly yet. 2. Whitespace is mostly trimmed and positioned according to the Phase 2 rules, but full support for removing whitespace at the end of lines is pending on a temporary data structure to hold lines under construction. 3. Completely empty box fragments left over immediately after line breaks are now trimmed from the fragment tree. 4. This change tries to detect when an inline formatting context collapses through. Fixes #29994. Co-authored-by: Mukilan Thiyagarajan <me@mukilan.in> Co-authored-by: Martin Robinson <mrobinson@igalia.com> Signed-off-by: Martin Robinson <mrobinson@igalia.com> * Update test results --------- Signed-off-by: Martin Robinson <mrobinson@igalia.com> Co-authored-by: Mukilan Thiyagarajan <me@mukilan.in> Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
parent
cc585b74d3
commit
2b67392fd5
129 changed files with 636 additions and 504 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3081,6 +3081,7 @@ dependencies = [
|
|||
"style_traits",
|
||||
"unicode-script",
|
||||
"webrender_api",
|
||||
"xi-unicode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -44,6 +44,7 @@ style = { path = "../style", features = ["servo"] }
|
|||
style_traits = { path = "../style_traits" }
|
||||
unicode-script = { workspace = true }
|
||||
webrender_api = { workspace = true }
|
||||
xi-unicode = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
lazy_static = { workspace = true }
|
||||
|
|
|
@ -150,6 +150,7 @@ where
|
|||
base_fragment_info: (&run.info).into(),
|
||||
text: run.text.into(),
|
||||
parent_style: run.info.style,
|
||||
has_uncollapsible_content: false,
|
||||
}),
|
||||
self.text_decoration_line,
|
||||
),
|
||||
|
|
|
@ -62,6 +62,7 @@ impl BlockFormattingContext {
|
|||
text_decoration_line,
|
||||
has_first_formatted_line: true,
|
||||
contains_floats: false,
|
||||
ends_with_whitespace: false,
|
||||
};
|
||||
let contents = BlockContainer::InlineFormattingContext(ifc);
|
||||
let bfc = Self {
|
||||
|
@ -186,6 +187,7 @@ impl BlockContainer {
|
|||
ongoing_inline_formatting_context: InlineFormattingContext::new(
|
||||
text_decoration_line,
|
||||
/* has_first_formatted_line = */ true,
|
||||
/* ends_with_whitespace */ false,
|
||||
),
|
||||
ongoing_inline_boxes_stack: Vec::new(),
|
||||
anonymous_style: None,
|
||||
|
@ -213,11 +215,7 @@ impl BlockContainer {
|
|||
|
||||
debug_assert!(builder.ongoing_inline_boxes_stack.is_empty());
|
||||
|
||||
if !builder
|
||||
.ongoing_inline_formatting_context
|
||||
.inline_level_boxes
|
||||
.is_empty()
|
||||
{
|
||||
if !builder.ongoing_inline_formatting_context.is_empty() {
|
||||
if builder.block_level_boxes.is_empty() {
|
||||
return BlockContainer::InlineFormattingContext(
|
||||
builder.ongoing_inline_formatting_context,
|
||||
|
@ -277,167 +275,166 @@ where
|
|||
}
|
||||
|
||||
fn handle_text(&mut self, info: &NodeAndStyleInfo<Node>, input: Cow<'dom, str>) {
|
||||
// Skip any leading whitespace as dictated by the node's style.
|
||||
let white_space = info.style.get_inherited_text().white_space;
|
||||
let (preserved_leading_whitespace, mut input) =
|
||||
self.handle_leading_whitespace(&input, white_space);
|
||||
|
||||
if !preserved_leading_whitespace && input.is_empty() {
|
||||
if input.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// This text node should be pushed either to the next ongoing
|
||||
// inline level box with the parent style of that inline level box
|
||||
// that will be ended, or directly to the ongoing inline formatting
|
||||
// context with the parent style of that builder.
|
||||
let (output, has_uncollapsible_content) = collapse_and_transform_whitespace(
|
||||
&input,
|
||||
info.style.get_inherited_text().white_space,
|
||||
self.ongoing_inline_formatting_context.ends_with_whitespace,
|
||||
);
|
||||
if output.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.ongoing_inline_formatting_context.ends_with_whitespace =
|
||||
output.chars().last().unwrap().is_ascii_whitespace();
|
||||
|
||||
let inlines = self.current_inline_level_boxes();
|
||||
|
||||
let mut new_text_run_contents;
|
||||
let output;
|
||||
|
||||
{
|
||||
let mut last_box = inlines.last_mut().map(|last| last.borrow_mut());
|
||||
let last_text = last_box.as_mut().and_then(|last| match &mut **last {
|
||||
InlineLevelBox::TextRun(last) => Some(&mut last.text),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
if let Some(text) = last_text {
|
||||
// Append to the existing text run
|
||||
new_text_run_contents = None;
|
||||
output = text;
|
||||
} else {
|
||||
new_text_run_contents = Some(String::new());
|
||||
output = new_text_run_contents.as_mut().unwrap();
|
||||
}
|
||||
|
||||
if preserved_leading_whitespace {
|
||||
output.push(' ')
|
||||
}
|
||||
|
||||
match (
|
||||
white_space.preserve_spaces(),
|
||||
white_space.preserve_newlines(),
|
||||
) {
|
||||
// All whitespace is significant, so we don't need to transform
|
||||
// the input at all.
|
||||
(true, true) => {
|
||||
output.push_str(input);
|
||||
match inlines.last_mut().map(|last| last.borrow_mut()) {
|
||||
Some(mut last_box) => match *last_box {
|
||||
InlineLevelBox::TextRun(ref mut text_run) => {
|
||||
text_run.text.push_str(&output);
|
||||
text_run.has_uncollapsible_content |= has_uncollapsible_content;
|
||||
return;
|
||||
},
|
||||
|
||||
// There are no cases in CSS where where need to preserve spaces
|
||||
// but not newlines.
|
||||
(true, false) => unreachable!(),
|
||||
|
||||
// Spaces are not significant, but newlines might be. We need
|
||||
// to collapse non-significant whitespace as appropriate.
|
||||
(false, preserve_newlines) => loop {
|
||||
// If there are any spaces that need preserving, split the string
|
||||
// that precedes them, collapse them into a single whitespace,
|
||||
// then process the remainder of the string independently.
|
||||
if let Some(i) = input
|
||||
.bytes()
|
||||
.position(|b| b.is_ascii_whitespace() && (!preserve_newlines || b != b'\n'))
|
||||
{
|
||||
let (non_whitespace, rest) = input.split_at(i);
|
||||
output.push_str(non_whitespace);
|
||||
output.push(' ');
|
||||
|
||||
// Find the first byte that is either significant whitespace or
|
||||
// non-whitespace to continue processing it.
|
||||
if let Some(i) = rest.bytes().position(|b| {
|
||||
!b.is_ascii_whitespace() || (preserve_newlines && b == b'\n')
|
||||
}) {
|
||||
input = &rest[i..];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// No whitespace found, so no transformation is required.
|
||||
output.push_str(input);
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
_ => {},
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
if let Some(text) = new_text_run_contents {
|
||||
inlines.push(ArcRefCell::new(InlineLevelBox::TextRun(TextRun {
|
||||
base_fragment_info: info.into(),
|
||||
parent_style: Arc::clone(&info.style),
|
||||
text,
|
||||
})))
|
||||
}
|
||||
inlines.push(ArcRefCell::new(InlineLevelBox::TextRun(TextRun {
|
||||
base_fragment_info: info.into(),
|
||||
parent_style: Arc::clone(&info.style),
|
||||
text: output,
|
||||
has_uncollapsible_content,
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
fn preserve_segment_break() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Collapse and transform whitespace in the given input according to the rules in
|
||||
/// <https://drafts.csswg.org/css-text-3/#white-space-phase-1>. This method doesn't
|
||||
/// follow the steps exactly since they are defined in a multi-pass appraoach, but it
|
||||
/// tries to be effectively the same transformation.
|
||||
///
|
||||
/// Returns the transformed text as a [String] and also whether or not the input had
|
||||
/// any uncollapsible content.
|
||||
fn collapse_and_transform_whitespace<'text>(
|
||||
input: &'text str,
|
||||
white_space: WhiteSpace,
|
||||
trim_beginning_white_space: bool,
|
||||
) -> (String, bool) {
|
||||
// Point 4.1.1 first bullet:
|
||||
// > If white-space is set to normal, nowrap, or pre-line, whitespace
|
||||
// > characters are considered collapsible
|
||||
// If whitespace is not considered collapsible, it is preserved entirely, which
|
||||
// means that we can simply return the input string exactly.
|
||||
if white_space.preserve_spaces() {
|
||||
return (input.to_owned(), true);
|
||||
}
|
||||
|
||||
let mut output = String::with_capacity(input.len());
|
||||
let mut has_uncollapsible_content = false;
|
||||
let mut had_whitespace = false;
|
||||
let mut following_newline = false;
|
||||
let mut in_whitespace_at_beginning = true;
|
||||
|
||||
let is_leading_trimmed_whitespace =
|
||||
|in_whitespace_at_beginning: bool| in_whitespace_at_beginning && trim_beginning_white_space;
|
||||
|
||||
// Point 4.1.1:
|
||||
// > 2. Any sequence of collapsible spaces and tabs immediately preceding or
|
||||
// > following a segment break is removed.
|
||||
// > 3. Every collapsible tab is converted to a collapsible space (U+0020).
|
||||
// > 4. Any collapsible space immediately following another collapsible space—even
|
||||
// > one outside the boundary of the inline containing that space, provided both
|
||||
// > spaces are within the same inline formatting context—is collapsed to have zero
|
||||
// > advance width.
|
||||
let push_pending_whitespace_if_needed =
|
||||
|output: &mut String,
|
||||
had_whitespace: bool,
|
||||
following_newline: bool,
|
||||
in_whitespace_at_beginning: bool| {
|
||||
if had_whitespace &&
|
||||
!following_newline &&
|
||||
!is_leading_trimmed_whitespace(in_whitespace_at_beginning)
|
||||
{
|
||||
output.push(' ');
|
||||
}
|
||||
};
|
||||
|
||||
for character in input.chars() {
|
||||
// Don't push non-newline whitespace immediately. Instead wait to push it until we
|
||||
// know that it isn't followed by a newline. See `push_pending_whitespace_if_needed`
|
||||
// above.
|
||||
if character.is_ascii_whitespace() && character != '\n' {
|
||||
had_whitespace = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Point 4.1.1:
|
||||
// > 2. Collapsible segment breaks are transformed for rendering according to the
|
||||
// > segment break transformation rules.
|
||||
if character == '\n' {
|
||||
// From <https://drafts.csswg.org/css-text-3/#line-break-transform>
|
||||
// (4.1.3 -- the segment break transformation rules):
|
||||
//
|
||||
// > When white-space is pre, pre-wrap, or pre-line, segment breaks are not
|
||||
// > collapsible and are instead transformed into a preserved line feed"
|
||||
if white_space == WhiteSpace::PreLine {
|
||||
has_uncollapsible_content = true;
|
||||
had_whitespace = false;
|
||||
output.push('\n');
|
||||
|
||||
// Point 4.1.3:
|
||||
// > 1. First, any collapsible segment break immediately following another
|
||||
// > collapsible segment break is removed.
|
||||
// > 2. Then any remaining segment break is either transformed into a space (U+0020)
|
||||
// > or removed depending on the context before and after the break.
|
||||
} else if !following_newline &&
|
||||
preserve_segment_break() &&
|
||||
!is_leading_trimmed_whitespace(in_whitespace_at_beginning)
|
||||
{
|
||||
had_whitespace = false;
|
||||
output.push(' ');
|
||||
}
|
||||
following_newline = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
push_pending_whitespace_if_needed(
|
||||
&mut output,
|
||||
had_whitespace,
|
||||
following_newline,
|
||||
in_whitespace_at_beginning,
|
||||
);
|
||||
|
||||
has_uncollapsible_content = true;
|
||||
had_whitespace = false;
|
||||
in_whitespace_at_beginning = false;
|
||||
following_newline = false;
|
||||
output.push(character);
|
||||
}
|
||||
|
||||
push_pending_whitespace_if_needed(
|
||||
&mut output,
|
||||
had_whitespace,
|
||||
following_newline,
|
||||
in_whitespace_at_beginning,
|
||||
);
|
||||
|
||||
(output, has_uncollapsible_content)
|
||||
}
|
||||
|
||||
impl<'dom, Node> BlockContainerBuilder<'dom, '_, Node>
|
||||
where
|
||||
Node: NodeExt<'dom>,
|
||||
{
|
||||
/// Returns:
|
||||
///
|
||||
/// * Whether this text run has preserved (non-collapsible) leading whitespace
|
||||
/// * The contents starting at the first non-whitespace character (or the empty string)
|
||||
fn handle_leading_whitespace<'text>(
|
||||
&mut self,
|
||||
text: &'text str,
|
||||
white_space: WhiteSpace,
|
||||
) -> (bool, &'text str) {
|
||||
// FIXME: this is only an approximation of
|
||||
// https://drafts.csswg.org/css2/text.html#white-space-model
|
||||
if !text.starts_with(|c: char| c.is_ascii_whitespace()) || white_space.preserve_spaces() {
|
||||
return (false, text);
|
||||
}
|
||||
|
||||
let preserved = match whitespace_is_preserved(self.current_inline_level_boxes()) {
|
||||
WhitespacePreservedResult::Unknown => {
|
||||
// Paragraph start.
|
||||
false
|
||||
},
|
||||
WhitespacePreservedResult::NotPreserved => false,
|
||||
WhitespacePreservedResult::Preserved => true,
|
||||
};
|
||||
|
||||
let text = text.trim_start_matches(|c: char| c.is_ascii_whitespace());
|
||||
return (preserved, text);
|
||||
|
||||
fn whitespace_is_preserved(
|
||||
inline_level_boxes: &[ArcRefCell<InlineLevelBox>],
|
||||
) -> WhitespacePreservedResult {
|
||||
for inline_level_box in inline_level_boxes.iter().rev() {
|
||||
match *inline_level_box.borrow() {
|
||||
InlineLevelBox::TextRun(ref r) => {
|
||||
if r.text.ends_with(' ') {
|
||||
return WhitespacePreservedResult::NotPreserved;
|
||||
}
|
||||
return WhitespacePreservedResult::Preserved;
|
||||
},
|
||||
InlineLevelBox::Atomic { .. } => {
|
||||
return WhitespacePreservedResult::NotPreserved;
|
||||
},
|
||||
InlineLevelBox::OutOfFlowAbsolutelyPositionedBox(_) |
|
||||
InlineLevelBox::OutOfFlowFloatBox(_) => {},
|
||||
InlineLevelBox::InlineBox(ref b) => {
|
||||
match whitespace_is_preserved(&b.children) {
|
||||
WhitespacePreservedResult::Unknown => {},
|
||||
result => return result,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
WhitespacePreservedResult::Unknown
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum WhitespacePreservedResult {
|
||||
Preserved,
|
||||
NotPreserved,
|
||||
Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_list_item_marker_inside(
|
||||
&mut self,
|
||||
info: &NodeAndStyleInfo<Node>,
|
||||
|
@ -502,6 +499,7 @@ where
|
|||
inline_box.last_fragment = true;
|
||||
ArcRefCell::new(InlineLevelBox::InlineBox(inline_box))
|
||||
} else {
|
||||
self.ongoing_inline_formatting_context.ends_with_whitespace = false;
|
||||
ArcRefCell::new(InlineLevelBox::Atomic(
|
||||
IndependentFormattingContext::construct(
|
||||
self.context,
|
||||
|
@ -668,14 +666,11 @@ where
|
|||
}
|
||||
|
||||
fn end_ongoing_inline_formatting_context(&mut self) {
|
||||
if self
|
||||
.ongoing_inline_formatting_context
|
||||
.inline_level_boxes
|
||||
.is_empty()
|
||||
{
|
||||
if self.ongoing_inline_formatting_context.is_empty() {
|
||||
// There should never be an empty inline formatting context.
|
||||
self.ongoing_inline_formatting_context
|
||||
.has_first_formatted_line = false;
|
||||
self.ongoing_inline_formatting_context.ends_with_whitespace = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -695,6 +690,7 @@ where
|
|||
let mut ifc = InlineFormattingContext::new(
|
||||
self.ongoing_inline_formatting_context.text_decoration_line,
|
||||
/* has_first_formatted_line = */ false,
|
||||
/* ends_with_whitespace */ false,
|
||||
);
|
||||
std::mem::swap(&mut self.ongoing_inline_formatting_context, &mut ifc);
|
||||
let kind = BlockLevelCreator::SameFormattingContextBlock(
|
||||
|
@ -709,6 +705,8 @@ where
|
|||
});
|
||||
}
|
||||
|
||||
// Retrieves the mutable reference of inline boxes either from the last
|
||||
// element of a stack or directly from the formatting context, depending on the situation.
|
||||
fn current_inline_level_boxes(&mut self) -> &mut Vec<ArcRefCell<InlineLevelBox>> {
|
||||
match self.ongoing_inline_boxes_stack.last_mut() {
|
||||
Some(last) => &mut last.children,
|
||||
|
@ -717,10 +715,7 @@ where
|
|||
}
|
||||
|
||||
fn has_ongoing_inline_formatting_context(&self) -> bool {
|
||||
!self
|
||||
.ongoing_inline_formatting_context
|
||||
.inline_level_boxes
|
||||
.is_empty() ||
|
||||
!self.ongoing_inline_formatting_context.is_empty() ||
|
||||
!self.ongoing_inline_boxes_stack.is_empty()
|
||||
}
|
||||
}
|
||||
|
@ -810,3 +805,50 @@ impl IntermediateBlockContainer {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collapase_and_transform_whitespace() {
|
||||
let output = collapse_and_transform_whitespace("H ", WhiteSpace::Normal, false);
|
||||
assert_eq!(output.0, "H ");
|
||||
assert!(output.1);
|
||||
|
||||
let output = collapse_and_transform_whitespace(" W", WhiteSpace::Normal, true);
|
||||
assert_eq!(output.0, "W");
|
||||
assert!(output.1);
|
||||
|
||||
let output = collapse_and_transform_whitespace(" W", WhiteSpace::Normal, false);
|
||||
assert_eq!(output.0, " W");
|
||||
assert!(output.1);
|
||||
|
||||
let output = collapse_and_transform_whitespace(" H W", WhiteSpace::Normal, false);
|
||||
assert_eq!(output.0, " H W");
|
||||
assert!(output.1);
|
||||
|
||||
let output = collapse_and_transform_whitespace("\n H \n \t W", WhiteSpace::Normal, false);
|
||||
assert_eq!(output.0, " H W");
|
||||
|
||||
let output = collapse_and_transform_whitespace("\n H \n \t W \n", WhiteSpace::Pre, false);
|
||||
assert_eq!(output.0, "\n H \n \t W \n");
|
||||
assert!(output.1);
|
||||
|
||||
let output =
|
||||
collapse_and_transform_whitespace("\n H \n \t W \n ", WhiteSpace::PreLine, false);
|
||||
assert_eq!(output.0, "\nH\nW\n");
|
||||
assert!(output.1);
|
||||
|
||||
let output = collapse_and_transform_whitespace(" ", WhiteSpace::Normal, true);
|
||||
assert_eq!(output.0, "");
|
||||
assert!(!output.1);
|
||||
|
||||
let output = collapse_and_transform_whitespace(" ", WhiteSpace::Normal, false);
|
||||
assert_eq!(output.0, " ");
|
||||
assert!(!output.1);
|
||||
|
||||
let output = collapse_and_transform_whitespace("\n ", WhiteSpace::Normal, true);
|
||||
assert_eq!(output.0, "");
|
||||
assert!(!output.1);
|
||||
|
||||
let output = collapse_and_transform_whitespace("\n ", WhiteSpace::Normal, false);
|
||||
assert_eq!(output.0, " ");
|
||||
assert!(!output.1);
|
||||
}
|
||||
|
|
|
@ -21,17 +21,19 @@ use crate::style_ext::{ComputedValuesExt, Display, DisplayGeneratingBox, Display
|
|||
use crate::ContainingBlock;
|
||||
use app_units::Au;
|
||||
use atomic_refcell::AtomicRef;
|
||||
use gfx::text::glyph::GlyphStore;
|
||||
use gfx::text::text_run::GlyphRun;
|
||||
use servo_arc::Arc;
|
||||
use style::computed_values::white_space::T as WhiteSpace;
|
||||
use style::logical_geometry::WritingMode;
|
||||
use style::properties::ComputedValues;
|
||||
use style::values::computed::{Length, LengthPercentage, Percentage};
|
||||
use style::values::generics::text::LineHeight;
|
||||
use style::values::specified::text::TextAlignKeyword;
|
||||
use style::values::specified::text::TextDecorationLine;
|
||||
use style::Zero;
|
||||
use webrender_api::FontInstanceKey;
|
||||
|
||||
use xi_unicode::LineBreakLeafIter;
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct InlineFormattingContext {
|
||||
pub(super) inline_level_boxes: Vec<ArcRefCell<InlineLevelBox>>,
|
||||
|
@ -40,6 +42,14 @@ pub(crate) struct InlineFormattingContext {
|
|||
// https://www.w3.org/TR/css-pseudo-4/#first-formatted-line
|
||||
pub(super) has_first_formatted_line: bool,
|
||||
pub(super) contains_floats: bool,
|
||||
/// Whether this IFC being constructed currently ends 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.)
|
||||
pub(super) ends_with_whitespace: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
@ -68,6 +78,7 @@ pub(crate) struct TextRun {
|
|||
#[serde(skip_serializing)]
|
||||
pub parent_style: Arc<ComputedValues>,
|
||||
pub text: String,
|
||||
pub has_uncollapsible_content: bool,
|
||||
}
|
||||
|
||||
struct InlineNestingLevelState<'box_tree> {
|
||||
|
@ -91,7 +102,13 @@ struct PartialInlineBoxFragment<'box_tree> {
|
|||
padding: Sides<Length>,
|
||||
border: Sides<Length>,
|
||||
margin: Sides<Length>,
|
||||
last_box_tree_fragment: bool,
|
||||
|
||||
/// Whether or not this inline box has already been part of a previous line.
|
||||
/// We need to create at least one Fragment for every inline box, but on following
|
||||
/// lines, if the inline box is totally empty (such as after a preserved line
|
||||
/// break), then we don't want to create empty Fragments for it.
|
||||
was_part_of_previous_line: bool,
|
||||
|
||||
parent_nesting_level: InlineNestingLevelState<'box_tree>,
|
||||
}
|
||||
|
||||
|
@ -99,7 +116,22 @@ struct InlineFormattingContextState<'box_tree, 'a, 'b> {
|
|||
positioning_context: &'a mut PositioningContext,
|
||||
containing_block: &'b ContainingBlock<'b>,
|
||||
lines: Lines,
|
||||
|
||||
/// The current inline position in this inline formatting context independent
|
||||
/// of the depth in the nesting level.
|
||||
inline_position: Length,
|
||||
|
||||
/// Whether any active line box has added a glyph, border, margin, or padding
|
||||
/// to this line, which indicates that the next run that exceeds the line length
|
||||
/// can cause a line break.
|
||||
line_had_any_content: bool,
|
||||
|
||||
// Whether or not this line had any absolutely positioned boxes.
|
||||
line_had_any_absolutes: bool,
|
||||
|
||||
/// The line breaking state for this inline formatting context.
|
||||
linebreaker: Option<LineBreakLeafIter>,
|
||||
|
||||
partial_inline_boxes_stack: Vec<PartialInlineBoxFragment<'box_tree>>,
|
||||
current_nesting_level: InlineNestingLevelState<'box_tree>,
|
||||
sequential_layout_state: Option<&'a mut SequentialLayoutState>,
|
||||
|
@ -110,6 +142,8 @@ impl<'box_tree, 'a, 'b> InlineFormattingContextState<'box_tree, 'a, 'b> {
|
|||
&mut self,
|
||||
hoisted_box: HoistedAbsolutelyPositionedBox,
|
||||
) {
|
||||
self.line_had_any_absolutes = true;
|
||||
|
||||
if let Some(context) = self.current_nesting_level.positioning_context.as_mut() {
|
||||
context.push(hoisted_box);
|
||||
return;
|
||||
|
@ -139,7 +173,9 @@ impl<'box_tree, 'a, 'b> InlineFormattingContextState<'box_tree, 'a, 'b> {
|
|||
layout_context,
|
||||
nesting_level,
|
||||
&mut self.inline_position,
|
||||
true,
|
||||
&mut self.line_had_any_content,
|
||||
self.line_had_any_absolutes,
|
||||
false, /* at_end_of_inline_element */
|
||||
);
|
||||
partial.start_corner.inline = Length::zero();
|
||||
partial.padding.inline_start = Length::zero();
|
||||
|
@ -155,6 +191,29 @@ impl<'box_tree, 'a, 'b> InlineFormattingContextState<'box_tree, 'a, 'b> {
|
|||
self.inline_position,
|
||||
);
|
||||
self.inline_position = Length::zero();
|
||||
self.line_had_any_content = false;
|
||||
self.line_had_any_absolutes = false;
|
||||
}
|
||||
|
||||
/// Determine if we are in the final box of this inline formatting context.
|
||||
///
|
||||
/// This is a big hack to trim the whitespace off the end of inline
|
||||
/// formatting contexts, that must stay in place until there is a
|
||||
/// better solution to use a temporary data structure to lay out
|
||||
/// lines.
|
||||
fn at_end_of_inline_formatting_context(&mut self) -> bool {
|
||||
let mut nesting_level = &mut self.current_nesting_level;
|
||||
if !nesting_level.remaining_boxes.at_end_of_iterator() {
|
||||
return false;
|
||||
}
|
||||
|
||||
for partial in self.partial_inline_boxes_stack.iter_mut().rev() {
|
||||
nesting_level = &mut partial.parent_nesting_level;
|
||||
if !nesting_level.remaining_boxes.at_end_of_iterator() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,12 +227,14 @@ impl InlineFormattingContext {
|
|||
pub(super) fn new(
|
||||
text_decoration_line: TextDecorationLine,
|
||||
has_first_formatted_line: bool,
|
||||
ends_with_whitespace: bool,
|
||||
) -> InlineFormattingContext {
|
||||
InlineFormattingContext {
|
||||
inline_level_boxes: Default::default(),
|
||||
text_decoration_line,
|
||||
has_first_formatted_line,
|
||||
contains_floats: false,
|
||||
ends_with_whitespace,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -191,6 +252,12 @@ impl InlineFormattingContext {
|
|||
paragraph: ContentSizes,
|
||||
current_line: ContentSizes,
|
||||
current_line_percentages: Percentage,
|
||||
/// Size for whitepsace pending to be added to this line.
|
||||
pending_whitespace: Length,
|
||||
/// Whether or not this IFC has seen any non-whitespace content.
|
||||
had_non_whitespace_content_yet: bool,
|
||||
/// The global linebreaking state.
|
||||
linebreaker: Option<LineBreakLeafIter>,
|
||||
}
|
||||
impl Computation<'_> {
|
||||
fn traverse(&mut self, inline_level_boxes: &[ArcRefCell<InlineLevelBox>]) {
|
||||
|
@ -225,18 +292,31 @@ impl InlineFormattingContext {
|
|||
runs,
|
||||
break_at_start,
|
||||
..
|
||||
} = text_run.break_and_shape(self.layout_context);
|
||||
} = text_run
|
||||
.break_and_shape(self.layout_context, &mut self.linebreaker);
|
||||
if break_at_start {
|
||||
self.line_break_opportunity()
|
||||
}
|
||||
for run in &runs {
|
||||
let advance = Length::from(run.glyph_store.total_advance());
|
||||
if run.glyph_store.is_whitespace() {
|
||||
self.line_break_opportunity()
|
||||
|
||||
if !run.glyph_store.is_whitespace() {
|
||||
self.had_non_whitespace_content_yet = true;
|
||||
self.current_line.min_content += advance;
|
||||
self.current_line.max_content +=
|
||||
self.pending_whitespace + advance;
|
||||
self.pending_whitespace = Length::zero();
|
||||
} else {
|
||||
self.current_line.min_content += advance
|
||||
// Discard any leading whitespace in the IFC. This will always be trimmed.
|
||||
if !self.had_non_whitespace_content_yet {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wait to take into account other whitespace until we see more content.
|
||||
// Whitespace at the end of the IFC will always be trimmed.
|
||||
self.line_break_opportunity();
|
||||
self.pending_whitespace += advance;
|
||||
}
|
||||
self.current_line.max_content += advance
|
||||
}
|
||||
},
|
||||
InlineLevelBox::Atomic(atomic) => {
|
||||
|
@ -244,9 +324,13 @@ impl InlineFormattingContext {
|
|||
self.layout_context,
|
||||
self.containing_block_writing_mode,
|
||||
);
|
||||
self.current_line.min_content += outer.min_content;
|
||||
|
||||
self.current_line.min_content +=
|
||||
self.pending_whitespace + outer.min_content;
|
||||
self.current_line.max_content += outer.max_content;
|
||||
self.current_line_percentages += pc;
|
||||
self.pending_whitespace = Length::zero();
|
||||
self.had_non_whitespace_content_yet = true;
|
||||
},
|
||||
InlineLevelBox::OutOfFlowFloatBox(_) |
|
||||
InlineLevelBox::OutOfFlowAbsolutelyPositionedBox(_) => {},
|
||||
|
@ -292,6 +376,9 @@ impl InlineFormattingContext {
|
|||
paragraph: ContentSizes::zero(),
|
||||
current_line: ContentSizes::zero(),
|
||||
current_line_percentages: Percentage::zero(),
|
||||
pending_whitespace: Length::zero(),
|
||||
had_non_whitespace_content_yet: false,
|
||||
linebreaker: None,
|
||||
};
|
||||
computation.traverse(&self.inline_level_boxes);
|
||||
computation.forced_line_break();
|
||||
|
@ -323,6 +410,9 @@ impl InlineFormattingContext {
|
|||
} else {
|
||||
Length::zero()
|
||||
},
|
||||
line_had_any_content: false,
|
||||
line_had_any_absolutes: false,
|
||||
linebreaker: None,
|
||||
current_nesting_level: InlineNestingLevelState {
|
||||
remaining_boxes: InlineBoxChildIter::from_formatting_context(self),
|
||||
fragments_so_far: Vec::with_capacity(self.inline_level_boxes.len()),
|
||||
|
@ -408,30 +498,65 @@ impl InlineFormattingContext {
|
|||
.push(Fragment::Float(box_fragment));
|
||||
},
|
||||
}
|
||||
} else
|
||||
// Reached the end of ifc.remaining_boxes
|
||||
if let Some(mut partial) = ifc.partial_inline_boxes_stack.pop() {
|
||||
} else if let Some(mut partial) = ifc.partial_inline_boxes_stack.pop() {
|
||||
// We reached the end of the remaining boxes in this nesting level, so we finish it and
|
||||
// start working on the parent nesting level again.
|
||||
partial.finish_layout(
|
||||
layout_context,
|
||||
&mut ifc.current_nesting_level,
|
||||
&mut ifc.inline_position,
|
||||
false,
|
||||
&mut ifc.line_had_any_content,
|
||||
ifc.line_had_any_absolutes,
|
||||
true, /* at_end_of_inline_element */
|
||||
);
|
||||
ifc.current_nesting_level = partial.parent_nesting_level
|
||||
} else {
|
||||
ifc.lines.finish_line(
|
||||
&mut ifc.current_nesting_level,
|
||||
containing_block,
|
||||
ifc.sequential_layout_state,
|
||||
ifc.inline_position,
|
||||
);
|
||||
return FlowLayout {
|
||||
fragments: ifc.lines.fragments,
|
||||
content_block_size: ifc.lines.next_line_block_position,
|
||||
collapsible_margins_in_children: CollapsedBlockMargins::zero(),
|
||||
};
|
||||
// We reached the end of the entire IFC.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ifc.lines.finish_line(
|
||||
&mut ifc.current_nesting_level,
|
||||
containing_block,
|
||||
ifc.sequential_layout_state,
|
||||
ifc.inline_position,
|
||||
);
|
||||
|
||||
let mut collapsible_margins_in_children = CollapsedBlockMargins::zero();
|
||||
let content_block_size = ifc.lines.next_line_block_position;
|
||||
if content_block_size == Length::zero() {
|
||||
collapsible_margins_in_children.collapsed_through = true;
|
||||
}
|
||||
|
||||
return FlowLayout {
|
||||
fragments: ifc.lines.fragments,
|
||||
content_block_size,
|
||||
collapsible_margins_in_children,
|
||||
};
|
||||
}
|
||||
|
||||
/// Return true if this [InlineFormattingContext] is empty for the purposes of ignoring
|
||||
/// during box tree construction. An IFC is empty if it only contains TextRuns with
|
||||
/// completely collapsible whitespace. When that happens it can be ignored completely.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
fn inline_level_boxes_are_empty(boxes: &[ArcRefCell<InlineLevelBox>]) -> bool {
|
||||
boxes
|
||||
.iter()
|
||||
.all(|inline_level_box| inline_level_box_is_empty(&*inline_level_box.borrow()))
|
||||
}
|
||||
|
||||
fn inline_level_box_is_empty(inline_level_box: &InlineLevelBox) -> bool {
|
||||
match inline_level_box {
|
||||
InlineLevelBox::InlineBox(_) => false,
|
||||
InlineLevelBox::TextRun(text_run) => !text_run.has_uncollapsible_content,
|
||||
InlineLevelBox::OutOfFlowAbsolutelyPositionedBox(_) => false,
|
||||
InlineLevelBox::OutOfFlowFloatBox(_) => false,
|
||||
InlineLevelBox::Atomic(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
inline_level_boxes_are_empty(&self.inline_level_boxes)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -510,12 +635,14 @@ impl Lines {
|
|||
sequential_layout_state.advance_block_position(size.block);
|
||||
}
|
||||
|
||||
self.fragments
|
||||
.push(Fragment::Anonymous(AnonymousFragment::new(
|
||||
Rect { start_corner, size },
|
||||
line_contents,
|
||||
containing_block.style.writing_mode,
|
||||
)))
|
||||
if !line_contents.is_empty() {
|
||||
self.fragments
|
||||
.push(Fragment::Anonymous(AnonymousFragment::new(
|
||||
Rect { start_corner, size },
|
||||
line_contents,
|
||||
containing_block.style.writing_mode,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -538,6 +665,7 @@ impl InlineBox {
|
|||
border.inline_start = Length::zero();
|
||||
margin.inline_start = Length::zero();
|
||||
}
|
||||
|
||||
let mut start_corner = Vec2 {
|
||||
block: Length::zero(),
|
||||
inline: ifc.inline_position - ifc.current_nesting_level.inline_start,
|
||||
|
@ -556,7 +684,7 @@ impl InlineBox {
|
|||
padding,
|
||||
border,
|
||||
margin,
|
||||
last_box_tree_fragment: self.last_fragment,
|
||||
was_part_of_previous_line: false,
|
||||
parent_nesting_level: std::mem::replace(
|
||||
&mut ifc.current_nesting_level,
|
||||
InlineNestingLevelState {
|
||||
|
@ -581,8 +709,37 @@ impl<'box_tree> PartialInlineBoxFragment<'box_tree> {
|
|||
layout_context: &LayoutContext,
|
||||
nesting_level: &mut InlineNestingLevelState,
|
||||
inline_position: &mut Length,
|
||||
at_line_break: bool,
|
||||
line_had_any_content: &mut bool,
|
||||
line_had_any_absolutes: bool,
|
||||
at_end_of_inline_element: bool,
|
||||
) {
|
||||
let mut padding = self.padding.clone();
|
||||
let mut border = self.border.clone();
|
||||
let mut margin = self.margin.clone();
|
||||
|
||||
if padding.inline_sum() > Length::zero() ||
|
||||
border.inline_sum() > Length::zero() ||
|
||||
margin.inline_sum() > Length::zero()
|
||||
{
|
||||
*line_had_any_content = true;
|
||||
}
|
||||
|
||||
if !*line_had_any_content && !line_had_any_absolutes && self.was_part_of_previous_line {
|
||||
return;
|
||||
}
|
||||
*line_had_any_content = true;
|
||||
|
||||
// If we are finishing in order to fragment this InlineBox into multiple lines, do
|
||||
// not add end margins, borders, and padding.
|
||||
if !at_end_of_inline_element {
|
||||
padding.inline_end = Length::zero();
|
||||
border.inline_end = Length::zero();
|
||||
margin.inline_end = Length::zero();
|
||||
}
|
||||
|
||||
// TODO(mrobinson): `inline_position` is relative to the IFC, but `self.start_corner` is relative
|
||||
// to the containing block, which means that this size will be incorrect with multiple levels
|
||||
// of nesting.
|
||||
let content_rect = Rect {
|
||||
size: Vec2 {
|
||||
inline: *inline_position - self.start_corner.inline,
|
||||
|
@ -591,35 +748,28 @@ impl<'box_tree> PartialInlineBoxFragment<'box_tree> {
|
|||
start_corner: self.start_corner.clone(),
|
||||
};
|
||||
|
||||
self.parent_nesting_level
|
||||
.max_block_size_of_fragments_so_far
|
||||
.max_assign(content_rect.size.block);
|
||||
|
||||
*inline_position += padding.inline_end + border.inline_end + margin.inline_end;
|
||||
|
||||
let mut fragment = BoxFragment::new(
|
||||
self.base_fragment_info,
|
||||
self.style.clone(),
|
||||
std::mem::take(&mut nesting_level.fragments_so_far),
|
||||
content_rect,
|
||||
self.padding.clone(),
|
||||
self.border.clone(),
|
||||
self.margin.clone(),
|
||||
padding,
|
||||
border,
|
||||
margin,
|
||||
None,
|
||||
CollapsedBlockMargins::zero(),
|
||||
);
|
||||
let last_fragment = self.last_box_tree_fragment && !at_line_break;
|
||||
if last_fragment {
|
||||
*inline_position += fragment.padding.inline_end +
|
||||
fragment.border.inline_end +
|
||||
fragment.margin.inline_end;
|
||||
} else {
|
||||
fragment.padding.inline_end = Length::zero();
|
||||
fragment.border.inline_end = Length::zero();
|
||||
fragment.margin.inline_end = Length::zero();
|
||||
}
|
||||
self.parent_nesting_level
|
||||
.max_block_size_of_fragments_so_far
|
||||
.max_assign(fragment.content_rect.size.block);
|
||||
|
||||
if let Some(context) = nesting_level.positioning_context.as_mut() {
|
||||
context.layout_collected_children(layout_context, &mut fragment);
|
||||
}
|
||||
|
||||
self.was_part_of_previous_line = true;
|
||||
self.parent_nesting_level
|
||||
.fragments_so_far
|
||||
.push(Fragment::Box(fragment));
|
||||
|
@ -774,6 +924,7 @@ fn layout_atomic(
|
|||
|
||||
fragment.content_rect.start_corner = start_corner;
|
||||
|
||||
ifc.line_had_any_content = true;
|
||||
ifc.inline_position += pbm_sums.inline_end + fragment.content_rect.size.inline;
|
||||
ifc.current_nesting_level
|
||||
.max_block_size_of_fragments_so_far
|
||||
|
@ -781,6 +932,11 @@ fn layout_atomic(
|
|||
ifc.current_nesting_level
|
||||
.fragments_so_far
|
||||
.push(Fragment::Box(fragment));
|
||||
|
||||
// After every atomic, we need to create a line breaking opportunity for the next TextRun.
|
||||
if let Some(linebreaker) = ifc.linebreaker.as_mut() {
|
||||
linebreaker.next(" ");
|
||||
}
|
||||
}
|
||||
|
||||
struct BreakAndShapeResult {
|
||||
|
@ -791,7 +947,11 @@ struct BreakAndShapeResult {
|
|||
}
|
||||
|
||||
impl TextRun {
|
||||
fn break_and_shape(&self, layout_context: &LayoutContext) -> BreakAndShapeResult {
|
||||
fn break_and_shape(
|
||||
&self,
|
||||
layout_context: &LayoutContext,
|
||||
linebreaker: &mut Option<LineBreakLeafIter>,
|
||||
) -> BreakAndShapeResult {
|
||||
use gfx::font::ShapingFlags;
|
||||
use style::computed_values::text_rendering::T as TextRendering;
|
||||
use style::computed_values::word_break::T as WordBreak;
|
||||
|
@ -847,7 +1007,7 @@ impl TextRun {
|
|||
&mut font,
|
||||
&self.text,
|
||||
&shaping_options,
|
||||
&mut None,
|
||||
linebreaker,
|
||||
);
|
||||
|
||||
BreakAndShapeResult {
|
||||
|
@ -860,109 +1020,177 @@ impl TextRun {
|
|||
}
|
||||
|
||||
fn layout(&self, layout_context: &LayoutContext, ifc: &mut InlineFormattingContextState) {
|
||||
use style::values::generics::text::LineHeight;
|
||||
let white_space = self.parent_style.get_inherited_text().white_space;
|
||||
let preserving_newlines = white_space.preserve_newlines();
|
||||
let preserving_spaces = white_space.preserve_spaces();
|
||||
let last_box_in_ifc = ifc.at_end_of_inline_formatting_context();
|
||||
|
||||
let BreakAndShapeResult {
|
||||
font_metrics,
|
||||
font_key,
|
||||
runs,
|
||||
break_at_start: _,
|
||||
} = self.break_and_shape(layout_context);
|
||||
let font_size = self.parent_style.get_font().font_size.size.0;
|
||||
let mut runs = runs.iter();
|
||||
loop {
|
||||
let mut glyphs = vec![];
|
||||
let mut advance_width = Length::zero();
|
||||
let mut last_break_opportunity = None;
|
||||
let mut force_line_break = false;
|
||||
// Fit as many glyphs within a single line as possible.
|
||||
loop {
|
||||
let next = runs.next();
|
||||
// If there are no more text runs we still need to check if the last
|
||||
// run was a forced line break
|
||||
if next
|
||||
.as_ref()
|
||||
.map_or(true, |run| run.glyph_store.is_whitespace())
|
||||
{
|
||||
// If this run exceeds the bounds of the containing block, then
|
||||
// we need to attempt to break the line.
|
||||
if advance_width > ifc.containing_block.inline_size - ifc.inline_position {
|
||||
// Reset the text run iterator to the last whitespace if possible,
|
||||
// to attempt to re-layout the most recent glyphs on a new line.
|
||||
if let Some((len, width, iter)) = last_break_opportunity.take() {
|
||||
glyphs.truncate(len);
|
||||
advance_width = width;
|
||||
runs = iter;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break_at_start,
|
||||
} = self.break_and_shape(layout_context, &mut ifc.linebreaker);
|
||||
|
||||
let mut glyphs = vec![];
|
||||
let mut inline_advance = Length::zero();
|
||||
let mut pending_whitespace = None;
|
||||
|
||||
let mut iterator = runs.iter().enumerate().peekable();
|
||||
while let Some((run_index, run)) = iterator.next() {
|
||||
if run.glyph_store.is_whitespace() {
|
||||
// If this whitespace forces a line break, finish the line and reset everything.
|
||||
let last_byte = self.text.as_bytes().get(run.range.end().to_usize() - 1);
|
||||
if last_byte == Some(&b'\n') && preserving_newlines {
|
||||
ifc.line_had_any_content = true;
|
||||
self.add_fragment_for_glyphs(
|
||||
ifc,
|
||||
glyphs.drain(..).collect(),
|
||||
inline_advance,
|
||||
font_metrics,
|
||||
font_key,
|
||||
);
|
||||
ifc.finish_line_and_reset(layout_context);
|
||||
inline_advance = Length::zero();
|
||||
continue;
|
||||
}
|
||||
if let Some(run) = next {
|
||||
if run.glyph_store.is_whitespace() {
|
||||
last_break_opportunity = Some((glyphs.len(), advance_width, runs.clone()));
|
||||
// If this whitespace ends with a newline, we need to check if
|
||||
// it's meaningful within the current style. If so, we force
|
||||
// a line break immediately.
|
||||
let last_byte = self.text.as_bytes().get(run.range.end().to_usize() - 1);
|
||||
if last_byte == Some(&b'\n') &&
|
||||
self.parent_style
|
||||
.get_inherited_text()
|
||||
.white_space
|
||||
.preserve_newlines()
|
||||
{
|
||||
force_line_break = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if !preserving_spaces {
|
||||
// From <https://www.w3.org/TR/css-text-3/#white-space-phase-2>:
|
||||
// "Then, the entire block is rendered. Inlines are laid out, taking bidi
|
||||
// reordering into account, and wrapping as specified by the text-wrap
|
||||
// property. As each line is laid out,
|
||||
//
|
||||
// > 1. A sequence of collapsible spaces at the beginning of a line is removed.
|
||||
if !ifc.line_had_any_content {
|
||||
continue;
|
||||
}
|
||||
glyphs.push(run.glyph_store.clone());
|
||||
advance_width += Length::from(run.glyph_store.total_advance());
|
||||
} else {
|
||||
// No more runs, so we can end the line.
|
||||
break;
|
||||
|
||||
// > 3. A sequence of collapsible spaces at the end of a line is removed,
|
||||
// > as well as any trailing U+1680 OGHAM SPACE MARK whose white-space
|
||||
// > property is normal, nowrap, or pre-line.
|
||||
// Try to trim whitespace at the end of lines. This is a hack. Ideally we
|
||||
// would keep a temporary data structure for a line and lay it out once we
|
||||
// know that we are going to make an entire one.
|
||||
if iterator.peek().is_none() && last_box_in_ifc {
|
||||
pending_whitespace = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't push a space until we know we aren't going to line break in the
|
||||
// next run.
|
||||
pending_whitespace = Some(run);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let line_height = match self.parent_style.get_inherited_text().line_height {
|
||||
LineHeight::Normal => font_metrics.line_gap,
|
||||
LineHeight::Number(n) => font_size * n.0,
|
||||
LineHeight::Length(l) => l.0,
|
||||
};
|
||||
let rect = Rect {
|
||||
start_corner: Vec2 {
|
||||
block: Length::zero(),
|
||||
inline: ifc.inline_position - ifc.current_nesting_level.inline_start,
|
||||
},
|
||||
size: Vec2 {
|
||||
block: line_height,
|
||||
inline: advance_width,
|
||||
},
|
||||
};
|
||||
ifc.inline_position += advance_width;
|
||||
ifc.current_nesting_level
|
||||
.max_block_size_of_fragments_so_far
|
||||
.max_assign(line_height);
|
||||
ifc.current_nesting_level
|
||||
.fragments_so_far
|
||||
.push(Fragment::Text(TextFragment {
|
||||
base: self.base_fragment_info.into(),
|
||||
parent_style: self.parent_style.clone(),
|
||||
rect,
|
||||
|
||||
let advance_from_pending_whitespace = pending_whitespace
|
||||
.map_or_else(Length::zero, |run| {
|
||||
Length::from(run.glyph_store.total_advance())
|
||||
});
|
||||
|
||||
// We break the line if this new advance and any advances from pending
|
||||
// whitespace bring us past the inline end of the containing block.
|
||||
let new_advance =
|
||||
Length::from(run.glyph_store.total_advance()) + advance_from_pending_whitespace;
|
||||
let will_advance_past_containing_block =
|
||||
(new_advance + inline_advance + ifc.inline_position) >
|
||||
ifc.containing_block.inline_size;
|
||||
|
||||
// We can only break the line, if this isn't the first actual content (non-whitespace or
|
||||
// preserved whitespace) on the line and this isn't the unbreakable run of this text run
|
||||
// (or we can break at the start according to the text breaker).
|
||||
let can_break = ifc.line_had_any_content && (break_at_start || run_index != 0);
|
||||
if will_advance_past_containing_block && can_break {
|
||||
self.add_fragment_for_glyphs(
|
||||
ifc,
|
||||
glyphs.drain(..).collect(),
|
||||
inline_advance,
|
||||
font_metrics,
|
||||
font_key,
|
||||
glyphs,
|
||||
text_decoration_line: ifc.current_nesting_level.text_decoration_line,
|
||||
}));
|
||||
// If this line is being broken because of a trailing newline, we can't ignore it.
|
||||
if runs.as_slice().is_empty() && !force_line_break {
|
||||
break;
|
||||
} else {
|
||||
);
|
||||
|
||||
pending_whitespace = None;
|
||||
ifc.finish_line_and_reset(layout_context);
|
||||
inline_advance = Length::zero();
|
||||
}
|
||||
|
||||
if let Some(pending_whitespace) = pending_whitespace.take() {
|
||||
inline_advance += Length::from(pending_whitespace.glyph_store.total_advance());
|
||||
glyphs.push(pending_whitespace.glyph_store.clone());
|
||||
}
|
||||
|
||||
inline_advance += Length::from(run.glyph_store.total_advance());
|
||||
glyphs.push(run.glyph_store.clone());
|
||||
ifc.line_had_any_content = true;
|
||||
}
|
||||
|
||||
if let Some(pending_whitespace) = pending_whitespace.take() {
|
||||
inline_advance += Length::from(pending_whitespace.glyph_store.total_advance());
|
||||
glyphs.push(pending_whitespace.glyph_store.clone());
|
||||
}
|
||||
|
||||
self.add_fragment_for_glyphs(
|
||||
ifc,
|
||||
glyphs.drain(..).collect(),
|
||||
inline_advance,
|
||||
font_metrics,
|
||||
font_key,
|
||||
);
|
||||
}
|
||||
|
||||
fn add_fragment_for_glyphs(
|
||||
&self,
|
||||
ifc: &mut InlineFormattingContextState,
|
||||
glyphs: Vec<std::sync::Arc<GlyphStore>>,
|
||||
inline_advance: Length,
|
||||
font_metrics: FontMetrics,
|
||||
font_key: FontInstanceKey,
|
||||
) {
|
||||
if glyphs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let font_size = self.parent_style.get_font().font_size.size.0;
|
||||
let line_height = match self.parent_style.get_inherited_text().line_height {
|
||||
LineHeight::Normal => font_metrics.line_gap,
|
||||
LineHeight::Number(n) => font_size * n.0,
|
||||
LineHeight::Length(l) => l.0,
|
||||
};
|
||||
|
||||
let rect = Rect {
|
||||
start_corner: Vec2 {
|
||||
block: Length::zero(),
|
||||
inline: ifc.inline_position - ifc.current_nesting_level.inline_start,
|
||||
},
|
||||
size: Vec2 {
|
||||
block: line_height,
|
||||
inline: inline_advance,
|
||||
},
|
||||
};
|
||||
|
||||
ifc.inline_position += inline_advance;
|
||||
ifc.current_nesting_level
|
||||
.max_block_size_of_fragments_so_far
|
||||
.max_assign(line_height);
|
||||
ifc.current_nesting_level
|
||||
.fragments_so_far
|
||||
.push(Fragment::Text(TextFragment {
|
||||
base: self.base_fragment_info.into(),
|
||||
parent_style: self.parent_style.clone(),
|
||||
rect,
|
||||
font_metrics,
|
||||
font_key,
|
||||
glyphs,
|
||||
text_decoration_line: ifc.current_nesting_level.text_decoration_line,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
enum InlineBoxChildIter<'box_tree> {
|
||||
InlineFormattingContext(std::slice::Iter<'box_tree, ArcRefCell<InlineLevelBox>>),
|
||||
InlineFormattingContext(
|
||||
std::iter::Peekable<std::slice::Iter<'box_tree, ArcRefCell<InlineLevelBox>>>,
|
||||
),
|
||||
InlineBox {
|
||||
inline_level_box: ArcRefCell<InlineLevelBox>,
|
||||
child_index: usize,
|
||||
|
@ -974,7 +1202,10 @@ impl<'box_tree> InlineBoxChildIter<'box_tree> {
|
|||
inline_formatting_context: &'box_tree InlineFormattingContext,
|
||||
) -> InlineBoxChildIter<'box_tree> {
|
||||
InlineBoxChildIter::InlineFormattingContext(
|
||||
inline_formatting_context.inline_level_boxes.iter(),
|
||||
inline_formatting_context
|
||||
.inline_level_boxes
|
||||
.iter()
|
||||
.peekable(),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -986,6 +1217,21 @@ impl<'box_tree> InlineBoxChildIter<'box_tree> {
|
|||
child_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn at_end_of_iterator(&mut self) -> bool {
|
||||
match *self {
|
||||
InlineBoxChildIter::InlineFormattingContext(ref mut iter) => iter.peek().is_none(),
|
||||
InlineBoxChildIter::InlineBox {
|
||||
ref inline_level_box,
|
||||
ref child_index,
|
||||
} => match *inline_level_box.borrow() {
|
||||
InlineLevelBox::InlineBox(ref inline_box) => {
|
||||
*child_index >= inline_box.children.len()
|
||||
},
|
||||
_ => unreachable!(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'box_tree> Iterator for InlineBoxChildIter<'box_tree> {
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
[remove-block-between-inline-and-abspos.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[bidi-box-model-019.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[bidi-box-model-020.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[bidi-box-model-021.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[bidi-box-model-023.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[bidi-box-model-024.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[bidi-box-model-025.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[bidi-box-model-026.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[bidi-box-model-027.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[bidi-box-model-037.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[bidi-box-model-038.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[bidi-box-model-039.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[bidi-box-model-041.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[bidi-box-model-042.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[bidi-box-model-043.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[bidi-box-model-044.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[bidi-box-model-045.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[border-color-012.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[anonymous-box-generation-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[insert-inline-in-blocks-n-inlines-begin-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[insert-inline-in-blocks-n-inlines-end-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[insert-inline-in-blocks-n-inlines-middle-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[c414-flt-fit-002.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[c414-flt-fit-003.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[c414-flt-fit-004.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[c414-flt-fit-005.xht]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/CSS2/css1/c542-letter-sp-001.xht.ini
Normal file
2
tests/wpt/meta/css/CSS2/css1/c542-letter-sp-001.xht.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[c542-letter-sp-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[c547-indent-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[c5501-imrgn-t-000.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[c5502-mrgn-r-000.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[c5503-imrgn-b-000.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[c5506-ipadn-t-000.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[c5506-ipadn-t-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[c5507-padn-r-000.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[c5508-ipadn-b-000.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[c5508-ipadn-b-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[c5509-padn-l-000.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[c5525-fltwidth-003.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[floats-124.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[floats-125.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[floats-143.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[float-no-content-beside-001.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[floats-wrap-bfc-002-right-overflow.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[floats-wrap-bfc-002-right-table.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[font-applies-to-017.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[font-variant-applies-to-017.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[font-weight-applies-to-017.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[after-content-display-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[after-content-display-018.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[content-173.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[content-white-space-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[border-padding-bleed-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[border-padding-bleed-002.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[border-padding-bleed-003.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[inline-formatting-context-007.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[inline-formatting-context-013.xht]
|
||||
expected: FAIL
|
|
@ -34,3 +34,9 @@
|
|||
|
||||
[[data-expected-height\] 13]
|
||||
expected: FAIL
|
||||
|
||||
[[data-expected-height\] 3]
|
||||
expected: FAIL
|
||||
|
||||
[[data-expected-height\] 4]
|
||||
expected: FAIL
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
[block-in-inline-align-001.html]
|
||||
expected: FAIL
|
|
@ -0,0 +1,2 @@
|
|||
[block-in-inline-empty-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[block-in-inline-first-line-001.html]
|
||||
expected: FAIL
|
|
@ -0,0 +1,2 @@
|
|||
[block-in-inline-insert-006-nosplit-ref.xht]
|
||||
expected: FAIL
|
|
@ -0,0 +1,2 @@
|
|||
[block-in-inline-insert-006-ref.xht]
|
||||
expected: FAIL
|
|
@ -0,0 +1,2 @@
|
|||
[block-in-inline-insert-006.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[block-in-inline-margins-003.html]
|
||||
expected: FAIL
|
|
@ -0,0 +1,2 @@
|
|||
[block-in-inline-nested-002.xht]
|
||||
expected: FAIL
|
|
@ -0,0 +1,2 @@
|
|||
[block-in-inline-whitespace-001a.xht]
|
||||
expected: FAIL
|
|
@ -0,0 +1,2 @@
|
|||
[block-in-inline-whitespace-001b.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[inline-block-replaced-width-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[inline-block-replaced-width-006.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[inline-block-width-001a.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[inline-block-width-001b.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[inline-block-width-002a.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[inline-block-width-002b.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[max-width-percentage-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[min-width-percentage-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[replaced-intrinsic-002.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[width-percentage-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[width-percentage-002.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[abspos-008.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[position-relative-029.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[position-relative-030.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[position-relative-031.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[text-indent-014.xht]
|
||||
expected: FAIL
|
|
@ -0,0 +1,2 @@
|
|||
[text-indent-on-blank-line-rtl-left-align.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[white-space-007.xht]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/CSS2/text/white-space-008.xht.ini
Normal file
2
tests/wpt/meta/css/CSS2/text/white-space-008.xht.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[white-space-008.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[white-space-collapsing-003.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[white-space-collapsing-breaks-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[white-space-normal-008.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[white-space-processing-043.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[white-space-processing-044.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[white-space-processing-045.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[white-space-processing-053.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[inline-block-baseline-001.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[inline-block-baseline-003.xht]
|
||||
expected: FAIL
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue