Skip to content

Commit

Permalink
LibWeb: Layout text chunks based on their Unicode direction
Browse files Browse the repository at this point in the history
Append text chunks to either the start or end of the text fragment,
depending on the text direction. The direction is determined by what
script its code points are from.

(cherry picked from commit 11e7d72686d86b3e900c0e9ab76e75d3922f06d3;
amended to minorly tweak dimensions in expected files due to serenity
not using harfbuzz for text shaping)
  • Loading branch information
BenJilks authored and nico committed Nov 24, 2024
1 parent 0197bfc commit c72b9e1
Show file tree
Hide file tree
Showing 23 changed files with 471 additions and 161 deletions.
8 changes: 4 additions & 4 deletions Tests/LibWeb/Layout/expected/acid1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
TextNode <#text>
TextNode <#text>
BlockContainer <li#baz> at (145,185) content-size 100x100 floating [BFC] children: inline
frag 0 from TextNode start: 0, length: 6, rect: [145,185 29.421875x10] baseline: 8
frag 0 from TextNode start: 0, length: 6, rect: [145,185 29.4375x10] baseline: 8
"pluot?"
TextNode <#text>
TextNode <#text>
Expand Down Expand Up @@ -89,15 +89,15 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <(anonymous)> at (20,30) content-size 480x0 children: inline
TextNode <#text>
BlockContainer <p> at (20,335) content-size 480x65 children: inline
frag 0 from TextNode start: 1, length: 90, rect: [20,335 473.625x13] baseline: 9.5
frag 0 from TextNode start: 1, length: 90, rect: [20,335 473.640625x13] baseline: 9.5
"This is a nonsensical document, but syntactically valid HTML 4.0. All 100%-conformant CSS1"
frag 1 from TextNode start: 92, length: 74, rect: [20,348 396.96875x13] baseline: 9.5
"agents should be able to render the document elements above this paragraph"
frag 2 from TextNode start: 167, length: 43, rect: [20,361 207.890625x13] baseline: 9.5
"indistinguishably (to the pixel) from this "
frag 3 from TextNode start: 0, length: 31, rect: [331,361 159.671875x13] baseline: 9.5
frag 3 from TextNode start: 0, length: 31, rect: [331,361 159.65625x13] baseline: 9.5
" (except font rasterization and"
frag 4 from TextNode start: 32, length: 89, rect: [20,374 465.015625x13] baseline: 9.5
frag 4 from TextNode start: 32, length: 89, rect: [20,374 465.03125x13] baseline: 9.5
"form widgets). All discrepancies should be traceable to CSS1 implementation shortcomings."
frag 5 from TextNode start: 122, length: 67, rect: [20,387 345.546875x13] baseline: 9.5
"Once you have finished evaluating this test, you can return to the "
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
"rutrum nisi eget dui"
frag 9 from TextNode start: 181, length: 19, rect: [252,212 208.828125x22] baseline: 17
"dictum, eu accumsan"
frag 10 from TextNode start: 201, length: 18, rect: [252,234 180.1875x22] baseline: 17
frag 10 from TextNode start: 201, length: 18, rect: [252,234 180.203125x22] baseline: 17
"enim tristique. Ut"
frag 11 from TextNode start: 220, length: 19, rect: [252,256 195.28125x22] baseline: 17
"lobortis lorem eget"
Expand All @@ -35,7 +35,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
"Donec a tincidunt"
frag 16 from TextNode start: 328, length: 22, rect: [252,366 231.078125x22] baseline: 17
"ante. Phasellus a arcu"
frag 17 from TextNode start: 351, length: 7, rect: [252,388 70.546875x22] baseline: 17
frag 17 from TextNode start: 351, length: 7, rect: [252,388 70.5625x22] baseline: 17
"tortor."
BlockContainer <div.left> at (253,11) content-size 300x200 floating [BFC] children: not-inline
TextNode <#text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <span> at (13,46.5) content-size 496x330 inline-block [BFC] children: inline
frag 0 from TextNode start: 0, length: 9, rect: [13,46.5 246.484375x55] baseline: 42.484375
"Skew is a"
frag 1 from TextNode start: 10, length: 10, rect: [13,101.5 240.53125x55] baseline: 42.484375
frag 1 from TextNode start: 10, length: 10, rect: [13,101.5 240.515625x55] baseline: 42.484375
"web-first,"
frag 2 from TextNode start: 21, length: 14, rect: [13,156.5 377.9375x55] baseline: 42.484375
"cross-platform"
frag 3 from TextNode start: 36, length: 11, rect: [13,211.5 314.015625x55] baseline: 42.484375
"programming"
frag 4 from TextNode start: 48, length: 16, rect: [13,266.5 415.734375x55] baseline: 42.484375
"language with an"
frag 5 from TextNode start: 65, length: 20, rect: [13,321.5 492.671875x55] baseline: 42.484375
frag 5 from TextNode start: 65, length: 20, rect: [13,321.5 492.6875x55] baseline: 42.484375
"optimizing compiler."
TextNode <#text>

