mirror of
https://github.com/RGBCube/serenity
synced 2025-07-24 20:27:35 +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:
parent
5cee150a91
commit
e2b4fef6c7
11 changed files with 38 additions and 27 deletions
|
@ -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())
|
if (!children_are_inline())
|
||||||
return LayoutBox::hit_test(position);
|
return LayoutBox::hit_test(position, type);
|
||||||
|
|
||||||
HitTestResult last_good_candidate;
|
HitTestResult last_good_candidate;
|
||||||
for (auto& line_box : m_line_boxes) {
|
for (auto& line_box : m_line_boxes) {
|
||||||
|
@ -729,7 +729,7 @@ HitTestResult LayoutBlock::hit_test(const Gfx::IntPoint& position) const
|
||||||
continue;
|
continue;
|
||||||
if (enclosing_int_rect(fragment.absolute_rect()).contains(position)) {
|
if (enclosing_int_rect(fragment.absolute_rect()).contains(position)) {
|
||||||
if (fragment.layout_node().is_block())
|
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()) };
|
return { fragment.layout_node(), fragment.text_index_at(position.x()) };
|
||||||
}
|
}
|
||||||
if (fragment.absolute_rect().top() <= position.y())
|
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 last_good_candidate;
|
||||||
return { absolute_rect().contains(position.x(), position.y()) ? this : nullptr };
|
return { absolute_rect().contains(position.x(), position.y()) ? this : nullptr };
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ public:
|
||||||
LineBox& ensure_last_line_box();
|
LineBox& ensure_last_line_box();
|
||||||
LineBox& add_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()); }
|
LayoutBlock* previous_sibling() { return downcast<LayoutBlock>(LayoutNode::previous_sibling()); }
|
||||||
const LayoutBlock* previous_sibling() const { return downcast<LayoutBlock>(LayoutNode::previous_sibling()); }
|
const LayoutBlock* previous_sibling() const { return downcast<LayoutBlock>(LayoutNode::previous_sibling()); }
|
||||||
|
|
|
@ -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
|
// 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
|
// 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) {
|
for_each_child([&](auto& child) {
|
||||||
if (is<LayoutBox>(child) && downcast<LayoutBox>(child).stacking_context())
|
if (is<LayoutBox>(child) && downcast<LayoutBox>(child).stacking_context())
|
||||||
return;
|
return;
|
||||||
auto child_result = child.hit_test(position);
|
auto child_result = child.hit_test(position, type);
|
||||||
if (child_result.layout_node)
|
if (child_result.layout_node)
|
||||||
result = child_result;
|
result = child_result;
|
||||||
});
|
});
|
||||||
|
|
|
@ -55,7 +55,7 @@ public:
|
||||||
float absolute_y() const { return absolute_rect().y(); }
|
float absolute_y() const { return absolute_rect().y(); }
|
||||||
Gfx::FloatPoint absolute_position() const { return absolute_rect().location(); }
|
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;
|
virtual void set_needs_display() override;
|
||||||
|
|
||||||
bool is_body() const;
|
bool is_body() const;
|
||||||
|
|
|
@ -113,9 +113,9 @@ void LayoutDocument::paint(PaintContext& context, PaintPhase phase)
|
||||||
stacking_context()->paint(context, 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ public:
|
||||||
void paint_all_phases(PaintContext&);
|
void paint_all_phases(PaintContext&);
|
||||||
virtual void paint(PaintContext&, PaintPhase) override;
|
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; }
|
const LayoutRange& selection() const { return m_selection; }
|
||||||
LayoutRange& selection() { return m_selection; }
|
LayoutRange& selection() { return m_selection; }
|
||||||
|
|
|
@ -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;
|
HitTestResult result;
|
||||||
for_each_child([&](auto& child) {
|
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.
|
// The outer loop who called us will take care of those.
|
||||||
if (is<LayoutBox>(child) && downcast<LayoutBox>(child).stacking_context())
|
if (is<LayoutBox>(child) && downcast<LayoutBox>(child).stacking_context())
|
||||||
return;
|
return;
|
||||||
auto child_result = child.hit_test(position);
|
auto child_result = child.hit_test(position, type);
|
||||||
if (child_result.layout_node)
|
if (child_result.layout_node)
|
||||||
result = child_result;
|
result = child_result;
|
||||||
});
|
});
|
||||||
|
|
|
@ -45,11 +45,16 @@ struct HitTestResult {
|
||||||
int index_in_node { 0 };
|
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> {
|
class LayoutNode : public TreeNode<LayoutNode> {
|
||||||
public:
|
public:
|
||||||
virtual ~LayoutNode();
|
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; }
|
bool is_anonymous() const { return !m_node; }
|
||||||
const DOM::Node* node() const { return m_node; }
|
const DOM::Node* node() const { return m_node; }
|
||||||
|
|
|
@ -77,7 +77,7 @@ bool EventHandler::handle_mouseup(const Gfx::IntPoint& position, unsigned button
|
||||||
return false;
|
return false;
|
||||||
bool handled_event = 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()) {
|
if (result.layout_node && result.layout_node->node()) {
|
||||||
RefPtr<DOM::Node> node = result.layout_node->node();
|
RefPtr<DOM::Node> node = result.layout_node->node();
|
||||||
if (is<HTML::HTMLIFrameElement>(*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();
|
NonnullRefPtr document = *m_frame.document();
|
||||||
auto& page_client = m_frame.page().client();
|
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)
|
if (!result.layout_node)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
@ -151,10 +151,13 @@ bool EventHandler::handle_mousedown(const Gfx::IntPoint& position, unsigned butt
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (button == GUI::MouseButton::Left) {
|
if (button == GUI::MouseButton::Left) {
|
||||||
m_frame.set_cursor_position(DOM::Position(*node, result.index_in_node));
|
auto result = layout_root()->hit_test(position, HitTestType::TextCursor);
|
||||||
layout_root()->selection().set({ result.layout_node, result.index_in_node }, {});
|
if (result.layout_node && result.layout_node->node()) {
|
||||||
dump_selection("MouseDown");
|
m_frame.set_cursor_position(DOM::Position(*node, result.index_in_node));
|
||||||
m_in_mouse_selection = true;
|
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) {
|
} else if (button == GUI::MouseButton::Right) {
|
||||||
page_client.page_did_request_context_menu(m_frame.to_main_frame_position(position));
|
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 hovered_node_changed = false;
|
||||||
bool is_hovering_link = 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;
|
const HTML::HTMLAnchorElement* hovered_link_element = nullptr;
|
||||||
if (result.layout_node) {
|
if (result.layout_node) {
|
||||||
RefPtr<DOM::Node> node = result.layout_node->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;
|
return true;
|
||||||
}
|
}
|
||||||
if (m_in_mouse_selection) {
|
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");
|
dump_selection("MouseMove");
|
||||||
page_client.page_did_change_selection();
|
page_client.page_did_change_selection();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
HitTestResult result;
|
||||||
if (!m_box.is_root()) {
|
if (!m_box.is_root()) {
|
||||||
result = m_box.hit_test(position);
|
result = m_box.hit_test(position, type);
|
||||||
} else {
|
} else {
|
||||||
// NOTE: LayoutDocument::hit_test() merely calls StackingContext::hit_test()
|
// NOTE: LayoutDocument::hit_test() merely calls StackingContext::hit_test()
|
||||||
// so we call its base class instead.
|
// 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) {
|
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)
|
if (result_here.layout_node)
|
||||||
result = result_here;
|
result = result_here;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ public:
|
||||||
const StackingContext* parent() const { return m_parent; }
|
const StackingContext* parent() const { return m_parent; }
|
||||||
|
|
||||||
void paint(PaintContext&, LayoutNode::PaintPhase);
|
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;
|
void dump(int indent = 0) const;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue