refactor: default-tool box selection (#11800)

### Changed
- Rewrite box selection in `default-tool`, the view can decide whether to be selected in box selection by return a boolean value in `onBoxSelected` method
- Cleanup unnecessary states in `default-tool` and some naming problem
This commit is contained in:
doouding
2025-04-22 08:18:26 +00:00
parent 1d58792631
commit b59f6ebde0
16 changed files with 209 additions and 150 deletions
@@ -2,7 +2,7 @@ import { DefaultTheme, type FrameBlockModel } from '@blocksuite/affine-model';
import { ThemeProvider } from '@blocksuite/affine-shared/services'; import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { Bound } from '@blocksuite/global/gfx'; import { Bound } from '@blocksuite/global/gfx';
import { GfxBlockComponent } from '@blocksuite/std'; import { GfxBlockComponent } from '@blocksuite/std';
import type { SelectedContext } from '@blocksuite/std/gfx'; import type { BoxSelectionContext, SelectedContext } from '@blocksuite/std/gfx';
import { cssVarV2 } from '@toeverything/theme/v2'; import { cssVarV2 } from '@toeverything/theme/v2';
import { html } from 'lit'; import { html } from 'lit';
import { state } from 'lit/decorators.js'; import { state } from 'lit/decorators.js';
@@ -69,6 +69,16 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
return super.onSelected(context); return super.onSelected(context);
} }
override onBoxSelected(context: BoxSelectionContext) {
const { box } = context;
const bound = new Bound(box.x, box.y, box.w, box.h);
const elementBound = this.model.elementBound;
return (
this.model.childElements.length === 0 || bound.contains(elementBound)
);
}
override renderGfxBlock() { override renderGfxBlock() {
const { model, showBorder, std } = this; const { model, showBorder, std } = this;
const backgroundColor = std const backgroundColor = std
@@ -10,7 +10,7 @@ import {
} from '@blocksuite/affine-shared/utils'; } from '@blocksuite/affine-shared/utils';
import { Bound } from '@blocksuite/global/gfx'; import { Bound } from '@blocksuite/global/gfx';
import { toGfxBlockComponent } from '@blocksuite/std'; import { toGfxBlockComponent } from '@blocksuite/std';
import type { SelectedContext } from '@blocksuite/std/gfx'; import type { BoxSelectionContext, SelectedContext } from '@blocksuite/std/gfx';
import { html, nothing, type PropertyValues } from 'lit'; import { html, nothing, type PropertyValues } from 'lit';
import { query, state } from 'lit/decorators.js'; import { query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
@@ -372,6 +372,10 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
} }
} }
override onBoxSelected(_: BoxSelectionContext) {
return this.model.props.displayMode !== NoteDisplayMode.DocOnly;
}
@state() @state()
private accessor _editing = false; private accessor _editing = false;
@@ -5,8 +5,10 @@ import { css, html, nothing, unsafeCSS } from 'lit';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js'; import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
import { DefaultTool } from '../../gfx-tool/default-tool.js'; import {
import { DefaultModeDragType } from '../../gfx-tool/default-tool-ext/ext.js'; DefaultModeDragType,
DefaultTool,
} from '../../gfx-tool/default-tool.js';
export const EDGELESS_DRAGGING_AREA_WIDGET = 'edgeless-dragging-area-rect'; export const EDGELESS_DRAGGING_AREA_WIDGET = 'edgeless-dragging-area-rect';
@@ -1,10 +0,0 @@
export enum DefaultModeDragType {
/** Moving selected contents */
ContentMoving = 'content-moving',
/** Native range dragging inside active note block */
NativeEditing = 'native-editing',
/** Default void state */
None = 'none',
/** Expanding the dragging area, select the content covered inside */
Selecting = 'selecting',
}
@@ -1,27 +1,27 @@
import { isFrameBlock } from '@blocksuite/affine-block-frame';
import {
GroupElementModel,
MindmapElementModel,
NoteBlockModel,
NoteDisplayMode,
} from '@blocksuite/affine-model';
import { resetNativeSelection } from '@blocksuite/affine-shared/utils'; import { resetNativeSelection } from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable'; import { DisposableGroup } from '@blocksuite/global/disposable';
import type { IVec } from '@blocksuite/global/gfx'; import type { IVec } from '@blocksuite/global/gfx';
import { Bound } from '@blocksuite/global/gfx';
import type { PointerEventState } from '@blocksuite/std'; import type { PointerEventState } from '@blocksuite/std';
import { import {
BaseTool, BaseTool,
getTopElements,
type GfxModel, type GfxModel,
InteractivityIdentifier, InteractivityIdentifier,
isGfxGroupCompatibleModel, isGfxGroupCompatibleModel,
type PointTestOptions,
} from '@blocksuite/std/gfx'; } from '@blocksuite/std/gfx';
import { effect } from '@preact/signals-core'; import { effect } from '@preact/signals-core';
import { calPanDelta } from '../utils/panning-utils.js'; import { calPanDelta } from '../utils/panning-utils.js';
import { DefaultModeDragType } from './default-tool-ext/ext.js';
export enum DefaultModeDragType {
/** Moving selected contents */
ContentMoving = 'content-moving',
/** Native range dragging inside active note block */
NativeEditing = 'native-editing',
/** Default void state */
None = 'none',
/** Expanding the dragging area, select the content covered inside */
Selecting = 'selecting',
}
export class DefaultTool extends BaseTool { export class DefaultTool extends BaseTool {
static override toolName: string = 'default'; static override toolName: string = 'default';
@@ -44,11 +44,11 @@ export class DefaultTool extends BaseTool {
private _disposables: DisposableGroup | null = null; private _disposables: DisposableGroup | null = null;
private readonly _panViewport = (delta: IVec) => { private _panViewport(delta: IVec) {
this._accumulateDelta[0] += delta[0]; this._accumulateDelta[0] += delta[0];
this._accumulateDelta[1] += delta[1]; this._accumulateDelta[1] += delta[1];
this.gfx.viewport.applyDeltaCenter(delta[0], delta[1]); this.gfx.viewport.applyDeltaCenter(delta[0], delta[1]);
}; }
private _selectionRectTransition: null | { private _selectionRectTransition: null | {
w: number; w: number;
@@ -117,44 +117,21 @@ export class DefaultTool extends BaseTool {
}; };
} }
const { x, y, w, h } = this.controller.draggingArea$.peek(); const elements = this.interactivity?.handleBoxSelection({
const bound = new Bound(x, y, w, h); box: this.controller.draggingArea$.peek(),
let elements = gfx.getElementsByBound(bound).filter(el => {
if (isFrameBlock(el)) {
return el.childElements.length === 0 || bound.contains(el.elementBound);
}
if (el instanceof MindmapElementModel) {
return bound.contains(el.elementBound);
}
if (
el instanceof NoteBlockModel &&
el.props.displayMode === NoteDisplayMode.DocOnly
) {
return false;
}
return true;
}); });
elements = getTopElements(elements).filter(el => !el.isLocked()); if (!elements) return;
const set = new Set( this.selection.set({
gfx.keyboard.shiftKey$.peek() elements: elements.map(el => el.id),
? [...elements, ...gfx.selection.selectedElements]
: elements
);
this.edgelessSelectionManager.set({
elements: Array.from(set).map(element => element.id),
editing: false, editing: false,
}); });
}; };
dragType = DefaultModeDragType.None; dragType = DefaultModeDragType.None;
enableHover = true; movementDragging = false;
dragging = false;
/** /**
* Get the end position of the dragging area in the model coordinate * Get the end position of the dragging area in the model coordinate
@@ -174,7 +151,7 @@ export class DefaultTool extends BaseTool {
return [startX, startY] as IVec; return [startX, startY] as IVec;
} }
get edgelessSelectionManager() { get selection() {
return this.gfx.selection; return this.gfx.selection;
} }
@@ -183,52 +160,55 @@ export class DefaultTool extends BaseTool {
} }
private async _cloneContent() { private async _cloneContent() {
const clonedResult = await this.interactivity?.requestElementsClone({ const clonedResult = await this.interactivity?.requestElementClone({
elements: this._toBeMoved, elements: this._toBeMoved,
}); });
if (!clonedResult) return; if (!clonedResult) return;
this._toBeMoved = clonedResult.elements; this._toBeMoved = clonedResult.elements;
this.edgelessSelectionManager.set({ this.selection.set({
elements: this._toBeMoved.map(e => e.id), elements: this._toBeMoved.map(e => e.id),
editing: false, editing: false,
}); });
} }
private _determineDragType(e: PointerEventState): DefaultModeDragType { private _determineDragType(evt: PointerEventState): DefaultModeDragType {
const { x, y } = e; const { x, y } = this.controller.lastMouseModelPos$.peek();
// Is dragging started from current selected rect if (this.selection.isInSelectedRect(x, y)) {
if (this.edgelessSelectionManager.isInSelectedRect(x, y)) { if (this.selection.selectedElements.length === 1) {
if (this.edgelessSelectionManager.selectedElements.length === 1) { const currentHoveredElem = this._getElementInGroup(x, y);
let selected = this.edgelessSelectionManager.selectedElements[0]; let curSelected = this.selection.selectedElements[0];
// double check
const currentSelected = this._pick(x, y); // If one of the following condition is true, keep the selection:
if ( // 1. if group is currently selected
!isFrameBlock(selected) && // 2. if the selected element is descendant of the hovered element
!(selected instanceof GroupElementModel) && // 3. not hovering any element or hovering the same element
currentSelected && //
currentSelected !== selected // Otherwise, we update the selection to the current hovered element
) { const shouldKeepSelection =
selected = currentSelected; isGfxGroupCompatibleModel(curSelected) ||
this.edgelessSelectionManager.set({ (isGfxGroupCompatibleModel(currentHoveredElem) &&
elements: [selected.id], currentHoveredElem.hasDescendant(curSelected)) ||
!currentHoveredElem ||
currentHoveredElem === curSelected;
if (!shouldKeepSelection) {
curSelected = currentHoveredElem;
this.selection.set({
elements: [curSelected.id],
editing: false, editing: false,
}); });
} }
} }
return this.edgelessSelectionManager.editing return this.selection.editing
? DefaultModeDragType.NativeEditing ? DefaultModeDragType.NativeEditing
: DefaultModeDragType.ContentMoving; : DefaultModeDragType.ContentMoving;
} else { } else {
const selected = this._pick(x, y); const checked = this.interactivity?.handleElementSelection(evt);
if (selected) {
this.edgelessSelectionManager.set({
elements: [selected.id],
editing: false,
});
if (checked) {
return DefaultModeDragType.ContentMoving; return DefaultModeDragType.ContentMoving;
} else { } else {
return DefaultModeDragType.Selecting; return DefaultModeDragType.Selecting;
@@ -236,9 +216,7 @@ export class DefaultTool extends BaseTool {
} }
} }
private _pick(x: number, y: number, options?: PointTestOptions) { private _getElementInGroup(modelX: number, modelY: number) {
const modelPos = this.gfx.viewport.toModelCoord(x, y);
const tryGetLockedAncestor = (e: GfxModel | null) => { const tryGetLockedAncestor = (e: GfxModel | null) => {
if (e?.isLockedByAncestor()) { if (e?.isLockedByAncestor()) {
return e.groups.findLast(group => group.isLocked()); return e.groups.findLast(group => group.isLocked());
@@ -246,34 +224,7 @@ export class DefaultTool extends BaseTool {
return e; return e;
}; };
const result = this.gfx.getElementInGroup( return tryGetLockedAncestor(this.gfx.getElementInGroup(modelX, modelY));
modelPos[0],
modelPos[1],
options
);
if (result instanceof MindmapElementModel) {
const picked = this.gfx.getElementByPoint(modelPos[0], modelPos[1], {
...((options ?? {}) as PointTestOptions),
all: true,
});
let pickedIdx = picked.length - 1;
while (pickedIdx >= 0) {
const element = picked[pickedIdx];
if (element === result) {
pickedIdx -= 1;
continue;
}
break;
}
return tryGetLockedAncestor(picked[pickedIdx]) ?? null;
}
return tryGetLockedAncestor(result);
} }
private initializeDragState( private initializeDragState(
@@ -305,7 +256,7 @@ export class DefaultTool extends BaseTool {
if (this.dragType === DefaultModeDragType.ContentMoving) { if (this.dragType === DefaultModeDragType.ContentMoving) {
if (this.interactivity) { if (this.interactivity) {
this.doc.captureSync(); this.doc.captureSync();
this.interactivity.initializeDrag({ this.interactivity.handleElementMove({
movingElements: this._toBeMoved, movingElements: this._toBeMoved,
event: event.raw, event: event.raw,
onDragEnd: () => { onDragEnd: () => {
@@ -320,8 +271,8 @@ export class DefaultTool extends BaseTool {
override click(e: PointerEventState) { override click(e: PointerEventState) {
if (this.doc.readonly) return; if (this.doc.readonly) return;
if (!this.interactivity?.dispatchOnSelected(e)) { if (!this.interactivity?.handleElementSelection(e)) {
this.edgelessSelectionManager.clear(); this.selection.clear();
resetNativeSelection(null); resetNativeSelection(null);
} }
@@ -353,9 +304,9 @@ export class DefaultTool extends BaseTool {
override dragEnd(e: PointerEventState) { override dragEnd(e: PointerEventState) {
this.interactivity?.dispatchEvent('dragend', e); this.interactivity?.dispatchEvent('dragend', e);
if (this.edgelessSelectionManager.editing || !this.dragging) return; if (this.selection.editing || !this.movementDragging) return;
this.dragging = false; this.movementDragging = false;
this._toBeMoved = []; this._toBeMoved = [];
this._clearSelectingState(); this._clearSelectingState();
this.dragType = DefaultModeDragType.None; this.dragType = DefaultModeDragType.None;
@@ -364,7 +315,7 @@ export class DefaultTool extends BaseTool {
override dragMove(e: PointerEventState) { override dragMove(e: PointerEventState) {
this.interactivity?.dispatchEvent('dragmove', e); this.interactivity?.dispatchEvent('dragmove', e);
if (!this.dragging) { if (!this.movementDragging) {
return; return;
} }
@@ -397,19 +348,14 @@ export class DefaultTool extends BaseTool {
const { preventDefaultState, handledByView } = const { preventDefaultState, handledByView } =
this.interactivity?.dispatchEvent('dragstart', e) ?? {}; this.interactivity?.dispatchEvent('dragstart', e) ?? {};
if ( if (this.selection.editing || preventDefaultState || handledByView) return;
this.edgelessSelectionManager.editing ||
preventDefaultState ||
handledByView
)
return;
this.dragging = true; this.movementDragging = true;
// Determine the drag type based on the current state and event // Determine the drag type based on the current state and event
let dragType = this._determineDragType(e); let dragType = this._determineDragType(e);
const elements = this.edgelessSelectionManager.selectedElements; const elements = this.selection.selectedElements;
if (elements.some(e => e.isLocked())) return; if (elements.some(e => e.isLocked())) return;
const toBeMoved = new Set(elements); const toBeMoved = new Set(elements);
@@ -468,8 +414,6 @@ export class DefaultTool extends BaseTool {
this.interactivity?.dispatchEvent('pointerup', e); this.interactivity?.dispatchEvent('pointerup', e);
} }
override tripleClick() {}
override unmounted(): void {} override unmounted(): void {}
} }
+40 -1
View File
@@ -7,8 +7,13 @@ import {
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { TelemetryProvider } from '@blocksuite/affine-shared/services'; import { TelemetryProvider } from '@blocksuite/affine-shared/services';
import { requestThrottledConnectedFrame } from '@blocksuite/affine-shared/utils'; import { requestThrottledConnectedFrame } from '@blocksuite/affine-shared/utils';
import { Bound } from '@blocksuite/global/gfx';
import type { PointerEventState } from '@blocksuite/std'; import type { PointerEventState } from '@blocksuite/std';
import { GfxElementModelView } from '@blocksuite/std/gfx'; import {
type BoxSelectionContext,
GfxElementModelView,
type SelectedContext,
} from '@blocksuite/std/gfx';
import { handleLayout } from './utils.js'; import { handleLayout } from './utils.js';
@@ -329,6 +334,40 @@ export class MindMapView extends GfxElementModelView<MindmapElementModel> {
return collapseButton; return collapseButton;
} }
override onSelected(context: SelectedContext): void | boolean {
const { position } = context;
const target = this.model.childElements.find(child => {
if (child.elementBound.containsPoint([position.x, position.y])) {
return true;
}
return false;
});
if (target) {
if (this.model.isLocked()) {
return super.onSelected(context);
}
if (context.multiSelect) {
this.gfx.selection.toggle(target);
} else {
this.gfx.selection.set({ elements: [target.id] });
}
return true;
}
return false;
}
override onBoxSelected(context: BoxSelectionContext) {
const { box } = context;
const bound = new Bound(box.x, box.y, box.w, box.h);
return bound.contains(this.model.elementBound);
}
override onCreated(): void { override onCreated(): void {
this._setLayoutMethod(); this._setLayoutMethod();
this._initCollapseButtons(); this._initCollapseButtons();
@@ -16,6 +16,7 @@ export { GfxExtension, GfxExtensionIdentifier } from './extension.js';
export { GridManager } from './grid.js'; export { GridManager } from './grid.js';
export { GfxControllerIdentifier } from './identifiers.js'; export { GfxControllerIdentifier } from './identifiers.js';
export type { export type {
BoxSelectionContext,
DragEndContext, DragEndContext,
DragExtensionInitializeContext, DragExtensionInitializeContext,
DragInitializationOption, DragInitializationOption,
@@ -10,6 +10,7 @@ export type {
ExtensionDragStartContext, ExtensionDragStartContext,
} from './types/drag.js'; } from './types/drag.js';
export type { export type {
BoxSelectionContext,
DragEndContext, DragEndContext,
DragMoveContext, DragMoveContext,
DragStartContext, DragStartContext,
@@ -3,6 +3,7 @@ import { DisposableGroup } from '@blocksuite/global/disposable';
import { Bound, Point } from '@blocksuite/global/gfx'; import { Bound, Point } from '@blocksuite/global/gfx';
import type { PointerEventState } from '../../event/state/pointer.js'; import type { PointerEventState } from '../../event/state/pointer.js';
import { getTopElements } from '../../utils/tree.js';
import { GfxExtension, GfxExtensionIdentifier } from '../extension.js'; import { GfxExtension, GfxExtensionIdentifier } from '../extension.js';
import type { GfxModel } from '../model/model.js'; import type { GfxModel } from '../model/model.js';
import { createInteractionContext, type SupportedEvents } from './event.js'; import { createInteractionContext, type SupportedEvents } from './event.js';
@@ -20,6 +21,7 @@ import type {
ExtensionDragMoveContext, ExtensionDragMoveContext,
ExtensionDragStartContext, ExtensionDragStartContext,
} from './types/drag.js'; } from './types/drag.js';
import type { BoxSelectionContext } from './types/view.js';
type ExtensionPointerHandler = Exclude< type ExtensionPointerHandler = Exclude<
SupportedEvents, SupportedEvents,
@@ -90,7 +92,12 @@ export class InteractivityManager extends GfxExtension {
}; };
} }
dispatchOnSelected(evt: PointerEventState) { /**
* Handle element selection.
* @param evt The pointer event that triggered the selection.
* @returns True if the element was selected, false otherwise.
*/
handleElementSelection(evt: PointerEventState) {
const { raw } = evt; const { raw } = evt;
const { gfx } = this; const { gfx } = this;
const [x, y] = gfx.viewport.toModelCoordFromClientCoord([raw.x, raw.y]); const [x, y] = gfx.viewport.toModelCoordFromClientCoord([raw.x, raw.y]);
@@ -122,15 +129,30 @@ export class InteractivityManager extends GfxExtension {
return false; return false;
} }
handleBoxSelection(context: { box: BoxSelectionContext['box'] }) {
const elements = this.gfx.getElementsByBound(context.box).filter(model => {
const view = this.gfx.view.get(model);
if (
!view ||
view.onBoxSelected({
box: context.box,
}) === false
)
return false;
return true;
});
return getTopElements(elements).filter(elm => !elm.isLocked());
}
/** /**
* Initialize drag operation for elements. * Initialize elements movements.
* Handles drag start, move and end events automatically. * It will handle drag start, move and end events automatically.
* Note: Call this when mouse is already down. * Note: Call this when mouse is already down.
*
* @param options
* @returns
*/ */
initializeDrag(options: DragInitializationOption) { handleElementMove(options: DragInitializationOption) {
let cancelledByExt = false; let cancelledByExt = false;
const context: DragExtensionInitializeContext = { const context: DragExtensionInitializeContext = {
@@ -294,7 +316,7 @@ export class InteractivityManager extends GfxExtension {
dragStart(); dragStart();
} }
requestElementsClone(options: RequestElementsCloneContext) { requestElementClone(options: RequestElementsCloneContext) {
const extensions = this.interactExtensions; const extensions = this.interactExtensions;
for (let ext of extensions.values()) { for (let ext of extensions.values()) {
@@ -1,4 +1,4 @@
import type { Bound, IPoint } from '@blocksuite/global/gfx'; import type { Bound, IBound, IPoint } from '@blocksuite/global/gfx';
import type { GfxBlockComponent } from '../../../view/element/gfx-block-component.js'; import type { GfxBlockComponent } from '../../../view/element/gfx-block-component.js';
import type { GfxModel } from '../../model/model.js'; import type { GfxModel } from '../../model/model.js';
@@ -56,11 +56,25 @@ export type SelectedContext = {
position: IPoint; position: IPoint;
/** /**
* If the current selection is a fallback selection, like selecting the element inside a group, the group will be selected instead * If the current selection is a fallback selection.
*
* E.g., if selecting a child element inside a group, the `onSelected` method will be executed on group, and
* the fallback is true because the it's not the original target(the child element).
*/ */
fallback: boolean; fallback: boolean;
}; };
export type BoxSelectionContext = {
box: Readonly<
IBound & {
startX: number;
startY: number;
endX: number;
endY: number;
}
>;
};
export type GfxViewTransformInterface = { export type GfxViewTransformInterface = {
onDragStart: (context: DragStartContext) => void; onDragStart: (context: DragStartContext) => void;
onDragMove: (context: DragMoveContext) => void; onDragMove: (context: DragMoveContext) => void;
@@ -70,8 +84,11 @@ export type GfxViewTransformInterface = {
/** /**
* When the element is selected by the pointer * When the element is selected by the pointer
* @param context
* @returns
*/ */
onSelected: (context: SelectedContext) => void; onSelected: (context: SelectedContext) => void;
/**
* When the element is selected by box selection, return false to prevent the default selection behavior.
*/
onBoxSelected: (context: BoxSelectionContext) => boolean | void;
}; };
@@ -204,12 +204,11 @@ export class GfxSelectionManager extends GfxExtension {
return selections.every(sel => sel.elements.length === 0); return selections.every(sel => sel.elements.length === 0);
} }
isInSelectedRect(viewX: number, viewY: number) { isInSelectedRect(modelX: number, modelY: number) {
const selected = this.selectedElements; const selected = this.selectedElements;
if (!selected.length) return false; if (!selected.length) return false;
const commonBound = getCommonBoundWithRotation(selected); const commonBound = getCommonBoundWithRotation(selected);
const [modelX, modelY] = this.gfx.viewport.toModelCoord(viewX, viewY);
if (commonBound && commonBound.isPointInBound([modelX, modelY])) { if (commonBound && commonBound.isPointInBound([modelX, modelY])) {
return true; return true;
} }
@@ -2,7 +2,7 @@ import type { ServiceIdentifier } from '@blocksuite/global/di';
import { DisposableGroup } from '@blocksuite/global/disposable'; import { DisposableGroup } from '@blocksuite/global/disposable';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import type { IBound, IPoint } from '@blocksuite/global/gfx'; import type { IBound, IPoint } from '@blocksuite/global/gfx';
import { Signal } from '@preact/signals-core'; import { computed, Signal } from '@preact/signals-core';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import type { PointerEventState } from '../../event/index.js'; import type { PointerEventState } from '../../event/index.js';
@@ -125,6 +125,18 @@ export class ToolController extends GfxExtension {
y: 0, y: 0,
}); });
readonly lastMouseModelPos$ = computed(() => {
const [x, y] = this.gfx.viewport.toModelCoord(
this.lastMousePos$.value.x,
this.lastMousePos$.value.y
);
return {
x,
y,
};
});
get currentTool$() { get currentTool$() {
// oxlint-disable-next-line typescript/no-this-alias // oxlint-disable-next-line typescript/no-this-alias
const self = this; const self = this;
@@ -330,6 +342,10 @@ export class ToolController extends GfxExtension {
w: 0, w: 0,
h: 0, h: 0,
}; };
this.lastMousePos$.value = {
x: evt.x,
y: evt.y,
};
// this means the dragEnd event is not even fired // this means the dragEnd event is not even fired
// so we need to manually call the dragEnd method // so we need to manually call the dragEnd method
@@ -372,6 +388,11 @@ export class ToolController extends GfxExtension {
endY: evt.y, endY: evt.y,
}; };
this.lastMousePos$.value = {
x: evt.x,
y: evt.y,
};
invokeToolHandler('dragMove', evt, dragContext?.tool); invokeToolHandler('dragMove', evt, dragContext?.tool);
}) })
); );
@@ -1,13 +1,14 @@
import { type Container, createIdentifier } from '@blocksuite/global/di'; import { type Container, createIdentifier } from '@blocksuite/global/di';
import { DisposableGroup } from '@blocksuite/global/disposable'; import { DisposableGroup } from '@blocksuite/global/disposable';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import type { Bound, IVec } from '@blocksuite/global/gfx'; import { type Bound, type IVec } from '@blocksuite/global/gfx';
import type { Extension } from '@blocksuite/store'; import type { Extension } from '@blocksuite/store';
import type { PointerEventState } from '../../event/index.js'; import type { PointerEventState } from '../../event/index.js';
import type { EditorHost } from '../../view/index.js'; import type { EditorHost } from '../../view/index.js';
import type { GfxController } from '../index.js'; import type { GfxController } from '../index.js';
import type { import type {
BoxSelectionContext,
DragEndContext, DragEndContext,
DragMoveContext, DragMoveContext,
DragStartContext, DragStartContext,
@@ -221,6 +222,8 @@ export class GfxElementModelView<
} }
} }
onBoxSelected(_: BoxSelectionContext): boolean | void {}
onResize = () => {}; onResize = () => {};
onRotate = () => {}; onRotate = () => {};
@@ -6,6 +6,7 @@ import { nothing } from 'lit';
import type { BlockService } from '../../extension/index.js'; import type { BlockService } from '../../extension/index.js';
import { GfxControllerIdentifier } from '../../gfx/identifiers.js'; import { GfxControllerIdentifier } from '../../gfx/identifiers.js';
import type { import type {
BoxSelectionContext,
DragMoveContext, DragMoveContext,
GfxViewTransformInterface, GfxViewTransformInterface,
SelectedContext, SelectedContext,
@@ -113,6 +114,8 @@ export abstract class GfxBlockComponent<
return true; return true;
} }
onBoxSelected(_: BoxSelectionContext) {}
onRotate() {} onRotate() {}
onResize() {} onResize() {}
@@ -231,6 +234,8 @@ export function toGfxBlockComponent<
return true; return true;
} }
onBoxSelected(_: BoxSelectionContext) {}
onRotate() {} onRotate() {}
onResize() {} onResize() {}
@@ -22,7 +22,7 @@ const dragBetweenViewCoords = async (
end: number[] end: number[]
) => { ) => {
// dragging slowly may drop frame if mindmap is existed, so for test we drag quickly // dragging slowly may drop frame if mindmap is existed, so for test we drag quickly
await _dragBetweenViewCoords(page, start, end, { steps: 2 }); await _dragBetweenViewCoords(page, start, end, { steps: 10 });
await waitNextFrame(page); await waitNextFrame(page);
}; };
@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'; import { expect } from '@playwright/test';
import { clickView } from '../utils/actions/click.js'; import { click, clickView } from '../utils/actions/click.js';
import { dragBetweenCoords } from '../utils/actions/drag.js'; import { dragBetweenCoords } from '../utils/actions/drag.js';
import { import {
addBasicRectShapeElement, addBasicRectShapeElement,
@@ -272,7 +272,7 @@ test('drag root node should layout in real time', async ({ page }) => {
// assert when dragging is in progress // assert when dragging is in progress
await waitNextFrame(page, 500); await waitNextFrame(page, 500);
await assertMindMapNodesPosition(50, 50); await assertMindMapNodesPosition(54, 54);
await page.mouse.up(); await page.mouse.up();
}); });
@@ -297,6 +297,7 @@ test('drag node out of mind map should detach the node and create a new mind map
}); });
const { rect } = await getMindMapNode(page, mindmapId, [0, 1]); const { rect } = await getMindMapNode(page, mindmapId, [0, 1]);
await click(page, { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 });
await dragBetweenCoords( await dragBetweenCoords(
page, page,
{ {