layout: Add initial support for bidirectional text (BiDi) (#33148)

This adds supports for right-to-left text assigning bidi levels to all
line items when necessary. This includes support for the `dir` attribute
as well as corresponding CSS properties like `unicode-bidi`. It only
implements right-to-left rendering for inline layout at the moment and
doesn't include support for `dir=auto`. Because of missing features,
this causes quite a few tests to start failing, as references become
incorrect due to right-to-left rendering being active in some cases,
but not others (before it didn't exist at all).

Analysis of most of the new failures:

```
- /css/css-flexbox/gap-001-rtl.html
  /css/css-flexbox/gap-004-rtl.html
 - Require implementing BiDi in Flexbox, because the start and
   end inline margins are opposite the order of items.

- /css/CSS2/bidi-text/direction-applies-to-*.xht
  /css/CSS2/bidi-text/direction-applies-to-002.xht
  /css/CSS2/bidi-text/direction-applies-to-003.xht
  /css/CSS2/bidi-text/direction-applies-to-004.xht
  - Broken due to a bug in tables, not allocating the
    right amount of width for a column.

- /css/css-lists/inline-list.html
  - This fails because we wrongly insert a soft wrap opportunity between the
    start of an inline box and its first content.

- /css/css-text/bidi/bidi-lines-001.html
  /css/css-text/bidi/bidi-lines-002.html
  /css/CSS2/text/bidi-flag-emoji.html
  - We do not fully support unicode-bidi: plaintext

- /css/css-text/text-align/text-align-end-010.html
  /css/css-text/text-align/text-align-justify-006.html
  /css/css-text/text-align/text-align-start-010.html
  /html/dom/elements/global-attributes/*
  - We do not support dir=auto yet.

- /css/css-text/white-space/tab-bidi-001.html
  - Servo doesn't support tab stops

- /css/CSS2/positioning/abspos-block-level-001.html
  /css/css-text/word-break/word-break-normal-ar-000.html
  - Do not yet support RTL layout in block

- /css/css-text/white-space/pre-wrap-018.html
  - Even in RTL contexts, spaces at the end of the line must hang and
    not be reordered

- /css/css-text/white-space/trailing-space-and-text-alignment-rtl-002.html
  - We are letting spaces hang with white-space: pre, but they shouldn't
    hang.
```

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Rakhi Sharma <atbrakhi@igalia.com>
This commit is contained in:
Martin Robinson 2024-08-21 07:28:54 -07:00 committed by GitHub
parent 65bd5a3b99
commit 56280c6242
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
189 changed files with 547 additions and 762 deletions

View file

@ -8,6 +8,7 @@ use std::char::{ToLowercase, ToUppercase};
use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
use style::values::computed::TextDecorationLine;
use style::values::specified::text::TextTransformCase;
use unicode_bidi::Level;
use unicode_segmentation::UnicodeSegmentation;
use super::text_run::TextRun;
@ -19,6 +20,7 @@ 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 {
@ -82,6 +84,11 @@ impl InlineFormattingContextBuilder {
!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.
@ -101,7 +108,7 @@ impl InlineFormattingContextBuilder {
// 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::OutOfFlowAbsolutelyPositionedBox(..) => false,
InlineItem::OutOfFlowFloatBox(_) => false,
InlineItem::Atomic(..) => false,
}
@ -119,14 +126,13 @@ impl InlineFormattingContextBuilder {
let inline_level_box = ArcRefCell::new(InlineItem::Atomic(
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.
let string_to_push = "\u{fffc}";
self.text_segments.push(string_to_push.to_owned());
self.current_text_offset += string_to_push.len();
self.push_control_character_string("\u{fffc}");
self.last_inline_box_ended_with_collapsible_white_space = false;
self.on_word_boundary = true;
@ -141,7 +147,9 @@ impl InlineFormattingContextBuilder {
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
}
@ -154,6 +162,8 @@ impl InlineFormattingContextBuilder {
}
pub(crate) fn start_inline_box(&mut self, inline_box: InlineBox) {
self.push_control_character_string(inline_box.style.bidi_control_chars().0);
let identifier = self.inline_boxes.start_inline_box(inline_box);
self.inline_items
.push(ArcRefCell::new(InlineItem::StartInlineBox(identifier)));
@ -164,6 +174,9 @@ impl InlineFormattingContextBuilder {
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().style.bidi_control_chars().1);
inline_level_box
}
@ -261,6 +274,7 @@ impl InlineFormattingContextBuilder {
layout_context: &LayoutContext,
text_decoration_line: TextDecorationLine,
has_first_formatted_line: bool,
default_bidi_level: Level,
) -> Option<InlineFormattingContext> {
if self.is_empty() {
return None;
@ -293,6 +307,7 @@ impl InlineFormattingContextBuilder {
text_decoration_line,
has_first_formatted_line,
/* is_single_line_text_input = */ false,
default_bidi_level,
)
}
@ -303,6 +318,7 @@ impl InlineFormattingContextBuilder {
text_decoration_line: TextDecorationLine,
has_first_formatted_line: bool,
is_single_line_text_input: bool,
default_bidi_level: Level,
) -> Option<InlineFormattingContext> {
if self.is_empty() {
return None;
@ -317,6 +333,7 @@ impl InlineFormattingContextBuilder {
text_decoration_line,
has_first_formatted_line,
is_single_line_text_input,
default_bidi_level,
))
}
}