fix(editor): switch to PanTool on same frame for middle mouse; restore selection snapshot (#13911)

Bug: In Edgeless mode, pressing and dragging the middle mouse button
over any element incorrectly triggers DefaultTool in the same frame,
causing unintended selection/drag instead of panning. Dragging on empty
area works because no element intercepts left-click logic.

Reproduction:
- Open an Edgeless canvas
- Press and hold middle mouse button over a shape/text/any element and
drag
- Expected: pan the canvas
- Actual: the element gets selected or moved; no panning occurs

Root cause:
1. PanTool switched via requestAnimationFrame; the current frame’s
pointerDown/pointerMove were handled by DefaultTool first (handing
middle mouse to left-click logic).
2. Selection restore used a live reference to
`this.gfx.selection.surfaceSelections`, which could be mutated by other
selection logic during the temporary pan, leading to incorrect
restoration.

Fix:
- Switch to PanTool immediately on the same frame when middle mouse is
pressed; add a guard to avoid switching if PanTool is already active.
- Snapshot `surfaceSelections` using `slice()` before the temporary
switch; restore it on `pointerup` so external mutations won’t affect
restoration.
- Only register the temporary `pointerup` listener when actually
switching; on release, restore the previous tool (including
`frameNavigator` with `restoredAfterPan: true`) and selection.
Additionally, disable black background when exiting from frameNavigator.

Affected files:
- blocksuite/affine/gfx/pointer/src/tools/pan-tool.ts

Tests:
-
packages/frontend/core/src/blocksuite/__tests__/pan-tool-middle-mouse.spec.ts
- Verifies immediate PanTool switch, selection snapshot restoration,
frameNavigator recovery flag, and no-op when PanTool is already active.

Notes:
- Aligned with docs/contributing/tutorial.md. Local validation
performed. Thanks for reviewing!

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
  * Prevented accidental re-activation of the middle-click pan tool.
* Preserved and restored the user's selection and previous tool options
after panning, including correct handling when returning to the frame
navigator.
* Ensured immediate tool switch to pan and reliable cleanup on
middle-button release.

* **Tests**
* Added tests covering middle-click pan behavior, restoration flows, and
no-op when pan is already active.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
This commit is contained in:
ents1008
2025-11-18 14:26:27 +08:00
committed by GitHub
parent 477e6f4106
commit 6c36fc5941
2 changed files with 237 additions and 9 deletions

View File

@@ -60,12 +60,20 @@ export class PanTool extends BaseTool<PanToolOption> {
return;
}
const currentTool = this.controller.currentToolOption$.peek();
const { toolType, options: originalToolOptions } = currentTool;
if (toolType?.toolName === PanTool.toolName) {
return;
}
evt.raw.preventDefault();
const currentTool = this.controller.currentToolOption$.peek();
const selectionToRestore = this.gfx.selection.surfaceSelections.slice();
const restoreToPrevious = () => {
const { toolType, options: originalToolOptions } = currentTool;
const selectionToRestore = this.gfx.selection.surfaceSelections;
this.gfx.selection.set(selectionToRestore);
if (!toolType) return;
// restore to DefaultTool if previous tool is CopilotTool
if (toolType.toolName === 'copilot') {
@@ -88,21 +96,18 @@ export class PanTool extends BaseTool<PanToolOption> {
} as RestorablePresentToolOptions;
}
this.controller.setTool(toolType, finalOptions);
this.gfx.selection.set(selectionToRestore);
};
// If in presentation mode, disable black background after middle mouse drag
if (currentTool.toolType?.toolName === 'frameNavigator') {
if (toolType?.toolName === 'frameNavigator') {
const slots = this.std.get(EdgelessLegacySlotIdentifier);
slots.navigatorSettingUpdated.next({
blackBackground: false,
});
}
requestAnimationFrame(() => {
this.controller.setTool(PanTool, {
panning: true,
});
this.controller.setTool(PanTool, {
panning: true,
});
const dispose = on(document, 'pointerup', evt => {