Use the client coordinates for the pinch-zoom origin

`TouchManager` computes the pinch distances with the screen coordinates, but it
was passing the pinch center in that space too, while the viewer expects a
client coordinate, like the origin coming from a wheel event.

Since a two-finger gesture can be synthesized with WebDriver BiDi, the
pinch-zoom test doesn't have to rely on CDP anymore and can run in Chrome, where
it fails without the above fix.
This commit is contained in:
Calixte Denizet 2026-07-28 22:38:05 +02:00
parent 61acf93172
commit 8960eddfc1
5 changed files with 80 additions and 52 deletions

View File

@ -189,7 +189,7 @@ class TouchManager {
const pDistance = Math.hypot(prevGapX, prevGapY) || 1; const pDistance = Math.hypot(prevGapX, prevGapY) || 1;
if ( if (
!this.#isPinching && !this.#isPinching &&
Math.abs(pDistance - distance) <= TouchManager.MIN_TOUCH_DISTANCE_TO_PINCH Math.abs(pDistance - distance) <= this.MIN_TOUCH_DISTANCE_TO_PINCH
) { ) {
return; return;
} }
@ -207,7 +207,12 @@ class TouchManager {
return; return;
} }
const origin = [(screen0X + screen1X) / 2, (screen0Y + screen1Y) / 2]; // The distances are in screen CSS pixels, but the origin must be in client
// coordinates, like the one coming from a wheel event.
const origin = [
(touch0.clientX + touch1.clientX) / 2,
(touch0.clientY + touch1.clientY) / 2,
];
this.#onPinching?.(origin, pDistance, distance); this.#onPinching?.(origin, pDistance, distance);
} }

View File