Expand Down
44 changes: 27 additions & 17 deletions Tests/LibWeb/Layout/expected/div_align.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,36 +39,46 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
"is"
frag 5 from TextNode start: 12, length: 1, rect: [106,479 8x17] baseline: 13.296875
" "
frag 6 from TextNode start: 13, length: 16, rect: [114,479 102.96875x17] baseline: 13.296875
"'full-justified'"
frag 7 from TextNode start: 29, length: 1, rect: [217,479 8x17] baseline: 13.296875
frag 6 from TextNode start: 13, length: 1, rect: [114,479 3.625x17] baseline: 13.296875
"'"
frag 7 from TextNode start: 14, length: 4, rect: [117,479 24.671875x17] baseline: 13.296875
"full"
frag 8 from TextNode start: 18, length: 1, rect: [142,479 6.484375x17] baseline: 13.296875
"-"
frag 9 from TextNode start: 19, length: 9, rect: [148,479 64.5625x17] baseline: 13.296875
"justified"
frag 10 from TextNode start: 28, length: 1, rect: [213,479 3.625x17] baseline: 13.296875
"'"
frag 11 from TextNode start: 29, length: 1, rect: [217,479 8x17] baseline: 13.296875
" "
frag 8 from TextNode start: 30, length: 3, rect: [225,479 26.8125x17] baseline: 13.296875
frag 12 from TextNode start: 30, length: 3, rect: [225,479 26.8125x17] baseline: 13.296875
"and"
frag 9 from TextNode start: 33, length: 1, rect: [251,479 8x17] baseline: 13.296875
frag 13 from TextNode start: 33, length: 1, rect: [251,479 8x17] baseline: 13.296875
" "
frag 10 from TextNode start: 34, length: 3, rect: [259,479 24.875x17] baseline: 13.296875
frag 14 from TextNode start: 34, length: 3, rect: [259,479 24.875x17] baseline: 13.296875
"the"
frag 11 from TextNode start: 37, length: 1, rect: [284,479 8x17] baseline: 13.296875
frag 15 from TextNode start: 37, length: 1, rect: [284,479 8x17] baseline: 13.296875
" "
frag 12 from TextNode start: 38, length: 5, rect: [292,479 43.4375x17] baseline: 13.296875
frag 16 from TextNode start: 38, length: 5, rect: [292,479 43.4375x17] baseline: 13.296875
"green"
frag 13 from TextNode start: 43, length: 1, rect: [336,479 8x17] baseline: 13.296875
frag 17 from TextNode start: 43, length: 1, rect: [336,479 8x17] baseline: 13.296875
" "
frag 14 from TextNode start: 44, length: 6, rect: [344,479 57.0625x17] baseline: 13.296875
frag 18 from TextNode start: 44, length: 6, rect: [344,479 57.0625x17] baseline: 13.296875
"square"
frag 15 from TextNode start: 50, length: 1, rect: [401,479 8x17] baseline: 13.296875
frag 19 from TextNode start: 50, length: 1, rect: [401,479 8x17] baseline: 13.296875
" "
frag 16 from TextNode start: 51, length: 2, rect: [409,479 13.90625x17] baseline: 13.296875
frag 20 from TextNode start: 51, length: 2, rect: [409,479 13.90625x17] baseline: 13.296875
"is"
frag 17 from TextNode start: 53, length: 1, rect: [423,479 8x17] baseline: 13.296875
frag 21 from TextNode start: 53, length: 1, rect: [423,479 8x17] baseline: 13.296875
" "
frag 18 from TextNode start: 54, length: 4, rect: [431,479 26.25x17] baseline: 13.296875
frag 22 from TextNode start: 54, length: 4, rect: [431,479 26.25x17] baseline: 13.296875
"left"
frag 19 from TextNode start: 58, length: 1, rect: [457,479 8x17] baseline: 13.296875
frag 23 from TextNode start: 58, length: 1, rect: [457,479 8x17] baseline: 13.296875
" "
frag 20 from TextNode start: 59, length: 8, rect: [465,479 55.671875x17] baseline: 13.296875
"aligned:"
frag 24 from TextNode start: 59, length: 7, rect: [465,479 51.890625x17] baseline: 13.296875
"aligned"
frag 25 from TextNode start: 66, length: 1, rect: [517,479 3.78125x17] baseline: 13.296875
":"
TextNode <#text>
BlockContainer <div.square> at (28,516) content-size 100x100 children: not-inline
BlockContainer <(anonymous)> at (8,636) content-size 784x0 children: inline
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <html> at (0,0) content-size 800x470 [BFC] children: not-inline
BlockContainer <body> at (8,8) content-size 784x454 children: not-inline
BlockContainer <(anonymous)> at (8,8) content-size 784x22 children: inline
frag 0 from TextNode start: 0, length: 40, rect: [8,8 391.65625x22] baseline: 17
frag 0 from TextNode start: 0, length: 40, rect: [8,8 391.640625x22] baseline: 17
"Variable set by inline style of element:"
TextNode <#text>
BreakNode <br>
Expand All @@ -12,7 +12,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <(anonymous)> at (8,30) content-size 200x100 inline-block [BFC] children: inline
TextNode <#text>
BlockContainer <(anonymous)> at (8,130) content-size 784x66 children: inline
frag 0 from TextNode start: 1, length: 42, rect: [8,174 441.28125x22] baseline: 17
frag 0 from TextNode start: 1, length: 42, rect: [8,174 441.265625x22] baseline: 17
"Variable set by CSS rule matching element:"
TextNode <#text>
BreakNode <br>
Expand All @@ -25,7 +25,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <(anonymous)> at (8,196) content-size 200x100 inline-block [BFC] children: inline
TextNode <#text>
BlockContainer <(anonymous)> at (8,296) content-size 784x66 children: inline
frag 0 from TextNode start: 1, length: 49, rect: [8,340 520.625x22] baseline: 17
frag 0 from TextNode start: 1, length: 49, rect: [8,340 520.609375x22] baseline: 17
"Variable set by CSS rule matching pseudo element:"
TextNode <#text>
BreakNode <br>
Expand Down
Binary file modified Tests/LibWeb/Screenshot/images/object-fit-position.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions Tests/LibWeb/Screenshot/reference/text-direction-ref.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<style>
* {
margin: 0;
}

