diff --git a/Tests/LibWeb/Layout/expected/tab-size-chars-should-vertically-align.txt b/Tests/LibWeb/Layout/expected/tab-size-chars-should-vertically-align.txt new file mode 100644 index 00000000000..8bf286f3bf6 --- /dev/null +++ b/Tests/LibWeb/Layout/expected/tab-size-chars-should-vertically-align.txt @@ -0,0 +1,52 @@ +Viewport <#document> at (0,0) content-size 800x600 children: not-inline + BlockContainer at (0,0) content-size 800x84 [BFC] children: not-inline + BlockContainer at (8,8) content-size 784x68 children: not-inline + BlockContainer
at (8,8) content-size 784x17 children: inline + frag 0 from BlockContainer start: 0, length: 0, rect: [8,21 60x0] baseline: 0 + frag 1 from TextNode start: 0, length: 2, rect: [68,8 82.265625x17] baseline: 13.296875 + " A" + BlockContainer at (8,21) content-size 60x0 inline-block [BFC] children: not-inline + TextNode <#text> + BlockContainer <(anonymous)> at (8,25) content-size 784x0 children: inline + TextNode <#text> + BlockContainer
at (8,25) content-size 784x17 children: inline + frag 0 from BlockContainer start: 0, length: 0, rect: [8,38 70x0] baseline: 0 + frag 1 from TextNode start: 0, length: 2, rect: [78,25 72.265625x17] baseline: 13.296875 + " A" + BlockContainer at (8,38) content-size 70x0 inline-block [BFC] children: not-inline + TextNode <#text> + BlockContainer <(anonymous)> at (8,42) content-size 784x0 children: inline + TextNode <#text> + BlockContainer
at (8,42) content-size 784x17 children: inline + frag 0 from BlockContainer start: 0, length: 0, rect: [8,55 73x0] baseline: 0 + frag 1 from TextNode start: 0, length: 2, rect: [81,42 69.265625x17] baseline: 13.296875 + " A" + BlockContainer at (8,55) content-size 73x0 inline-block [BFC] children: not-inline + TextNode <#text> + BlockContainer <(anonymous)> at (8,59) content-size 784x0 children: inline + TextNode <#text> + BlockContainer
at (8,59) content-size 784x17 children: inline + frag 0 from TextNode start: 0, length: 9, rect: [8,59 78.265625x17] baseline: 13.296875 + " A" + TextNode <#text> + BlockContainer <(anonymous)> at (8,76) content-size 784x0 children: inline + TextNode <#text> + +ViewportPaintable (Viewport<#document>) [0,0 800x600] + PaintableWithLines (BlockContainer) [0,0 800x84] + PaintableWithLines (BlockContainer) [8,8 784x68] + PaintableWithLines (BlockContainer
) [8,8 784x17] + PaintableWithLines (BlockContainer#s1) [8,21 60x0] + TextPaintable (TextNode<#text>) + PaintableWithLines (BlockContainer(anonymous)) [8,25 784x0] + PaintableWithLines (BlockContainer
) [8,25 784x17] + PaintableWithLines (BlockContainer#s2) [8,38 70x0] + TextPaintable (TextNode<#text>) + PaintableWithLines (BlockContainer(anonymous)) [8,42 784x0] + PaintableWithLines (BlockContainer
) [8,42 784x17] + PaintableWithLines (BlockContainer#s3) [8,55 73x0] + TextPaintable (TextNode<#text>) + PaintableWithLines (BlockContainer(anonymous)) [8,59 784x0] + PaintableWithLines (BlockContainer
) [8,59 784x17] + TextPaintable (TextNode<#text>) + PaintableWithLines (BlockContainer(anonymous)) [8,76 784x0] diff --git a/Tests/LibWeb/Layout/expected/tab-size-text-wrap.txt b/Tests/LibWeb/Layout/expected/tab-size-text-wrap.txt new file mode 100644 index 00000000000..1520932dd7c --- /dev/null +++ b/Tests/LibWeb/Layout/expected/tab-size-text-wrap.txt @@ -0,0 +1,33 @@ +Viewport <#document> at (0,0) content-size 800x600 children: not-inline + BlockContainer at (0,0) content-size 800x67 [BFC] children: not-inline + BlockContainer at (8,8) content-size 784x51 children: not-inline + BlockContainer
at (8,8) content-size 100x51 children: inline + frag 0 from BlockContainer start: 0, length: 0, rect: [8,8 114.265625x17] baseline: 13.296875 + frag 1 from BlockContainer start: 0, length: 0, rect: [8,25 123.609375x17] baseline: 13.296875 + frag 2 from BlockContainer start: 0, length: 0, rect: [8,42 133.921875x17] baseline: 13.296875 + BlockContainer at (8,8) content-size 114.265625x17 inline-block [BFC] children: inline + frag 0 from TextNode start: 0, length: 2, rect: [8,8 114.265625x17] baseline: 13.296875 + " A" + TextNode <#text> + BlockContainer at (8,25) content-size 123.609375x17 inline-block [BFC] children: inline + frag 0 from TextNode start: 0, length: 3, rect: [8,25 123.609375x17] baseline: 13.296875 + " AB" + TextNode <#text> + BlockContainer at (8,42) content-size 133.921875x17 inline-block [BFC] children: inline + frag 0 from TextNode start: 0, length: 4, rect: [8,42 133.921875x17] baseline: 13.296875 + " ABC" + TextNode <#text> + BlockContainer <(anonymous)> at (8,59) content-size 784x0 children: inline + TextNode <#text> + +ViewportPaintable (Viewport<#document>) [0,0 800x600] + PaintableWithLines (BlockContainer) [0,0 800x67] + PaintableWithLines (BlockContainer) [8,8 784x51] + PaintableWithLines (BlockContainer
) [8,8 100x51] overflow: [8,8 133.921875x51] + PaintableWithLines (BlockContainer) [8,8 114.265625x17] + TextPaintable (TextNode<#text>) + PaintableWithLines (BlockContainer) [8,25 123.609375x17] + TextPaintable (TextNode<#text>) + PaintableWithLines (BlockContainer) [8,42 133.921875x17] + TextPaintable (TextNode<#text>) + PaintableWithLines (BlockContainer(anonymous)) [8,59 784x0] diff --git a/Tests/LibWeb/Layout/input/tab-size-chars-should-vertically-align.html b/Tests/LibWeb/Layout/input/tab-size-chars-should-vertically-align.html new file mode 100644 index 00000000000..5d9dfdb03b0 --- /dev/null +++ b/Tests/LibWeb/Layout/input/tab-size-chars-should-vertically-align.html @@ -0,0 +1,26 @@ + + + +
A
+
A
+
A
+
A
diff --git a/Tests/LibWeb/Layout/input/tab-size-text-wrap.html b/Tests/LibWeb/Layout/input/tab-size-text-wrap.html new file mode 100644 index 00000000000..80867babb7e --- /dev/null +++ b/Tests/LibWeb/Layout/input/tab-size-text-wrap.html @@ -0,0 +1,16 @@ + + + +
A AB ABC
diff --git a/Userland/Libraries/LibGfx/TextLayout.cpp b/Userland/Libraries/LibGfx/TextLayout.cpp index db190a15a60..f3f4a4d70ea 100644 --- a/Userland/Libraries/LibGfx/TextLayout.cpp +++ b/Userland/Libraries/LibGfx/TextLayout.cpp @@ -32,6 +32,9 @@ RefPtr shape_text(FloatPoint baseline_start, Utf8View string, Gfx::Fon Vector glyph_run; FloatPoint point = baseline_start; for (size_t i = 0; i < glyph_count; ++i) { + if (input_glyph_info[i].codepoint == '\t') + continue; + auto position = point - FloatPoint { 0, font.pixel_metrics().ascent } + FloatPoint { positions[i].x_offset, positions[i].y_offset } / text_shaping_resolution; diff --git a/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.cpp b/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.cpp index 28c2d5d6af6..d950452bf0a 100644 --- a/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.cpp +++ b/Userland/Libraries/LibWeb/Layout/InlineLevelIterator.cpp @@ -241,7 +241,55 @@ Optional InlineLevelIterator::next_without_lookahead( }; } - auto glyph_run = Gfx::shape_text({ 0, 0 }, chunk.view, chunk.font, text_type); + auto x = 0.0f; + if (chunk.has_breaking_tab) { + CSSPixels accumulated_width; + + // make sure to account for any fragments that take up a portion of the measured tab stop distance + auto fragments = m_containing_block_used_values.line_boxes.last().fragments(); + for (auto const& frag : fragments) { + accumulated_width += frag.width(); + } + + // https://drafts.csswg.org/css-text/#tab-size-property + auto tab_size = text_node.computed_values().tab_size(); + CSSPixels tab_width; + tab_width = tab_size.visit( + [&](CSS::LengthOrCalculated const& t) -> CSSPixels { + auto resolution_context = CSS::Length::ResolutionContext::for_layout_node(text_node); + auto value = t.resolved(resolution_context); + + return value.to_px(text_node); + }, + [&](CSS::NumberOrCalculated const& n) -> CSSPixels { + auto number = n.resolved(text_node); + + return CSSPixels::nearest_value_for(number * chunk.font->glyph_width(' ')); + }); + + // https://drafts.csswg.org/css-text/#white-space-phase-2 + // if fragments have added to the width, calculate the net distance to the next tab stop, otherwise the shift will just be the tab width + auto tab_stop_dist = accumulated_width > 0 ? (ceil((accumulated_width / tab_width)) * tab_width) - accumulated_width : tab_width; + auto ch_width = chunk.font->glyph_width('0'); + + // If this distance is less than 0.5ch, then the subsequent tab stop is used instead + if (tab_stop_dist < ch_width * 0.5) + tab_stop_dist += tab_width; + + // account for consecutive tabs + auto num_of_tabs = 0; + for (auto code_point : chunk.view) { + if (code_point != '\t') + break; + num_of_tabs++; + } + tab_stop_dist = tab_stop_dist * num_of_tabs; + + x = tab_stop_dist.to_float(); + } + + auto glyph_run = Gfx::shape_text({ x, 0 }, chunk.view, chunk.font, text_type); + CSSPixels chunk_width = CSSPixels::nearest_value_for(glyph_run->width()); // NOTE: We never consider `content: ""` to be collapsible whitespace. diff --git a/Userland/Libraries/LibWeb/Layout/TextNode.cpp b/Userland/Libraries/LibWeb/Layout/TextNode.cpp index 6030a81ce51..2325db0d577 100644 --- a/Userland/Libraries/LibWeb/Layout/TextNode.cpp +++ b/Userland/Libraries/LibWeb/Layout/TextNode.cpp @@ -489,30 +489,43 @@ Optional TextNode::ChunkIterator::next_without_peek() Gfx::Font const& font = m_font_cascade_list.font_for_code_point(code_point); auto text_type = text_type_for_code_point(code_point); + auto broken_on_tab = false; + while (m_current_index < m_utf8_view.byte_length()) { code_point = current_code_point(); + if (code_point == '\t') { + if (auto result = try_commit_chunk(start_of_chunk, m_current_index, false, broken_on_tab, font, text_type); result.has_value()) + return result.release_value(); + + broken_on_tab = true; + // consume any consecutive tabs + while (m_current_index < m_utf8_view.byte_length() && current_code_point() == '\t') { + m_current_index = next_grapheme_boundary(); + } + } + if (&font != &m_font_cascade_list.font_for_code_point(code_point)) { - if (auto result = try_commit_chunk(start_of_chunk, m_current_index, false, font, text_type); result.has_value()) + if (auto result = try_commit_chunk(start_of_chunk, m_current_index, false, broken_on_tab, font, text_type); result.has_value()) return result.release_value(); } if (m_respect_linebreaks && code_point == '\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_current_index, false, font, text_type); result.has_value()) + if (auto result = try_commit_chunk(start_of_chunk, m_current_index, false, broken_on_tab, font, text_type); result.has_value()) return result.release_value(); // Otherwise, commit the newline! m_current_index = next_grapheme_boundary(); - auto result = try_commit_chunk(start_of_chunk, m_current_index, true, font, text_type); + auto result = try_commit_chunk(start_of_chunk, m_current_index, true, broken_on_tab, font, text_type); VERIFY(result.has_value()); return result.release_value(); } if (m_wrap_lines) { if (text_type != text_type_for_code_point(code_point)) { - if (auto result = try_commit_chunk(start_of_chunk, m_current_index, false, font, text_type); result.has_value()) { + if (auto result = try_commit_chunk(start_of_chunk, m_current_index, false, broken_on_tab, font, text_type); result.has_value()) { return result.release_value(); } } @@ -520,31 +533,32 @@ Optional TextNode::ChunkIterator::next_without_peek() if (is_ascii_space(code_point)) { // 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_current_index, false, font, text_type); result.has_value()) { + if (auto result = try_commit_chunk(start_of_chunk, m_current_index, false, broken_on_tab, font, text_type); result.has_value()) { return result.release_value(); } // Otherwise, commit the whitespace! m_current_index = next_grapheme_boundary(); - if (auto result = try_commit_chunk(start_of_chunk, m_current_index, false, font, text_type); result.has_value()) + if (auto result = try_commit_chunk(start_of_chunk, m_current_index, false, broken_on_tab, font, text_type); result.has_value()) return result.release_value(); continue; } } - m_current_index = next_grapheme_boundary(); + m_current_index + = next_grapheme_boundary(); } if (start_of_chunk != m_utf8_view.byte_length()) { // 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.byte_length(), false, font, text_type); result.has_value()) + if (auto result = try_commit_chunk(start_of_chunk, m_utf8_view.byte_length(), false, broken_on_tab, font, text_type); result.has_value()) return result.release_value(); } return {}; } -Optional TextNode::ChunkIterator::try_commit_chunk(size_t start, size_t end, bool has_breaking_newline, Gfx::Font const& font, Gfx::GlyphRun::TextType text_type) const +Optional TextNode::ChunkIterator::try_commit_chunk(size_t start, size_t end, bool has_breaking_newline, bool has_breaking_tab, Gfx::Font const& font, Gfx::GlyphRun::TextType text_type) const { if (auto byte_length = end - start; byte_length > 0) { auto chunk_view = m_utf8_view.substring_view(start, byte_length); @@ -554,6 +568,7 @@ Optional TextNode::ChunkIterator::try_commit_chunk(size_t start .start = start, .length = byte_length, .has_breaking_newline = has_breaking_newline, + .has_breaking_tab = has_breaking_tab, .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 6c34587ced4..0b29167b16b 100644 --- a/Userland/Libraries/LibWeb/Layout/TextNode.h +++ b/Userland/Libraries/LibWeb/Layout/TextNode.h @@ -33,6 +33,7 @@ public: size_t start { 0 }; size_t length { 0 }; bool has_breaking_newline { false }; + bool has_breaking_tab { false }; bool is_all_whitespace { false }; Gfx::GlyphRun::TextType text_type; }; @@ -46,7 +47,7 @@ public: private: Optional next_without_peek(); - Optional try_commit_chunk(size_t start, size_t end, bool has_breaking_newline, Gfx::Font const&, Gfx::GlyphRun::TextType) const; + Optional try_commit_chunk(size_t start, size_t end, bool has_breaking_newline, bool has_breaking_tab, Gfx::Font const&, Gfx::GlyphRun::TextType) const; bool const m_wrap_lines; bool const m_respect_linebreaks;