From f3f0b08d4318a132f528a03575dada47e8eb08b8 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 5 Nov 2019 22:13:26 +0100 Subject: [PATCH] LibHTML: Build some foundation for text selection Add LayoutPosition and LayoutRange classes. The layout tree root node now has a selection() LayoutRange. It's essentially a start and end LayoutPosition. A LayoutPosition is a LayoutNode, and an optional index into that node. The index is only relevant for text nodes, where it's the character index into the rendered text. HtmlView now updates the selection start/end of the LayoutDocument when clicking and dragging with the left mouse button. We don't paint the selection yet, and there's no way to copy what's selected. It only exists as a LayoutRange. --- Libraries/LibHTML/HtmlView.cpp | 30 ++++++++++- Libraries/LibHTML/HtmlView.h | 3 ++ Libraries/LibHTML/Layout/LayoutBlock.cpp | 2 +- Libraries/LibHTML/Layout/LayoutDocument.h | 4 ++ Libraries/LibHTML/Layout/LayoutNode.h | 2 + Libraries/LibHTML/Layout/LayoutPosition.h | 56 ++++++++++++++++++++ Libraries/LibHTML/Layout/LineBoxFragment.cpp | 22 ++++++++ Libraries/LibHTML/Layout/LineBoxFragment.h | 2 + 8 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 Libraries/LibHTML/Layout/LayoutPosition.h diff --git a/Libraries/LibHTML/HtmlView.cpp b/Libraries/LibHTML/HtmlView.cpp index a8948f8f18..b77b547f6e 100644 --- a/Libraries/LibHTML/HtmlView.cpp +++ b/Libraries/LibHTML/HtmlView.cpp @@ -64,7 +64,6 @@ void HtmlView::set_document(Document* new_document) }; } - #ifdef HTML_DEBUG if (document != nullptr) { dbgprintf("\033[33;1mLayout tree before layout:\033[0m\n"); @@ -159,6 +158,11 @@ void HtmlView::mousemove_event(GMouseEvent& event) is_hovering_link = true; } } + if (m_in_mouse_selection) { + layout_root()->selection().set_end({ result.layout_node, result.index_in_node }); + dump_selection("MouseMove"); + update(); + } } if (window()) window()->set_override_cursor(is_hovering_link ? GStandardCursor::Hand : GStandardCursor::None); @@ -196,6 +200,12 @@ void HtmlView::mousedown_event(GMouseEvent& event) dbg() << "HtmlView: clicking on a link to " << link->href(); if (on_link_click) on_link_click(link->href()); + } else { + if (event.button() == GMouseButton::Left) { + layout_root()->selection().set({ result.layout_node, result.index_in_node }, {}); + dump_selection("MouseDown"); + m_in_mouse_selection = true; + } } } } @@ -204,6 +214,17 @@ void HtmlView::mousedown_event(GMouseEvent& event) event.accept(); } +void HtmlView::mouseup_event(GMouseEvent& event) +{ + if (!layout_root()) + return GScrollableWidget::mouseup_event(event); + + if (event.button() == GMouseButton::Left) { + dump_selection("MouseUp"); + m_in_mouse_selection = false; + } +} + void HtmlView::keydown_event(GKeyEvent& event) { if (event.modifiers() == 0) { @@ -352,3 +373,10 @@ const Document* HtmlView::document() const { return main_frame().document(); } + +void HtmlView::dump_selection(const char* event_name) +{ + dbg() << event_name << " selection start: " + << layout_root()->selection().start().layout_node << ":" << layout_root()->selection().start().index_in_node << ", end: " + << layout_root()->selection().end().layout_node << ":" << layout_root()->selection().end().index_in_node; +} diff --git a/Libraries/LibHTML/HtmlView.h b/Libraries/LibHTML/HtmlView.h index 189133a79e..ea2b8b733e 100644 --- a/Libraries/LibHTML/HtmlView.h +++ b/Libraries/LibHTML/HtmlView.h @@ -43,12 +43,15 @@ protected: virtual void paint_event(GPaintEvent&) override; virtual void mousemove_event(GMouseEvent&) override; virtual void mousedown_event(GMouseEvent&) override; + virtual void mouseup_event(GMouseEvent&) override; virtual void keydown_event(GKeyEvent&) override; private: void layout_and_sync_size(); + void dump_selection(const char* event_name); RefPtr m_main_frame; bool m_should_show_line_box_borders { false }; + bool m_in_mouse_selection { false }; }; diff --git a/Libraries/LibHTML/Layout/LayoutBlock.cpp b/Libraries/LibHTML/Layout/LayoutBlock.cpp index 3ba37a9088..f5cb6f3935 100644 --- a/Libraries/LibHTML/Layout/LayoutBlock.cpp +++ b/Libraries/LibHTML/Layout/LayoutBlock.cpp @@ -293,7 +293,7 @@ HitTestResult LayoutBlock::hit_test(const Point& position) const for (auto& line_box : m_line_boxes) { for (auto& fragment : line_box.fragments()) { if (enclosing_int_rect(fragment.rect()).contains(position)) { - return { fragment.layout_node() }; + return { fragment.layout_node(), fragment.text_index_at(position.x()) }; } } } diff --git a/Libraries/LibHTML/Layout/LayoutDocument.h b/Libraries/LibHTML/Layout/LayoutDocument.h index ec1ce9d276..18b44c21be 100644 --- a/Libraries/LibHTML/Layout/LayoutDocument.h +++ b/Libraries/LibHTML/Layout/LayoutDocument.h @@ -12,5 +12,9 @@ public: virtual const char* class_name() const override { return "LayoutDocument"; } virtual void layout() override; + const LayoutRange& selection() const { return m_selection; } + LayoutRange& selection() { return m_selection; } + private: + LayoutRange m_selection; }; diff --git a/Libraries/LibHTML/Layout/LayoutNode.h b/Libraries/LibHTML/Layout/LayoutNode.h index ad504df269..f4e2181068 100644 --- a/Libraries/LibHTML/Layout/LayoutNode.h +++ b/Libraries/LibHTML/Layout/LayoutNode.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,7 @@ class Node; struct HitTestResult { RefPtr layout_node; + int index_in_node { 0 }; }; class LayoutNode : public TreeNode { diff --git a/Libraries/LibHTML/Layout/LayoutPosition.h b/Libraries/LibHTML/Layout/LayoutPosition.h new file mode 100644 index 0000000000..30fac23368 --- /dev/null +++ b/Libraries/LibHTML/Layout/LayoutPosition.h @@ -0,0 +1,56 @@ +#pragma once + +#include + +class LayoutNode; + +struct LayoutPosition { + bool operator>=(const LayoutPosition& other) const + { + if (layout_node == other.layout_node) + return index_in_node >= other.index_in_node; + + // FIXME: Implement. + return true; + } + + bool operator<=(const LayoutPosition& other) const + { + if (layout_node == other.layout_node) + return index_in_node <= other.index_in_node; + + // FIXME: Implement. + return false; + } + + RefPtr layout_node; + int index_in_node { 0 }; +}; + +class LayoutRange { +public: + LayoutRange() {} + LayoutRange(const LayoutPosition& start, const LayoutPosition& end) + : m_start(start) + , m_end(end) + { + } + + bool is_valid() const { return m_start.layout_node && m_end.layout_node; } + + void set(const LayoutPosition& start, const LayoutPosition& end) + { + m_start = start; + m_end = end; + } + + void set_start(const LayoutPosition& start) { m_start = start; } + void set_end(const LayoutPosition& end) { m_end = end; } + + const LayoutPosition& start() const { return m_start; } + const LayoutPosition& end() const { return m_end; } + +private: + LayoutPosition m_start; + LayoutPosition m_end; +}; diff --git a/Libraries/LibHTML/Layout/LineBoxFragment.cpp b/Libraries/LibHTML/Layout/LineBoxFragment.cpp index c987483b2b..40c88e1764 100644 --- a/Libraries/LibHTML/Layout/LineBoxFragment.cpp +++ b/Libraries/LibHTML/Layout/LineBoxFragment.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -26,3 +27,24 @@ StringView LineBoxFragment::text() const return {}; return to(layout_node()).node().data().substring_view(m_start, m_length); } + +int LineBoxFragment::text_index_at(float x) const +{ + if (!layout_node().is_text()) + return 0; + auto& layout_text = to(layout_node()); + auto& font = layout_text.style().font(); + Utf8View view(text()); + + float relative_x = x - m_rect.location().x(); + float glyph_spacing = font.glyph_spacing(); + + float width_so_far = 0; + for (auto it = view.begin(); it != view.end(); ++it) { + float glyph_width = font.glyph_or_emoji_width(*it); + if ((width_so_far + glyph_width + glyph_spacing) > relative_x) + return m_start + view.byte_offset_of(it); + width_so_far += glyph_width + glyph_spacing; + } + return m_start + m_length - 1; +} diff --git a/Libraries/LibHTML/Layout/LineBoxFragment.h b/Libraries/LibHTML/Layout/LineBoxFragment.h index 0b12cce81f..565f960f9e 100644 --- a/Libraries/LibHTML/Layout/LineBoxFragment.h +++ b/Libraries/LibHTML/Layout/LineBoxFragment.h @@ -29,6 +29,8 @@ public: bool is_justifiable_whitespace() const; StringView text() const; + int text_index_at(float x) const; + private: const LayoutNode& m_layout_node; int m_start { 0 };