body {
background-color: white;
}
</style>
<img src="../images/text-direction-ref.png">
18 changes: 18 additions & 0 deletions Tests/LibWeb/Screenshot/text-direction.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<link rel="match" href="reference/text-direction-ref.html"/>
<div dir="ltr" align="left">hello test 1, 2, 3!</div>
<div dir="rtl" align="left">hello test 1, 2, 3!</div>
<div dir="ltr" align="right">hello test 1, 2, 3!</div>
<div dir="rtl" align="right">hello test 1, 2, 3!</div>

<div dir=ltr>אא aaa bbb ccc מממ</div>
<div dir=rtl>אא aaa bbb ccc מממ</div>

<div dir=ltr>אא 1 2 3 מממ</div>
<div dir=rtl>אא 1 2 3 מממ</div>

<div dir=ltr>aa....!!!</div>
<div dir=rtl>aa....!!!</div>

<div dir=ltr>حسنًا ، hello friends مرحباً أيها ا test لأصدقاء end</div>
<div dir=rtl>حسنًا ، hello friends مرحباً أيها ا test لأصدقاء end</div>
13 changes: 12 additions & 1 deletion Userland/Libraries/LibGfx/TextLayout.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,23 @@ using DrawGlyphOrEmoji = Variant<DrawGlyph, DrawEmoji>;

