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 4781bb5
Show file tree
Hide file tree
Showing 18 changed files with 354 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -79,29 +79,31 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
"Integer"
frag 38 from TextNode start: 160, length: 6, rect: [554,186 70.3125x22] baseline: 17
"rutrum"
frag 39 from TextNode start: 166, length: 1, rect: [624,186 21x22] baseline: 17
frag 45 from TextNode start: 166, length: 1, rect: [624,186 21x22] baseline: 17
" "
frag 40 from TextNode start: 167, length: 4, rect: [645,186 35.09375x22] baseline: 17
"nisi"
frag 41 from TextNode start: 171, length: 1, rect: [680,186 21x22] baseline: 17
frag 47 from TextNode start: 171, length: 1, rect: [680,186 21x22] baseline: 17
" "
frag 42 from TextNode start: 172, length: 4, rect: [701,186 39.828125x22] baseline: 17
"eget"
frag 43 from TextNode start: 176, length: 1, rect: [741,186 21x22] baseline: 17
frag 49 from TextNode start: 176, length: 1, rect: [741,186 21x22] baseline: 17
" "
frag 44 from TextNode start: 177, length: 3, rect: [762,186 27.734375x22] baseline: 17
frag 50 from TextNode start: 177, length: 3, rect: [762,186 27.734375x22] baseline: 17
"dui"
frag 45 from TextNode start: 181, length: 7, rect: [252,212 68.984375x22] baseline: 17
"dictum,"
frag 46 from TextNode start: 188, length: 1, rect: [321,212 23.578125x22] baseline: 17
frag 51 from TextNode start: 181, length: 6, rect: [252,212 62.671875x22] baseline: 17
"dictum"
frag 52 from TextNode start: 187, length: 1, rect: [315,212 6.3125x22] baseline: 17
","
frag 53 from TextNode start: 188, length: 1, rect: [321,212 23.578125x22] baseline: 17
" "
frag 47 from TextNode start: 189, length: 2, rect: [344.578125,212 23.109375x22] baseline: 17
frag 54 from TextNode start: 189, length: 2, rect: [344.578125,212 23.109375x22] baseline: 17
"eu"
frag 48 from TextNode start: 191, length: 1, rect: [367.578125,212 23.578125x22] baseline: 17
frag 55 from TextNode start: 191, length: 1, rect: [367.578125,212 23.578125x22] baseline: 17
" "
frag 49 from TextNode start: 192, length: 8, rect: [391.15625,212 96.734375x22] baseline: 17
"accumsan"
frag 50 from TextNode start: 201, length: 4, rect: [252,234 43.875x22] baseline: 17
frag 57 from TextNode start: 201, length: 4, rect: [252,234 43.875x22] baseline: 17
"enim"
frag 51 from TextNode start: 205, length: 1, rect: [296,234 37.90625x22] baseline: 17
" "
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
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
6 changes: 5 additions & 1 deletion Userland/Libraries/LibWeb/Layout/LineBox.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ namespace Web::Layout {

class LineBox {
public:
LineBox() = default;
LineBox(CSS::Direction direction)
: m_direction(direction)
{
}

CSSPixels width() const { return m_width; }
CSSPixels height() const { return m_height; }
Expand Down Expand Up @@ -42,6 +45,7 @@ class LineBox {
CSSPixels m_height { 0 };
CSSPixels m_bottom { 0 };
CSSPixels m_baseline { 0 };
CSS::Direction m_direction { CSS::Direction::Ltr };

// The amount of available width that was originally available when creating this line box. Used for text justification.
AvailableSize m_original_available_width { AvailableSize::make_indefinite() };
Expand Down
Loading

0 comments on commit 4781bb5

Please sign in to comment.