LibWeb: Invalidate async wheel state on listener changes
Bump a page-level wheel listener generation whenever non-passive wheel listeners are added or removed. Store that generation in async scrolling snapshots, and mark compositor wheel admission stale when listener state changes before a current snapshot arrives. This keeps the UI-process compositor wheel bypass from trusting an old no-listener snapshot after script has installed a cancelable wheel listener. Add async-scrolling coverage for the listener generation bump and the fresh routing state after a blocking wheel listener is added.
This commit is contained in:
parent
d36d336c82
commit
b57cc2bb7a
10 changed files with 98 additions and 2 deletions
|
|
@ -124,6 +124,7 @@ AsyncScrollingState collect_async_scrolling_state(HTML::Navigable& navigable, Pa
|
|||
AsyncScrollingState async_scrolling_state;
|
||||
Vector<Painting::ScrollFrameIndex> parent_scroll_frame_indices;
|
||||
async_scrolling_state.viewport_rect = viewport_rect;
|
||||
async_scrolling_state.wheel_event_listener_state_generation = navigable.page().wheel_event_listener_state_generation();
|
||||
collect_root_blocking_wheel_event_regions(async_scrolling_state, document_paintable.document());
|
||||
|
||||
document_paintable.for_each_in_inclusive_subtree_of_type<Painting::PaintableBox>([&](auto& paintable_box) {
|
||||
|
|
@ -222,6 +223,8 @@ StringView wheel_routing_admission_to_string(WheelRoutingAdmission admission)
|
|||
return "blocking wheel event listeners"sv;
|
||||
case WheelRoutingAdmission::NoViewportScrollNode:
|
||||
return "no viewport scroll node"sv;
|
||||
case WheelRoutingAdmission::StaleWheelEventListeners:
|
||||
return "stale wheel event listeners"sv;
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,11 @@ struct AsyncScrollingState {
|
|||
// async scrolling elsewhere.
|
||||
Vector<BlockingWheelEventRegion> blocking_wheel_event_regions;
|
||||
Gfx::IntRect viewport_rect;
|
||||
|
||||
// Bumped whenever wheel listener state changes so queued compositor snapshots
|
||||
// cannot re-enable async wheel routing after a non-passive listener has been
|
||||
// added.
|
||||
u64 wheel_event_listener_state_generation { 0 };
|
||||
bool has_blocking_wheel_event_listeners { false };
|
||||
bool blocking_wheel_event_regions_are_current { false };
|
||||
bool has_blocking_wheel_event_region_covering_viewport { false };
|
||||
|
|
@ -89,6 +94,7 @@ enum class WheelRoutingAdmission {
|
|||
NoAsyncScrollingState,
|
||||
BlockingWheelEventListeners,
|
||||
NoViewportScrollNode,
|
||||
StaleWheelEventListeners,
|
||||
};
|
||||
|
||||
enum class WheelScrollAdmission {
|
||||
|
|
|
|||
|
|
@ -255,6 +255,15 @@ public:
|
|||
&& (m_is_rasterizing || m_has_deferred_async_scroll_present || m_queued_rasterization_tasks > 0);
|
||||
}
|
||||
|
||||
void invalidate_wheel_event_listener_state(u64 generation)
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
m_wheel_event_listener_state_generation = max(m_wheel_event_listener_state_generation, generation);
|
||||
m_wheel_routing_admission = WheelRoutingAdmission::StaleWheelEventListeners;
|
||||
m_can_accept_async_wheel_events = false;
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Invalidated compositor wheel listener state (generation={})", generation);
|
||||
}
|
||||
|
||||
struct ViewportWheelTarget {
|
||||
Optional<AsyncScrollNodeID> node_id;
|
||||
bool rejected_non_viewport_target { false };
|
||||
|
|
@ -373,9 +382,14 @@ public:
|
|||
m_cached_scroll_state_snapshot = move(cmd.scroll_state_snapshot);
|
||||
if (cmd.async_scrolling_state.has_value()) {
|
||||
auto async_scrolling_state = cmd.async_scrolling_state.release_value();
|
||||
auto const wheel_event_listener_state_generation = async_scrolling_state.wheel_event_listener_state_generation;
|
||||
auto wheel_routing_admission = wheel_routing_admission_for(async_scrolling_state);
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
if (wheel_event_listener_state_generation < m_wheel_event_listener_state_generation)
|
||||
wheel_routing_admission = WheelRoutingAdmission::StaleWheelEventListeners;
|
||||
else
|
||||
m_wheel_event_listener_state_generation = wheel_event_listener_state_generation;
|
||||
m_wheel_routing_admission = wheel_routing_admission;
|
||||
m_async_scrolling_viewport_rect = async_scrolling_state.viewport_rect;
|
||||
}
|
||||
|
|
@ -716,6 +730,7 @@ private:
|
|||
Gfx::IntRect m_async_scrolling_viewport_rect;
|
||||
Atomic<bool> m_has_async_scrolling_state { false };
|
||||
Atomic<bool> m_can_accept_async_wheel_events { false };
|
||||
u64 m_wheel_event_listener_state_generation { 0 };
|
||||
WheelRoutingAdmission m_wheel_routing_admission { WheelRoutingAdmission::NoAsyncScrollingState };
|
||||
|
||||
public:
|
||||
|
|
@ -1004,6 +1019,11 @@ void CompositorThread::update_display_list_and_async_scrolling_state(NonnullRefP
|
|||
m_thread_data->enqueue_command(UpdateDisplayListCommand { move(display_list), move(scroll_state_snapshot), Optional<AsyncScrollingState> { move(async_scrolling_state) } });
|
||||
}
|
||||
|
||||
void CompositorThread::invalidate_wheel_event_listener_state(u64 generation)
|
||||
{
|
||||
m_thread_data->invalidate_wheel_event_listener_state(generation);
|
||||
}
|
||||
|
||||
bool CompositorThread::async_scroll_by(Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect)
|
||||
{
|
||||
return m_thread_data->enqueue_async_scroll_by(position, delta, viewport_rect);
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ public:
|
|||
void update_display_list(NonnullRefPtr<Painting::DisplayList>, Painting::ScrollStateSnapshot&&);
|
||||
void update_scroll_state(Painting::ScrollStateSnapshot&&);
|
||||
void update_display_list_and_async_scrolling_state(NonnullRefPtr<Painting::DisplayList>, Painting::ScrollStateSnapshot&&, AsyncScrollingState&&);
|
||||
void invalidate_wheel_event_listener_state(u64 generation);
|
||||
bool async_scroll_by(Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect);
|
||||
Optional<Gfx::FloatPoint> pending_async_viewport_scroll_offset() const;
|
||||
bool should_defer_async_viewport_scroll_offset_adoption() const;
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
#include <LibWeb/HTML/HTMLFrameSetElement.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
#include <LibWeb/HighResolutionTime/TimeOrigin.h>
|
||||
#include <LibWeb/Page/Page.h>
|
||||
#include <LibWeb/UIEvents/EventNames.h>
|
||||
#include <LibWeb/UIEvents/KeyCode.h>
|
||||
#include <LibWeb/UIEvents/KeyboardEvent.h>
|
||||
|
|
@ -186,6 +187,25 @@ static bool default_passive_value(FlyString const& type, EventTarget* event_targ
|
|||
return false;
|
||||
}
|
||||
|
||||
static bool is_blocking_wheel_event_listener(DOMEventListener const& listener)
|
||||
{
|
||||
return AK::first_is_one_of(listener.type, "wheel"sv, "mousewheel"sv) && listener.passive != true;
|
||||
}
|
||||
|
||||
static void invalidate_compositor_wheel_event_listener_state(EventTarget& event_target, DOMEventListener const& listener)
|
||||
{
|
||||
if (!is_blocking_wheel_event_listener(listener))
|
||||
return;
|
||||
|
||||
if (auto* window = as_if<HTML::Window>(event_target)) {
|
||||
window->associated_document().page().invalidate_compositor_wheel_event_listener_state();
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto* node = as_if<Node>(event_target))
|
||||
node->document().page().invalidate_compositor_wheel_event_listener_state();
|
||||
}
|
||||
|
||||
// https://dom.spec.whatwg.org/#dom-eventtarget-addeventlistener
|
||||
void EventTarget::add_event_listener(FlyString const& type, IDLEventListener* callback, Variant<Bindings::AddEventListenerOptions, bool> const& options)
|
||||
{
|
||||
|
|
@ -239,8 +259,10 @@ void EventTarget::add_an_event_listener(DOMEventListener& listener)
|
|||
&& entry->callback->callback().callback == listener.callback->callback().callback
|
||||
&& entry->capture == listener.capture;
|
||||
});
|
||||
if (it == event_listener_list.end())
|
||||
if (it == event_listener_list.end()) {
|
||||
event_listener_list.append(listener);
|
||||
invalidate_compositor_wheel_event_listener_state(*this, listener);
|
||||
}
|
||||
|
||||
// 6. If listener’s signal is not null, then add the following abort steps to it:
|
||||
if (listener.signal) {
|
||||
|
|
@ -292,7 +314,9 @@ void EventTarget::remove_an_event_listener(DOMEventListener& listener)
|
|||
// 2. Set listener’s removed to true and remove listener from eventTarget’s event listener list.
|
||||
listener.removed = true;
|
||||
VERIFY(m_data);
|
||||
m_data->event_listener_list.remove_first_matching([&](auto& entry) { return entry.ptr() == &listener; });
|
||||
auto did_remove = m_data->event_listener_list.remove_first_matching([&](auto& entry) { return entry.ptr() == &listener; });
|
||||
if (did_remove)
|
||||
invalidate_compositor_wheel_event_listener_state(*this, listener);
|
||||
}
|
||||
|
||||
// https://dom.spec.whatwg.org/#dom-eventtarget-dispatchevent
|
||||
|
|
|
|||
|
|
@ -642,6 +642,7 @@ JS::Object* Internals::async_scrolling_state()
|
|||
object->define_direct_property("hasBlockingWheelEventListeners"_utf16_fly_string, JS::Value(false), JS::default_attributes);
|
||||
object->define_direct_property("blockingWheelEventRegionCount"_utf16_fly_string, JS::Value(0), JS::default_attributes);
|
||||
object->define_direct_property("mainThreadWheelEventRegionCount"_utf16_fly_string, JS::Value(0), JS::default_attributes);
|
||||
object->define_direct_property("wheelEventListenerStateGeneration"_utf16_fly_string, JS::Value(0), JS::default_attributes);
|
||||
object->define_direct_property("blockingWheelEventRegionsAreCurrent"_utf16_fly_string, JS::Value(false), JS::default_attributes);
|
||||
object->define_direct_property("hasBlockingWheelEventRegionCoveringViewport"_utf16_fly_string, JS::Value(false), JS::default_attributes);
|
||||
return object;
|
||||
|
|
@ -685,6 +686,7 @@ JS::Object* Internals::async_scrolling_state()
|
|||
object->define_direct_property("hasBlockingWheelEventListeners"_utf16_fly_string, JS::Value(state.has_blocking_wheel_event_listeners), JS::default_attributes);
|
||||
object->define_direct_property("blockingWheelEventRegionCount"_utf16_fly_string, JS::Value(state.blocking_wheel_event_regions.size()), JS::default_attributes);
|
||||
object->define_direct_property("mainThreadWheelEventRegionCount"_utf16_fly_string, JS::Value(state.main_thread_wheel_event_regions.size()), JS::default_attributes);
|
||||
object->define_direct_property("wheelEventListenerStateGeneration"_utf16_fly_string, JS::Value(state.wheel_event_listener_state_generation), JS::default_attributes);
|
||||
object->define_direct_property("blockingWheelEventRegionsAreCurrent"_utf16_fly_string, JS::Value(state.blocking_wheel_event_regions_are_current), JS::default_attributes);
|
||||
object->define_direct_property("hasBlockingWheelEventRegionCoveringViewport"_utf16_fly_string, JS::Value(state.has_blocking_wheel_event_region_covering_viewport), JS::default_attributes);
|
||||
return object;
|
||||
|
|
|
|||
|
|
@ -297,6 +297,16 @@ void Page::handle_sdl_input_events()
|
|||
top_level_traversable()->event_handler().handle_sdl_input_events();
|
||||
}
|
||||
|
||||
void Page::invalidate_compositor_wheel_event_listener_state()
|
||||
{
|
||||
++m_wheel_event_listener_state_generation;
|
||||
|
||||
if (!m_async_scrolling_enabled || !top_level_traversable_is_initialized())
|
||||
return;
|
||||
|
||||
top_level_traversable()->rendering_thread().invalidate_wheel_event_listener_state(m_wheel_event_listener_state_generation);
|
||||
}
|
||||
|
||||
void Page::set_top_level_traversable(GC::Ref<HTML::TraversableNavigable> navigable)
|
||||
{
|
||||
VERIFY(!m_top_level_traversable); // Replacement is not allowed!
|
||||
|
|
|
|||
|
|
@ -134,6 +134,8 @@ public:
|
|||
|
||||
bool async_scrolling_enabled() const { return m_async_scrolling_enabled; }
|
||||
void set_async_scrolling_enabled(bool b) { m_async_scrolling_enabled = b; }
|
||||
u64 wheel_event_listener_state_generation() const { return m_wheel_event_listener_state_generation; }
|
||||
void invalidate_compositor_wheel_event_listener_state();
|
||||
|
||||
bool is_webdriver_active() const { return m_is_webdriver_active; }
|
||||
void set_is_webdriver_active(bool b) { m_is_webdriver_active = b; }
|
||||
|
|
@ -294,6 +296,7 @@ private:
|
|||
bool m_should_block_pop_ups { true };
|
||||
bool m_enable_autoscroll { true };
|
||||
bool m_async_scrolling_enabled { false };
|
||||
u64 m_wheel_event_listener_state_generation { 0 };
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-webdriver-active-flag
|
||||
// The webdriver-active flag is set to true when the user agent is under remote control. It is initially false.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
before routing admission: accepted
|
||||
listener generation increased: true
|
||||
after routing admission: blocking wheel event listeners
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="../include.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#spacer {
|
||||
height: 2000px;
|
||||
}
|
||||
</style>
|
||||
<div id="spacer"></div>
|
||||
<script>
|
||||
test(() => {
|
||||
const before = internals.asyncScrollingState();
|
||||
println(`before routing admission: ${internals.asyncScrollingStateWheelRoutingAdmission()}`);
|
||||
|
||||
document.addEventListener("wheel", event => event.preventDefault(), { passive: false });
|
||||
|
||||
const after = internals.asyncScrollingState();
|
||||
println(`listener generation increased: ${after.wheelEventListenerStateGeneration > before.wheelEventListenerStateGeneration}`);
|
||||
println(`after routing admission: ${internals.asyncScrollingStateWheelRoutingAdmission()}`);
|
||||
});
|
||||
</script>
|
||||
Loading…
Reference in a new issue