@ -31,6 +31,7 @@ import {
kbUndo, kbUndo,
loadAndWait, loadAndWait,
moveEditor, moveEditor,
pinch,
scrollIntoView, scrollIntoView,
selectEditor, selectEditor,
selectEditors, selectEditors,
@ -1419,30 +1420,9 @@ describe("Pinch to resize a drawing", () => {
await closePages(pages); await closePages(pages);
}); });
// Spread two fingers apart, centered on the editor.
async function pinchOut(page, selector) {
const { x, y, width, height } = await getRect(page, selector);
const centerX = x + width / 2;
const centerY = y + height / 2;
const finger0 = await page.touchscreen.touchStart(centerX - 25, centerY);
const finger1 = await page.touchscreen.touchStart(centerX + 25, centerY);
for (let i = 1; i <= 12; i++) {
const gap = 25 + i * 12;
await finger0.move(centerX - gap, centerY);
await finger1.move(centerX + gap, centerY);
}
await finger0.end();
await finger1.end();
}
it("must keep resizing a drawing which came back with an undo", async () => { it("must keep resizing a drawing which came back with an undo", async () => {
await Promise.all( await Promise.all(
pages.map(async ([browserName, page]) => { pages.map(async ([browserName, page]) => {
if (browserName === "firefox") {
pending(
"Touch events are not supported on devices without touch screen in Firefox."
);
}
await switchToInk(page); await switchToInk(page);
const rect = await getRect(page, ".annotationEditorLayer"); const rect = await getRect(page, ".annotationEditorLayer");
@ -1462,11 +1442,21 @@ describe("Pinch to resize a drawing", () => {
await page.waitForSelector(editorSelector); await page.waitForSelector(editorSelector);
await selectEditor(page, editorSelector); await selectEditor(page, editorSelector);
const { width: before } = await getRect(page, editorSelector); const before = await getRect(page, editorSelector);
await pinchOut(page, editorSelector); const startGap = Math.min(before.width, before.height) * 0.2;
const endGap = Math.max(before.width, before.height) * 1.8;
await pinch(
page,
before.x + before.width / 2,
before.y + before.height / 2,
startGap,
endGap
);
const { width: after } = await getRect(page, editorSelector); const { width: after } = await getRect(page, editorSelector);
expect(after).withContext(`In ${browserName}`).toBeGreaterThan(before); expect(after)
.withContext(`In ${browserName}`)
.toBeGreaterThan(before.width);
}) })
); );
}); });

View File

@ -600,6 +600,31 @@ async function dragAndDrop(page, selector, translations, steps = 1) {
await page.waitForSelector("#viewer:not(.noUserSelect)"); await page.waitForSelector("#viewer:not(.noUserSelect)");
} }
// Move two fingers, horizontally centered on (centerX, centerY), from startGap
// to endGap: it's a pinch out when endGap is larger than startGap.
// Keep in mind that `TouchManager` starts to pinch only once the distance
// between the two fingers changed by more than `MIN_TOUCH_DISTANCE_TO_PINCH`
// (35 CSS pixels), hence the first moves are swallowed and the resulting zoom
// factor is smaller than endGap / startGap.
async function pinch(page, centerX, centerY, startGap, endGap, steps = 12) {
const finger0 = await page.touchscreen.touchStart(
centerX - startGap,
centerY
);
const finger1 = await page.touchscreen.touchStart(
centerX + startGap,
centerY
);
const delta = (endGap - startGap) / steps;
for (let i = 1; i <= steps; i++) {
const gap = startGap + i * delta;
await finger0.move(centerX - gap, centerY);
await finger1.move(centerX + gap, centerY);
}
await finger0.end();
await finger1.end();
}
function waitForPageChanging(page) { function waitForPageChanging(page) {
return createPromise(page, resolve => { return createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on("pagechanging", resolve, { window.PDFViewerApplication.eventBus.on("pagechanging", resolve, {
@ -1163,6 +1188,7 @@ export {
paste, paste,
pasteFromClipboard, pasteFromClipboard,
PDI, PDI,
pinch,
scrollIntoView, scrollIntoView,
selectEditor, selectEditor,
selectEditors, selectEditors,

View File

@ -20,6 +20,7 @@ import {
getRect, getRect,
getSpanRectFromText, getSpanRectFromText,
loadAndWait, loadAndWait,
pinch,
scrollIntoView, scrollIntoView,
showViewsManager, showViewsManager,
waitAndClick, waitAndClick,
@ -1527,7 +1528,11 @@ describe("PDF viewer", () => {
beforeEach(async () => { beforeEach(async () => {
pages = await loadAndWait( pages = await loadAndWait(
"tracemonkey.pdf", "tracemonkey.pdf",
`.page[data-page-number = "1"] .endOfContent` `.page[data-page-number = "1"] .endOfContent`,
// Pin the zoom: the drift checked below is proportional to the zoom
// level reached at the end of the pinch, and the default `page-fit`
// depends on the size of the window.
50
); );
}); });
@ -1535,20 +1540,9 @@ describe("PDF viewer", () => {
await closePages(pages); await closePages(pages);
}); });
it("keeps the content under the pinch centre fixed on the screen", async () => { it("keeps the content under the pinch center fixed on the screen", async () => {
await Promise.all( await Promise.all(
pages.map(async ([browserName, page]) => { pages.map(async ([browserName, page]) => {
if (browserName === "firefox") {
pending(
"Touch events are not supported on devices without touch screen in Firefox."
);
}
if (browserName === "chrome") {
pending(
"Pinch zoom emulation is not supported for WebDriver BiDi in Chrome."
);
}
const rect = await getSpanRectFromText(page, 1, "type-stable"); const rect = await getSpanRectFromText(page, 1, "type-stable");
const originX = rect.x + rect.width / 2; const originX = rect.x + rect.width / 2;
const originY = rect.y + rect.height / 2; const originY = rect.y + rect.height / 2;
@ -1564,14 +1558,12 @@ describe("PDF viewer", () => {
}; };
window.PDFViewerApplication.eventBus.on("textlayerrendered", cb); window.PDFViewerApplication.eventBus.on("textlayerrendered", cb);
}); });
const client = await page.target().createCDPSession(); // Spread the two fingers from 50 to 200 pixels apart: the first
await client.send("Input.synthesizePinchGesture", { // moves are swallowed until the distance between them changed by
x: originX, // more than 35 pixels, hence a zoom factor of about 200/85 = 2.4.
y: originY, await pinch(page, originX, originY, 25, 100);
scaleFactor: 3,
gestureSourceType: "touch",
});
await awaitPromise(rendered); await awaitPromise(rendered);
const spanHandle = await page.evaluateHandle(() => const spanHandle = await page.evaluateHandle(() =>
Array.from( Array.from(
document.querySelectorAll( document.querySelectorAll(
@ -1579,7 +1571,21 @@ describe("PDF viewer", () => {
) )
).find(span => span.textContent.includes("type-stable")) ).find(span => span.textContent.includes("type-stable"))
); );
expect(await spanHandle.isIntersectingViewport()).toBeTrue(); expect(await spanHandle.isIntersectingViewport())
.withContext(`In ${browserName}`)
.toBeTrue();
// The text which was under the fingers must still be at the same
// height: only vertically because a page which is larger than its
// container isn't centered in it anymore.
// A few pixels are tolerated because the origin is preserved by
// scrolling: Chrome snaps the scroll offsets to the device pixels and
// the discarded fractions show up as a small drift. It's exact in
// Firefox, which keeps them.
const newRect = await getSpanRectFromText(page, 1, "type-stable");
expect(Math.abs(newRect.y + newRect.height / 2 - originY))
.withContext(`In ${browserName}`)
.toBeLessThan(5);
}) })
); );
}); });

View File

@ -1890,19 +1890,20 @@ class PDFViewer {
container.scrollLeft - firstPage.x, container.scrollLeft - firstPage.x,
container.scrollTop - firstPage.y container.scrollTop - firstPage.y
); );
const intLeft = Math.round(topLeft[0]); const [left, top] = topLeft;
const intTop = Math.round(topLeft[1]);
let pdfOpenParams = `#page=${pageNumber}`; let pdfOpenParams = `#page=${pageNumber}`;
if (!this.isInPresentationMode) { if (!this.isInPresentationMode) {
pdfOpenParams += `&zoom=${normalizedScaleValue},${intLeft},${intTop}`; pdfOpenParams +=
`&zoom=${normalizedScaleValue},` +
`${Math.round(left)},${Math.round(top)}`;
} }
this._location = { this._location = {
pageNumber, pageNumber,
scale: normalizedScaleValue, scale: normalizedScaleValue,
top: intTop, top,
left: intLeft, left,
rotation: this._pagesRotation, rotation: this._pagesRotation,
pdfOpenParams, pdfOpenParams,
}; };