LibWeb: Resolve selection from retained hit-test data
Clamp mouse selection positions to the active scrollport during autoscroll. This keeps selection stable when the pointer leaves the viewport or crosses fixed page chrome while a drag is active. Harden user-select boundary adjustment for document edge clamping. Avoid null traversal results, and cover viewport and subtree edge cases with regression tests.
This commit is contained in:
parent
7445cff5e8
commit
0c6bb97898
5 changed files with 128 additions and 30 deletions
|
|
@ -7,7 +7,6 @@
|
|||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/DocumentFragment.h>
|
||||
#include <LibWeb/DOM/Element.h>
|
||||
#include <LibWeb/HTML/FormAssociatedElement.h>
|
||||
#include <LibWeb/HTML/Navigable.h>
|
||||
#include <LibWeb/Page/AutoScrollHandler.h>
|
||||
#include <LibWeb/Page/EventHandler.h>
|
||||
|
|
@ -73,12 +72,6 @@ static CSSPixelPoint compute_auto_scroll_speed(CSSPixelPoint mouse, CSSPixelRect
|
|||
};
|
||||
}
|
||||
|
||||
static bool is_in_form_associated_text_control(DOM::Element const& element)
|
||||
{
|
||||
auto const& host = element.containing_shadow_root() ? *element.containing_shadow_root()->host() : element;
|
||||
return is<HTML::FormAssociatedTextControlElement>(host);
|
||||
}
|
||||
|
||||
AutoScrollHandler::AutoScrollHandler(HTML::Navigable& navigable, DOM::Element& container)
|
||||
: m_navigable(navigable)
|
||||
, m_container_element(container)
|
||||
|
|
@ -113,9 +106,7 @@ CSSPixelPoint AutoScrollHandler::process(CSSPixelPoint mouse_position)
|
|||
}
|
||||
|
||||
activate();
|
||||
if (is_in_form_associated_text_control(m_container_element))
|
||||
return constrained(mouse_position, *scrollport);
|
||||
return mouse_position;
|
||||
return constrained(mouse_position, *scrollport);
|
||||
}
|
||||
|
||||
GC::Ptr<DOM::Element> AutoScrollHandler::find_scrollable_ancestor(Painting::Paintable const& paintable)
|
||||
|
|
@ -206,10 +197,7 @@ void AutoScrollHandler::perform_tick()
|
|||
if (paintable_box->scroll_by(scroll_x, scroll_y) == Painting::PaintableBox::ScrollHandled::No)
|
||||
return;
|
||||
|
||||
auto selection_position = is_in_form_associated_text_control(m_container_element)
|
||||
? constrained(m_mouse_position, *scrollport)
|
||||
: m_mouse_position;
|
||||
m_navigable->event_handler().apply_mouse_selection(selection_position);
|
||||
m_navigable->event_handler().apply_mouse_selection(constrained(m_mouse_position, *scrollport));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1760,6 +1760,34 @@ bool EventHandler::maybe_request_paste_for_middle_click(DOM::Document& document,
|
|||
// https://drafts.csswg.org/css-ui/#propdef-user-select
|
||||
static void set_user_selection(GC::Ptr<DOM::Node> anchor_node, size_t anchor_offset, GC::Ptr<DOM::Node> focus_node, size_t focus_offset, Selection::Selection* selection, CSS::UserSelect user_select)
|
||||
{
|
||||
auto move_focus_before_node = [&](GC::Ref<DOM::Node> node) -> bool {
|
||||
focus_node = node->previous_in_pre_order();
|
||||
if (focus_node) {
|
||||
focus_offset = focus_node->length();
|
||||
return true;
|
||||
}
|
||||
if (!node->parent())
|
||||
return false;
|
||||
focus_node = *node->parent();
|
||||
focus_offset = node->index();
|
||||
return true;
|
||||
};
|
||||
|
||||
auto move_focus_after_node = [&](GC::Ref<DOM::Node> node) -> bool {
|
||||
focus_node = node->next_in_pre_order();
|
||||
while (focus_node && node->is_inclusive_ancestor_of(*focus_node))
|
||||
focus_node = focus_node->next_in_pre_order();
|
||||
if (focus_node) {
|
||||
focus_offset = 0;
|
||||
return true;
|
||||
}
|
||||
if (!node->parent())
|
||||
return false;
|
||||
focus_node = *node->parent();
|
||||
focus_offset = node->index() + 1;
|
||||
return true;
|
||||
};
|
||||
|
||||
// https://drafts.csswg.org/css-ui/#valdef-user-select-contain
|
||||
// NB: This is clamping the focus node to any node with user-select: contain that stands between it and the anchor node.
|
||||
if (focus_node != anchor_node) {
|
||||
|
|
@ -1802,17 +1830,11 @@ static void set_user_selection(GC::Ptr<DOM::Node> anchor_node, size_t anchor_off
|
|||
if (
|
||||
potential_contain_node->layout_node() && potential_contain_node->layout_node()->user_select_used_value() == CSS::UserSelect::Contain && !potential_contain_node->is_inclusive_ancestor_of(*anchor_node)) {
|
||||
if (potential_contain_node->is_before(*anchor_node)) {
|
||||
focus_node = potential_contain_node->next_in_pre_order();
|
||||
while (potential_contain_node->is_inclusive_ancestor_of(*focus_node)) {
|
||||
focus_node = focus_node->next_in_pre_order();
|
||||
}
|
||||
focus_offset = 0;
|
||||
if (!move_focus_after_node(*potential_contain_node))
|
||||
return;
|
||||
} else {
|
||||
focus_node = potential_contain_node->previous_in_pre_order();
|
||||
while (potential_contain_node->is_inclusive_ancestor_of(*focus_node)) {
|
||||
focus_node = focus_node->previous_in_pre_order();
|
||||
}
|
||||
focus_offset = focus_node->length();
|
||||
if (!move_focus_before_node(*potential_contain_node))
|
||||
return;
|
||||
}
|
||||
// NB: Prevents this from being handled again further down
|
||||
user_select = CSS::UserSelect::Contain;
|
||||
|
|
@ -1836,13 +1858,11 @@ static void set_user_selection(GC::Ptr<DOM::Node> anchor_node, size_t anchor_off
|
|||
}
|
||||
if (focus_node->is_before(*anchor_node)) {
|
||||
auto none_element = focus_node;
|
||||
do {
|
||||
focus_node = focus_node->next_in_pre_order();
|
||||
} while (none_element->is_inclusive_ancestor_of(*focus_node));
|
||||
focus_offset = 0;
|
||||
if (!move_focus_after_node(*none_element))
|
||||
return;
|
||||
} else {
|
||||
focus_node = focus_node->previous_in_pre_order();
|
||||
focus_offset = focus_node->length();
|
||||
if (!move_focus_before_node(*focus_node))
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case CSS::UserSelect::All:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<style>
|
||||
html {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
<div id="start">start</div>
|
||||
<div id="end">end</div>
|
||||
<script>
|
||||
const startRect = start.getBoundingClientRect();
|
||||
const endRect = end.getBoundingClientRect();
|
||||
|
||||
internals.mouseDown(endRect.x + 2, endRect.y + 5);
|
||||
internals.mouseMove(startRect.x + 2, startRect.y + 5);
|
||||
internals.mouseUp(startRect.x + 2, startRect.y + 5);
|
||||
</script>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
selection reached viewport top: true
|
||||
above viewport matches viewport top: true
|
||||
above viewport selected forward text: false
|
||||
71
Tests/LibWeb/Text/input/selection-drag-above-viewport.html
Normal file
71
Tests/LibWeb/Text/input/selection-drag-above-viewport.html
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="include.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font: 20px Arial, sans-serif;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
#menu {
|
||||
height: 28px;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.main {
|
||||
margin-top: 42px;
|
||||
width: 870px;
|
||||
}
|
||||
|
||||
.article {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
li {
|
||||
height: 30px;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div id="menu">Applications Blog Posts GitHub Packages</div>
|
||||
<div class="main">
|
||||
<div class="article">
|
||||
<p>ToaruOS 2.3 is now available.</p>
|
||||
<p>This release comes with many new features, including:</p>
|
||||
<ul>
|
||||
<li>Terminal tabs</li>
|
||||
<li id="target">A manpage viewer</li>
|
||||
<li>Several new system calls</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
test(() => {
|
||||
document.body.offsetWidth;
|
||||
|
||||
const textNode = target.firstChild;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(textNode);
|
||||
const rect = range.getBoundingClientRect();
|
||||
const startX = rect.x + rect.width / 2;
|
||||
const startY = rect.y + rect.height / 2;
|
||||
|
||||
internals.mouseDown(startX, startY);
|
||||
for (let y = Math.floor(startY) - 1; y >= 0; --y)
|
||||
internals.mouseMove(startX, y);
|
||||
|
||||
const selectedAtViewportTop = getSelection().toString();
|
||||
internals.mouseLeave();
|
||||
internals.mouseMove(startX, -50);
|
||||
internals.mouseUp(startX, -50);
|
||||
const selectedAboveViewport = getSelection().toString();
|
||||
|
||||
println(`selection reached viewport top: ${selectedAtViewportTop.includes("Terminal tabs") && selectedAtViewportTop.includes("A manpa")}`);
|
||||
println(`above viewport matches viewport top: ${selectedAboveViewport === selectedAtViewportTop}`);
|
||||
println(`above viewport selected forward text: ${selectedAboveViewport.includes("Several new system calls")}`);
|
||||
});
|
||||
</script>
|
||||
Loading…
Reference in a new issue