From a11c281dc345b2f789b1c61d0564ac8b2eede29e Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sun, 14 Jun 2026 21:30:28 +0200 Subject: [PATCH] Compositor: Handle pinch zoom asynchronously Apply pinch zoom deltas to the compositor's visual viewport transform so the currently presented display list can respond without waiting for the WebContent main thread. Keep the normal WebContent pinch event path so the real VisualViewport state and DOM-visible events catch up after. Only take the compositor path when async scrolling is enabled and there are no blocking wheel listeners, since pinch zoom dispatches a synthetic wheel event that script may cancel. Coalesce queued pinch events in WebContent so main-thread catch-up can adopt multiple gesture deltas together. Use the compositor visual viewport transform for wheel hit testing and consume wheel deltas as visual viewport pan while zoomed. Scale the handoff to layout viewport scrolling by the inverse visual viewport scale, so touchpad momentum does not jump when the visual viewport hits an edge. --- Libraries/LibWeb/HTML/Navigable.cpp | 16 +- .../Painting/AccumulatedVisualContext.h | 2 +- Libraries/LibWebView/Application.cpp | 11 + Libraries/LibWebView/Application.h | 2 + Libraries/LibWebView/ViewImplementation.cpp | 10 + Libraries/LibWebView/WebContentClient.cpp | 11 + Libraries/LibWebView/WebContentClient.h | 1 + .../Compositor/CompositorControlServer.ipc | 1 + Services/Compositor/CompositorState.cpp | 9 + Services/Compositor/CompositorState.h | 2 + Services/Compositor/ConnectionFromClient.cpp | 5 + Services/Compositor/ConnectionFromClient.h | 1 + Services/Compositor/ContextState.cpp | 214 +++++++++++++++++- Services/Compositor/ContextState.h | 13 ++ Services/WebContent/ConnectionFromClient.cpp | 27 ++- 15 files changed, 310 insertions(+), 15 deletions(-) diff --git a/Libraries/LibWeb/HTML/Navigable.cpp b/Libraries/LibWeb/HTML/Navigable.cpp index 57de3c1c57..9b6ebfe21e 100644 --- a/Libraries/LibWeb/HTML/Navigable.cpp +++ b/Libraries/LibWeb/HTML/Navigable.cpp @@ -3290,12 +3290,18 @@ void Navigable::resolve_all_pending_async_scroll_operations() static bool adopt_async_viewport_scroll_delta(Navigable& navigable, CSSPixelPoint scroll_delta) { - auto scroll_offset = navigable.viewport_scroll_offset(); - scroll_offset.translate_by(scroll_delta); - if (scroll_offset == navigable.viewport_scroll_offset()) + auto document = navigable.active_document(); + if (!document) return false; - navigable.perform_scroll_of_viewport_scrolling_box(scroll_offset); - return true; + + auto visual_viewport = document->visual_viewport(); + CSSPixelPoint page_position { CSSPixels(visual_viewport->page_left()), CSSPixels(visual_viewport->page_top()) }; + auto viewport_scroll_offset = navigable.viewport_scroll_offset(); + navigable.scroll_viewport_by_delta(scroll_delta); + + CSSPixelPoint new_page_position { CSSPixels(visual_viewport->page_left()), CSSPixels(visual_viewport->page_top()) }; + return new_page_position != page_position + || navigable.viewport_scroll_offset() != viewport_scroll_offset; } void Navigable::adopt_pending_async_scroll_offsets() diff --git a/Libraries/LibWeb/Painting/AccumulatedVisualContext.h b/Libraries/LibWeb/Painting/AccumulatedVisualContext.h index d32417ebae..3a6a16a19c 100644 --- a/Libraries/LibWeb/Painting/AccumulatedVisualContext.h +++ b/Libraries/LibWeb/Painting/AccumulatedVisualContext.h @@ -105,7 +105,7 @@ public: u64 version() const { return m_version; } VisualContextIndex append(VisualContextData data, VisualContextIndex parent_index); - void set_visual_viewport_transform(TransformData); + WEB_API void set_visual_viewport_transform(TransformData); AccumulatedVisualContextNode const& node_at(VisualContextIndex index) const { return m_nodes[index.value()]; } ReadonlySpan nodes() const { return m_nodes.span(); } diff --git a/Libraries/LibWebView/Application.cpp b/Libraries/LibWebView/Application.cpp index 5a273aa329..b6ae00289d 100644 --- a/Libraries/LibWebView/Application.cpp +++ b/Libraries/LibWebView/Application.cpp @@ -645,6 +645,17 @@ bool Application::handle_mouse_event_in_compositor(Web::Compositor::CompositorCo return result.release_value(); } +bool Application::handle_pinch_event_in_compositor(Web::Compositor::CompositorContextId context_id, Web::PinchEvent const& event) +{ + if (!can_send_compositor_process_ipc(m_compositor_client)) + return false; + + auto result = m_compositor_client->try_handle_pinch_event(context_id, event); + if (result.is_error()) + return false; + return result.release_value(); +} + bool Application::dispatch_mouse_event_to_web_content(Web::Compositor::CompositorContextId context_id, Web::MouseEvent const& event) { if (!can_send_compositor_process_ipc(m_compositor_client)) diff --git a/Libraries/LibWebView/Application.h b/Libraries/LibWebView/Application.h index cc822819ff..8aafc24839 100644 --- a/Libraries/LibWebView/Application.h +++ b/Libraries/LibWebView/Application.h @@ -45,6 +45,7 @@ namespace Web { struct MouseEvent; +struct PinchEvent; } @@ -103,6 +104,7 @@ public: void update_compositor_display_metadata(Web::Compositor::CompositorContextId, Optional display_id, double refresh_rate); bool send_async_scroll_to_compositor(Web::Compositor::CompositorContextId, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels); bool handle_mouse_event_in_compositor(Web::Compositor::CompositorContextId, Web::MouseEvent const&); + bool handle_pinch_event_in_compositor(Web::Compositor::CompositorContextId, Web::PinchEvent const&); bool dispatch_mouse_event_to_web_content(Web::Compositor::CompositorContextId, Web::MouseEvent const&); void notify_compositor_presented_bitmap_ready_to_paint(Web::Compositor::CompositorContextId, i32 bitmap_id); diff --git a/Libraries/LibWebView/ViewImplementation.cpp b/Libraries/LibWebView/ViewImplementation.cpp index 0cad8d009c..8fa72f5f2b 100644 --- a/Libraries/LibWebView/ViewImplementation.cpp +++ b/Libraries/LibWebView/ViewImplementation.cpp @@ -524,6 +524,7 @@ void ViewImplementation::reset_zoom() void ViewImplementation::enqueue_input_event(Web::InputEvent event) { auto* mouse_event = event.get_pointer(); + auto* pinch_event = event.get_pointer(); if (mouse_event && mouse_event->type == Web::MouseEvent::Type::MouseWheel) { mouse_event->wheel_delta_x /= zoom_level(); mouse_event->wheel_delta_y /= zoom_level(); @@ -556,6 +557,15 @@ void ViewImplementation::enqueue_input_event(Web::InputEvent event) return; } } + if (Application::web_content_options().enable_async_scrolling == EnableAsyncScrolling::Yes + && m_client_state.has_usable_bitmap + && pinch_event) { + dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI attempting compositor pinch bypass for page {} at {},{} scale delta {}", + m_client_state.page_index, pinch_event->position.x().value(), pinch_event->position.y().value(), pinch_event->scale_delta); + auto handled = client().handle_pinch_event_in_compositor(m_client_state.page_index, *pinch_event); + dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI compositor pinch bypass result for page {}: {}", + m_client_state.page_index, handled ? "accepted"sv : "rejected"sv); + } // Send the next event over to the WebContent to be handled by JS. We'll later get a message to say whether JS // prevented the default event behavior, at which point we either discard or handle that event, and then try to diff --git a/Libraries/LibWebView/WebContentClient.cpp b/Libraries/LibWebView/WebContentClient.cpp index f8cf4c546b..1914eaf1bf 100644 --- a/Libraries/LibWebView/WebContentClient.cpp +++ b/Libraries/LibWebView/WebContentClient.cpp @@ -312,6 +312,17 @@ bool WebContentClient::handle_mouse_event_in_compositor(u64 page_id, Web::MouseE return handled; } +bool WebContentClient::handle_pinch_event_in_compositor(u64 page_id, Web::PinchEvent const& event) +{ + auto timer = Core::ElapsedTimer::start_new(Core::TimerType::Precise); + + auto handled = Application::the().handle_pinch_event_in_compositor(compositor_context_id_for_page(page_id), event); + + dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI compositor IPC pinch_event page {} returned {} in {} us", + page_id, handled, timer.elapsed_time().to_microseconds()); + return handled; +} + void WebContentClient::dispatch_mouse_event_to_web_content(u64 page_id, Web::MouseEvent const& event) { auto context_id = compositor_context_id_for_page(page_id); diff --git a/Libraries/LibWebView/WebContentClient.h b/Libraries/LibWebView/WebContentClient.h index a269f4b275..06f23484b0 100644 --- a/Libraries/LibWebView/WebContentClient.h +++ b/Libraries/LibWebView/WebContentClient.h @@ -80,6 +80,7 @@ public: Optional page_id_for_compositor_context_id(Web::Compositor::CompositorContextId) const; bool send_async_scroll_to_compositor(u64 page_id, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels); bool handle_mouse_event_in_compositor(u64 page_id, Web::MouseEvent const&); + bool handle_pinch_event_in_compositor(u64 page_id, Web::PinchEvent const&); void dispatch_mouse_event_to_web_content(u64 page_id, Web::MouseEvent const&); void notify_presented_bitmap_ready_to_paint(u64 page_id, i32 bitmap_id); void did_present_backing_stores(u64 page_id, i32 front_bitmap_id, Gfx::SharedImage front_backing_store, i32 back_bitmap_id, Gfx::SharedImage back_backing_store); diff --git a/Services/Compositor/CompositorControlServer.ipc b/Services/Compositor/CompositorControlServer.ipc index 0aacef7b29..04ee8cd2a8 100644 --- a/Services/Compositor/CompositorControlServer.ipc +++ b/Services/Compositor/CompositorControlServer.ipc @@ -18,6 +18,7 @@ endpoint CompositorControlServer handle_mouse_event(Web::Compositor::CompositorContextId context_id, Web::MouseEvent event) => (bool handled) dispatch_mouse_event_to_web_content(Web::Compositor::CompositorContextId context_id, Web::MouseEvent event) => (bool dispatched) + handle_pinch_event(Web::Compositor::CompositorContextId context_id, Web::PinchEvent event) => (bool handled) async_scroll_by(Web::Compositor::CompositorContextId context_id, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels) => (bool handled) presented_bitmap_ready_to_paint(Web::Compositor::CompositorContextId context_id, i32 bitmap_id) =| crash() =| diff --git a/Services/Compositor/CompositorState.cpp b/Services/Compositor/CompositorState.cpp index 0a21bc7c78..d033a12770 100644 --- a/Services/Compositor/CompositorState.cpp +++ b/Services/Compositor/CompositorState.cpp @@ -200,6 +200,15 @@ bool CompositorState::dispatch_mouse_event_to_web_content(Web::Compositor::Compo return true; } +bool CompositorState::handle_pinch_event(Web::Compositor::CompositorContextId context_id, Web::PinchEvent const& event) +{ + auto* context = context_if_present(context_id); + if (!context) + return false; + + return apply_context_update_result(context_id, *context, context->handle_pinch_event(event)); +} + Web::Compositor::AsyncScrollEnqueueResult CompositorState::async_scroll_by(Web::Compositor::CompositorContextId context_id, Web::UniqueNodeID expected_document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::AsyncScrollOperationTracking operation_tracking) { if (!m_async_scrolling_enabled) diff --git a/Services/Compositor/CompositorState.h b/Services/Compositor/CompositorState.h index 975232cd3e..2ad04e891b 100644 --- a/Services/Compositor/CompositorState.h +++ b/Services/Compositor/CompositorState.h @@ -32,6 +32,7 @@ namespace Web { struct MouseEvent; +struct PinchEvent; } @@ -83,6 +84,7 @@ public: void invalidate_wheel_event_listener_state(Web::Compositor::CompositorContextId, u64 generation); bool handle_mouse_event(Web::Compositor::CompositorContextId, Web::MouseEvent const&); bool dispatch_mouse_event_to_web_content(Web::Compositor::CompositorContextId, Web::MouseEvent const&); + bool handle_pinch_event(Web::Compositor::CompositorContextId, Web::PinchEvent const&); Web::Compositor::AsyncScrollEnqueueResult async_scroll_by(Web::Compositor::CompositorContextId, Web::UniqueNodeID document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::AsyncScrollOperationTracking); bool async_scroll_by(Web::Compositor::CompositorContextId, Gfx::FloatPoint position, Gfx::FloatPoint delta); bool should_defer_main_thread_present_for_async_scroll(Web::Compositor::CompositorContextId) const; diff --git a/Services/Compositor/ConnectionFromClient.cpp b/Services/Compositor/ConnectionFromClient.cpp index 6093eb620c..9a96debce0 100644 --- a/Services/Compositor/ConnectionFromClient.cpp +++ b/Services/Compositor/ConnectionFromClient.cpp @@ -89,6 +89,11 @@ Messages::CompositorControlServer::DispatchMouseEventToWebContentResponse Connec return m_compositor_state->dispatch_mouse_event_to_web_content(context_id, event); } +Messages::CompositorControlServer::HandlePinchEventResponse ConnectionFromClient::handle_pinch_event(Web::Compositor::CompositorContextId context_id, Web::PinchEvent event) +{ + return m_compositor_state->handle_pinch_event(context_id, event); +} + Messages::CompositorControlServer::AsyncScrollByResponse ConnectionFromClient::async_scroll_by(Web::Compositor::CompositorContextId context_id, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels) { return m_compositor_state->async_scroll_by(context_id, position, delta_in_device_pixels); diff --git a/Services/Compositor/ConnectionFromClient.h b/Services/Compositor/ConnectionFromClient.h index 6a2a4a723f..d3198b6928 100644 --- a/Services/Compositor/ConnectionFromClient.h +++ b/Services/Compositor/ConnectionFromClient.h @@ -42,6 +42,7 @@ private: virtual void set_display_metadata(Web::Compositor::CompositorContextId, Optional, double) override; virtual Messages::CompositorControlServer::HandleMouseEventResponse handle_mouse_event(Web::Compositor::CompositorContextId, Web::MouseEvent) override; virtual Messages::CompositorControlServer::DispatchMouseEventToWebContentResponse dispatch_mouse_event_to_web_content(Web::Compositor::CompositorContextId, Web::MouseEvent) override; + virtual Messages::CompositorControlServer::HandlePinchEventResponse handle_pinch_event(Web::Compositor::CompositorContextId, Web::PinchEvent) override; virtual Messages::CompositorControlServer::AsyncScrollByResponse async_scroll_by(Web::Compositor::CompositorContextId, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels) override; virtual void presented_bitmap_ready_to_paint(Web::Compositor::CompositorContextId, i32 bitmap_id) override; virtual void crash() override; diff --git a/Services/Compositor/ContextState.cpp b/Services/Compositor/ContextState.cpp index 9435020483..1bc0f74d8e 100644 --- a/Services/Compositor/ContextState.cpp +++ b/Services/Compositor/ContextState.cpp @@ -31,6 +31,52 @@ static void set_or_append_pending_scroll_offset( pending_scroll_offsets.append(scroll_offset); } +static Web::Painting::TransformData const& visual_viewport_transform(Web::Painting::AccumulatedVisualContextTree const& visual_context_tree) +{ + auto const& visual_viewport_node = visual_context_tree.node_at(Web::Painting::VISUAL_VIEWPORT_NODE_INDEX); + auto const* transform = visual_viewport_node.data.get_pointer(); + VERIFY(transform); + return *transform; +} + +static bool visual_viewport_transforms_match(Web::Painting::TransformData const& a, Web::Painting::TransformData const& b) +{ + static constexpr float epsilon = 0.01f; + + for (size_t row = 0; row < 4; ++row) { + for (size_t column = 0; column < 4; ++column) { + if (AK::fabs(a.matrix[row, column] - b.matrix[row, column]) > epsilon) + return false; + } + } + + return AK::fabs(a.origin.x() - b.origin.x()) <= epsilon + && AK::fabs(a.origin.y() - b.origin.y()) <= epsilon; +} + +static Web::Compositor::AsyncScrollNodeStableID viewport_stable_id_from(Web::Compositor::AsyncScrollNodeID node_id) +{ + return { + .node_id = node_id.document_id, + .kind = Web::Compositor::AsyncScrollNodeKind::Viewport, + }; +} + +static void clamp_visual_viewport_transform_to_viewport(Web::Painting::TransformData& transform, Gfx::IntRect viewport_rect) +{ + auto scale = transform.matrix[0, 0]; + if (scale <= 1.0f) { + transform.matrix[0, 3] = 0; + transform.matrix[1, 3] = 0; + return; + } + + auto min_x = -static_cast(viewport_rect.width()) * (scale - 1.0f); + auto min_y = -static_cast(viewport_rect.height()) * (scale - 1.0f); + transform.matrix[0, 3] = clamp(transform.matrix[0, 3], min_x, 0.0f); + transform.matrix[1, 3] = clamp(transform.matrix[1, 3], min_y, 0.0f); +} + ContextState::ContextState(Optional page_id, CompositorStateWebContentClient& web_content_client, bool async_scrolling_enabled) : m_web_content_client(web_content_client) , m_page_id(page_id) @@ -134,7 +180,10 @@ void ContextState::install_display_list_update( VERIFY(display_list->compatible_visual_context_tree_version() == visual_context_tree.version()); m_display_list = move(display_list); m_visual_context_tree = move(visual_context_tree); + m_visual_context_tree_for_compositing.clear(); m_scroll_state_snapshot = move(scroll_state_snapshot); + if (m_async_visual_viewport_transform.has_value() && visual_viewport_transforms_match(visual_viewport_transform(*m_visual_context_tree), *m_async_visual_viewport_transform)) + m_async_visual_viewport_transform.clear(); if (!m_async_scrolling_enabled) return; @@ -150,6 +199,7 @@ void ContextState::install_display_list_update( m_wheel_routing_admission = wheel_routing_admission; m_can_accept_async_wheel_events = wheel_routing_admission == Web::Compositor::WheelRoutingAdmission::Accepted; + m_has_blocking_wheel_event_listeners = async_scrolling_state.has_blocking_wheel_event_listeners; m_viewport_scrollbar_controller.set_scrollbars(async_scrolling_state.viewport_scrollbars); m_async_scroll_tree.set_state(move(async_scrolling_state)); @@ -167,6 +217,9 @@ void ContextState::update_visual_context_tree(Web::Painting::AccumulatedVisualCo VERIFY(m_display_list); VERIFY(m_display_list->compatible_visual_context_tree_version() == visual_context_tree.version()); m_visual_context_tree = move(visual_context_tree); + m_visual_context_tree_for_compositing.clear(); + if (m_async_visual_viewport_transform.has_value() && visual_viewport_transforms_match(visual_viewport_transform(*m_visual_context_tree), *m_async_visual_viewport_transform)) + m_async_visual_viewport_transform.clear(); if (m_has_async_scrolling_state) rebuild_wheel_hit_test_targets(); @@ -217,6 +270,7 @@ void ContextState::invalidate_wheel_event_listener_state(u64 generation) m_wheel_event_listener_state_generation = max(m_wheel_event_listener_state_generation, generation); m_wheel_routing_admission = Web::Compositor::WheelRoutingAdmission::StaleWheelEventListeners; m_can_accept_async_wheel_events = false; + m_has_blocking_wheel_event_listeners = true; } ContextState::ContextUpdateResult ContextState::handle_mouse_event(Web::MouseEvent const& event) @@ -301,6 +355,52 @@ ContextState::ContextUpdateResult ContextState::handle_mouse_event(Web::MouseEve VERIFY_NOT_REACHED(); } +ContextState::ContextUpdateResult ContextState::handle_pinch_event(Web::PinchEvent const& event) +{ + if (!presents_to_client()) + return {}; + if (!m_can_accept_async_wheel_events) + return {}; + if (m_has_blocking_wheel_event_listeners) + return {}; + if (!m_visual_context_tree.has_value()) + return {}; + + auto scale = 1.0 + event.scale_delta; + if (scale == 1.0) + return {}; + + auto transform = m_async_visual_viewport_transform.value_or(visual_viewport_transform(*m_visual_context_tree)); + auto old_scale = transform.matrix[0, 0]; + if (old_scale <= 0) + return {}; + + auto new_scale = clamp(static_cast(old_scale) * scale, 1.0, 5.0); + auto applied_scale = static_cast(new_scale / old_scale); + if (applied_scale == 1.0f) + return {}; + + auto position = Gfx::FloatPoint { + static_cast(event.position.x().value()), + static_cast(event.position.y().value()), + }; + auto gesture_transform = Gfx::translation_matrix(Gfx::FloatVector3 { position.x(), position.y(), 0 }) + * Gfx::scale_matrix(Gfx::FloatVector3 { applied_scale, applied_scale, 1 }) + * Gfx::translation_matrix(Gfx::FloatVector3 { -position.x(), -position.y(), 0 }); + + transform.matrix = gesture_transform * transform.matrix; + clamp_visual_viewport_transform_to_viewport(transform, m_async_scrolling_viewport_rect); + m_async_visual_viewport_transform = transform; + m_visual_context_tree_for_compositing.clear(); + rebuild_wheel_hit_test_targets(); + + return { + .accepted = true, + .frame_to_present = m_async_scrolling_viewport_rect, + .should_request_rendering_update = false, + }; +} + ContextState::AsyncScrollResult ContextState::async_scroll_by( Web::UniqueNodeID expected_document_id, Gfx::FloatPoint position, @@ -347,17 +447,49 @@ ContextState::ContextUpdateResult ContextState::async_scroll_by(Gfx::FloatPoint if (!m_can_accept_async_wheel_events) return {}; - auto scroll_target = m_async_scroll_tree.hit_test_scroll_node_for_wheel(position, delta); - if (scroll_target.blocked_by_main_thread_region || scroll_target.blocked_by_wheel_event_region || !scroll_target.node_id.has_value()) + auto initial_scroll_target = m_async_scroll_tree.hit_test_scroll_node_for_wheel(position, delta); + if (initial_scroll_target.blocked_by_main_thread_region || initial_scroll_target.blocked_by_wheel_event_region) return {}; + Optional frame_to_present; + auto remaining_delta = delta; + if (auto visual_viewport_scroll_delta = apply_visual_viewport_scroll_delta(delta); visual_viewport_scroll_delta.has_value()) { + Vector scroll_offsets; + scroll_offsets.append(visual_viewport_scroll_delta->scroll_offset); + store_pending_async_scroll_offsets(scroll_offsets); + remaining_delta.translate_by(-visual_viewport_scroll_delta->consumed_delta.x(), -visual_viewport_scroll_delta->consumed_delta.y()); + frame_to_present = m_async_scrolling_viewport_rect; + } + + if (remaining_delta.is_zero()) + return { + .accepted = true, + .frame_to_present = frame_to_present, + .should_request_rendering_update = frame_to_present.has_value(), + }; + + auto async_scroll_delta = remaining_delta; + if (auto scale = visual_viewport_scale_for_compositing(); scale.has_value() && *scale > 1.0f) + async_scroll_delta.scale_by(1.0f / *scale); + + auto scroll_target = m_async_scroll_tree.hit_test_scroll_node_for_wheel(position, async_scroll_delta); + if (scroll_target.blocked_by_main_thread_region || scroll_target.blocked_by_wheel_event_region || !scroll_target.node_id.has_value()) { + if (frame_to_present.has_value()) + return { + .accepted = true, + .frame_to_present = frame_to_present, + .should_request_rendering_update = true, + }; + return {}; + } + auto async_scroll_viewport_rect = m_async_scrolling_viewport_rect; - auto scroll_offsets = m_async_scroll_tree.apply_scroll_delta(*scroll_target.node_id, delta, m_scroll_state_snapshot); + auto scroll_offsets = m_async_scroll_tree.apply_scroll_delta(*scroll_target.node_id, async_scroll_delta, m_scroll_state_snapshot); if (scroll_offsets.is_empty()) return { .accepted = true, - .frame_to_present = {}, - .should_request_rendering_update = false, + .frame_to_present = frame_to_present, + .should_request_rendering_update = frame_to_present.has_value(), }; rebuild_wheel_hit_test_targets(); @@ -599,6 +731,64 @@ Optional ContextState::viewport_scroll_offset_from(Vector ContextState::visual_viewport_scale_for_compositing() const +{ + if (!m_visual_context_tree.has_value()) + return {}; + + if (m_async_visual_viewport_transform.has_value()) + return m_async_visual_viewport_transform->matrix[0, 0]; + + return visual_viewport_transform(*m_visual_context_tree).matrix[0, 0]; +} + +Optional ContextState::apply_visual_viewport_scroll_delta(Gfx::FloatPoint delta) +{ + if (delta.is_zero()) + return {}; + if (!m_visual_context_tree.has_value()) + return {}; + + auto viewport_node_id = m_async_scroll_tree.viewport_scroll_node_id(); + if (!viewport_node_id.has_value()) + return {}; + + auto viewport_scroll_offset = m_async_scroll_tree.scroll_offset_for_node(*viewport_node_id, m_scroll_state_snapshot); + if (!viewport_scroll_offset.has_value()) + return {}; + + auto transform = m_async_visual_viewport_transform.value_or(visual_viewport_transform(*m_visual_context_tree)); + auto scale = transform.matrix[0, 0]; + if (scale <= 1.0f) + return {}; + + auto min_x = -static_cast(m_async_scrolling_viewport_rect.width()) * (scale - 1.0f); + auto min_y = -static_cast(m_async_scrolling_viewport_rect.height()) * (scale - 1.0f); + + auto old_x = transform.matrix[0, 3]; + auto old_y = transform.matrix[1, 3]; + auto new_x = clamp(old_x - delta.x(), min_x, 0.0f); + auto new_y = clamp(old_y - delta.y(), min_y, 0.0f); + Gfx::FloatPoint consumed_delta { old_x - new_x, old_y - new_y }; + if (consumed_delta.is_zero()) + return {}; + + transform.matrix[0, 3] = new_x; + transform.matrix[1, 3] = new_y; + m_async_visual_viewport_transform = transform; + m_visual_context_tree_for_compositing.clear(); + rebuild_wheel_hit_test_targets(); + + return VisualViewportScrollDelta { + .scroll_offset = { + .stable_node_id = viewport_stable_id_from(*viewport_node_id), + .compositor_scroll_offset = *viewport_scroll_offset, + .unadopted_scroll_delta = consumed_delta.scaled(1.0f / scale), + }, + .consumed_delta = consumed_delta, + }; +} + Optional ContextState::reapply_pending_async_scroll_offsets(Vector const& pending_scroll_offsets) { Optional viewport_scroll_offset; @@ -653,7 +843,7 @@ void ContextState::rebuild_wheel_hit_test_targets() VERIFY(m_display_list); m_async_scroll_tree.rebuild_wheel_hit_test_targets( m_display_list, - ¤t_visual_context_tree(), + &visual_context_tree_for_compositing(), m_scroll_state_snapshot); } @@ -668,10 +858,20 @@ bool ContextState::can_render_frame() const return m_display_list && m_backing_store_manager.is_valid(); } +Web::Painting::AccumulatedVisualContextTree const& ContextState::visual_context_tree_for_compositing() const +{ + if (!m_async_visual_viewport_transform.has_value()) + return current_visual_context_tree(); + + m_visual_context_tree_for_compositing = current_visual_context_tree(); + m_visual_context_tree_for_compositing->set_visual_viewport_transform(*m_async_visual_viewport_transform); + return *m_visual_context_tree_for_compositing; +} + void ContextState::paint_current_display_list(Web::Painting::DisplayListPlayerSkia& display_list_player, Gfx::PaintingSurface& surface) { VERIFY(m_display_list); - display_list_player.execute(*m_display_list, current_visual_context_tree(), m_display_list_resource_storage, m_scroll_state_snapshot, surface); + display_list_player.execute(*m_display_list, visual_context_tree_for_compositing(), m_display_list_resource_storage, m_scroll_state_snapshot, surface); m_viewport_scrollbar_controller.paint(surface, display_list_player, m_scroll_state_snapshot); } diff --git a/Services/Compositor/ContextState.h b/Services/Compositor/ContextState.h index 2d2459e72b..31c68203f0 100644 --- a/Services/Compositor/ContextState.h +++ b/Services/Compositor/ContextState.h @@ -40,6 +40,7 @@ class SkiaBackendContext; namespace Web { struct MouseEvent; +struct PinchEvent; } @@ -121,6 +122,7 @@ public: void invalidate_wheel_event_listener_state(u64 generation); ContextUpdateResult handle_mouse_event(Web::MouseEvent const&); + ContextUpdateResult handle_pinch_event(Web::PinchEvent const&); AsyncScrollResult async_scroll_by( Web::UniqueNodeID document_id, Gfx::FloatPoint position, @@ -157,15 +159,23 @@ public: void did_finish_gpu_present(i32 bitmap_id); private: + struct VisualViewportScrollDelta { + Web::Compositor::AsyncScrollOffset scroll_offset; + Gfx::FloatPoint consumed_delta; + }; + void stop_backing_store_shrink_timer(); Web::Painting::AccumulatedVisualContextTree const& current_visual_context_tree() const; Optional viewport_scroll_offset_from(Vector const&) const; + Optional visual_viewport_scale_for_compositing() const; + Optional apply_visual_viewport_scroll_delta(Gfx::FloatPoint); Optional reapply_pending_async_scroll_offsets(Vector const&); void store_pending_async_scroll_offsets(Vector const&, Optional = {}); Optional apply_viewport_scrollbar_drag(ViewportScrollbarController::Drag const&); void rebuild_wheel_hit_test_targets(); bool is_present_blocked() const; bool can_render_frame() const; + Web::Painting::AccumulatedVisualContextTree const& visual_context_tree_for_compositing() const; void paint_current_display_list(Web::Painting::DisplayListPlayerSkia&, Gfx::PaintingSurface&); CompositorStateWebContentClient& m_web_content_client; @@ -178,6 +188,7 @@ private: RefPtr m_display_list; Optional m_visual_context_tree; + mutable Optional m_visual_context_tree_for_compositing; Web::Painting::DisplayListResourceStorage m_display_list_resource_storage; Web::Painting::ScrollStateSnapshot m_scroll_state_snapshot; BackingStoreManager m_backing_store_manager; @@ -191,8 +202,10 @@ private: Gfx::IntRect m_async_scrolling_viewport_rect; bool m_has_async_scrolling_state { false }; bool m_can_accept_async_wheel_events { false }; + bool m_has_blocking_wheel_event_listeners { false }; u64 m_wheel_event_listener_state_generation { 0 }; Web::Compositor::WheelRoutingAdmission m_wheel_routing_admission { Web::Compositor::WheelRoutingAdmission::NoAsyncScrollingState }; + Optional m_async_visual_viewport_transform; Gfx::IntSize m_viewport_size; Web::Compositor::WindowResizingInProgress m_window_resize_in_progress { Web::Compositor::WindowResizingInProgress::No }; diff --git a/Services/WebContent/ConnectionFromClient.cpp b/Services/WebContent/ConnectionFromClient.cpp index c2ba5974a7..b341d9d0ae 100644 --- a/Services/WebContent/ConnectionFromClient.cpp +++ b/Services/WebContent/ConnectionFromClient.cpp @@ -369,8 +369,12 @@ void ConnectionFromClient::mouse_event(u64 page_id, Web::MouseEvent event) return nullptr; if (auto const* mouse_event = m_input_event_queue.tail().event.get_pointer()) { - if (mouse_event->type == event.type) - return mouse_event; + if (mouse_event->type != event.type) + return nullptr; + if (event.type == Web::MouseEvent::Type::MouseWheel + && mouse_event->async_scroll_performed_default_action != event.async_scroll_performed_default_action) + return nullptr; + return mouse_event; } return nullptr; @@ -397,6 +401,25 @@ void ConnectionFromClient::drag_event(u64 page_id, Web::DragEvent event) void ConnectionFromClient::pinch_event(u64 page_id, Web::PinchEvent event) { + auto page = m_page_host->page(page_id); + if (!page.has_value()) { + async_did_finish_handling_input_event(page_id, Web::EventResult::Dropped); + return; + } + + // OPTIMIZATION: Coalesce consecutive unprocessed pinch events. Pinch scale + // deltas are multiplicative, so preserve the combined scale change. + if (!m_input_event_queue.is_empty() && m_input_event_queue.tail().page_id == page_id) { + if (auto const* pinch_event = m_input_event_queue.tail().event.get_pointer()) { + event.scale_delta = (1.0 + pinch_event->scale_delta) * (1.0 + event.scale_delta) - 1.0; + m_input_event_queue.tail().event = move(event); + ++m_input_event_queue.tail().coalesced_event_count; + + page->page().client().request_frame(); + return; + } + } + enqueue_input_event({ page_id, move(event), 0 }); }