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) {