class GlyphRun : public RefCounted<GlyphRun> {
public:
GlyphRun(Vector<Gfx::DrawGlyphOrEmoji>&& glyphs, NonnullRefPtr<Font> font)
enum class TextType {
Common,
ContextDependent,
EndPadding,
Ltr,
Rtl,
};

GlyphRun(Vector<Gfx::DrawGlyphOrEmoji>&& glyphs, NonnullRefPtr<Font> font, TextType text_type)
: m_glyphs(move(glyphs))
, m_font(move(font))
, m_text_type(text_type)
{
}

[[nodiscard]] Font const& font() const { return m_font; }
[[nodiscard]] TextType text_type() const { return m_text_type; }
[[nodiscard]] Vector<Gfx::DrawGlyphOrEmoji> const& glyphs() const { return m_glyphs; }
[[nodiscard]] Vector<Gfx::DrawGlyphOrEmoji>& glyphs() { return m_glyphs; }
[[nodiscard]] bool is_empty() const { return m_glyphs.is_empty(); }
Expand All @@ -116,6 +126,7 @@ class GlyphRun : public RefCounted<GlyphRun> {
private:
Vector<Gfx::DrawGlyphOrEmoji> m_glyphs;
NonnullRefPtr<Font> m_font;
TextType m_text_type;
};

Variant<DrawGlyph, DrawEmoji> prepare_draw_glyph_or_emoji(FloatPoint point, Utf8CodePointIterator& it, Font const& font);
Expand Down
4 changes: 3 additions & 1 deletion Userland/Libraries/LibWeb/Layout/InlineFormattingContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,10 @@ void InlineFormattingContext::generate_line_boxes(LayoutMode layout_mode)
auto& line_boxes = m_containing_block_used_values.line_boxes;
line_boxes.clear_with_capacity();

auto direction = m_context_box->computed_values().direction();

InlineLevelIterator iterator(*this, m_state, containing_block(), m_containing_block_used_values, layout_mode);
LineBuilder line_builder(*this, m_state, m_containing_block_used_values);
LineBuilder line_builder(*this, m_state, m_containing_block_used_values, direction);

// NOTE: When we ignore collapsible whitespace chunks at the start of a line,
// we have to remember how much start margin that chunk had in the inline
Expand Down
53 changes: 48 additions & 5 deletions Userland/Libraries/LibWeb/Layout/InlineLevelIterator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,39 @@ CSSPixels InlineLevelIterator::next_non_whitespace_sequence_width()
return next_width;
}

Gfx::GlyphRun::TextType InlineLevelIterator::resolve_text_direction_from_context()
{
VERIFY(m_text_node_context.has_value());

Optional<Gfx::GlyphRun::TextType> next_known_direction;
for (size_t i = 0;; ++i) {
auto peek = m_text_node_context->chunk_iterator.peek(i);
if (!peek.has_value())
break;
if (peek->text_type == Gfx::GlyphRun::TextType::Ltr || peek->text_type == Gfx::GlyphRun::TextType::Rtl) {
next_known_direction = peek->text_type;
break;
}
}

auto last_known_direction = m_text_node_context->last_known_direction;
if (last_known_direction.has_value() && next_known_direction.has_value() && *last_known_direction != *next_known_direction) {
switch (m_containing_block->computed_values().direction()) {
case CSS::Direction::Ltr:
return Gfx::GlyphRun::TextType::Ltr;
case CSS::Direction::Rtl:
return Gfx::GlyphRun::TextType::Rtl;
}
}

if (last_known_direction.has_value())
return *last_known_direction;
if (next_known_direction.has_value())
return *next_known_direction;

return Gfx::GlyphRun::TextType::ContextDependent;
}

