diff --git a/Tests/LibWeb/Layout/expected/block-and-inline/float-left-and-right-with-justified-text-in-between.txt b/Tests/LibWeb/Layout/expected/block-and-inline/float-left-and-right-with-justified-text-in-between.txt
index ce4720b15ba9e2..9729b6b750e851 100644
--- a/Tests/LibWeb/Layout/expected/block-and-inline/float-left-and-right-with-justified-text-in-between.txt
+++ b/Tests/LibWeb/Layout/expected/block-and-inline/float-left-and-right-with-justified-text-in-between.txt
@@ -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
" "
diff --git a/Tests/LibWeb/Layout/expected/div_align.txt b/Tests/LibWeb/Layout/expected/div_align.txt
index 471d74c8605b6d..e5d9aa6c1f48e5 100644
--- a/Tests/LibWeb/Layout/expected/div_align.txt
+++ b/Tests/LibWeb/Layout/expected/div_align.txt
@@ -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
at (28,516) content-size 100x100 children: not-inline
BlockContainer <(anonymous)> at (8,636) content-size 784x0 children: inline
diff --git a/Tests/LibWeb/Screenshot/images/text-direction-ref.png b/Tests/LibWeb/Screenshot/images/text-direction-ref.png
new file mode 100644
index 00000000000000..c53f55b8c0af8d
Binary files /dev/null and b/Tests/LibWeb/Screenshot/images/text-direction-ref.png differ
diff --git a/Tests/LibWeb/Screenshot/reference/text-direction-ref.html b/Tests/LibWeb/Screenshot/reference/text-direction-ref.html
new file mode 100644
index 00000000000000..db173eac534cc1
--- /dev/null
+++ b/Tests/LibWeb/Screenshot/reference/text-direction-ref.html
@@ -0,0 +1,10 @@
+
+
diff --git a/Tests/LibWeb/Screenshot/text-direction.html b/Tests/LibWeb/Screenshot/text-direction.html
new file mode 100644
index 00000000000000..e7000d0c8cd90b
--- /dev/null
+++ b/Tests/LibWeb/Screenshot/text-direction.html
@@ -0,0 +1,18 @@
+
+
+hello test 1, 2, 3!
+hello test 1, 2, 3!
+hello test 1, 2, 3!
+hello test 1, 2, 3!
+
+אא aaa bbb ccc מממ
+אא aaa bbb ccc מממ
+
+אא 1 2 3 מממ
+אא 1 2 3 מממ
+
+aa....!!!
+aa....!!!
+
+حسنًا ، hello friends مرحباً أيها ا test لأصدقاء end
+حسنًا ، hello friends مرحباً أيها ا test لأصدقاء end
diff --git a/Userland/Libraries/LibGfx/TextLayout.h b/Userland/Libraries/LibGfx/TextLayout.h
index 4280c38dd3c716..d7e10354ab262d 100644
--- a/Userland/Libraries/LibGfx/TextLayout.h
+++ b/Userland/Libraries/LibGfx/TextLayout.h
@@ -100,13 +100,23 @@ using DrawGlyphOrEmoji = Variant;
class GlyphRun : public RefCounted {
public:
- GlyphRun(Vector&& glyphs, NonnullRefPtr font)
+ enum class TextType {
+ Common,
+ ContextDependent,
+ EndPadding,
+ Ltr,
+ Rtl,
+ };
+
+ GlyphRun(Vector&& glyphs, NonnullRefPtr 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 const& glyphs() const { return m_glyphs; }
[[nodiscard]] Vector& glyphs() { return m_glyphs; }
[[nodiscard]] bool is_empty() const { return m_glyphs.is_empty(); }
@@ -116,6 +126,7 @@ class GlyphRun : public RefCounted {
private:
Vector m_glyphs;
NonnullRefPtr m_font;
+ TextType m_text_type;
};
Variant prepare_draw_glyph_or_emoji(FloatPoint point, Utf8CodePointIterator& it, Font const& font);
diff --git a/Userland/Libraries/LibWeb/Layout/InlineFormattingContext.cpp b/Userland/Libraries/LibWeb/Layout/InlineFormattingContext.cpp
index 7ea39b66f51406..518d3ece64c810 100644
--- a/Userland/Libraries/LibWeb/Layout/InlineFormattingContext.cpp
+++ b/Userland/Libraries/LibWeb/Layout/InlineFormattingContext.cpp
@@ -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
diff --git a/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.cpp b/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.cpp
index 33818c5d8c18c4..c2c7eeb43572ff 100644
--- a/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.cpp
+++ b/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.cpp
@@ -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 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::next_without_lookahead()
{
if (!m_current_node)
@@ -176,18 +209,29 @@ Optional 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 {
@@ -215,7 +259,7 @@ Optional 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,
@@ -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)
diff --git a/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.h b/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.h
index 43fd444db33fb1..bc5ba0c0c81e4b 100644
--- a/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.h
+++ b/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.h
@@ -57,6 +57,7 @@ class InlineLevelIterator {
private:
Optional- next_without_lookahead();
+ Gfx::GlyphRun::TextType resolve_text_direction_from_context();
void skip_to_next();
void compute_next();
@@ -84,7 +85,7 @@ class InlineLevelIterator {
bool is_first_chunk {};
bool is_last_chunk {};
TextNode::ChunkIterator chunk_iterator;
- Optional next_chunk {};
+ Optional last_known_direction {};
};
Optional m_text_node_context;
diff --git a/Userland/Libraries/LibWeb/Layout/LineBox.cpp b/Userland/Libraries/LibWeb/Layout/LineBox.cpp
index 360efab2a35168..22e0d4cd642a5d 100644
--- a/Userland/Libraries/LibWeb/Layout/LineBox.cpp
+++ b/Userland/Libraries/LibWeb/Layout/LineBox.cpp
@@ -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);
diff --git a/Userland/Libraries/LibWeb/Layout/LineBox.h b/Userland/Libraries/LibWeb/Layout/LineBox.h
index bc4debde8ffb68..f46fc1b39cef19 100644
--- a/Userland/Libraries/LibWeb/Layout/LineBox.h
+++ b/Userland/Libraries/LibWeb/Layout/LineBox.h
@@ -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; }
@@ -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() };
diff --git a/Userland/Libraries/LibWeb/Layout/LineBoxFragment.cpp b/Userland/Libraries/LibWeb/Layout/LineBoxFragment.cpp
index c60e2919369fcc..52a1272054e2b2 100644
--- a/Userland/Libraries/LibWeb/Layout/LineBoxFragment.cpp
+++ b/Userland/Libraries/LibWeb/Layout/LineBoxFragment.cpp
@@ -12,6 +12,23 @@
namespace Web::Layout {
+LineBoxFragment::LineBoxFragment(Node const& layout_node, int start, int length, CSSPixelPoint offset, CSSPixelSize size, CSSPixels border_box_top, CSS::Direction direction, RefPtr glyph_run)
+ : m_layout_node(layout_node)
+ , m_start(start)
+ , m_length(length)
+ , m_offset(offset)
+ , m_size(size)
+ , m_border_box_top(border_box_top)
+ , m_direction(direction)
+ , m_glyph_run(move(glyph_run))
+{
+ if (m_glyph_run) {
+ m_current_insert_direction = resolve_glyph_run_direction(m_glyph_run->text_type());
+ if (m_direction == CSS::Direction::Rtl)
+ m_insert_position = m_size.width().to_float();
+ }
+}
+
bool LineBoxFragment::ends_in_whitespace() const
{
auto text = this->text();
@@ -45,4 +62,105 @@ bool LineBoxFragment::is_atomic_inline() const
return layout_node().is_replaced_box() || (layout_node().display().is_inline_outside() && !layout_node().display().is_flow_inside());
}
+CSS::Direction LineBoxFragment::resolve_glyph_run_direction(Gfx::GlyphRun::TextType text_type) const
+{
+ switch (text_type) {
+ case Gfx::GlyphRun::TextType::Common:
+ case Gfx::GlyphRun::TextType::ContextDependent:
+ case Gfx::GlyphRun::TextType::EndPadding:
+ return m_direction;
+ case Gfx::GlyphRun::TextType::Ltr:
+ return CSS::Direction::Ltr;
+ case Gfx::GlyphRun::TextType::Rtl:
+ return CSS::Direction::Rtl;
+ default:
+ VERIFY_NOT_REACHED();
+ }
+}
+
+void LineBoxFragment::append_glyph_run(RefPtr const& glyph_run, CSSPixels run_width)
+{
+ switch (m_direction) {
+ case CSS::Direction::Ltr:
+ append_glyph_run_ltr(glyph_run, run_width);
+ break;
+ case CSS::Direction::Rtl:
+ append_glyph_run_rtl(glyph_run, run_width);
+ break;
+ }
+}
+
+void LineBoxFragment::append_glyph_run_ltr(RefPtr const& glyph_run, CSSPixels run_width)
+{
+ auto run_direction = resolve_glyph_run_direction(glyph_run->text_type());
+
+ if (m_current_insert_direction != run_direction) {
+ if (run_direction == CSS::Direction::Rtl)
+ m_insert_position = width().to_float();
+ m_current_insert_direction = run_direction;
+ }
+
+ switch (run_direction) {
+ case CSS::Direction::Ltr:
+ for (auto& glyph : glyph_run->glyphs()) {
+ glyph.visit([&](auto& glyph) { glyph.position.translate_by(width().to_float(), 0); });
+ m_glyph_run->append(glyph);
+ }
+ break;
+ case CSS::Direction::Rtl:
+ for (auto& glyph : m_glyph_run->glyphs()) {
+ glyph.visit([&](auto& glyph) {
+ if (glyph.position.x() >= m_insert_position)
+ glyph.position.translate_by(run_width.to_float(), 0);
+ });
+ }
+ for (auto& glyph : glyph_run->glyphs()) {
+ glyph.visit([&](auto& glyph) { glyph.position.translate_by(m_insert_position, 0); });
+ m_glyph_run->append(glyph);
+ }
+ break;
+ }
+
+ m_size.set_width(width() + run_width);
+}
+
+void LineBoxFragment::append_glyph_run_rtl(RefPtr const& glyph_run, CSSPixels run_width)
+{
+ auto run_direction = resolve_glyph_run_direction(glyph_run->text_type());
+
+ if (m_current_insert_direction != run_direction) {
+ if (run_direction == CSS::Direction::Ltr)
+ m_insert_position = 0;
+ m_current_insert_direction = run_direction;
+ }
+
+ switch (run_direction) {
+ case CSS::Direction::Ltr:
+ for (auto& glyph : m_glyph_run->glyphs()) {
+ glyph.visit([&](auto& glyph) {
+ if (glyph.position.x() >= m_insert_position)
+ glyph.position.translate_by(run_width.to_float(), 0);
+ });
+ }
+ for (auto& glyph : glyph_run->glyphs()) {
+ glyph.visit([&](auto& glyph) { glyph.position.translate_by(m_insert_position, 0); });
+ m_glyph_run->append(glyph);
+ }
+ break;
+ case CSS::Direction::Rtl:
+ if (glyph_run->text_type() != Gfx::GlyphRun::TextType::EndPadding) {
+ for (auto& glyph : m_glyph_run->glyphs()) {
+ glyph.visit([&](auto& glyph) { glyph.position.translate_by(run_width.to_float(), 0); });
+ }
+ }
+ for (auto& glyph : glyph_run->glyphs()) {
+ m_glyph_run->append(glyph);
+ }
+ break;
+ }
+
+ m_size.set_width(width() + run_width);
+ m_insert_position += run_width.to_float();
+}
+
}
diff --git a/Userland/Libraries/LibWeb/Layout/LineBoxFragment.h b/Userland/Libraries/LibWeb/Layout/LineBoxFragment.h
index b5fb84a2aeb2ce..4c343c17850237 100644
--- a/Userland/Libraries/LibWeb/Layout/LineBoxFragment.h
+++ b/Userland/Libraries/LibWeb/Layout/LineBoxFragment.h
@@ -19,16 +19,7 @@ class LineBoxFragment {
friend class LineBox;
public:
- LineBoxFragment(Node const& layout_node, int start, int length, CSSPixelPoint offset, CSSPixelSize size, CSSPixels border_box_top, RefPtr glyph_run)
- : m_layout_node(layout_node)
- , m_start(start)
- , m_length(length)
- , m_offset(offset)
- , m_size(size)
- , m_border_box_top(border_box_top)
- , m_glyph_run(move(glyph_run))
- {
- }
+ LineBoxFragment(Node const& layout_node, int start, int length, CSSPixelPoint offset, CSSPixelSize size, CSSPixels border_box_top, CSS::Direction, RefPtr);
Node const& layout_node() const { return m_layout_node; }
int start() const { return m_start; }
@@ -60,8 +51,13 @@ class LineBoxFragment {
bool is_atomic_inline() const;
RefPtr glyph_run() const { return m_glyph_run; }
+ void append_glyph_run(RefPtr const&, CSSPixels run_width);
private:
+ CSS::Direction resolve_glyph_run_direction(Gfx::GlyphRun::TextType) const;
+ void append_glyph_run_ltr(RefPtr const&, CSSPixels run_width);
+ void append_glyph_run_rtl(RefPtr const&, CSSPixels run_width);
+
JS::NonnullGCPtr m_layout_node;
int m_start { 0 };
int m_length { 0 };
@@ -69,7 +65,11 @@ class LineBoxFragment {
CSSPixelSize m_size;
CSSPixels m_border_box_top { 0 };
CSSPixels m_baseline { 0 };
+ CSS::Direction m_direction { CSS::Direction::Ltr };
+
RefPtr m_glyph_run;
+ float m_insert_position { 0 };
+ CSS::Direction m_current_insert_direction { CSS::Direction::Ltr };
};
}
diff --git a/Userland/Libraries/LibWeb/Layout/LineBuilder.cpp b/Userland/Libraries/LibWeb/Layout/LineBuilder.cpp
index 35bfc39f30f46d..9f8531d596e48d 100644
--- a/Userland/Libraries/LibWeb/Layout/LineBuilder.cpp
+++ b/Userland/Libraries/LibWeb/Layout/LineBuilder.cpp
@@ -10,10 +10,11 @@
namespace Web::Layout {
-LineBuilder::LineBuilder(InlineFormattingContext& context, LayoutState& layout_state, LayoutState::UsedValues& containing_block_used_values)
+LineBuilder::LineBuilder(InlineFormattingContext& context, LayoutState& layout_state, LayoutState::UsedValues& containing_block_used_values, CSS::Direction direction)
: m_context(context)
, m_layout_state(layout_state)
, m_containing_block_used_values(containing_block_used_values)
+ , m_direction(direction)
{
m_text_indent = m_context.containing_block().computed_values().text_indent().to_px(m_context.containing_block(), m_containing_block_used_values.content_width());
begin_new_line(false);
@@ -35,7 +36,7 @@ void LineBuilder::break_line(ForcedBreak forced_break, Optional next_
size_t break_count = 0;
bool floats_intrude_at_current_y = false;
do {
- m_containing_block_used_values.line_boxes.append(LineBox());
+ m_containing_block_used_values.line_boxes.append(LineBox(m_direction));
begin_new_line(true, break_count == 0);
break_count++;
floats_intrude_at_current_y = m_context.any_floats_intrude_at_y(m_current_y);
@@ -80,7 +81,7 @@ LineBox& LineBuilder::ensure_last_line_box()
{
auto& line_boxes = m_containing_block_used_values.line_boxes;
if (line_boxes.is_empty())
- line_boxes.append(LineBox {});
+ line_boxes.append(LineBox(m_direction));
return line_boxes.last();
}
diff --git a/Userland/Libraries/LibWeb/Layout/LineBuilder.h b/Userland/Libraries/LibWeb/Layout/LineBuilder.h
index 7b3f6ff8aeee0f..178c45bac1494a 100644
--- a/Userland/Libraries/LibWeb/Layout/LineBuilder.h
+++ b/Userland/Libraries/LibWeb/Layout/LineBuilder.h
@@ -15,7 +15,7 @@ class LineBuilder {
AK_MAKE_NONMOVABLE(LineBuilder);
public:
- LineBuilder(InlineFormattingContext&, LayoutState&, LayoutState::UsedValues& containing_block_used_values);
+ LineBuilder(InlineFormattingContext&, LayoutState&, LayoutState::UsedValues& containing_block_used_values, CSS::Direction);
~LineBuilder();
enum class ForcedBreak {
@@ -63,6 +63,7 @@ class LineBuilder {
CSSPixels m_current_y { 0 };
CSSPixels m_max_height_on_current_line { 0 };
CSSPixels m_text_indent { 0 };
+ CSS::Direction m_direction { CSS::Direction::Ltr };
bool m_last_line_needs_update { false };
};
diff --git a/Userland/Libraries/LibWeb/Layout/TextNode.cpp b/Userland/Libraries/LibWeb/Layout/TextNode.cpp
index 09912ce8022b12..329d50219ef40a 100644
--- a/Userland/Libraries/LibWeb/Layout/TextNode.cpp
+++ b/Userland/Libraries/LibWeb/Layout/TextNode.cpp
@@ -400,7 +400,67 @@ TextNode::ChunkIterator::ChunkIterator(StringView text, bool wrap_lines, bool re
{
}
+static Gfx::GlyphRun::TextType text_type_for_code_point(u32 code_point)
+{
+ switch (Unicode::bidirectional_class(code_point)) {
+ case Unicode::BidiClass::WhiteSpaceNeutral:
+
+ case Unicode::BidiClass::BlockSeparator:
+ case Unicode::BidiClass::SegmentSeparator:
+ case Unicode::BidiClass::CommonNumberSeparator:
+ case Unicode::BidiClass::DirNonSpacingMark:
+
+ case Unicode::BidiClass::ArabicNumber:
+ case Unicode::BidiClass::EuropeanNumber:
+ case Unicode::BidiClass::EuropeanNumberSeparator:
+ case Unicode::BidiClass::EuropeanNumberTerminator:
+ return Gfx::GlyphRun::TextType::ContextDependent;
+
+ case Unicode::BidiClass::BoundaryNeutral:
+ case Unicode::BidiClass::OtherNeutral:
+ case Unicode::BidiClass::FirstStrongIsolate:
+ case Unicode::BidiClass::PopDirectionalFormat:
+ case Unicode::BidiClass::PopDirectionalIsolate:
+ return Gfx::GlyphRun::TextType::Common;
+
+ case Unicode::BidiClass::LeftToRight:
+ case Unicode::BidiClass::LeftToRightEmbedding:
+ case Unicode::BidiClass::LeftToRightIsolate:
+ case Unicode::BidiClass::LeftToRightOverride:
+ return Gfx::GlyphRun::TextType::Ltr;
+
+ case Unicode::BidiClass::RightToLeft:
+ case Unicode::BidiClass::RightToLeftArabic:
+ case Unicode::BidiClass::RightToLeftEmbedding:
+ case Unicode::BidiClass::RightToLeftIsolate:
+ case Unicode::BidiClass::RightToLeftOverride:
+ return Gfx::GlyphRun::TextType::Rtl;
+
+ default:
+ VERIFY_NOT_REACHED();
+ }
+}
+
Optional TextNode::ChunkIterator::next()
+{
+ if (!m_peek_queue.is_empty())
+ return m_peek_queue.take_first();
+ return next_without_peek();
+}
+
+Optional TextNode::ChunkIterator::peek(size_t count)
+{
+ while (m_peek_queue.size() <= count) {
+ auto next = next_without_peek();
+ if (!next.has_value())
+ return {};
+ m_peek_queue.append(*next);
+ }
+
+ return m_peek_queue[count];
+}
+
+Optional TextNode::ChunkIterator::next_without_peek()
{
if (m_iterator == m_utf8_view.end())
return {};
@@ -408,35 +468,41 @@ Optional TextNode::ChunkIterator::next()
auto start_of_chunk = m_iterator;
Gfx::Font const& font = m_font_cascade_list.font_for_code_point(*m_iterator);
+ auto text_type = text_type_for_code_point(*m_iterator);
while (m_iterator != m_utf8_view.end()) {
if (&font != &m_font_cascade_list.font_for_code_point(*m_iterator)) {
- if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font); result.has_value())
+ if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font, text_type); result.has_value())
return result.release_value();
}
if (m_respect_linebreaks && *m_iterator == '\n') {
// Newline encountered, and we're supposed to preserve them.
// If we have accumulated some code points in the current chunk, commit them now and continue with the newline next time.
- if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font); result.has_value())
+ if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font, text_type); result.has_value())
return result.release_value();
// Otherwise, commit the newline!
++m_iterator;
- auto result = try_commit_chunk(start_of_chunk, m_iterator, true, font);
+ auto result = try_commit_chunk(start_of_chunk, m_iterator, true, font, text_type);
VERIFY(result.has_value());
return result.release_value();
}
if (m_wrap_lines) {
+ if (text_type != text_type_for_code_point(*m_iterator)) {
+ if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font, text_type); result.has_value())
+ return result.release_value();
+ }
+
if (is_ascii_space(*m_iterator)) {
// Whitespace encountered, and we're allowed to break on whitespace.
// If we have accumulated some code points in the current chunk, commit them now and continue with the whitespace next time.
- if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font); result.has_value())
+ if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font, text_type); result.has_value())
return result.release_value();
// Otherwise, commit the whitespace!
++m_iterator;
- if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font); result.has_value())
+ if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false, font, text_type); result.has_value())
return result.release_value();
continue;
}
@@ -447,14 +513,14 @@ Optional TextNode::ChunkIterator::next()
if (start_of_chunk != m_utf8_view.end()) {
// Try to output whatever's left at the end of the text node.
- if (auto result = try_commit_chunk(start_of_chunk, m_utf8_view.end(), false, font); result.has_value())
+ if (auto result = try_commit_chunk(start_of_chunk, m_utf8_view.end(), false, font, text_type); result.has_value())
return result.release_value();
}
return {};
}
-Optional TextNode::ChunkIterator::try_commit_chunk(Utf8View::Iterator const& start, Utf8View::Iterator const& end, bool has_breaking_newline, Gfx::Font const& font) const
+Optional TextNode::ChunkIterator::try_commit_chunk(Utf8View::Iterator const& start, Utf8View::Iterator const& end, bool has_breaking_newline, Gfx::Font const& font, Gfx::GlyphRun::TextType text_type) const
{
auto byte_offset = m_utf8_view.byte_offset_of(start);
auto byte_length = m_utf8_view.byte_offset_of(end) - byte_offset;
@@ -468,6 +534,7 @@ Optional TextNode::ChunkIterator::try_commit_chunk(Utf8View::It
.length = byte_length,
.has_breaking_newline = has_breaking_newline,
.is_all_whitespace = is_all_whitespace(chunk_view.as_string()),
+ .text_type = text_type,
};
}
diff --git a/Userland/Libraries/LibWeb/Layout/TextNode.h b/Userland/Libraries/LibWeb/Layout/TextNode.h
index 72c3b09033e534..c6f3d691cfb87d 100644
--- a/Userland/Libraries/LibWeb/Layout/TextNode.h
+++ b/Userland/Libraries/LibWeb/Layout/TextNode.h
@@ -33,21 +33,26 @@ class TextNode final : public Node {
size_t length { 0 };
bool has_breaking_newline { false };
bool is_all_whitespace { false };
+ Gfx::GlyphRun::TextType text_type;
};
class ChunkIterator {
public:
ChunkIterator(StringView text, bool wrap_lines, bool respect_linebreaks, Gfx::FontCascadeList const&);
Optional next();
+ Optional peek(size_t);
private:
- Optional try_commit_chunk(Utf8View::Iterator const& start, Utf8View::Iterator const& end, bool has_breaking_newline, Gfx::Font const&) const;
+ Optional next_without_peek();
+ Optional try_commit_chunk(Utf8View::Iterator const& start, Utf8View::Iterator const& end, bool has_breaking_newline, Gfx::Font const&, Gfx::GlyphRun::TextType) const;
bool const m_wrap_lines;
bool const m_respect_linebreaks;
Utf8View m_utf8_view;
Utf8View::Iterator m_iterator;
Gfx::FontCascadeList const& m_font_cascade_list;
+
+ Vector m_peek_queue;
};
void invalidate_text_for_rendering();
diff --git a/Userland/Libraries/LibWeb/Painting/DisplayListRecorder.cpp b/Userland/Libraries/LibWeb/Painting/DisplayListRecorder.cpp
index 94abe18f2f7966..95ec0377be2222 100644
--- a/Userland/Libraries/LibWeb/Painting/DisplayListRecorder.cpp
+++ b/Userland/Libraries/LibWeb/Painting/DisplayListRecorder.cpp
@@ -233,7 +233,7 @@ void DisplayListRecorder::draw_text(Gfx::IntRect const& rect, String raw_text, G
if (rect.is_empty())
return;
- auto glyph_run = adopt_ref(*new Gfx::GlyphRun({}, font));
+ auto glyph_run = adopt_ref(*new Gfx::GlyphRun({}, font, Gfx::GlyphRun::TextType::Ltr));
float glyph_run_width = 0;
Gfx::for_each_glyph_position(
{ 0, 0 }, raw_text.code_points(), font, [&](Gfx::DrawGlyphOrEmoji const& glyph_or_emoji) {