LibWeb: Scroll the viewport on the compositor thread
Use the snapshot from the previous commit to let CompositorThread apply experimental viewport wheel deltas when async scrolling is enabled. The event handler first performs synchronous admission on the main thread, then enqueues a compositor scroll command instead of mutating live document scroll state directly. Rasterize accepted scrolls through the same compositor presentation path added earlier. The compositor stores the newest async viewport offset so the next main-thread display-list recording can adopt it before repainting, preventing older paints from snapping the visible position backward. Keep DOM wheel dispatch on the main thread. When the compositor already performed the default action, dispatch the wheel as non-cancelable and suppress a second default scroll. Non-viewport targets, nested scrollers, and pages with blocking wheel listeners stay synchronous.
This commit is contained in:
parent
191bd46cb8
commit
f07b55c2df
17 changed files with 586 additions and 76 deletions
|
|
@ -11,10 +11,13 @@
|
|||
#include <LibGfx/SharedImageBuffer.h>
|
||||
#include <LibGfx/SkiaBackendContext.h>
|
||||
#include <LibThreading/Thread.h>
|
||||
#include <LibWeb/Compositor/AsyncScrollTree.h>
|
||||
#include <LibWeb/Compositor/AsyncScrollingState.h>
|
||||
#include <LibWeb/Compositor/CompositorThread.h>
|
||||
#include <LibWeb/Painting/DisplayListPlayerSkia.h>
|
||||
#include <LibWeb/Painting/ExternalContentSource.h>
|
||||
|
||||
#include <AK/Debug.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/NeverDestroyed.h>
|
||||
#include <AK/Queue.h>
|
||||
|
|
@ -50,6 +53,13 @@ struct BackingStoreState {
|
|||
struct UpdateDisplayListCommand {
|
||||
NonnullRefPtr<Painting::DisplayList> display_list;
|
||||
Painting::ScrollStateSnapshot scroll_state_snapshot;
|
||||
Optional<AsyncScrollingState> async_scrolling_state;
|
||||
};
|
||||
|
||||
struct AsyncScrollByCommand {
|
||||
Gfx::FloatPoint position;
|
||||
Gfx::FloatPoint delta;
|
||||
Gfx::IntRect viewport_rect;
|
||||
};
|
||||
|
||||
struct UpdateScrollStateCommand {
|
||||
|
|
@ -67,7 +77,57 @@ struct ScreenshotCommand {
|
|||
Function<void()> callback;
|
||||
};
|
||||
|
||||
using CompositorCommand = Variant<UpdateDisplayListCommand, UpdateScrollStateCommand, UpdateBackingStoresCommand, ScreenshotCommand>;
|
||||
using CompositorCommand = Variant<UpdateDisplayListCommand, AsyncScrollByCommand, UpdateScrollStateCommand, UpdateBackingStoresCommand, ScreenshotCommand>;
|
||||
|
||||
static Optional<AsyncScrollNode> viewport_scroll_node(AsyncScrollingState const& state)
|
||||
{
|
||||
for (auto const& node : state.scroll_nodes) {
|
||||
if (node.is_viewport)
|
||||
return node;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
enum class WheelRoutingAdmission {
|
||||
Accepted,
|
||||
NoAsyncScrollingState,
|
||||
BlockingWheelEventListeners,
|
||||
NoViewportScrollNode,
|
||||
HasNonViewportScrollNode,
|
||||
};
|
||||
|
||||
[[maybe_unused]] static StringView wheel_routing_admission_to_string(WheelRoutingAdmission admission)
|
||||
{
|
||||
switch (admission) {
|
||||
case WheelRoutingAdmission::Accepted:
|
||||
return "accepted"sv;
|
||||
case WheelRoutingAdmission::NoAsyncScrollingState:
|
||||
return "no async scrolling state"sv;
|
||||
case WheelRoutingAdmission::BlockingWheelEventListeners:
|
||||
return "blocking wheel event listeners"sv;
|
||||
case WheelRoutingAdmission::NoViewportScrollNode:
|
||||
return "no viewport scroll node"sv;
|
||||
case WheelRoutingAdmission::HasNonViewportScrollNode:
|
||||
return "has non-viewport scroll node"sv;
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
static WheelRoutingAdmission wheel_routing_admission_for(AsyncScrollingState const& state)
|
||||
{
|
||||
if (state.has_blocking_wheel_event_listeners)
|
||||
return WheelRoutingAdmission::BlockingWheelEventListeners;
|
||||
|
||||
bool found_viewport_node = false;
|
||||
for (auto const& node : state.scroll_nodes) {
|
||||
if (!node.is_viewport)
|
||||
return WheelRoutingAdmission::HasNonViewportScrollNode;
|
||||
found_viewport_node = true;
|
||||
}
|
||||
if (!found_viewport_node)
|
||||
return WheelRoutingAdmission::NoViewportScrollNode;
|
||||
return WheelRoutingAdmission::Accepted;
|
||||
}
|
||||
|
||||
struct BackingStorePair {
|
||||
RefPtr<Gfx::PaintingSurface> front;
|
||||
|
|
@ -178,6 +238,8 @@ public:
|
|||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
m_command_queue.enqueue(move(command));
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor command queued (queue_size={}, raster_tasks={}, needs_present={}, deferred_async_present={})",
|
||||
m_command_queue.size(), m_queued_rasterization_tasks.load(), m_needs_present, m_has_deferred_async_scroll_present);
|
||||
m_command_ready.signal();
|
||||
}
|
||||
|
||||
|
|
@ -205,6 +267,73 @@ public:
|
|||
m_frame_completed.wait();
|
||||
}
|
||||
|
||||
Optional<Gfx::FloatPoint> take_pending_async_viewport_scroll_offset()
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
auto scroll_offset = m_pending_async_viewport_scroll_offset;
|
||||
m_pending_async_viewport_scroll_offset.clear();
|
||||
return scroll_offset;
|
||||
}
|
||||
|
||||
Optional<Gfx::FloatPoint> pending_async_viewport_scroll_offset() const
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
return m_pending_async_viewport_scroll_offset;
|
||||
}
|
||||
|
||||
bool should_defer_async_viewport_scroll_offset_adoption() const
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
return m_pending_async_viewport_scroll_offset.has_value()
|
||||
&& m_is_rasterizing;
|
||||
}
|
||||
|
||||
bool should_defer_main_thread_present_for_async_scroll() const
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
return m_pending_async_viewport_scroll_offset.has_value()
|
||||
&& (m_is_rasterizing || m_has_deferred_async_scroll_present || m_queued_rasterization_tasks > 0);
|
||||
}
|
||||
|
||||
bool enqueue_async_scroll_by(Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect)
|
||||
{
|
||||
if (!m_can_accept_async_wheel_events.load()) {
|
||||
auto wheel_routing_admission = [this] {
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
return m_wheel_routing_admission;
|
||||
}();
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Rejecting async scroll enqueue: compositor cannot accept async wheel events ({})",
|
||||
wheel_routing_admission_to_string(wheel_routing_admission));
|
||||
return false;
|
||||
}
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor accepted main-thread async scroll enqueue at {},{} delta {},{} viewport={}x{} at {},{}",
|
||||
position.x(), position.y(), delta.x(), delta.y(), viewport_rect.width(), viewport_rect.height(), viewport_rect.x(), viewport_rect.y());
|
||||
enqueue_command(AsyncScrollByCommand { position, delta, viewport_rect });
|
||||
return true;
|
||||
}
|
||||
|
||||
bool enqueue_async_scroll_by(Gfx::FloatPoint position, Gfx::FloatPoint delta)
|
||||
{
|
||||
if (!m_can_accept_async_wheel_events.load()) {
|
||||
auto wheel_routing_admission = [this] {
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
return m_wheel_routing_admission;
|
||||
}();
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Rejecting async scroll enqueue: compositor cannot accept async wheel events ({})",
|
||||
wheel_routing_admission_to_string(wheel_routing_admission));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto viewport_rect = [this] {
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
return m_async_scrolling_viewport_rect;
|
||||
}();
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor accepted async scroll enqueue at {},{} delta {},{} viewport={}x{} at {},{}",
|
||||
position.x(), position.y(), delta.x(), delta.y(), viewport_rect.width(), viewport_rect.height(), viewport_rect.x(), viewport_rect.y());
|
||||
enqueue_command(AsyncScrollByCommand { position, delta, viewport_rect });
|
||||
return true;
|
||||
}
|
||||
|
||||
void compositor_loop(DisplayListPlayerType display_list_player_type)
|
||||
{
|
||||
initialize_skia_player(display_list_player_type);
|
||||
|
|
@ -212,7 +341,7 @@ public:
|
|||
while (true) {
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
while (m_command_queue.is_empty() && !m_needs_present && !m_exit) {
|
||||
while (m_command_queue.is_empty() && !has_presentable_work() && !m_exit) {
|
||||
m_command_ready.wait();
|
||||
}
|
||||
if (m_exit)
|
||||
|
|
@ -228,21 +357,105 @@ public:
|
|||
Sync::MutexLocker const locker { m_mutex };
|
||||
if (m_command_queue.is_empty())
|
||||
return {};
|
||||
return m_command_queue.dequeue();
|
||||
auto command = m_command_queue.dequeue();
|
||||
return command;
|
||||
}();
|
||||
|
||||
if (!command.has_value())
|
||||
break;
|
||||
|
||||
bool should_yield_to_async_scroll_present = false;
|
||||
command->visit(
|
||||
[this](UpdateDisplayListCommand& cmd) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor processing display list update (has_async_state={}, raster_tasks={}, deferred_async_present={})",
|
||||
cmd.async_scrolling_state.has_value(), m_queued_rasterization_tasks.load(), m_has_deferred_async_scroll_present);
|
||||
m_cached_display_list = move(cmd.display_list);
|
||||
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 wheel_routing_admission = wheel_routing_admission_for(async_scrolling_state);
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
m_wheel_routing_admission = wheel_routing_admission;
|
||||
m_async_scrolling_viewport_rect = async_scrolling_state.viewport_rect;
|
||||
}
|
||||
m_can_accept_async_wheel_events = wheel_routing_admission == WheelRoutingAdmission::Accepted;
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor wheel routing admission: {} (scroll_nodes={}, sticky_areas={}, blocking_regions={})",
|
||||
wheel_routing_admission_to_string(wheel_routing_admission),
|
||||
async_scrolling_state.scroll_nodes.size(),
|
||||
async_scrolling_state.sticky_areas.size(),
|
||||
async_scrolling_state.blocking_wheel_event_regions.size());
|
||||
auto viewport_node = viewport_scroll_node(async_scrolling_state);
|
||||
auto pending_async_viewport_scroll_offset = [this] {
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
return m_pending_async_viewport_scroll_offset;
|
||||
}();
|
||||
|
||||
m_async_scroll_tree.set_state(move(async_scrolling_state));
|
||||
if (viewport_node.has_value() && pending_async_viewport_scroll_offset.has_value()) {
|
||||
auto delta = pending_async_viewport_scroll_offset->translated(-viewport_node->scroll_offset.x(), -viewport_node->scroll_offset.y());
|
||||
if (delta.x() != 0 || delta.y() != 0) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Reapplying pending async viewport offset {},{} to display list update",
|
||||
pending_async_viewport_scroll_offset->x(), pending_async_viewport_scroll_offset->y());
|
||||
m_async_scroll_tree.apply_scroll_delta(viewport_node->node_id, delta, m_cached_scroll_state_snapshot);
|
||||
}
|
||||
}
|
||||
m_async_scroll_tree.rebuild_wheel_scroll_targets(m_cached_display_list, m_cached_scroll_state_snapshot);
|
||||
m_has_async_scrolling_state = true;
|
||||
} else {
|
||||
m_async_scroll_tree.clear_wheel_scroll_targets();
|
||||
m_has_async_scrolling_state = false;
|
||||
m_can_accept_async_wheel_events = false;
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
m_wheel_routing_admission = WheelRoutingAdmission::NoAsyncScrollingState;
|
||||
}
|
||||
}
|
||||
},
|
||||
[this, &should_yield_to_async_scroll_present](AsyncScrollByCommand& cmd) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor processing async scroll command at {},{} delta {},{} (raster_tasks={}, deferred_async_present={})",
|
||||
cmd.position.x(), cmd.position.y(), cmd.delta.x(), cmd.delta.y(), m_queued_rasterization_tasks.load(), m_has_deferred_async_scroll_present);
|
||||
auto async_scroll_viewport_rect = cmd.viewport_rect;
|
||||
auto scroll_target = m_async_scroll_tree.viewport_scroll_node_for_delta(cmd.delta);
|
||||
if (!scroll_target.has_value()) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Dropping async scroll command: no viewport node can scroll by delta {},{}",
|
||||
cmd.delta.x(), cmd.delta.y());
|
||||
return;
|
||||
}
|
||||
if (!m_async_scroll_tree.apply_scroll_delta(*scroll_target, cmd.delta, m_cached_scroll_state_snapshot)) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Dropping async scroll command: scroll tree consumed no delta");
|
||||
return;
|
||||
}
|
||||
if (auto scroll_offset = m_async_scroll_tree.scroll_offset_for_node(*scroll_target); scroll_offset.has_value()) {
|
||||
async_scroll_viewport_rect.set_location(scroll_offset->to_type<int>());
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
m_pending_async_viewport_scroll_offset = *scroll_offset;
|
||||
m_async_scrolling_viewport_rect = async_scroll_viewport_rect;
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Stored pending async viewport offset {},{}",
|
||||
scroll_offset->x(), scroll_offset->y());
|
||||
}
|
||||
m_async_scroll_tree.rebuild_wheel_scroll_targets(m_cached_display_list, m_cached_scroll_state_snapshot);
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
m_has_deferred_async_scroll_present = true;
|
||||
m_deferred_async_scroll_present_viewport_rect = async_scroll_viewport_rect;
|
||||
}
|
||||
should_yield_to_async_scroll_present = can_present_deferred_async_scroll();
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor async scroll command complete (yield_to_present={}, raster_tasks={})",
|
||||
should_yield_to_async_scroll_present, m_queued_rasterization_tasks.load());
|
||||
},
|
||||
[this](UpdateScrollStateCommand& cmd) {
|
||||
m_cached_scroll_state_snapshot = move(cmd.scroll_state_snapshot);
|
||||
if (m_has_async_scrolling_state) {
|
||||
Sync::MutexLocker const locker { m_async_scroll_tree_mutex };
|
||||
m_async_scroll_tree.rebuild_wheel_scroll_targets(m_cached_display_list, m_cached_scroll_state_snapshot);
|
||||
}
|
||||
},
|
||||
[this](UpdateBackingStoresCommand& cmd) {
|
||||
if (m_has_async_scrolling_state) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor received backing stores front={} back={} size={}x{}",
|
||||
cmd.front_bitmap_id, cmd.back_bitmap_id, cmd.size.width(), cmd.size.height());
|
||||
}
|
||||
allocate_backing_stores(cmd);
|
||||
m_backing_stores.front_bitmap_id = cmd.front_bitmap_id;
|
||||
m_backing_stores.back_bitmap_id = cmd.back_bitmap_id;
|
||||
|
|
@ -260,6 +473,10 @@ public:
|
|||
|
||||
if (m_exit)
|
||||
break;
|
||||
if (should_yield_to_async_scroll_present) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor yielding command drain to present async scroll");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_exit)
|
||||
|
|
@ -268,9 +485,22 @@ public:
|
|||
bool should_present = false;
|
||||
Gfx::IntRect viewport_rect;
|
||||
u64 presenting_frame_id = 0;
|
||||
bool should_present_deferred_async_scroll = false;
|
||||
Gfx::IntRect deferred_async_scroll_viewport_rect;
|
||||
Optional<u64> deferred_async_scroll_presenting_frame_id;
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
if (m_needs_present) {
|
||||
if (m_has_deferred_async_scroll_present && m_queued_rasterization_tasks == 0) {
|
||||
should_present_deferred_async_scroll = true;
|
||||
deferred_async_scroll_viewport_rect = m_deferred_async_scroll_present_viewport_rect;
|
||||
m_has_deferred_async_scroll_present = false;
|
||||
if (m_needs_present) {
|
||||
deferred_async_scroll_presenting_frame_id = m_submitted_frame_id;
|
||||
m_needs_present = false;
|
||||
}
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor selected deferred async present (raster_tasks={}, pending_main_thread_present={})",
|
||||
m_queued_rasterization_tasks.load(), deferred_async_scroll_presenting_frame_id.has_value());
|
||||
} else if (m_needs_present && m_queued_rasterization_tasks == 0) {
|
||||
should_present = true;
|
||||
viewport_rect = m_pending_viewport_rect;
|
||||
presenting_frame_id = m_submitted_frame_id;
|
||||
|
|
@ -278,53 +508,111 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
if (should_present) {
|
||||
// Block if we already have a frame queued (back pressure).
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
while (m_queued_rasterization_tasks > 0 && !m_exit) {
|
||||
m_ready_to_paint.wait();
|
||||
}
|
||||
if (m_exit)
|
||||
break;
|
||||
}
|
||||
|
||||
auto presentation_mode = [this] {
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
return m_presentation_mode;
|
||||
}();
|
||||
|
||||
if (m_cached_display_list && m_backing_stores.is_valid()) {
|
||||
auto should_clear_back_store = presentation_mode.visit(
|
||||
[](CompositorThread::PresentToUI) { return false; },
|
||||
[](CompositorThread::PublishToExternalContent const&) { return true; });
|
||||
if (should_clear_back_store) {
|
||||
// Embedded navigables leave their PaintConfig canvas unfilled, so double-buffered back stores
|
||||
// must be cleared before repainting.
|
||||
m_backing_stores.back_store->canvas().clear(SK_ColorTRANSPARENT);
|
||||
}
|
||||
m_skia_player->execute(*m_cached_display_list, m_cached_scroll_state_snapshot, *m_backing_stores.back_store);
|
||||
i32 rendered_bitmap_id = m_backing_stores.back_bitmap_id;
|
||||
m_backing_stores.swap();
|
||||
|
||||
presentation_mode.visit(
|
||||
[this, viewport_rect, rendered_bitmap_id](CompositorThread::PresentToUI) {
|
||||
if (m_presents_to_client) {
|
||||
finish_rasterizing(rendered_bitmap_id);
|
||||
VERIFY(CompositorThread::present_frame_to_client(m_page_id, viewport_rect, rendered_bitmap_id));
|
||||
}
|
||||
},
|
||||
[this](CompositorThread::PublishToExternalContent const& mode) {
|
||||
auto snapshot = Gfx::DecodedImageFrame { *m_backing_stores.front_store->snapshot_bitmap() };
|
||||
mode.source->update(move(snapshot));
|
||||
});
|
||||
}
|
||||
mark_frame_complete(presenting_frame_id);
|
||||
}
|
||||
if (should_present_deferred_async_scroll)
|
||||
present_frame(deferred_async_scroll_viewport_rect, deferred_async_scroll_presenting_frame_id, PresentFrameDelivery::AsyncScroll);
|
||||
else if (should_present)
|
||||
present_frame(viewport_rect, presenting_frame_id, PresentFrameDelivery::MainThread);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
enum class PresentFrameDelivery {
|
||||
MainThread,
|
||||
AsyncScroll,
|
||||
};
|
||||
|
||||
bool has_presentable_work() const
|
||||
{
|
||||
return (m_has_deferred_async_scroll_present && m_queued_rasterization_tasks == 0)
|
||||
|| (m_needs_present && m_queued_rasterization_tasks == 0);
|
||||
}
|
||||
|
||||
bool can_present_deferred_async_scroll() const
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
return m_has_deferred_async_scroll_present && m_queued_rasterization_tasks == 0;
|
||||
}
|
||||
|
||||
void present_frame(Gfx::IntRect viewport_rect, Optional<u64> presenting_frame_id = {}, PresentFrameDelivery delivery = PresentFrameDelivery::MainThread)
|
||||
{
|
||||
auto delivery_name = delivery == PresentFrameDelivery::AsyncScroll ? "compositor-thread"sv : "main-thread"sv;
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Begin {} present (frame={}, raster_tasks={}, viewport={}x{} at {},{})",
|
||||
delivery_name, presenting_frame_id.value_or(0), m_queued_rasterization_tasks.load(), viewport_rect.width(), viewport_rect.height(), viewport_rect.x(), viewport_rect.y());
|
||||
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
if (delivery == PresentFrameDelivery::AsyncScroll && m_queued_rasterization_tasks > 0) {
|
||||
m_has_deferred_async_scroll_present = true;
|
||||
m_deferred_async_scroll_present_viewport_rect = viewport_rect;
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Deferring async scroll present until a backing store is ready (raster_tasks={})",
|
||||
m_queued_rasterization_tasks.load());
|
||||
return;
|
||||
}
|
||||
VERIFY(m_queued_rasterization_tasks == 0);
|
||||
if (m_exit)
|
||||
return;
|
||||
m_is_rasterizing = true;
|
||||
}
|
||||
|
||||
auto presentation_mode = [this] {
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
return m_presentation_mode;
|
||||
}();
|
||||
|
||||
if (m_cached_display_list && m_backing_stores.is_valid()) {
|
||||
auto should_clear_back_store = presentation_mode.visit(
|
||||
[](CompositorThread::PresentToUI) { return false; },
|
||||
[](CompositorThread::PublishToExternalContent const&) { return true; });
|
||||
if (should_clear_back_store) {
|
||||
// Embedded navigables leave their PaintConfig canvas unfilled, so double-buffered back stores must be
|
||||
// cleared before repainting.
|
||||
m_backing_stores.back_store->canvas().clear(SK_ColorTRANSPARENT);
|
||||
}
|
||||
m_skia_player->execute(*m_cached_display_list, m_cached_scroll_state_snapshot, *m_backing_stores.back_store);
|
||||
i32 rendered_bitmap_id = m_backing_stores.back_bitmap_id;
|
||||
m_backing_stores.swap();
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Finished {} display-list replay into bitmap {}",
|
||||
delivery_name, rendered_bitmap_id);
|
||||
|
||||
presentation_mode.visit(
|
||||
[this, viewport_rect, rendered_bitmap_id, delivery](CompositorThread::PresentToUI) {
|
||||
if (m_presents_to_client) {
|
||||
finish_rasterizing(rendered_bitmap_id);
|
||||
if (delivery == PresentFrameDelivery::AsyncScroll) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Finished async scroll raster into bitmap {} (raster_tasks={})",
|
||||
rendered_bitmap_id, m_queued_rasterization_tasks.load());
|
||||
}
|
||||
VERIFY(CompositorThread::present_frame_to_client(m_page_id, viewport_rect, rendered_bitmap_id));
|
||||
} else {
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
m_is_rasterizing = false;
|
||||
}
|
||||
},
|
||||
[this](CompositorThread::PublishToExternalContent const& mode) {
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
m_is_rasterizing = false;
|
||||
}
|
||||
if (m_has_async_scrolling_state)
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Publishing present to external content source");
|
||||
auto snapshot = Gfx::DecodedImageFrame { *m_backing_stores.front_store->snapshot_bitmap() };
|
||||
mode.source->update(move(snapshot));
|
||||
});
|
||||
} else {
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
m_is_rasterizing = false;
|
||||
}
|
||||
if (m_has_async_scrolling_state) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Skipping {} present: cached_display_list={}, backing_stores_valid={}",
|
||||
delivery_name, !!m_cached_display_list, m_backing_stores.is_valid());
|
||||
}
|
||||
}
|
||||
|
||||
if (presenting_frame_id.has_value())
|
||||
mark_frame_complete(*presenting_frame_id);
|
||||
}
|
||||
|
||||
void initialize_skia_player(DisplayListPlayerType display_list_player_type)
|
||||
{
|
||||
switch (display_list_player_type) {
|
||||
|
|
@ -368,6 +656,10 @@ private:
|
|||
m_backing_stores.front_store = move(backing_store_pair.front);
|
||||
m_backing_stores.back_store = move(backing_store_pair.back);
|
||||
publish_backing_store_pair(cmd, move(front_shared_image), move(back_shared_image));
|
||||
if (m_has_async_scrolling_state) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Allocated bitmap backing stores front={} back={} size={}x{}",
|
||||
cmd.front_bitmap_id, cmd.back_bitmap_id, cmd.size.width(), cmd.size.height());
|
||||
}
|
||||
}
|
||||
|
||||
template<typename Invokee>
|
||||
|
|
@ -397,24 +689,34 @@ private:
|
|||
RefPtr<Gfx::SkiaBackendContext> m_skia_backend_context;
|
||||
RefPtr<Painting::DisplayList> m_cached_display_list;
|
||||
Painting::ScrollStateSnapshot m_cached_scroll_state_snapshot;
|
||||
AsyncScrollTree m_async_scroll_tree;
|
||||
BackingStoreState m_backing_stores;
|
||||
CompositorThread::PresentationMode m_presentation_mode { CompositorThread::PresentToUI {} };
|
||||
|
||||
Atomic<i32> m_queued_rasterization_tasks { 0 };
|
||||
Optional<i32> m_presented_bitmap_id_awaiting_ack;
|
||||
mutable Sync::ConditionVariable m_ready_to_paint { m_mutex };
|
||||
bool m_is_rasterizing { false };
|
||||
|
||||
bool m_needs_present { false };
|
||||
Gfx::IntRect m_pending_viewport_rect;
|
||||
bool m_has_deferred_async_scroll_present { false };
|
||||
Gfx::IntRect m_deferred_async_scroll_present_viewport_rect;
|
||||
|
||||
u64 m_submitted_frame_id { 0 };
|
||||
u64 m_completed_frame_id { 0 };
|
||||
mutable Sync::ConditionVariable m_frame_completed { m_mutex };
|
||||
Optional<Gfx::FloatPoint> m_pending_async_viewport_scroll_offset;
|
||||
Gfx::IntRect m_async_scrolling_viewport_rect;
|
||||
Atomic<bool> m_has_async_scrolling_state { false };
|
||||
Atomic<bool> m_can_accept_async_wheel_events { false };
|
||||
WheelRoutingAdmission m_wheel_routing_admission { WheelRoutingAdmission::NoAsyncScrollingState };
|
||||
|
||||
public:
|
||||
void finish_rasterizing(i32 bitmap_id)
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
m_is_rasterizing = false;
|
||||
VERIFY(!m_presented_bitmap_id_awaiting_ack.has_value());
|
||||
m_presented_bitmap_id_awaiting_ack = bitmap_id;
|
||||
m_queued_rasterization_tasks++;
|
||||
|
|
@ -424,8 +726,11 @@ public:
|
|||
void decrement_queued_tasks(i32 bitmap_id)
|
||||
{
|
||||
Sync::MutexLocker const locker { m_mutex };
|
||||
if (m_presented_bitmap_id_awaiting_ack != bitmap_id)
|
||||
if (m_presented_bitmap_id_awaiting_ack != bitmap_id) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Ignoring stale ready_to_paint for bitmap {} while awaiting bitmap {} (raster_tasks={})",
|
||||
bitmap_id, m_presented_bitmap_id_awaiting_ack.value_or(-1), m_queued_rasterization_tasks.load());
|
||||
return;
|
||||
}
|
||||
|
||||
VERIFY(m_queued_rasterization_tasks == 1);
|
||||
m_presented_bitmap_id_awaiting_ack.clear();
|
||||
|
|
@ -586,6 +891,21 @@ void CompositorThread::presented_bitmap_ready_to_paint(u64 page_id, i32 bitmap_i
|
|||
thread_data->decrement_queued_tasks(bitmap_id);
|
||||
}
|
||||
|
||||
bool CompositorThread::async_scroll_by(u64 page_id, Gfx::FloatPoint position, Gfx::FloatPoint delta)
|
||||
{
|
||||
RefPtr<ThreadData> thread_data;
|
||||
{
|
||||
Sync::MutexLocker const locker { compositor_presentation_state_mutex() };
|
||||
auto compositor = page_compositors().find(page_id);
|
||||
if (compositor == page_compositors().end()) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Async scroll cannot scroll page {}: no compositor registered", page_id);
|
||||
return false;
|
||||
}
|
||||
thread_data = compositor->value;
|
||||
}
|
||||
return thread_data->enqueue_async_scroll_by(position, delta);
|
||||
}
|
||||
|
||||
void CompositorThread::start(DisplayListPlayerType display_list_player_type)
|
||||
{
|
||||
m_thread = Threading::Thread::construct("Compositor"sv, [thread_data = m_thread_data, display_list_player_type] {
|
||||
|
|
@ -609,7 +929,37 @@ void CompositorThread::stop_presenting_to_client()
|
|||
|
||||
void CompositorThread::update_display_list(NonnullRefPtr<Painting::DisplayList> display_list, Painting::ScrollStateSnapshot&& scroll_state_snapshot)
|
||||
{
|
||||
m_thread_data->enqueue_command(UpdateDisplayListCommand { move(display_list), move(scroll_state_snapshot) });
|
||||
m_thread_data->enqueue_command(UpdateDisplayListCommand { move(display_list), move(scroll_state_snapshot), {} });
|
||||
}
|
||||
|
||||
void CompositorThread::update_display_list_and_async_scrolling_state(NonnullRefPtr<Painting::DisplayList> display_list, Painting::ScrollStateSnapshot&& scroll_state_snapshot, AsyncScrollingState&& async_scrolling_state)
|
||||
{
|
||||
m_thread_data->enqueue_command(UpdateDisplayListCommand { move(display_list), move(scroll_state_snapshot), Optional<AsyncScrollingState> { move(async_scrolling_state) } });
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Optional<Gfx::FloatPoint> CompositorThread::pending_async_viewport_scroll_offset() const
|
||||
{
|
||||
return m_thread_data->pending_async_viewport_scroll_offset();
|
||||
}
|
||||
|
||||
bool CompositorThread::should_defer_async_viewport_scroll_offset_adoption() const
|
||||
{
|
||||
return m_thread_data->should_defer_async_viewport_scroll_offset_adoption();
|
||||
}
|
||||
|
||||
bool CompositorThread::should_defer_main_thread_present_for_async_scroll() const
|
||||
{
|
||||
return m_thread_data->should_defer_main_thread_present_for_async_scroll();
|
||||
}
|
||||
|
||||
Optional<Gfx::FloatPoint> CompositorThread::take_pending_async_viewport_scroll_offset()
|
||||
{
|
||||
return m_thread_data->take_pending_async_viewport_scroll_offset();
|
||||
}
|
||||
|
||||
void CompositorThread::update_scroll_state(Painting::ScrollStateSnapshot&& scroll_state_snapshot)
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@
|
|||
|
||||
#include <AK/Noncopyable.h>
|
||||
#include <AK/NonnullRefPtr.h>
|
||||
#include <AK/Optional.h>
|
||||
#include <AK/Variant.h>
|
||||
#include <LibCore/Forward.h>
|
||||
#include <LibGfx/Point.h>
|
||||
#include <LibGfx/Rect.h>
|
||||
#include <LibGfx/SharedImage.h>
|
||||
#include <LibSync/ConditionVariable.h>
|
||||
|
|
@ -20,6 +22,8 @@
|
|||
|
||||
namespace Web::Compositor {
|
||||
|
||||
struct AsyncScrollingState;
|
||||
|
||||
class WEB_API CompositorThread {
|
||||
AK_MAKE_NONCOPYABLE(CompositorThread);
|
||||
AK_MAKE_NONMOVABLE(CompositorThread);
|
||||
|
|
@ -47,6 +51,7 @@ public:
|
|||
static void set_frame_presentation_callbacks(NonnullRefPtr<Core::WeakEventLoopReference>, BackingStorePresentationCallback, FramePresentationCallback);
|
||||
static void clear_frame_presentation_callbacks();
|
||||
static void presented_bitmap_ready_to_paint(u64 page_id, i32 bitmap_id);
|
||||
static bool async_scroll_by(u64 page_id, Gfx::FloatPoint position, Gfx::FloatPoint delta);
|
||||
|
||||
void start(DisplayListPlayerType);
|
||||
void stop_presenting_to_client();
|
||||
|
|
@ -54,6 +59,12 @@ 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&&);
|
||||
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;
|
||||
bool should_defer_main_thread_present_for_async_scroll() const;
|
||||
Optional<Gfx::FloatPoint> take_pending_async_viewport_scroll_offset();
|
||||
void update_backing_stores(Gfx::IntSize, i32 front_id, i32 back_id);
|
||||
u64 present_frame(Gfx::IntRect);
|
||||
void wait_for_frame(u64 frame_id);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Debug.h>
|
||||
#include <AK/TemporaryChange.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibJS/Runtime/VM.h>
|
||||
|
|
@ -261,6 +262,10 @@ void EventLoop::process_input_events() const
|
|||
case MouseEvent::Type::MouseLeave:
|
||||
return page.handle_mouseleave();
|
||||
case MouseEvent::Type::MouseWheel:
|
||||
if (mouse_event.async_scroll_performed_default_action) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Main thread handling DOM wheel after async default action");
|
||||
return page.handle_mousewheel(mouse_event.position, mouse_event.screen_position, mouse_event.button, mouse_event.buttons, mouse_event.modifiers, mouse_event.wheel_delta_x, mouse_event.wheel_delta_y, true);
|
||||
}
|
||||
return page.handle_mousewheel(mouse_event.position, mouse_event.screen_position, mouse_event.button, mouse_event.buttons, mouse_event.modifiers, mouse_event.wheel_delta_x, mouse_event.wheel_delta_y);
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include <LibWeb/CSS/ComputedProperties.h>
|
||||
#include <LibWeb/CSS/SystemColor.h>
|
||||
#include <LibWeb/CSS/VisualViewport.h>
|
||||
#include <LibWeb/Compositor/AsyncScrollingState.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/BlockingAlgorithms.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/DirectiveOperations.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/PolicyList.h>
|
||||
|
|
@ -61,6 +62,8 @@
|
|||
#include <LibWeb/UIEvents/InputTypes.h>
|
||||
#include <LibWeb/XHR/FormData.h>
|
||||
|
||||
#include <AK/Debug.h>
|
||||
|
||||
namespace Web::HTML {
|
||||
|
||||
GC_DEFINE_ALLOCATOR(Navigable);
|
||||
|
|
@ -3130,6 +3133,22 @@ void Navigable::record_display_list_and_scroll_state(PaintConfig paint_config)
|
|||
if (!document)
|
||||
return;
|
||||
|
||||
if (page().async_scrolling_enabled()) {
|
||||
// The compositor thread may have already presented newer viewport scroll offsets. Adopt the latest one before
|
||||
// recording so a main-thread repaint catches up to the visible async position.
|
||||
if (m_rendering_thread.should_defer_async_viewport_scroll_offset_adoption()) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Main thread deferred async viewport offset adoption before recording display list");
|
||||
} else if (auto async_scroll_offset = m_rendering_thread.take_pending_async_viewport_scroll_offset(); async_scroll_offset.has_value()) {
|
||||
auto device_pixels_per_css_pixel = page().client().device_pixels_per_css_pixel();
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Main thread adopting async viewport offset {},{} before recording display list",
|
||||
async_scroll_offset->x(), async_scroll_offset->y());
|
||||
perform_scroll_of_viewport_scrolling_box({
|
||||
CSSPixels { async_scroll_offset->x() / device_pixels_per_css_pixel },
|
||||
CSSPixels { async_scroll_offset->y() / device_pixels_per_css_pixel },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
auto should_record_display_list = m_needs_to_record_display_list
|
||||
|| !m_rendering_thread_display_list_paint_config.has_value()
|
||||
|| !(m_rendering_thread_display_list_paint_config.value() == paint_config);
|
||||
|
|
@ -3147,7 +3166,16 @@ void Navigable::record_display_list_and_scroll_state(PaintConfig paint_config)
|
|||
|
||||
Painting::ScrollStateSnapshot scroll_state_snapshot { document_paintable->scroll_state_snapshot() };
|
||||
if (should_record_display_list) {
|
||||
m_rendering_thread.update_display_list(*display_list, move(scroll_state_snapshot));
|
||||
if (page().async_scrolling_enabled()) {
|
||||
auto viewport_rect = page().css_to_device_rect(this->viewport_rect()).to_type<int>();
|
||||
auto async_scrolling_state = Compositor::collect_async_scrolling_state(*this, *document_paintable, viewport_rect);
|
||||
m_rendering_thread.update_display_list_and_async_scrolling_state(
|
||||
*display_list,
|
||||
move(scroll_state_snapshot),
|
||||
move(async_scrolling_state));
|
||||
} else {
|
||||
m_rendering_thread.update_display_list(*display_list, move(scroll_state_snapshot));
|
||||
}
|
||||
m_needs_to_record_display_list = false;
|
||||
m_rendering_thread_display_list_paint_config = paint_config;
|
||||
} else {
|
||||
|
|
@ -3169,6 +3197,11 @@ void Navigable::paint_next_frame()
|
|||
|
||||
record_display_list_and_scroll_state(paint_config);
|
||||
|
||||
if (page().async_scrolling_enabled() && m_rendering_thread.should_defer_main_thread_present_for_async_scroll()) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Main thread deferred present while async scroll is pending");
|
||||
return;
|
||||
}
|
||||
|
||||
auto frame_id = m_rendering_thread.present_frame(viewport_rect);
|
||||
if (!is_top_level_traversable())
|
||||
m_rendering_thread.wait_for_frame(frame_id);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include <LibUnicode/Segmenter.h>
|
||||
#include <LibWeb/Bindings/InputEvent.h>
|
||||
#include <LibWeb/CSS/VisualViewport.h>
|
||||
#include <LibWeb/Compositor/AsyncScrollingState.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/EditingHostManager.h>
|
||||
#include <LibWeb/DOM/Element.h>
|
||||
|
|
@ -58,6 +59,8 @@
|
|||
#include <SDL3/SDL_events.h>
|
||||
#include <SDL3/SDL_joystick.h>
|
||||
|
||||
#include <AK/Debug.h>
|
||||
|
||||
namespace Web {
|
||||
|
||||
#define FIRE(expression) \
|
||||
|
|
@ -104,6 +107,39 @@ static bool parent_element_for_event_dispatch(Painting::Paintable& paintable, GC
|
|||
return node && layout_node;
|
||||
}
|
||||
|
||||
static bool async_scrolling_state_has_scrollable_viewport_node(Compositor::AsyncScrollingState const& async_scrolling_state, Gfx::FloatPoint delta, Optional<Gfx::FloatPoint> pending_async_viewport_scroll_offset)
|
||||
{
|
||||
for (auto const& node : async_scrolling_state.scroll_nodes) {
|
||||
if (!node.is_viewport)
|
||||
continue;
|
||||
|
||||
auto scroll_offset = pending_async_viewport_scroll_offset.value_or(node.scroll_offset);
|
||||
if (delta.x() < 0 && scroll_offset.x() > 0)
|
||||
return true;
|
||||
if (delta.x() > 0 && scroll_offset.x() < node.max_scroll_offset.x())
|
||||
return true;
|
||||
if (delta.y() < 0 && scroll_offset.y() > 0)
|
||||
return true;
|
||||
if (delta.y() > 0 && scroll_offset.y() < node.max_scroll_offset.y())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool target_is_inside_non_viewport_wheel_scrollable_box(Painting::Paintable const& target)
|
||||
{
|
||||
for (RefPtr<Painting::Paintable const> paintable = target; paintable; paintable = paintable->containing_block()) {
|
||||
auto const* paintable_box = as_if<Painting::PaintableBox const>(*paintable);
|
||||
if (!paintable_box)
|
||||
continue;
|
||||
if (paintable_box->is_viewport_paintable())
|
||||
return false;
|
||||
if (paintable_box->could_be_scrolled_by_wheel_event())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static Gfx::Cursor css_to_gfx_cursor(CSS::CursorPredefined css_cursor)
|
||||
{
|
||||
switch (css_cursor) {
|
||||
|
|
@ -565,7 +601,7 @@ RefPtr<Painting::PaintableBox const> EventHandler::paint_root() const
|
|||
return m_navigable->active_document()->paintable_box();
|
||||
}
|
||||
|
||||
EventResult EventHandler::handle_mousewheel(CSSPixelPoint visual_viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers, int wheel_delta_x, int wheel_delta_y)
|
||||
EventResult EventHandler::handle_mousewheel(CSSPixelPoint visual_viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers, int wheel_delta_x, int wheel_delta_y, bool async_scroll_performed_default_action)
|
||||
{
|
||||
if (should_ignore_device_input_event())
|
||||
return EventResult::Dropped;
|
||||
|
|
@ -586,30 +622,71 @@ EventResult EventHandler::handle_mousewheel(CSSPixelPoint visual_viewport_positi
|
|||
if (modifiers & UIEvents::KeyModifier::Mod_Shift)
|
||||
swap(wheel_delta_x, wheel_delta_y);
|
||||
|
||||
auto handled_event = EventResult::Dropped;
|
||||
|
||||
RefPtr<Painting::Paintable> paintable;
|
||||
if (auto result = target_for_mouse_position(visual_viewport_position); result.has_value())
|
||||
paintable = result->paintable;
|
||||
|
||||
if (paintable) {
|
||||
RefPtr<Painting::Paintable> containing_block = paintable;
|
||||
while (containing_block) {
|
||||
auto handled_scroll_event = containing_block->handle_mousewheel({}, visual_viewport_position, buttons, modifiers, wheel_delta_x, wheel_delta_y);
|
||||
if (handled_scroll_event)
|
||||
return EventResult::Handled;
|
||||
|
||||
containing_block = containing_block->containing_block();
|
||||
if (m_navigable->page().async_scrolling_enabled() && async_scroll_performed_default_action) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Not attempting wheel async scroll: default action already performed");
|
||||
} else if (m_navigable->page().async_scrolling_enabled()) {
|
||||
auto& document_paintable = *document->paintable();
|
||||
document_paintable.refresh_scroll_state();
|
||||
auto viewport_rect = m_navigable->page().css_to_device_rect(m_navigable->viewport_rect()).to_type<int>();
|
||||
auto async_scrolling_state = Compositor::collect_async_scrolling_state(*m_navigable, document_paintable, viewport_rect);
|
||||
auto async_scroll_delta = Gfx::FloatPoint { static_cast<float>(wheel_delta_x), static_cast<float>(wheel_delta_y) };
|
||||
auto pending_async_viewport_scroll_offset = m_navigable->rendering_thread().pending_async_viewport_scroll_offset();
|
||||
// Non-passive wheel listeners can cancel the default scroll action, so the first live async path only handles
|
||||
// pages where the current snapshot proves that no such listener exists anywhere in this page. Everything else
|
||||
// stays on the synchronous path.
|
||||
auto can_try_async_viewport_scroll = paintable
|
||||
&& !is<Painting::NavigableContainerViewportPaintable>(*paintable)
|
||||
&& !target_is_inside_non_viewport_wheel_scrollable_box(*paintable)
|
||||
&& !async_scrolling_state.has_blocking_wheel_event_listeners
|
||||
&& async_scrolling_state_has_scrollable_viewport_node(async_scrolling_state, async_scroll_delta, pending_async_viewport_scroll_offset);
|
||||
if (can_try_async_viewport_scroll) {
|
||||
auto device_position = m_navigable->page().css_to_device_point(visual_viewport_position);
|
||||
auto async_scroll_position = Gfx::FloatPoint { static_cast<float>(device_position.x().value()), static_cast<float>(device_position.y().value()) };
|
||||
async_scroll_performed_default_action = m_navigable->rendering_thread().async_scroll_by(async_scroll_position, async_scroll_delta, viewport_rect);
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] {} wheel async scroll for viewport at {},{} with delta {},{}",
|
||||
async_scroll_performed_default_action ? "Enqueued"sv : "Could not enqueue"sv,
|
||||
async_scroll_position.x(), async_scroll_position.y(),
|
||||
async_scroll_delta.x(), async_scroll_delta.y());
|
||||
} else if (!paintable) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Not attempting wheel async scroll: no paintable target");
|
||||
} else if (is<Painting::NavigableContainerViewportPaintable>(*paintable)) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Not attempting wheel async scroll: target is a nested navigable");
|
||||
} else if (target_is_inside_non_viewport_wheel_scrollable_box(*paintable)) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Not attempting wheel async scroll: target is inside a nested scroll container");
|
||||
} else if (async_scrolling_state.has_blocking_wheel_event_listeners) {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Not attempting wheel async scroll: blocking wheel event listeners exist");
|
||||
} else {
|
||||
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Not attempting wheel async scroll: viewport cannot scroll by delta {},{}",
|
||||
async_scroll_delta.x(), async_scroll_delta.y());
|
||||
}
|
||||
}
|
||||
|
||||
if (paintable->handle_mousewheel({}, visual_viewport_position, buttons, modifiers, wheel_delta_x, wheel_delta_y))
|
||||
return EventResult::Handled;
|
||||
auto handled_event = EventResult::Dropped;
|
||||
|
||||
if (paintable) {
|
||||
if (!async_scroll_performed_default_action) {
|
||||
RefPtr<Painting::Paintable> containing_block = paintable;
|
||||
while (containing_block) {
|
||||
auto handled_scroll_event = containing_block->handle_mousewheel({}, visual_viewport_position, buttons, modifiers, wheel_delta_x, wheel_delta_y);
|
||||
if (handled_scroll_event)
|
||||
return EventResult::Handled;
|
||||
|
||||
containing_block = containing_block->containing_block();
|
||||
}
|
||||
|
||||
if (paintable->handle_mousewheel({}, visual_viewport_position, buttons, modifiers, wheel_delta_x, wheel_delta_y))
|
||||
return EventResult::Handled;
|
||||
}
|
||||
|
||||
auto node = dom_node_for_event_dispatch(*paintable);
|
||||
|
||||
if (node) {
|
||||
if (auto result = dispatch_event_to_nested_navigable(*paintable, visual_viewport_position, [screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y](EventHandler& event_handler, CSSPixelPoint position) -> EventResult {
|
||||
return event_handler.handle_mousewheel(position, screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y);
|
||||
if (auto result = dispatch_event_to_nested_navigable(*paintable, visual_viewport_position, [screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y, async_scroll_performed_default_action](EventHandler& event_handler, CSSPixelPoint position) -> EventResult {
|
||||
return event_handler.handle_mousewheel(position, screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y, async_scroll_performed_default_action);
|
||||
});
|
||||
result.has_value()) {
|
||||
if (result.value() == EventResult::Handled || result.value() == EventResult::Cancelled)
|
||||
|
|
@ -628,8 +705,11 @@ EventResult EventHandler::handle_mousewheel(CSSPixelPoint visual_viewport_positi
|
|||
auto scroll_offset = document->navigable()->viewport_scroll_offset();
|
||||
auto offset = compute_mouse_event_offset(visual_viewport_position.translated(scroll_offset), *offset_paintable);
|
||||
bool could_scroll_viewport = document->paintable_box()->could_be_scrolled_by_wheel_event();
|
||||
if (node->dispatch_event(UIEvents::WheelEvent::create_from_platform_event(node->realm(), m_navigable->active_window_proxy(), UIEvents::EventNames::wheel, screen_position, page_offset, viewport_position, offset, wheel_delta_x, wheel_delta_y, button, buttons, modifiers).release_value_but_fixme_should_propagate_errors())) {
|
||||
if (could_scroll_viewport) {
|
||||
auto is_cancelable = async_scroll_performed_default_action ? UIEvents::WheelEventIsCancelable::No : UIEvents::WheelEventIsCancelable::Yes;
|
||||
if (node->dispatch_event(UIEvents::WheelEvent::create_from_platform_event(node->realm(), m_navigable->active_window_proxy(), UIEvents::EventNames::wheel, screen_position, page_offset, viewport_position, offset, wheel_delta_x, wheel_delta_y, button, buttons, modifiers, is_cancelable).release_value_but_fixme_should_propagate_errors())) {
|
||||
if (async_scroll_performed_default_action) {
|
||||
handled_event = EventResult::Handled;
|
||||
} else if (could_scroll_viewport) {
|
||||
auto viewport_scroll_position_before = CSSPixelPoint { CSSPixels(document->visual_viewport()->page_left()), CSSPixels(document->visual_viewport()->page_top()) };
|
||||
m_navigable->scroll_viewport_by_delta({ wheel_delta_x, wheel_delta_y });
|
||||
auto viewport_scroll_position_after = CSSPixelPoint { CSSPixels(document->visual_viewport()->page_left()), CSSPixels(document->visual_viewport()->page_top()) };
|
||||
|
|
@ -637,6 +717,8 @@ EventResult EventHandler::handle_mousewheel(CSSPixelPoint visual_viewport_positi
|
|||
} else {
|
||||
handled_event = EventResult::Accepted;
|
||||
}
|
||||
} else if (async_scroll_performed_default_action) {
|
||||
handled_event = EventResult::Handled;
|
||||
} else {
|
||||
handled_event = EventResult::Cancelled;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public:
|
|||
EventResult handle_mousedown(CSSPixelPoint, CSSPixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, int click_count);
|
||||
EventResult handle_mousemove(CSSPixelPoint, CSSPixelPoint screen_position, unsigned buttons, unsigned modifiers);
|
||||
EventResult handle_mouseleave();
|
||||
EventResult handle_mousewheel(CSSPixelPoint, CSSPixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, int wheel_delta_x, int wheel_delta_y);
|
||||
EventResult handle_mousewheel(CSSPixelPoint, CSSPixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, int wheel_delta_x, int wheel_delta_y, bool async_scroll_performed_default_action = false);
|
||||
|
||||
EventResult handle_drag_and_drop_event(DragEvent::Type, CSSPixelPoint, CSSPixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, Vector<HTML::SelectedFile> files);
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ KeyEvent KeyEvent::clone_without_browser_data() const
|
|||
|
||||
MouseEvent MouseEvent::clone_without_browser_data() const
|
||||
{
|
||||
return { type, position, screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y, click_count, nullptr };
|
||||
return { type, position, screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y, click_count, nullptr, async_scroll_performed_default_action };
|
||||
}
|
||||
|
||||
DragEvent DragEvent::clone_without_browser_data() const
|
||||
|
|
@ -63,6 +63,7 @@ ErrorOr<void> IPC::encode(Encoder& encoder, Web::MouseEvent const& event)
|
|||
TRY(encoder.encode(event.wheel_delta_x));
|
||||
TRY(encoder.encode(event.wheel_delta_y));
|
||||
TRY(encoder.encode(event.click_count));
|
||||
TRY(encoder.encode(event.async_scroll_performed_default_action));
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
@ -78,8 +79,9 @@ ErrorOr<Web::MouseEvent> IPC::decode(Decoder& decoder)
|
|||
auto wheel_delta_x = TRY(decoder.decode<int>());
|
||||
auto wheel_delta_y = TRY(decoder.decode<int>());
|
||||
auto click_count = TRY(decoder.decode<int>());
|
||||
auto async_scroll_performed_default_action = TRY(decoder.decode<bool>());
|
||||
|
||||
return Web::MouseEvent { type, position, screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y, click_count, nullptr };
|
||||
return Web::MouseEvent { type, position, screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y, click_count, nullptr, async_scroll_performed_default_action };
|
||||
}
|
||||
|
||||
template<>
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ struct WEB_API MouseEvent {
|
|||
int click_count { 0 };
|
||||
|
||||
OwnPtr<BrowserInputData> browser_data;
|
||||
bool async_scroll_performed_default_action { false };
|
||||
};
|
||||
|
||||
struct WEB_API DragEvent {
|
||||
|
|
|
|||
|
|
@ -267,9 +267,9 @@ EventResult Page::handle_mouseleave()
|
|||
return top_level_traversable()->event_handler().handle_mouseleave();
|
||||
}
|
||||
|
||||
EventResult Page::handle_mousewheel(DevicePixelPoint position, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, DevicePixels wheel_delta_x, DevicePixels wheel_delta_y)
|
||||
EventResult Page::handle_mousewheel(DevicePixelPoint position, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, DevicePixels wheel_delta_x, DevicePixels wheel_delta_y, bool async_scroll_performed_default_action)
|
||||
{
|
||||
return top_level_traversable()->event_handler().handle_mousewheel(device_to_css_point(position), device_to_css_point(screen_position), button, buttons, modifiers, wheel_delta_x.value(), wheel_delta_y.value());
|
||||
return top_level_traversable()->event_handler().handle_mousewheel(device_to_css_point(position), device_to_css_point(screen_position), button, buttons, modifiers, wheel_delta_x.value(), wheel_delta_y.value(), async_scroll_performed_default_action);
|
||||
}
|
||||
|
||||
EventResult Page::handle_drag_and_drop_event(DragEvent::Type type, DevicePixelPoint position, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, Vector<HTML::SelectedFile> files)
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ public:
|
|||
EventResult handle_mousedown(DevicePixelPoint, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, int click_count);
|
||||
EventResult handle_mousemove(DevicePixelPoint, DevicePixelPoint screen_position, unsigned buttons, unsigned modifiers);
|
||||
EventResult handle_mouseleave();
|
||||
EventResult handle_mousewheel(DevicePixelPoint, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, DevicePixels wheel_delta_x, DevicePixels wheel_delta_y);
|
||||
EventResult handle_mousewheel(DevicePixelPoint, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, DevicePixels wheel_delta_x, DevicePixels wheel_delta_y, bool async_scroll_performed_default_action = false);
|
||||
|
||||
EventResult handle_drag_and_drop_event(DragEvent::Type, DevicePixelPoint, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, Vector<HTML::SelectedFile> files);
|
||||
EventResult handle_pinch_event(DevicePixelPoint point, double scale);
|
||||
|
|
@ -132,6 +132,9 @@ public:
|
|||
bool enable_autoscroll() const { return m_enable_autoscroll; }
|
||||
void set_enable_autoscroll(bool b) { m_enable_autoscroll = b; }
|
||||
|
||||
bool async_scrolling_enabled() const { return m_async_scrolling_enabled; }
|
||||
void set_async_scrolling_enabled(bool b) { m_async_scrolling_enabled = b; }
|
||||
|
||||
bool is_webdriver_active() const { return m_is_webdriver_active; }
|
||||
void set_is_webdriver_active(bool b) { m_is_webdriver_active = b; }
|
||||
|
||||
|
|
@ -290,6 +293,7 @@ private:
|
|||
bool m_is_scripting_enabled { true };
|
||||
bool m_should_block_pop_ups { true };
|
||||
bool m_enable_autoscroll { true };
|
||||
bool m_async_scrolling_enabled { false };
|
||||
|
||||
// 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.
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ GC::Ref<WheelEvent> WheelEvent::create(JS::Realm& realm, FlyString const& event_
|
|||
return realm.create<WheelEvent>(realm, event_name, event_init, page_x, page_y, offset_x, offset_y);
|
||||
}
|
||||
|
||||
WebIDL::ExceptionOr<GC::Ref<WheelEvent>> WheelEvent::create_from_platform_event(JS::Realm& realm, GC::Ptr<HTML::WindowProxy> window_proxy, FlyString const& event_name, CSSPixelPoint screen, CSSPixelPoint page, CSSPixelPoint client, CSSPixelPoint offset, double delta_x, double delta_y, unsigned button, unsigned buttons, unsigned modifiers)
|
||||
WebIDL::ExceptionOr<GC::Ref<WheelEvent>> WheelEvent::create_from_platform_event(JS::Realm& realm, GC::Ptr<HTML::WindowProxy> window_proxy, FlyString const& event_name, CSSPixelPoint screen, CSSPixelPoint page, CSSPixelPoint client, CSSPixelPoint offset, double delta_x, double delta_y, unsigned button, unsigned buttons, unsigned modifiers, WheelEventIsCancelable is_cancelable)
|
||||
{
|
||||
Bindings::WheelEventInit event_init {};
|
||||
event_init.ctrl_key = modifiers & Mod_Ctrl;
|
||||
|
|
@ -64,7 +64,7 @@ WebIDL::ExceptionOr<GC::Ref<WheelEvent>> WheelEvent::create_from_platform_event(
|
|||
auto event = WheelEvent::create(realm, event_name, event_init, page.x().to_double(), page.y().to_double(), offset.x().to_double(), offset.y().to_double());
|
||||
event->set_is_trusted(true);
|
||||
event->set_bubbles(true);
|
||||
event->set_cancelable(true);
|
||||
event->set_cancelable(is_cancelable == WheelEventIsCancelable::Yes);
|
||||
event->set_composed(true);
|
||||
return event;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ enum WheelDeltaMode : WebIDL::UnsignedLong {
|
|||
DOM_DELTA_PAGE = 2,
|
||||
};
|
||||
|
||||
enum class WheelEventIsCancelable : u8 {
|
||||
No,
|
||||
Yes,
|
||||
};
|
||||
|
||||
class WheelEvent final : public MouseEvent {
|
||||
WEB_PLATFORM_OBJECT(WheelEvent, MouseEvent);
|
||||
GC_DECLARE_ALLOCATOR(WheelEvent);
|
||||
|
|
@ -27,7 +32,7 @@ public:
|
|||
[[nodiscard]] static GC::Ref<WheelEvent> create(JS::Realm&, FlyString const& event_name, Bindings::WheelEventInit const& = {}, double page_x = 0, double page_y = 0, double offset_x = 0, double offset_y = 0);
|
||||
[[nodiscard]] static GC::Ref<WheelEvent> construct_impl(JS::Realm&, FlyString const& event_name, Bindings::WheelEventInit const& = {});
|
||||
|
||||
static WebIDL::ExceptionOr<GC::Ref<WheelEvent>> create_from_platform_event(JS::Realm&, GC::Ptr<HTML::WindowProxy>, FlyString const& event_name, CSSPixelPoint screen, CSSPixelPoint page, CSSPixelPoint client, CSSPixelPoint offset, double delta_x, double delta_y, unsigned button, unsigned buttons, unsigned modifiers);
|
||||
static WebIDL::ExceptionOr<GC::Ref<WheelEvent>> create_from_platform_event(JS::Realm&, GC::Ptr<HTML::WindowProxy>, FlyString const& event_name, CSSPixelPoint screen, CSSPixelPoint page, CSSPixelPoint client, CSSPixelPoint offset, double delta_x, double delta_y, unsigned button, unsigned buttons, unsigned modifiers, WheelEventIsCancelable = WheelEventIsCancelable::Yes);
|
||||
|
||||
virtual ~WheelEvent() override;
|
||||
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
|
|||
bool force_fontconfig = false;
|
||||
bool collect_garbage_on_every_allocation = false;
|
||||
bool disable_scrollbar_painting = false;
|
||||
bool enable_async_scrolling = false;
|
||||
bool file_scheme_urls_have_tuple_origins = false;
|
||||
Optional<u64> style_invalidation_counter_dump_interval;
|
||||
|
||||
|
|
@ -236,6 +237,7 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
|
|||
args_parser.add_option(force_fontconfig, "Force using fontconfig for font loading", "force-fontconfig");
|
||||
args_parser.add_option(collect_garbage_on_every_allocation, "Collect garbage after every JS heap allocation", "collect-garbage-on-every-allocation", 'g');
|
||||
args_parser.add_option(disable_scrollbar_painting, "Don't paint horizontal or vertical scrollbars on the main viewport", "disable-scrollbar-painting");
|
||||
args_parser.add_option(enable_async_scrolling, "Enable experimental async scrolling", "enable-async-scrolling");
|
||||
args_parser.add_option(dns_server_address, "Set the DNS server address", "dns-server", 0, "host|address");
|
||||
args_parser.add_option(dns_server_port, "Set the DNS server port", "dns-port", 0, "port (default: 53 or 853 if --dot)");
|
||||
args_parser.add_option(use_dns_over_tls, "Use DNS over TLS", "dot");
|
||||
|
|
@ -363,6 +365,7 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
|
|||
.enable_autoplay = enable_autoplay ? EnableAutoplay::Yes : EnableAutoplay::No,
|
||||
.collect_garbage_on_every_allocation = collect_garbage_on_every_allocation ? CollectGarbageOnEveryAllocation::Yes : CollectGarbageOnEveryAllocation::No,
|
||||
.paint_viewport_scrollbars = disable_scrollbar_painting ? PaintViewportScrollbars::No : PaintViewportScrollbars::Yes,
|
||||
.enable_async_scrolling = enable_async_scrolling ? EnableAsyncScrolling::Yes : EnableAsyncScrolling::No,
|
||||
.file_scheme_urls_have_tuple_origins = file_scheme_urls_have_tuple_origins ? FileSchemeUrlsHaveTupleOrigins::Yes : FileSchemeUrlsHaveTupleOrigins::No,
|
||||
.default_time_zone = default_time_zone,
|
||||
.style_invalidation_counter_dump_interval = style_invalidation_counter_dump_interval,
|
||||
|
|
|
|||
|
|
@ -118,6 +118,8 @@ static ErrorOr<NonnullRefPtr<WebView::WebContentClient>> launch_web_content_proc
|
|||
arguments.append("--collect-garbage-on-every-allocation"sv);
|
||||
if (web_content_options.paint_viewport_scrollbars == PaintViewportScrollbars::No)
|
||||
arguments.append("--disable-scrollbar-painting"sv);
|
||||
if (web_content_options.enable_async_scrolling == EnableAsyncScrolling::Yes)
|
||||
arguments.append("--enable-async-scrolling"sv);
|
||||
if (web_content_options.file_scheme_urls_have_tuple_origins == FileSchemeUrlsHaveTupleOrigins::Yes)
|
||||
arguments.append("--tuple-file-origins"sv);
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ namespace WebContent {
|
|||
|
||||
static PageClient::UseSkiaPainter s_use_skia_painter = PageClient::UseSkiaPainter::GPUBackendIfAvailable;
|
||||
static bool s_is_headless { false };
|
||||
static bool s_async_scrolling_enabled { false };
|
||||
|
||||
GC_DEFINE_ALLOCATOR(PageClient);
|
||||
|
||||
|
|
@ -65,6 +66,11 @@ void PageClient::set_is_headless(bool is_headless)
|
|||
s_is_headless = is_headless;
|
||||
}
|
||||
|
||||
void PageClient::set_async_scrolling_enabled(bool enabled)
|
||||
{
|
||||
s_async_scrolling_enabled = enabled;
|
||||
}
|
||||
|
||||
GC::Ref<PageClient> PageClient::create(JS::VM& vm, PageHost& page_host, u64 id)
|
||||
{
|
||||
return vm.heap().allocate<PageClient>(page_host, id);
|
||||
|
|
@ -75,6 +81,7 @@ PageClient::PageClient(PageHost& owner, u64 id)
|
|||
, m_page(Web::Page::create(Web::Bindings::main_thread_vm(), *this))
|
||||
, m_id(id)
|
||||
{
|
||||
m_page->set_async_scrolling_enabled(s_async_scrolling_enabled);
|
||||
setup_palette();
|
||||
|
||||
m_frame_timer = Core::Timer::create_single_shot(0, [this] {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ public:
|
|||
virtual bool is_headless() const override;
|
||||
static void set_is_headless(bool);
|
||||
|
||||
static void set_async_scrolling_enabled(bool);
|
||||
|
||||
virtual Web::Page& page() override { return *m_page; }
|
||||
virtual Web::Page const& page() const override { return *m_page; }
|
||||
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
|
|||
bool collect_garbage_on_every_allocation = false;
|
||||
bool is_headless = false;
|
||||
bool disable_scrollbar_painting = false;
|
||||
bool enable_async_scrolling = false;
|
||||
StringView echo_server_port_string_view {};
|
||||
StringView default_time_zone {};
|
||||
StringView style_invalidation_counter_dump_interval {};
|
||||
|
|
@ -169,6 +170,7 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
|
|||
args_parser.add_option(force_fontconfig, "Force using fontconfig for font loading", "force-fontconfig");
|
||||
args_parser.add_option(collect_garbage_on_every_allocation, "Collect garbage after every JS heap allocation", "collect-garbage-on-every-allocation");
|
||||
args_parser.add_option(disable_scrollbar_painting, "Don't paint horizontal or vertical viewport scrollbars", "disable-scrollbar-painting");
|
||||
args_parser.add_option(enable_async_scrolling, "Enable experimental async scrolling", "enable-async-scrolling");
|
||||
args_parser.add_option(echo_server_port_string_view, "Echo server port used in test internals", "echo-server-port", 0, "echo_server_port");
|
||||
args_parser.add_option(is_headless, "Report that the browser is running in headless mode", "headless");
|
||||
args_parser.add_option(default_time_zone, "Default time zone", "default-time-zone", 0, "time-zone-id");
|
||||
|
|
@ -219,6 +221,7 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
|
|||
Web::Fetch::Fetching::set_http_memory_cache_enabled(true);
|
||||
|
||||
Web::Painting::set_paint_viewport_scrollbars(!disable_scrollbar_painting);
|
||||
WebContent::PageClient::set_async_scrolling_enabled(enable_async_scrolling);
|
||||
|
||||
if (!echo_server_port_string_view.is_empty()) {
|
||||
if (auto maybe_echo_server_port = echo_server_port_string_view.to_number<u16>(); maybe_echo_server_port.has_value())
|
||||
|
|
|
|||
Loading…
Reference in a new issue