Просмотр исходного кода

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.
Andreas Kling 5 лет назад
Родитель
Сommit
f3f0b08d43

+ 29 - 1
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;
+}

+ 3 - 0
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<Frame> m_main_frame;
 
     bool m_should_show_line_box_borders { false };
+    bool m_in_mouse_selection { false };
 };

+ 1 - 1
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()) };
             }
         }
     }

+ 4 - 0
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;
 };

+ 2 - 0
Libraries/LibHTML/Layout/LayoutNode.h

@@ -5,6 +5,7 @@
 #include <LibDraw/Rect.h>
 #include <LibHTML/CSS/StyleProperties.h>
 #include <LibHTML/Layout/BoxModelMetrics.h>
+#include <LibHTML/Layout/LayoutPosition.h>
 #include <LibHTML/RenderingContext.h>
 #include <LibHTML/TreeNode.h>
 
@@ -19,6 +20,7 @@ class Node;
 
 struct HitTestResult {
     RefPtr<LayoutNode> layout_node;
+    int index_in_node { 0 };
 };
 
 class LayoutNode : public TreeNode<LayoutNode> {

+ 56 - 0
Libraries/LibHTML/Layout/LayoutPosition.h

@@ -0,0 +1,56 @@
+#pragma once
+
+#include <AK/RefPtr.h>
+
+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<LayoutNode> 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;
+};

+ 22 - 0
Libraries/LibHTML/Layout/LineBoxFragment.cpp

@@ -1,4 +1,5 @@
 #include <LibGUI/GPainter.h>
+#include <LibHTML/Layout/LayoutDocument.h>
 #include <LibHTML/Layout/LayoutText.h>
 #include <LibHTML/Layout/LineBoxFragment.h>
 #include <LibHTML/RenderingContext.h>
@@ -26,3 +27,24 @@ StringView LineBoxFragment::text() const
         return {};
     return to<LayoutText>(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<LayoutText>(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;
+}

+ 2 - 0
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 };