diff --git a/Libraries/LibHTML/Dump.cpp b/Libraries/LibHTML/Dump.cpp index 03d8263d1b7..5707042b285 100644 --- a/Libraries/LibHTML/Dump.cpp +++ b/Libraries/LibHTML/Dump.cpp @@ -1,8 +1,10 @@ +#include #include #include #include #include #include +#include #include #include #include @@ -78,13 +80,40 @@ void dump_tree(const LayoutNode& layout_node) layout_node.style().border().bottom.to_px(), layout_node.style().margin().bottom.to_px()); - if (layout_node.is_text()) { - const LayoutText& layout_text = static_cast(layout_node); - dbgprintf(" \"%s\", %d runs", layout_text.text().characters(), layout_text.runs().size()); - } - dbgprintf("\n"); + if (layout_node.is_block() && static_cast(layout_node).children_are_inline()) { + auto& block = static_cast(layout_node); + for (int i = 0; i < indent; ++i) + dbgprintf(" "); + dbgprintf(" Line boxes (%d):\n", block.line_boxes().size()); + for (int line_box_index = 0; line_box_index < block.line_boxes().size(); ++line_box_index) { + auto& line_box = block.line_boxes()[line_box_index]; + for (int i = 0; i < indent; ++i) + dbgprintf(" "); + dbgprintf(" [%d] width: %d\n", line_box_index, line_box.width()); + for (int fragment_index = 0; fragment_index < line_box.fragments().size(); ++fragment_index) { + auto& fragment = line_box.fragments()[fragment_index]; + for (int i = 0; i < indent; ++i) + dbgprintf(" "); + dbgprintf(" [%d] layout_node: %s{%p}, start: %d, length: %d, rect: %s\n", + fragment_index, + fragment.layout_node().class_name(), + &fragment.layout_node(), + fragment.start(), + fragment.length(), + fragment.rect().to_string().characters()); + if (fragment.layout_node().is_text()) { + for (int i = 0; i < indent; ++i) + dbgprintf(" "); + auto& layout_text = static_cast(fragment.layout_node()); + dbgprintf(" text: \"%s\"\n", + String(Utf8View(layout_text.node().data()).substring_view(fragment.start(), fragment.length()).as_string()).characters()); + } + } + } + } + layout_node.style_properties().for_each_property([&](auto& key, auto& value) { for (int i = 0; i < indent; ++i) dbgprintf(" "); diff --git a/Libraries/LibHTML/Layout/LayoutBlock.cpp b/Libraries/LibHTML/Layout/LayoutBlock.cpp index 5ca7a763814..04f6ad80bc5 100644 --- a/Libraries/LibHTML/Layout/LayoutBlock.cpp +++ b/Libraries/LibHTML/Layout/LayoutBlock.cpp @@ -1,6 +1,7 @@ #include #include #include +#include LayoutBlock::LayoutBlock(const Node* node, StyleProperties&& style_properties) : LayoutNode(node, move(style_properties)) @@ -24,14 +25,52 @@ void LayoutBlock::layout() compute_width(); compute_position(); + if (children_are_inline()) + layout_inline_children(); + else + layout_block_children(); + + compute_height(); +} + +void LayoutBlock::layout_block_children() +{ + ASSERT(!children_are_inline()); int content_height = 0; for_each_child([&](auto& child) { child.layout(); content_height = child.rect().bottom() + child.style().full_margin().bottom - rect().top(); }); rect().set_height(content_height); +} - compute_height(); +void LayoutBlock::layout_inline_children() +{ + ASSERT(children_are_inline()); + m_line_boxes.clear(); + for_each_child([&](auto& child) { + ASSERT(child.is_inline()); + static_cast(child).split_into_lines(*this); + }); + + int content_height = 0; + + for (auto& line_box : m_line_boxes) { + int max_height = 0; + for (auto& fragment : line_box.fragments()) { + max_height = max(max_height, fragment.rect().height()); + } + for (auto& fragment : line_box.fragments()) { + // Vertically align everyone's bottom to the line. + // FIXME: Support other kinds of vertical alignment. + fragment.rect().set_x(rect().x() + fragment.rect().x()); + fragment.rect().set_y(rect().y() + content_height + (max_height - fragment.rect().height())); + } + + content_height += max_height; + } + + rect().set_height(content_height); } void LayoutBlock::compute_width() @@ -163,4 +202,33 @@ void LayoutBlock::render(RenderingContext& context) }; context.painter().fill_rect(bullet_rect, Color::Black); } + + if (children_are_inline()) { + for (auto& line_box : m_line_boxes) { + for (auto& fragment : line_box.fragments()) { + fragment.render(context); + } + } + } +} + +bool LayoutBlock::children_are_inline() const +{ + return first_child() && !first_child()->is_block(); +} + +HitTestResult LayoutBlock::hit_test(const Point& position) const +{ + if (!children_are_inline()) + return LayoutNode::hit_test(position); + + HitTestResult result; + for (auto& line_box : m_line_boxes) { + for (auto& fragment : line_box.fragments()) { + if (fragment.rect().contains(position)) { + return { fragment.layout_node() }; + } + } + } + return {}; } diff --git a/Libraries/LibHTML/Layout/LayoutBlock.h b/Libraries/LibHTML/Layout/LayoutBlock.h index 575e73430b2..093f0adb6f7 100644 --- a/Libraries/LibHTML/Layout/LayoutBlock.h +++ b/Libraries/LibHTML/Layout/LayoutBlock.h @@ -1,6 +1,7 @@ #pragma once #include +#include class Element; @@ -16,10 +17,22 @@ public: virtual LayoutNode& inline_wrapper() override; + bool children_are_inline() const; + + Vector& line_boxes() { return m_line_boxes; } + const Vector& line_boxes() const { return m_line_boxes; } + + virtual HitTestResult hit_test(const Point&) const override; + private: virtual bool is_block() const override { return true; } + void layout_inline_children(); + void layout_block_children(); + void compute_width(); void compute_position(); void compute_height(); + + Vector m_line_boxes; }; diff --git a/Libraries/LibHTML/Layout/LayoutInline.cpp b/Libraries/LibHTML/Layout/LayoutInline.cpp index d8c1652dab2..687a78b8edd 100644 --- a/Libraries/LibHTML/Layout/LayoutInline.cpp +++ b/Libraries/LibHTML/Layout/LayoutInline.cpp @@ -1,4 +1,5 @@ #include +#include #include LayoutInline::LayoutInline(const Node& node, StyleProperties&& style_properties) @@ -35,3 +36,14 @@ void LayoutInline::layout() rect().set_bottom(child.rect().bottom() + child.style().full_margin().bottom); }); } + +void LayoutInline::split_into_lines(LayoutBlock& container) +{ + for_each_child([&](auto& child) { + if (child.is_inline()) { + static_cast(child).split_into_lines(container); + } else { + // FIXME: Support block children of inlines. + } + }); +} diff --git a/Libraries/LibHTML/Layout/LayoutInline.h b/Libraries/LibHTML/Layout/LayoutInline.h index 424fe654de5..c8dcfb0625f 100644 --- a/Libraries/LibHTML/Layout/LayoutInline.h +++ b/Libraries/LibHTML/Layout/LayoutInline.h @@ -3,6 +3,7 @@ #include class Element; +class LayoutBlock; class LayoutInline : public LayoutNode { public: @@ -14,5 +15,7 @@ public: virtual void layout() override; + virtual void split_into_lines(LayoutBlock& container); + private: }; diff --git a/Libraries/LibHTML/Layout/LayoutText.cpp b/Libraries/LibHTML/Layout/LayoutText.cpp index 9ea5c58a387..d9195e4018b 100644 --- a/Libraries/LibHTML/Layout/LayoutText.cpp +++ b/Libraries/LibHTML/Layout/LayoutText.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -7,7 +8,7 @@ #include LayoutText::LayoutText(const Text& text, StyleProperties&& style_properties) - : LayoutNode(&text, move(style_properties)) + : LayoutInline(text, move(style_properties)) { } @@ -86,158 +87,7 @@ const String& LayoutText::text() const return node().data(); } -static void split_first_word(const StringView& str, StringView& out_space, StringView& out_word) -{ - int first_nonspace = -1; - for (int i = 0; i < str.length(); i++) - if (!isspace(str[i])) { - first_nonspace = i; - break; - } - - if (first_nonspace == -1) { - out_space = str; - out_word = {}; - return; - } - - int first_space = str.length(); - for (int i = first_nonspace + 1; i < str.length(); i++) - if (isspace(str[i])) { - first_space = i; - break; - } - - out_space = str.substring_view(0, first_nonspace); - out_word = str.substring_view(first_nonspace, first_space - first_nonspace); -} - -void LayoutText::compute_runs() -{ - StringView remaining_text = node().data(); - if (remaining_text.is_empty()) - return; - - int right_border = containing_block()->rect().x() + containing_block()->rect().width(); - - StringBuilder builder; - Point run_origin = rect().location(); - - int total_right_margin = style().full_margin().right; - bool is_preformatted = style_properties().string_or_fallback("white-space", "normal") != "normal"; - - while (!remaining_text.is_empty()) { - String saved_text = builder.string_view(); - - // Try to append a new word. - StringView space; - StringView word; - split_first_word(remaining_text, space, word); - - int forced_line_break_index = -1; - if (is_preformatted) - for (int i = 0; i < space.length(); i++) - if (space[i] == '\n') { - forced_line_break_index = i; - break; - } - - if (!space.is_empty()) { - if (!is_preformatted) { - builder.append(' '); - } else if (forced_line_break_index != -1) { - builder.append(space.substring_view(0, forced_line_break_index)); - } else { - builder.append(space); - } - } - if (forced_line_break_index == -1) - builder.append(word); - - if (forced_line_break_index != -1) - remaining_text = remaining_text.substring_view(forced_line_break_index + 1, remaining_text.length() - forced_line_break_index - 1); - else if (!word.is_null()) - remaining_text = remaining_text.substring_view_starting_after_substring(word); - else - remaining_text = {}; - - // See if that fits. - int width = m_font->width(builder.string_view()); - if (forced_line_break_index == -1 && run_origin.x() + width + total_right_margin < right_border) - continue; - - // If it doesn't, create a run from - // what we had there previously. - if (forced_line_break_index == -1) - m_runs.append({ run_origin, move(saved_text) }); - else - m_runs.append({ run_origin, builder.string_view() }); - - // Start a new run at the new line. - int line_spacing = 4; - run_origin.set_x(containing_block()->rect().x() + style().full_margin().left); - run_origin.move_by(0, m_font->glyph_height() + line_spacing); - builder = StringBuilder(); - if (forced_line_break_index != -1) - continue; - if (is_preformatted) - builder.append(space); - builder.append(word); - } - - // Add the last run. - m_runs.append({ run_origin, builder.build() }); -} - -void LayoutText::layout() -{ - ASSERT(!has_children()); - - if (!m_font) - load_font(); - - int origin_x = -1; - int origin_y = -1; - if (previous_sibling() != nullptr) { - auto& previous_sibling_rect = previous_sibling()->rect(); - auto& previous_sibling_style = previous_sibling()->style(); - origin_x = previous_sibling_rect.x() + previous_sibling_rect.width(); - origin_x += previous_sibling_style.full_margin().right; - origin_y = previous_sibling_rect.y() + previous_sibling_rect.height() - m_font->glyph_height() - previous_sibling_style.full_margin().top; - } else { - origin_x = parent()->rect().x(); - origin_y = parent()->rect().y(); - } - rect().set_x(origin_x + style().full_margin().left); - rect().set_y(origin_y + style().full_margin().top); - - m_runs.clear(); - compute_runs(); - - if (m_runs.is_empty()) - return; - - const Run& last_run = m_runs[m_runs.size() - 1]; - rect().set_right(last_run.pos.x() + m_font->width(last_run.text)); - rect().set_bottom(last_run.pos.y() + m_font->glyph_height()); -} - -template -void LayoutText::for_each_run(Callback callback) const -{ - for (auto& run : m_runs) { - Rect rect { - run.pos.x(), - run.pos.y(), - m_font->width(run.text), - m_font->glyph_height() - }; - if (callback(run, rect) == IterationDecision::Break) - break; - } -} - -void LayoutText::render(RenderingContext& context) +void LayoutText::render_fragment(RenderingContext& context, const LineBoxFragment& fragment) const { auto& painter = context.painter(); painter.set_font(*m_font); @@ -246,24 +96,126 @@ void LayoutText::render(RenderingContext& context) auto text_decoration = style_properties().string_or_fallback("text-decoration", "none"); bool is_underline = text_decoration == "underline"; + if (is_underline) + painter.draw_line(fragment.rect().bottom_left().translated(0, 1), fragment.rect().bottom_right().translated(0, 1), color); - for_each_run([&](auto& run, auto& rect) { - painter.draw_text(rect, run.text, TextAlignment::TopLeft, color); - if (is_underline) - painter.draw_line(rect.bottom_left().translated(0, 1), rect.bottom_right().translated(0, 1), color); - return IterationDecision::Continue; - }); + painter.draw_text(fragment.rect(), node().data().substring_view(fragment.start(), fragment.length()), TextAlignment::TopLeft, color); } -HitTestResult LayoutText::hit_test(const Point& position) const +template +void LayoutText::for_each_word(Callback callback) const { - HitTestResult result; - for_each_run([&](auto&, auto& rect) { - if (rect.contains(position)) { - result.layout_node = this; - return IterationDecision::Break; + Utf8View view(node().data()); + if (view.is_empty()) + return; + + auto start_of_word = view.begin(); + + auto commit_word = [&](auto it) { + int start = view.byte_offset_of(start_of_word); + int length = view.byte_offset_of(it) - view.byte_offset_of(start_of_word); + + if (length > 0) { + callback(view.substring_view(start, length), start, length); } - return IterationDecision::Continue; - }); - return result; + + start_of_word = it; + }; + + bool last_was_space = isspace(*view.begin()); + + for (auto it = view.begin(); it != view.end();) { + bool is_space = isspace(*it); + if (is_space == last_was_space) { + ++it; + continue; + } + last_was_space = is_space; + commit_word(it); + ++it; + } + if (start_of_word != view.end()) + commit_word(view.end()); +} + +template +void LayoutText::for_each_source_line(Callback callback) const +{ + Utf8View view(node().data()); + if (view.is_empty()) + return; + + auto start_of_line = view.begin(); + + auto commit_line = [&](auto it) { + int start = view.byte_offset_of(start_of_line); + int length = view.byte_offset_of(it) - view.byte_offset_of(start_of_line); + + if (length > 0) { + callback(view.substring_view(start, length), start, length); + } + }; + + for (auto it = view.begin(); it != view.end();) { + if (*it == '\n') + commit_line(it); + ++it; + start_of_line = it; + } + if (start_of_line != view.end()) + commit_line(view.end()); +} + +void LayoutText::split_into_lines(LayoutBlock& container) +{ + if (!m_font) + load_font(); + + int space_width = m_font->glyph_width(' ') + m_font->glyph_spacing(); + // FIXME: Allow overriding the line-height. We currently default to 140% which seems to look nice. + int line_height = (int)(m_font->glyph_height() * 1.4f); + + auto& line_boxes = container.line_boxes(); + if (line_boxes.is_empty()) + line_boxes.append(LineBox()); + int available_width = container.rect().width() - line_boxes.last().width(); + + bool is_preformatted = style_properties().string_or_fallback("white-space", "normal") == "pre"; + if (is_preformatted) { + for_each_source_line([&](const Utf8View& view, int start, int length) { + line_boxes.last().add_fragment(*this, start, length, m_font->width(view), line_height); + line_boxes.append(LineBox()); + }); + return; + } + + struct Word { + Utf8View view; + int start; + int length; + }; + Vector words; + + for_each_word([&](const Utf8View& view, int start, int length) { + words.append({ Utf8View(view), start, length }); + dbg() << "Added _" << words.last().view.as_string() << "_"; + }); + + for (int i = 0; i < words.size(); ++i) { + auto& word = words[i]; + + int word_width; + if (isspace(*word.view.begin())) + word_width = space_width; + else + word_width = m_font->width(word.view); + + if (word_width > available_width) { + line_boxes.append(LineBox()); + available_width = container.rect().width(); + } + + line_boxes.last().add_fragment(*this, word.start, word.length, word_width, line_height); + available_width -= word_width; + } } diff --git a/Libraries/LibHTML/Layout/LayoutText.h b/Libraries/LibHTML/Layout/LayoutText.h index 43d0f7265a8..5d89f6e3a10 100644 --- a/Libraries/LibHTML/Layout/LayoutText.h +++ b/Libraries/LibHTML/Layout/LayoutText.h @@ -1,11 +1,12 @@ #pragma once #include -#include +#include class Font; +class LineBoxFragment; -class LayoutText : public LayoutNode { +class LayoutText : public LayoutInline { public: LayoutText(const Text&, StyleProperties&&); virtual ~LayoutText() override; @@ -16,25 +17,19 @@ public: virtual const char* class_name() const override { return "LayoutText"; } virtual bool is_text() const final { return true; } - virtual void layout() override; - virtual void render(RenderingContext&) override; - struct Run { - Point pos; - String text; - }; + void render_fragment(RenderingContext&, const LineBoxFragment&) const; - const Vector& runs() const { return m_runs; } - - virtual HitTestResult hit_test(const Point&) const override; + virtual void split_into_lines(LayoutBlock& container) override; private: template - void for_each_run(Callback) const; + void for_each_word(Callback) const; + template + void for_each_source_line(Callback) const; void load_font(); void compute_runs(); - Vector m_runs; RefPtr m_font; }; diff --git a/Libraries/LibHTML/Layout/LineBox.cpp b/Libraries/LibHTML/Layout/LineBox.cpp new file mode 100644 index 00000000000..55ea426b4b3 --- /dev/null +++ b/Libraries/LibHTML/Layout/LineBox.cpp @@ -0,0 +1,14 @@ +#include + +void LineBox::add_fragment(const LayoutNode& layout_node, int start, int length, int width, int height) +{ + if (!m_fragments.is_empty() && &m_fragments.last().layout_node() == &layout_node) { + // The fragment we're adding is from the last LayoutNode on the line. + // Expand the last fragment instead of adding a new one with the same LayoutNode. + m_fragments.last().m_length = (start - m_fragments.last().m_start) + length; + m_fragments.last().m_rect.set_width(m_fragments.last().m_rect.width() + width); + } else { + m_fragments.empend(layout_node, start, length, Rect(m_width, 0, width, height)); + } + m_width += width; +} diff --git a/Libraries/LibHTML/Layout/LineBox.h b/Libraries/LibHTML/Layout/LineBox.h new file mode 100644 index 00000000000..a6cdf2810ce --- /dev/null +++ b/Libraries/LibHTML/Layout/LineBox.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +class LineBox { +public: + LineBox() {} + + int width() const { return m_width; } + + void add_fragment(const LayoutNode& layout_node, int start, int length, int width, int height); + + const Vector& fragments() const { return m_fragments; } + Vector& fragments() { return m_fragments; } + +private: + Vector m_fragments; + int m_width { 0 }; +}; diff --git a/Libraries/LibHTML/Layout/LineBoxFragment.cpp b/Libraries/LibHTML/Layout/LineBoxFragment.cpp new file mode 100644 index 00000000000..1ff6755d6d0 --- /dev/null +++ b/Libraries/LibHTML/Layout/LineBoxFragment.cpp @@ -0,0 +1,12 @@ +#include +#include +#include +#include + +void LineBoxFragment::render(RenderingContext& context) +{ + if (layout_node().is_text()) { + auto& layout_text = static_cast(layout_node()); + layout_text.render_fragment(context, *this); + } +} diff --git a/Libraries/LibHTML/Layout/LineBoxFragment.h b/Libraries/LibHTML/Layout/LineBoxFragment.h new file mode 100644 index 00000000000..28e028cf8b5 --- /dev/null +++ b/Libraries/LibHTML/Layout/LineBoxFragment.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +class LayoutNode; +class RenderingContext; + +class LineBoxFragment { + friend class LineBox; +public: + LineBoxFragment(const LayoutNode& layout_node, int start, int length, const Rect& rect) + : m_layout_node(layout_node) + , m_start(start) + , m_length(length) + , m_rect(rect) + { + } + + const LayoutNode& layout_node() const { return m_layout_node; } + int start() const { return m_start; } + int length() const { return m_length; } + const Rect& rect() const { return m_rect; } + Rect& rect() { return m_rect; } + + void render(RenderingContext&); + +private: + const LayoutNode& m_layout_node; + int m_start { 0 }; + int m_length { 0 }; + Rect m_rect; +}; diff --git a/Libraries/LibHTML/Makefile.shared b/Libraries/LibHTML/Makefile.shared index 0f90ab67d29..61081b967c6 100644 --- a/Libraries/LibHTML/Makefile.shared +++ b/Libraries/LibHTML/Makefile.shared @@ -28,6 +28,8 @@ LIBHTML_OBJS = \ Layout/LayoutInline.o \ Layout/LayoutDocument.o \ Layout/ComputedStyle.o \ + Layout/LineBox.o \ + Layout/LineBoxFragment.o \ HtmlView.o \ Dump.o