1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-24 22:17:42 +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; }

View file

@ -77,7 +77,7 @@ bool EventHandler::handle_mouseup(const Gfx::IntPoint& position, unsigned button
return false;
bool handled_event = false;
auto result = layout_root()->hit_test(position);
auto result = layout_root()->hit_test(position, HitTestType::Exact);
if (result.layout_node && result.layout_node->node()) {
RefPtr<DOM::Node> node = result.layout_node->node();
if (is<HTML::HTMLIFrameElement>(*node)) {
@ -104,7 +104,7 @@ bool EventHandler::handle_mousedown(const Gfx::IntPoint& position, unsigned butt
NonnullRefPtr document = *m_frame.document();
auto& page_client = m_frame.page().client();
auto result = layout_root()->hit_test(position);
auto result = layout_root()->hit_test(position, HitTestType::Exact);
if (!result.layout_node)
return false;
@ -151,10 +151,13 @@ bool EventHandler::handle_mousedown(const Gfx::IntPoint& position, unsigned butt
}
} else {
if (button == GUI::MouseButton::Left) {
m_frame.set_cursor_position(DOM::Position(*node, result.index_in_node));
layout_root()->selection().set({ result.layout_node, result.index_in_node }, {});
dump_selection("MouseDown");
m_in_mouse_selection = true;
auto result = layout_root()->hit_test(position, HitTestType::TextCursor);
if (result.layout_node && result.layout_node->node()) {
m_frame.set_cursor_position(DOM::Position(*node, result.index_in_node));
layout_root()->selection().set({ result.layout_node, result.index_in_node }, {});
dump_selection("MouseDown");
m_in_mouse_selection = true;
}
} else if (button == GUI::MouseButton::Right) {
page_client.page_did_request_context_menu(m_frame.to_main_frame_position(position));
}
@ -171,7 +174,7 @@ bool EventHandler::handle_mousemove(const Gfx::IntPoint& position, unsigned butt
bool hovered_node_changed = false;
bool is_hovering_link = false;
auto result = layout_root()->hit_test(position);
auto result = layout_root()->hit_test(position, HitTestType::Exact);
const HTML::HTMLAnchorElement* hovered_link_element = nullptr;
if (result.layout_node) {
RefPtr<DOM::Node> node = result.layout_node->node();
@ -198,7 +201,10 @@ bool EventHandler::handle_mousemove(const Gfx::IntPoint& position, unsigned butt
return true;
}
if (m_in_mouse_selection) {
layout_root()->selection().set_end({ result.layout_node, result.index_in_node });
auto hit = layout_root()->hit_test(position, HitTestType::TextCursor);
if (hit.layout_node && hit.layout_node->node()) {
layout_root()->selection().set_end({ hit.layout_node, hit.index_in_node });
}
dump_selection("MouseMove");
page_client.page_did_change_selection();
}

View file

@ -61,19 +61,19 @@ void StackingContext::paint(PaintContext& context, LayoutNode::PaintPhase phase)
}
}
HitTestResult StackingContext::hit_test(const Gfx::IntPoint& position) const
HitTestResult StackingContext::hit_test(const Gfx::IntPoint& position, HitTestType type) const
{
HitTestResult result;
if (!m_box.is_root()) {
result = m_box.hit_test(position);
result = m_box.hit_test(position, type);
} else {
// NOTE: LayoutDocument::hit_test() merely calls StackingContext::hit_test()
// so we call its base class instead.
result = downcast<LayoutDocument>(m_box).LayoutBlock::hit_test(position);
result = downcast<LayoutDocument>(m_box).LayoutBlock::hit_test(position, type);
}
for (auto* child : m_children) {
auto result_here = child->hit_test(position);
auto result_here = child->hit_test(position, type);
if (result_here.layout_node)
result = result_here;
}

View file

@ -41,7 +41,7 @@ public:
const StackingContext* parent() const { return m_parent; }
void paint(PaintContext&, LayoutNode::PaintPhase);
HitTestResult hit_test(const Gfx::IntPoint&) const;
HitTestResult hit_test(const Gfx::IntPoint&, HitTestType) const;
void dump(int indent = 0) const;