mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-21 16:26:58 +08:00
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>
124 lines
3.5 KiB
TypeScript
124 lines
3.5 KiB
TypeScript
import {
|
|
DefaultTool,
|
|
EdgelessLegacySlotIdentifier,
|
|
} from '@blocksuite/affine-block-surface';
|
|
import { on } from '@blocksuite/affine-shared/utils';
|
|
import type { PointerEventState } from '@blocksuite/std';
|
|
import { BaseTool, MouseButton, type ToolOptions } from '@blocksuite/std/gfx';
|
|
import { Signal } from '@preact/signals-core';
|
|
|
|
interface RestorablePresentToolOptions {
|
|
mode?: string; // 'fit' | 'fill', simplified to string for local use
|
|
restoredAfterPan?: boolean;
|
|
}
|
|
|
|
export type PanToolOption = {
|
|
panning: boolean;
|
|
};
|
|
|
|
export class PanTool extends BaseTool<PanToolOption> {
|
|
static override toolName = 'pan';
|
|
|
|
private _lastPoint: [number, number] | null = null;
|
|
|
|
readonly panning$ = new Signal<boolean>(false);
|
|
|
|
override get allowDragWithRightButton(): boolean {
|
|
return true;
|
|
}
|
|
|
|
override dragEnd(_: PointerEventState): void {
|
|
this._lastPoint = null;
|
|
this.panning$.value = false;
|
|
}
|
|
|
|
override dragMove(e: PointerEventState): void {
|
|
if (!this._lastPoint) return;
|
|
|
|
const { viewport } = this.gfx;
|
|
const { zoom } = viewport;
|
|
|
|
const [lastX, lastY] = this._lastPoint;
|
|
const deltaX = lastX - e.x;
|
|
const deltaY = lastY - e.y;
|
|
|
|
this._lastPoint = [e.x, e.y];
|
|
|
|
viewport.applyDeltaCenter(deltaX / zoom, deltaY / zoom);
|
|
}
|
|
|
|
override dragStart(e: PointerEventState): void {
|
|
this._lastPoint = [e.x, e.y];
|
|
this.panning$.value = true;
|
|
}
|
|
|
|
override mounted(): void {
|
|
this.addHook('pointerDown', evt => {
|
|
const shouldPanWithMiddle = evt.raw.button === MouseButton.MIDDLE;
|
|
|
|
if (!shouldPanWithMiddle) {
|
|
return;
|
|
}
|
|
|
|
const currentTool = this.controller.currentToolOption$.peek();
|
|
const { toolType, options: originalToolOptions } = currentTool;
|
|
|
|
if (toolType?.toolName === PanTool.toolName) {
|
|
return;
|
|
}
|
|
|
|
evt.raw.preventDefault();
|
|
|
|
const selectionToRestore = this.gfx.selection.surfaceSelections.slice();
|
|
|
|
const restoreToPrevious = () => {
|
|
this.gfx.selection.set(selectionToRestore);
|
|
|
|
if (!toolType) return;
|
|
// restore to DefaultTool if previous tool is CopilotTool
|
|
if (toolType.toolName === 'copilot') {
|
|
this.controller.setTool(DefaultTool);
|
|
return;
|
|
}
|
|
|
|
let finalOptions: ToolOptions<BaseTool<any>> | undefined =
|
|
originalToolOptions;
|
|
if (toolType.toolName === 'frameNavigator') {
|
|
// When restoring PresentTool (frameNavigator) after a temporary pan (e.g., via middle mouse button),
|
|
// set 'restoredAfterPan' to true. This allows PresentTool to avoid an unwanted viewport reset
|
|
// and maintain the panned position.
|
|
const currentPresentOptions = originalToolOptions as
|
|
| RestorablePresentToolOptions
|
|
| undefined;
|
|
finalOptions = {
|
|
...currentPresentOptions,
|
|
restoredAfterPan: true,
|
|
} as RestorablePresentToolOptions;
|
|
}
|
|
this.controller.setTool(toolType, finalOptions);
|
|
};
|
|
|
|
// If in presentation mode, disable black background after middle mouse drag
|
|
if (toolType?.toolName === 'frameNavigator') {
|
|
const slots = this.std.get(EdgelessLegacySlotIdentifier);
|
|
slots.navigatorSettingUpdated.next({
|
|
blackBackground: false,
|
|
});
|
|
}
|
|
|
|
this.controller.setTool(PanTool, {
|
|
panning: true,
|
|
});
|
|
|
|
const dispose = on(document, 'pointerup', evt => {
|
|
if (evt.button === MouseButton.MIDDLE) {
|
|
restoreToPrevious();
|
|
}
|
|
dispose();
|
|
});
|
|
|
|
return false;
|
|
});
|
|
}
|
|
}
|