1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-26 15:07:45 +00:00

LibWeb: Specialize hit testing for text cursor purposes

The text cursor follows slightly different "intuitive" rules than the
regular hit testing. Clicking past the right edge of a text box should
still "hit" the text box, and place the cursor at its end, for example.

We solve this by adding a HitTestType enum that is passed to hit_test()
and determines whether past-the-edge candidates are considered.
This commit is contained in:
Andreas Kling 2020-08-05 16:55:56 +02:00
parent 5cee150a91
commit e2b4fef6c7
11 changed files with 38 additions and 27 deletions

View file

@ -717,10 +717,10 @@ void LayoutBlock::paint(PaintContext& context, PaintPhase phase)
}
}
HitTestResult LayoutBlock::hit_test(const Gfx::IntPoint& position) const
HitTestResult LayoutBlock::hit_test(const Gfx::IntPoint& position, HitTestType type) const
{
if (!children_are_inline())
return LayoutBox::hit_test(position);
return LayoutBox::hit_test(position, type);
HitTestResult last_good_candidate;
for (auto& line_box : m_line_boxes) {
@ -729,7 +729,7 @@ HitTestResult LayoutBlock::hit_test(const Gfx::IntPoint& position) const
continue;
if (enclosing_int_rect(fragment.absolute_rect()).contains(position)) {
if (fragment.layout_node().is_block())
return downcast<LayoutBlock>(fragment.layout_node()).hit_test(position);
return downcast<LayoutBlock>(fragment.layout_node()).hit_test(position, type);
return { fragment.layout_node(), fragment.text_index_at(position.x()) };
}
if (fragment.absolute_rect().top() <= position.y())
@ -737,7 +737,7 @@ HitTestResult LayoutBlock::hit_test(const Gfx::IntPoint& position) const
}
}
if (last_good_candidate.layout_node)
if (type == HitTestType::TextCursor && last_good_candidate.layout_node)
return last_good_candidate;
return { absolute_rect().contains(position.x(), position.y()) ? this : nullptr };
}

View file

@ -49,7 +49,7 @@ public:
LineBox& ensure_last_line_box();
LineBox& add_line_box();
virtual HitTestResult hit_test(const Gfx::IntPoint&) const override;
virtual HitTestResult hit_test(const Gfx::IntPoint&, HitTestType) const override;
LayoutBlock* previous_sibling() { return downcast<LayoutBlock>(LayoutNode::previous_sibling()); }
const LayoutBlock* previous_sibling() const { return downcast<LayoutBlock>(LayoutNode::previous_sibling()); }

View file

@ -224,7 +224,7 @@ void LayoutBox::paint(PaintContext& context, PaintPhase phase)
}
}
HitTestResult LayoutBox::hit_test(const Gfx::IntPoint& position) const
HitTestResult LayoutBox::hit_test(const Gfx::IntPoint& position, HitTestType type) const
{
// FIXME: It would be nice if we could confidently skip over hit testing
// parts of the layout tree, but currently we can't just check
@ -233,7 +233,7 @@ HitTestResult LayoutBox::hit_test(const Gfx::IntPoint& position) const
for_each_child([&](auto& child) {
if (is<LayoutBox>(child) && downcast<LayoutBox>(child).stacking_context())
return;
auto child_result = child.hit_test(position);
auto child_result = child.hit_test(position, type);
if (child_result.layout_node)
result = child_result;
});

View file

@ -55,7 +55,7 @@ public:
float absolute_y() const { return absolute_rect().y(); }
Gfx::FloatPoint absolute_position() const { return absolute_rect().location(); }
virtual HitTestResult hit_test(const Gfx::IntPoint& absolute_position) const override;
virtual HitTestResult hit_test(const Gfx::IntPoint&, HitTestType) const override;
virtual void set_needs_display() override;
bool is_body() const;

View file

@ -113,9 +113,9 @@ void LayoutDocument::paint(PaintContext& context, PaintPhase phase)
stacking_context()->paint(context, phase);
}
HitTestResult LayoutDocument::hit_test(const Gfx::IntPoint& position) const
HitTestResult LayoutDocument::hit_test(const Gfx::IntPoint& position, HitTestType type) const
{
return stacking_context()->hit_test(position);
return stacking_context()->hit_test(position, type);
}
}

View file

@ -43,7 +43,7 @@ public:
void paint_all_phases(PaintContext&);
virtual void paint(PaintContext&, PaintPhase) override;
virtual HitTestResult hit_test(const Gfx::IntPoint&) const override;
virtual HitTestResult hit_test(const Gfx::IntPoint&, HitTestType) const override;
const LayoutRange& selection() const { return m_selection; }
LayoutRange& selection() { return m_selection; }

View file

@ -102,7 +102,7 @@ void LayoutNode::paint(PaintContext& context, PaintPhase phase)
});
}
HitTestResult LayoutNode::hit_test(const Gfx::IntPoint& position) const
HitTestResult LayoutNode::hit_test(const Gfx::IntPoint& position, HitTestType type) const
{
HitTestResult result;
for_each_child([&](auto& child) {
@ -110,7 +110,7 @@ HitTestResult LayoutNode::hit_test(const Gfx::IntPoint& position) const
// The outer loop who called us will take care of those.
if (is<LayoutBox>(child) && downcast<LayoutBox>(child).stacking_context())
return;
auto child_result = child.hit_test(position);
auto child_result = child.hit_test(position, type);
if (child_result.layout_node)
result = child_result;
});

View file

@ -45,11 +45,16 @@ struct HitTestResult {
int index_in_node { 0 };
};
enum class HitTestType {
Exact, // Exact matches only
TextCursor, // Clicking past the right/bottom edge of text will still hit the text
};
class LayoutNode : public TreeNode<LayoutNode> {
public:
virtual ~LayoutNode();
virtual HitTestResult hit_test(const Gfx::IntPoint&) const;
virtual HitTestResult hit_test(const Gfx::IntPoint&, HitTestType) const;
bool is_anonymous() const { return !m_node; }
const DOM::Node* node() const { return m_node; }