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

@ -3,7 +3,6 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::rc::Rc;
use std::vec::IntoIter;
use app_units::Au;
use bitflags::bitflags;
@ -18,6 +17,7 @@ use style::values::specified::box_::DisplayOutside;
use style::values::specified::text::TextDecorationLine;
use style::values::Either;
use style::Zero;
use unicode_bidi::{BidiInfo, Level};
use webrender_api::FontInstanceKey;
use super::inline_box::{
@ -170,7 +170,7 @@ pub(super) struct LineItemLayout<'a> {
impl<'a> LineItemLayout<'a> {
pub(super) fn layout_line_items(
state: &mut InlineFormattingContextState,
iterator: &mut IntoIter<LineItem>,
line_items: Vec<LineItem>,
start_position: LogicalVec2<Au>,
effective_block_advance: &LineBlockSizes,
justification_adjustment: Au,
@ -191,7 +191,7 @@ impl<'a> LineItemLayout<'a> {
},
justification_adjustment,
}
.layout(iterator)
.layout(line_items, state.has_right_to_left_content)
}
/// Start and end inline boxes in tree order, so that it reflects the given inline box.
@ -217,8 +217,40 @@ impl<'a> LineItemLayout<'a> {
}
}
pub(super) fn layout(&mut self, iterator: &mut IntoIter<LineItem>) -> Vec<Fragment> {
for item in iterator.by_ref() {
pub(super) fn layout(
&mut self,
mut line_items: Vec<LineItem>,
has_right_to_left_content: bool,
) -> Vec<Fragment> {
let mut last_level: 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::StartInlineBoxPaddingBorderMargin(_) => last_level,
LineItem::EndInlineBoxPaddingBorderMargin(_) => 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 has_right_to_left_content {
sort_by_indices_in_place(&mut line_items, BidiInfo::reorder_visual(&levels));
}
for item in line_items.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
@ -304,10 +336,10 @@ impl<'a> LineItemLayout<'a> {
}
fn end_inline_box(&mut self) {
let outer_state = self.state_stack.pop().expect("Ended unknown inline box 11");
let outer_state = self.state_stack.pop().expect("Ended unknown inline box");
let mut inner_state = std::mem::replace(&mut self.state, outer_state);
let identifier = inner_state.identifier.expect("Ended unknown inline box 22");
let identifier = inner_state.identifier.expect("Ended unknown inline box");
let inline_box_state = &*self.inline_box_states[identifier.index_in_inline_boxes as usize];
let inline_box = self.inline_boxes.get(&identifier);
let inline_box = &*(inline_box.borrow());
@ -315,23 +347,24 @@ impl<'a> LineItemLayout<'a> {
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);
if !inner_state
let had_start = inner_state
.flags
.contains(LineLayoutInlineContainerFlags::HAD_START_PBM)
{
.contains(LineLayoutInlineContainerFlags::HAD_START_PBM);
let had_end = inner_state
.flags
.contains(LineLayoutInlineContainerFlags::HAD_END_PBM);
if !had_start {
padding.inline_start = Au::zero();
border.inline_start = Au::zero();
margin.inline_start = Au::zero();
}
if !inner_state
.flags
.contains(LineLayoutInlineContainerFlags::HAD_END_PBM)
{
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.
@ -339,12 +372,7 @@ impl<'a> LineItemLayout<'a> {
// 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() &&
!inner_state
.flags
.contains(LineLayoutInlineContainerFlags::HAD_START_PBM) &&
pbm_sums.inline_sum().is_zero()
{
if inner_state.fragments.is_empty() && !had_start && pbm_sums.inline_sum().is_zero() {
return;
}
@ -645,6 +673,8 @@ pub(super) struct TextRunLineItem {
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,
}
impl TextRunLineItem {
@ -697,6 +727,10 @@ impl TextRunLineItem {
// 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 {
@ -711,6 +745,9 @@ pub(super) struct AtomicLineItem {
/// 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 {
@ -753,3 +790,24 @@ fn line_height(parent_style: &ComputedValues, font_metrics: &FontMetrics) -> Len
LineHeight::Length(length) => length.0,
}
}
/// 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;
}
}
}