Optional<InlineLevelIterator::Item> InlineLevelIterator::next_without_lookahead()
{
if (!m_current_node)
Expand All @@ -176,18 +209,29 @@ Optional<InlineLevelIterator::Item> InlineLevelIterator::next_without_lookahead(
if (!m_text_node_context.has_value())
enter_text_node(text_node);

auto chunk_opt = m_text_node_context->next_chunk;
auto chunk_opt = m_text_node_context->chunk_iterator.next();
if (!chunk_opt.has_value()) {
m_text_node_context = {};
skip_to_next();
return next_without_lookahead();
}

m_text_node_context->next_chunk = m_text_node_context->chunk_iterator.next();
if (!m_text_node_context->next_chunk.has_value())
if (!m_text_node_context->chunk_iterator.peek(0).has_value())
m_text_node_context->is_last_chunk = true;

auto& chunk = chunk_opt.value();
auto text_type = chunk.text_type;
if (text_type == Gfx::GlyphRun::TextType::Ltr || text_type == Gfx::GlyphRun::TextType::Rtl)
m_text_node_context->last_known_direction = text_type;

if (m_text_node_context->do_respect_linebreaks && chunk.has_breaking_newline) {
m_text_node_context->is_last_chunk = true;
if (chunk.is_all_whitespace)
text_type = Gfx::GlyphRun::TextType::EndPadding;
}

if (text_type == Gfx::GlyphRun::TextType::ContextDependent)
text_type = resolve_text_direction_from_context();

if (m_text_node_context->do_respect_linebreaks && chunk.has_breaking_newline) {
return Item {
Expand Down Expand Up @@ -215,7 +259,7 @@ Optional<InlineLevelIterator::Item> InlineLevelIterator::next_without_lookahead(
Item item {
.type = Item::Type::Text,
.node = &text_node,
.glyph_run = adopt_ref(*new Gfx::GlyphRun(move(glyph_run), chunk.font)),
.glyph_run = adopt_ref(*new Gfx::GlyphRun(move(glyph_run), chunk.font, text_type)),
.offset_in_node = chunk.start,
.length_in_node = chunk.length,
.width = chunk_width,
Expand Down Expand Up @@ -326,7 +370,6 @@ void InlineLevelIterator::enter_text_node(Layout::TextNode const& text_node)
.is_last_chunk = false,
.chunk_iterator = TextNode::ChunkIterator { text_node.text_for_rendering(), do_wrap_lines, do_respect_linebreaks, text_node.computed_values().font_list() },
};
m_text_node_context->next_chunk = m_text_node_context->chunk_iterator.next();
}

void InlineLevelIterator::add_extra_box_model_metrics_to_item(Item& item, bool add_leading_metrics, bool add_trailing_metrics)
Expand Down
3 changes: 2 additions & 1 deletion Userland/Libraries/LibWeb/Layout/InlineLevelIterator.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class InlineLevelIterator {

private:
Optional<Item> next_without_lookahead();
Gfx::GlyphRun::TextType resolve_text_direction_from_context();
void skip_to_next();
void compute_next();

Expand Down Expand Up @@ -84,7 +85,7 @@ class InlineLevelIterator {
bool is_first_chunk {};
bool is_last_chunk {};
TextNode::ChunkIterator chunk_iterator;
Optional<TextNode::Chunk> next_chunk {};
Optional<Gfx::GlyphRun::TextType> last_known_direction {};
};

Optional<TextNodeContext> m_text_node_context;
Expand Down
9 changes: 2 additions & 7 deletions Userland/Libraries/LibWeb/Layout/LineBox.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,14 @@ void LineBox::add_fragment(Node const& layout_node, int start, int length, CSSPi
{
bool text_align_is_justify = layout_node.computed_values().text_align() == CSS::TextAlign::Justify;
if (glyph_run && !text_align_is_justify && !m_fragments.is_empty() && &m_fragments.last().layout_node() == &layout_node && &m_fragments.last().m_glyph_run->font() == &glyph_run->font()) {
auto const fragment_width = m_fragments.last().width();
// The fragment we're adding is from the last Layout::Node on the line.
// Expand the last fragment instead of adding a new one with the same Layout::Node.
m_fragments.last().m_length = (start - m_fragments.last().m_start) + length;
m_fragments.last().set_width(m_fragments.last().width() + content_width);
for (auto& glyph : glyph_run->glyphs()) {
glyph.visit([&](auto& glyph) { glyph.position.translate_by(fragment_width.to_float(), 0); });
m_fragments.last().m_glyph_run->append(glyph);
}
m_fragments.last().append_glyph_run(glyph_run, content_width);
} else {
CSSPixels x_offset = leading_margin + leading_size + m_width;
CSSPixels y_offset = 0;
m_fragments.append(LineBoxFragment { layout_node, start, length, CSSPixelPoint(x_offset, y_offset), CSSPixelSize(content_width, content_height), border_box_top, move(glyph_run) });
m_fragments.append(LineBoxFragment { layout_node, start, length, CSSPixelPoint(x_offset, y_offset), CSSPixelSize(content_width, content_height), border_box_top, m_direction, move(glyph_run) });
}
m_width += leading_margin + leading_size + content_width + trailing_size + trailing_margin;
m_height = max(m_height, content_height + border_box_top + border_box_bottom);
Expand Down
Loading

0 comments on commit c72b9e1

Please sign in to comment.