mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-12-08 06:09:58 +00:00
LibWeb: Implement pinch-to-zoom support
Adds pinch event handling that adjusts the VisualViewport scale and offset. VisualViewport's (offset, scale) is then used to construct a transformation matrix which is applied before display list execution.
This commit is contained in:
parent
b477c6bfc4
commit
9862d8b4a6
Notes:
github-actions[bot]
2025-10-10 13:39:26 +00:00
Author: https://github.com/kalenikaliaksandr
Commit: 9862d8b4a6
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/6430
Reviewed-by: https://github.com/gmta
Reviewed-by: https://github.com/konradekk
14 changed files with 149 additions and 40 deletions
|
|
@ -145,4 +145,41 @@ WebIDL::CallbackType* VisualViewport::onscrollend()
|
|||
return event_handler_attribute(HTML::EventNames::scrollend);
|
||||
}
|
||||
|
||||
Gfx::AffineTransform VisualViewport::transform() const
|
||||
{
|
||||
Gfx::AffineTransform transform;
|
||||
auto offset = m_offset.to_type<double>() * m_scale;
|
||||
transform.translate(-offset.x(), -offset.y());
|
||||
transform.scale({ m_scale, m_scale });
|
||||
return transform;
|
||||
}
|
||||
|
||||
void VisualViewport::zoom(CSSPixelPoint position, double scale_delta)
|
||||
{
|
||||
static constexpr double MIN_ALLOWED_SCALE = 1.0;
|
||||
static constexpr double MAX_ALLOWED_SCALE = 5.0;
|
||||
double new_scale = clamp(m_scale * (1 + scale_delta), MIN_ALLOWED_SCALE, MAX_ALLOWED_SCALE);
|
||||
double applied_delta = new_scale / m_scale;
|
||||
|
||||
// For pinch zoom we want focal_point to stay put on screen:
|
||||
// scale_new * (focal_point - offset_new) = scale_old * (focal_point - offset_old)
|
||||
auto new_offset = m_offset.to_type<double>() * m_scale * applied_delta;
|
||||
new_offset += position.to_type<int>().to_type<double>() * (applied_delta - 1.0f);
|
||||
|
||||
auto viewport_float_size = m_document->navigable()->viewport_rect().size().to_type<double>();
|
||||
auto max_x_offset = max(0.0, viewport_float_size.width() * (new_scale - 1.0f));
|
||||
auto max_y_offset = max(0.0, viewport_float_size.height() * (new_scale - 1.0f));
|
||||
new_offset = { clamp(new_offset.x(), 0.0f, max_x_offset), clamp(new_offset.y(), 0.0f, max_y_offset) };
|
||||
|
||||
m_scale = new_scale;
|
||||
m_offset = (new_offset / m_scale).to_type<CSSPixels>();
|
||||
m_document->set_needs_display(InvalidateDisplayList::No);
|
||||
}
|
||||
|
||||
CSSPixelPoint VisualViewport::map_to_layout_viewport(CSSPixelPoint position) const
|
||||
{
|
||||
auto inverse = transform().inverse().value_or({});
|
||||
return inverse.map(position.to_type<int>()).to_type<CSSPixels>();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ public:
|
|||
|
||||
void scroll_by(CSSPixelPoint delta) { m_offset += delta; }
|
||||
|
||||
[[nodiscard]] Gfx::AffineTransform transform() const;
|
||||
void zoom(CSSPixelPoint position, double scale_delta);
|
||||
CSSPixelPoint map_to_layout_viewport(CSSPixelPoint) const;
|
||||
|
||||
private:
|
||||
explicit VisualViewport(DOM::Document&);
|
||||
|
||||
|
|
|
|||
|
|
@ -6478,7 +6478,17 @@ RefPtr<Painting::DisplayList> Document::cached_display_list() const
|
|||
|
||||
RefPtr<Painting::DisplayList> Document::record_display_list(HTML::PaintConfig config)
|
||||
{
|
||||
auto update_visual_viewport_transform = [&](Painting::DisplayList& display_list) {
|
||||
auto transform = visual_viewport()->transform();
|
||||
auto matrix = transform.to_matrix();
|
||||
matrix[0, 3] *= display_list.device_pixels_per_css_pixel();
|
||||
matrix[1, 3] *= display_list.device_pixels_per_css_pixel();
|
||||
matrix[2, 3] *= display_list.device_pixels_per_css_pixel();
|
||||
display_list.set_visual_viewport_transform(matrix);
|
||||
};
|
||||
|
||||
if (m_cached_display_list && m_cached_display_list_paint_config == config) {
|
||||
update_visual_viewport_transform(*m_cached_display_list);
|
||||
return m_cached_display_list;
|
||||
}
|
||||
|
||||
|
|
@ -6542,6 +6552,7 @@ RefPtr<Painting::DisplayList> Document::record_display_list(HTML::PaintConfig co
|
|||
m_cached_display_list = display_list;
|
||||
m_cached_display_list_paint_config = config;
|
||||
|
||||
update_visual_viewport_transform(*m_cached_display_list);
|
||||
return display_list;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -291,8 +291,8 @@ void EventLoop::process_input_events() const
|
|||
[&](Web::DragEvent& drag_event) {
|
||||
return page.handle_drag_and_drop_event(drag_event.type, drag_event.position, drag_event.screen_position, drag_event.button, drag_event.buttons, drag_event.modifiers, move(drag_event.files));
|
||||
},
|
||||
[&](Web::PinchEvent&) {
|
||||
return EventResult::Dropped;
|
||||
[&](Web::PinchEvent& pinch_event) {
|
||||
return page.handle_pinch_event(pinch_event.position, pinch_event.scale_delta);
|
||||
});
|
||||
|
||||
for (size_t i = 0; i < event.coalesced_event_count; ++i)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
#include <LibUnicode/CharacterTypes.h>
|
||||
#include <LibUnicode/Segmenter.h>
|
||||
#include <LibWeb/CSS/VisualViewport.h>
|
||||
#include <LibWeb/DOM/Text.h>
|
||||
#include <LibWeb/Editing/Internal/Algorithms.h>
|
||||
#include <LibWeb/HTML/CloseWatcherManager.h>
|
||||
|
|
@ -383,17 +384,20 @@ GC::Ptr<Painting::PaintableBox const> EventHandler::paint_root() const
|
|||
return m_navigable->active_document()->paintable_box();
|
||||
}
|
||||
|
||||
EventResult EventHandler::handle_mousewheel(CSSPixelPoint 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)
|
||||
{
|
||||
if (should_ignore_device_input_event())
|
||||
return EventResult::Dropped;
|
||||
|
||||
if (!m_navigable->active_document())
|
||||
auto document = m_navigable->active_document();
|
||||
if (!document)
|
||||
return EventResult::Dropped;
|
||||
if (!m_navigable->active_document()->is_fully_active())
|
||||
if (!document->is_fully_active())
|
||||
return EventResult::Dropped;
|
||||
|
||||
m_navigable->active_document()->update_layout(DOM::UpdateLayoutReason::EventHandlerHandleMouseWheel);
|
||||
auto viewport_position = document->visual_viewport()->map_to_layout_viewport(visual_viewport_position);
|
||||
|
||||
document->update_layout(DOM::UpdateLayoutReason::EventHandlerHandleMouseWheel);
|
||||
|
||||
if (!paint_root())
|
||||
return EventResult::Dropped;
|
||||
|
|
@ -448,16 +452,19 @@ EventResult EventHandler::handle_mousewheel(CSSPixelPoint viewport_position, CSS
|
|||
return handled_event;
|
||||
}
|
||||
|
||||
EventResult EventHandler::handle_mouseup(CSSPixelPoint viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers)
|
||||
EventResult EventHandler::handle_mouseup(CSSPixelPoint visual_viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers)
|
||||
{
|
||||
if (should_ignore_device_input_event())
|
||||
return EventResult::Dropped;
|
||||
|
||||
if (!m_navigable->active_document())
|
||||
auto document = m_navigable->active_document();
|
||||
if (!document)
|
||||
return EventResult::Dropped;
|
||||
if (!m_navigable->active_document()->is_fully_active())
|
||||
if (!document->is_fully_active())
|
||||
return EventResult::Dropped;
|
||||
|
||||
auto viewport_position = document->visual_viewport()->map_to_layout_viewport(visual_viewport_position);
|
||||
|
||||
m_navigable->active_document()->update_layout(DOM::UpdateLayoutReason::EventHandlerHandleMouseUp);
|
||||
|
||||
if (!paint_root())
|
||||
|
|
@ -594,22 +601,24 @@ after_node_use:
|
|||
return handled_event;
|
||||
}
|
||||
|
||||
EventResult EventHandler::handle_mousedown(CSSPixelPoint viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers)
|
||||
EventResult EventHandler::handle_mousedown(CSSPixelPoint visual_viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers)
|
||||
{
|
||||
if (should_ignore_device_input_event())
|
||||
return EventResult::Dropped;
|
||||
|
||||
if (!m_navigable->active_document())
|
||||
auto document = m_navigable->active_document();
|
||||
if (!document)
|
||||
return EventResult::Dropped;
|
||||
if (!m_navigable->active_document()->is_fully_active())
|
||||
if (!document->is_fully_active())
|
||||
return EventResult::Dropped;
|
||||
|
||||
auto viewport_position = document->visual_viewport()->map_to_layout_viewport(visual_viewport_position);
|
||||
|
||||
m_navigable->active_document()->update_layout(DOM::UpdateLayoutReason::EventHandlerHandleMouseDown);
|
||||
|
||||
if (!paint_root())
|
||||
return EventResult::Dropped;
|
||||
|
||||
GC::Ref<DOM::Document> document = *m_navigable->active_document();
|
||||
GC::Ptr<DOM::Node> node;
|
||||
|
||||
ScopeGuard update_hovered_node_guard = [&node, &document] {
|
||||
|
|
@ -723,7 +732,7 @@ EventResult EventHandler::handle_mousedown(CSSPixelPoint viewport_position, CSSP
|
|||
return EventResult::Handled;
|
||||
}
|
||||
|
||||
EventResult EventHandler::handle_mousemove(CSSPixelPoint viewport_position, CSSPixelPoint screen_position, u32 buttons, u32 modifiers)
|
||||
EventResult EventHandler::handle_mousemove(CSSPixelPoint visual_viewport_position, CSSPixelPoint screen_position, u32 buttons, u32 modifiers)
|
||||
{
|
||||
if (should_ignore_device_input_event())
|
||||
return EventResult::Dropped;
|
||||
|
|
@ -733,13 +742,14 @@ EventResult EventHandler::handle_mousemove(CSSPixelPoint viewport_position, CSSP
|
|||
if (!m_navigable->active_document()->is_fully_active())
|
||||
return EventResult::Dropped;
|
||||
|
||||
auto& document = *m_navigable->active_document();
|
||||
auto viewport_position = document.visual_viewport()->map_to_layout_viewport(visual_viewport_position);
|
||||
|
||||
m_navigable->active_document()->update_layout(DOM::UpdateLayoutReason::EventHandlerHandleMouseMove);
|
||||
|
||||
if (!paint_root())
|
||||
return EventResult::Dropped;
|
||||
|
||||
auto& document = *m_navigable->active_document();
|
||||
|
||||
bool hovered_node_changed = false;
|
||||
bool is_hovering_link = false;
|
||||
Gfx::Cursor hovered_node_cursor = Gfx::StandardCursor::None;
|
||||
|
|
@ -912,7 +922,7 @@ EventResult EventHandler::handle_mouseleave()
|
|||
return EventResult::Handled;
|
||||
}
|
||||
|
||||
EventResult EventHandler::handle_doubleclick(CSSPixelPoint viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers)
|
||||
EventResult EventHandler::handle_doubleclick(CSSPixelPoint visual_viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers)
|
||||
{
|
||||
if (should_ignore_device_input_event())
|
||||
return EventResult::Dropped;
|
||||
|
|
@ -923,6 +933,7 @@ EventResult EventHandler::handle_doubleclick(CSSPixelPoint viewport_position, CS
|
|||
return EventResult::Dropped;
|
||||
|
||||
auto& document = *m_navigable->active_document();
|
||||
auto viewport_position = document.visual_viewport()->map_to_layout_viewport(visual_viewport_position);
|
||||
|
||||
document.update_layout(DOM::UpdateLayoutReason::EventHandlerHandleDoubleClick);
|
||||
|
||||
|
|
@ -1004,7 +1015,7 @@ EventResult EventHandler::handle_doubleclick(CSSPixelPoint viewport_position, CS
|
|||
return EventResult::Handled;
|
||||
}
|
||||
|
||||
EventResult EventHandler::handle_drag_and_drop_event(DragEvent::Type type, CSSPixelPoint viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers, Vector<HTML::SelectedFile> files)
|
||||
EventResult EventHandler::handle_drag_and_drop_event(DragEvent::Type type, CSSPixelPoint visual_viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers, Vector<HTML::SelectedFile> files)
|
||||
{
|
||||
if (!m_navigable->active_document())
|
||||
return EventResult::Dropped;
|
||||
|
|
@ -1012,6 +1023,8 @@ EventResult EventHandler::handle_drag_and_drop_event(DragEvent::Type type, CSSPi
|
|||
return EventResult::Dropped;
|
||||
|
||||
auto& document = *m_navigable->active_document();
|
||||
auto viewport_position = document.visual_viewport()->map_to_layout_viewport(visual_viewport_position);
|
||||
|
||||
document.update_layout(DOM::UpdateLayoutReason::EventHandlerHandleDragAndDrop);
|
||||
|
||||
if (!paint_root())
|
||||
|
|
@ -1050,6 +1063,19 @@ EventResult EventHandler::handle_drag_and_drop_event(DragEvent::Type type, CSSPi
|
|||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
EventResult EventHandler::handle_pinch_event(CSSPixelPoint point, double scale_delta)
|
||||
{
|
||||
auto document = m_navigable->active_document();
|
||||
if (!document)
|
||||
return EventResult::Dropped;
|
||||
if (!document->is_fully_active())
|
||||
return EventResult::Dropped;
|
||||
|
||||
auto visual_viewport = document->visual_viewport();
|
||||
visual_viewport->zoom(point, scale_delta);
|
||||
return EventResult::Handled;
|
||||
}
|
||||
|
||||
EventResult EventHandler::focus_next_element()
|
||||
{
|
||||
if (!m_navigable->active_document())
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ public:
|
|||
|
||||
EventResult handle_drag_and_drop_event(DragEvent::Type, CSSPixelPoint, CSSPixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, Vector<HTML::SelectedFile> files);
|
||||
|
||||
EventResult handle_pinch_event(CSSPixelPoint, double scale_delta);
|
||||
|
||||
EventResult handle_keydown(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat);
|
||||
EventResult handle_keyup(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat);
|
||||
|
||||
|
|
|
|||
|
|
@ -234,6 +234,11 @@ EventResult Page::handle_drag_and_drop_event(DragEvent::Type type, DevicePixelPo
|
|||
return top_level_traversable()->event_handler().handle_drag_and_drop_event(type, device_to_css_point(position), device_to_css_point(screen_position), button, buttons, modifiers, move(files));
|
||||
}
|
||||
|
||||
EventResult Page::handle_pinch_event(DevicePixelPoint position, double scale)
|
||||
{
|
||||
return top_level_traversable()->event_handler().handle_pinch_event(device_to_css_point(position), scale);
|
||||
}
|
||||
|
||||
EventResult Page::handle_keydown(UIEvents::KeyCode key, unsigned modifiers, u32 code_point, bool repeat)
|
||||
{
|
||||
return focused_navigable().event_handler().handle_keydown(key, modifiers, code_point, repeat);
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ public:
|
|||
EventResult handle_doubleclick(DevicePixelPoint, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers);
|
||||
|
||||
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);
|
||||
|
||||
EventResult handle_keydown(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat);
|
||||
EventResult handle_keyup(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat);
|
||||
|
|
|
|||
|
|
@ -109,6 +109,9 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
static constexpr size_t VISUAL_VIEWPORT_TRANSFORM_INDEX = 1;
|
||||
void set_visual_viewport_transform(Gfx::FloatMatrix4x4 t) { m_commands[VISUAL_VIEWPORT_TRANSFORM_INDEX].command.get<ApplyTransform>().matrix = t; }
|
||||
|
||||
private:
|
||||
DisplayList(double device_pixels_per_css_pixel)
|
||||
: m_device_pixels_per_css_pixel(device_pixels_per_css_pixel)
|
||||
|
|
@ -117,6 +120,7 @@ private:
|
|||
|
||||
AK::SegmentedVector<DisplayListCommandWithScrollAndClip, 512> m_commands;
|
||||
double m_device_pixels_per_css_pixel;
|
||||
Optional<Gfx::FloatMatrix4x4> m_visual_viewport_transform;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,16 @@ StackingContextTransform::StackingContextTransform(Gfx::FloatPoint origin, Gfx::
|
|||
DisplayListRecorder::DisplayListRecorder(DisplayList& command_list)
|
||||
: m_display_list(command_list)
|
||||
{
|
||||
save();
|
||||
// Reserve for visual viewport transform
|
||||
VERIFY(m_display_list.commands().size() == DisplayList::VISUAL_VIEWPORT_TRANSFORM_INDEX);
|
||||
apply_transform({}, Gfx::FloatMatrix4x4::identity());
|
||||
}
|
||||
|
||||
DisplayListRecorder::~DisplayListRecorder() = default;
|
||||
DisplayListRecorder::~DisplayListRecorder()
|
||||
{
|
||||
restore();
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
consteval static int command_nesting_level_change(T const& command)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
Save
|
||||
ApplyTransform matrix=[1 0 0 1 0 0]
|
||||
SaveLayer
|
||||
DrawGlyphRun rect=[35,8 8x18] translation=[35.15625,21.796875] color=rgb(0, 0, 0) scale=1
|
||||
DrawGlyphRun rect=[8,8 28x18] translation=[8,21.796875] color=rgb(0, 0, 0) scale=1
|
||||
|
|
@ -5,4 +7,5 @@ SaveLayer
|
|||
DrawGlyphRun rect=[43,8 28x18] translation=[43.15625,21.796875] color=rgb(0, 0, 0) scale=1
|
||||
DrawLine from=[43,27] to=[71,27] color=rgb(0, 0, 0) thickness=2
|
||||
Restore
|
||||
Restore
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
Save
|
||||
ApplyTransform matrix=[1 0 0 1 0 0]
|
||||
SaveLayer
|
||||
FillRect rect=[8,8 106x22] color=rgb(212, 208, 200)
|
||||
FillPath
|
||||
DrawGlyphRun rect=[13,10 96x18] translation=[13,23.796875] color=rgb(0, 0, 0) scale=1
|
||||
DrawLine from=[13,26] to=[109,26] color=rgb(0, 0, 0) thickness=2
|
||||
Restore
|
||||
Restore
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
Save
|
||||
ApplyTransform matrix=[1 0 0 1 0 0]
|
||||
SaveLayer
|
||||
FillRect rect=[8,8 29x30] color=rgb(0, 0, 0)
|
||||
FillRect rect=[38,8 28x30] color=rgb(0, 0, 0)
|
||||
|
|
@ -5,4 +7,5 @@ SaveLayer
|
|||
FillRect rect=[97,8 28x30] color=rgb(0, 0, 0)
|
||||
FillRect rect=[126,8 29x30] color=rgb(0, 0, 0)
|
||||
Restore
|
||||
Restore
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
Save
|
||||
ApplyTransform matrix=[1 0 0 1 0 0]
|
||||
SaveLayer
|
||||
FillPath
|
||||
FillRect rect=[10,10 300x150] color=rgb(240, 128, 128)
|
||||
DrawGlyphRun rect=[10,10 38x18] translation=[10,23.796875] color=rgb(0, 0, 0) scale=1
|
||||
Restore
|
||||
Restore
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue