fix: avoid unnecessary rerendering of selected-rect (#12583)

### Changed

- Note scale issue
- Overlay should call refresh when `clear` is called
- Optimize edgeless-selected-rect to avoid unecessary rerendering

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

- **New Features**
  - Edgeless note blocks now respect both minimum and maximum size limits when resizing.

- **Improvements**
  - Enhanced performance and responsiveness of resize and rotate handles in selection overlays by caching allowed handles and optimizing cursor management.
  - Cursor styles for resize and rotate handles are now set more reliably and efficiently through declarative styling.

- **Bug Fixes**
  - Ensured overlay clearing now properly refreshes the renderer for more consistent visual updates.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
doouding
2025-05-28 03:53:51 +00:00
parent 87dfd2b77d
commit d49ecfbecc
4 changed files with 82 additions and 73 deletions

View File

@@ -400,7 +400,7 @@ export const EdgelessNoteInteraction =
onResizeMove(context): void { onResizeMove(context): void {
const { originalBound, newBound, lockRatio, constraint } = context; const { originalBound, newBound, lockRatio, constraint } = context;
const { minWidth, minHeight } = constraint; const { minWidth, minHeight, maxHeight, maxWidth } = constraint;
let scale = initialScale; let scale = initialScale;
let edgelessProp = { ...model.props.edgeless }; let edgelessProp = { ...model.props.edgeless };
@@ -411,8 +411,8 @@ export const EdgelessNoteInteraction =
edgelessProp.scale = scale; edgelessProp.scale = scale;
} }
newBound.w = clamp(newBound.w, minWidth, Number.MAX_SAFE_INTEGER); newBound.w = clamp(newBound.w, minWidth * scale, maxWidth);
newBound.h = clamp(newBound.h, minHeight, Number.MAX_SAFE_INTEGER); newBound.h = clamp(newBound.h, minHeight * scale, maxHeight);
if (newBound.h > minHeight * scale) { if (newBound.h > minHeight * scale) {
edgelessProp.collapse = true; edgelessProp.collapse = true;

View File

@@ -35,7 +35,9 @@ export abstract class Overlay extends Extension {
]); ]);
} }
clear() {} clear() {
this.refresh();
}
dispose() {} dispose() {}

View File

@@ -28,7 +28,6 @@ import { state } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { type Subscription } from 'rxjs';
import { RenderResizeHandles } from './resize-handles.js'; import { RenderResizeHandles } from './resize-handles.js';
import { generateCursorUrl, getRotatedResizeCursor } from './utils.js'; import { generateCursorUrl, getRotatedResizeCursor } from './utils.js';
@@ -359,21 +358,6 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
} }
`; `;
private readonly _initSelectedSlot = () => {
this._propDisposables.forEach(disposable => disposable.unsubscribe());
this._propDisposables = [];
this.selection.selectedElements.forEach(element => {
if ('flavour' in element) {
this._propDisposables.push(
element.propsUpdated.subscribe(() => {
this._updateOnElementChange(element.id);
})
);
}
});
};
private readonly _dragEndCleanup = () => { private readonly _dragEndCleanup = () => {
this._isWidthLimit = false; this._isWidthLimit = false;
this._isHeightLimit = false; this._isHeightLimit = false;
@@ -386,16 +370,15 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
this.frameOverlay.clear(); this.frameOverlay.clear();
}; };
private _propDisposables: Subscription[] = [];
private readonly _updateCursor = (options?: { private readonly _updateCursor = (options?: {
type: 'resize' | 'rotate'; type: 'resize' | 'rotate';
angle: number; angle: number;
handle: ResizeHandle; handle: ResizeHandle;
pure?: boolean;
}) => { }) => {
if (!options) { if (!options) {
!this._isResizing && (this.gfx.cursor$.value = 'default'); !this._isResizing && (this.gfx.cursor$.value = 'default');
return; return 'default';
} }
const { type, angle, handle } = options; const { type, angle, handle } = options;
@@ -410,7 +393,11 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
}); });
} }
this.gfx.cursor$.value = cursor; if (options.pure !== true) {
this.gfx.cursor$.value = cursor;
}
return cursor;
}; };
private readonly _updateOnElementChange = ( private readonly _updateOnElementChange = (
@@ -423,8 +410,30 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
} }
}; };
private readonly _updateHandles = () => {
const interaction = this._interaction;
const { store, selection } = this;
const elements = selection.selectedElements;
if (interaction && !selection.editing && !store.readonly) {
const resizeHandles = interaction.getResizeHandlers({
elements,
});
const { rotatable } = interaction.getRotateConfig({
elements,
});
this._allowedHandles = {
rotatable,
resizeHandles,
};
} else {
this._allowedHandles = null;
}
};
private readonly _updateOnSelectionChange = () => { private readonly _updateOnSelectionChange = () => {
this._initSelectedSlot(); this._updateHandles();
this._updateSelectedRect(); this._updateSelectedRect();
// Reset the cursor // Reset the cursor
this._updateCursor(); this._updateCursor();
@@ -554,10 +563,6 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
}) })
); );
} }
_disposables.add(() => {
this._propDisposables.forEach(disposable => disposable.unsubscribe());
});
} }
private get _interaction() { private get _interaction() {
@@ -569,7 +574,7 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
} }
private _renderHandles() { private _renderHandles() {
const { selection, gfx, block, store } = this; const { selection, gfx, block } = this;
const elements = selection.selectedElements; const elements = selection.selectedElements;
if (selection.inoperable) { if (selection.inoperable) {
@@ -579,23 +584,16 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
const handles = []; const handles = [];
if ( if (
this._allowedHandles &&
this._interaction && this._interaction &&
!selection.editing &&
!store.readonly &&
!elements.some(element => element.isLocked()) !elements.some(element => element.isLocked())
) { ) {
const interaction = this._interaction; const interaction = this._interaction;
const resizeHandlers = interaction.getResizeHandlers({
elements,
});
const { rotatable } = interaction.getRotateConfig({
elements,
});
handles.push( handles.push(
RenderResizeHandles( RenderResizeHandles(
resizeHandlers, this._allowedHandles.resizeHandles,
rotatable, this._allowedHandles.rotatable,
(e: PointerEvent, handle: ResizeHandle) => { (e: PointerEvent, handle: ResizeHandle) => {
const isRotate = (e.target as HTMLElement).classList.contains( const isRotate = (e.target as HTMLElement).classList.contains(
'rotate' 'rotate'
@@ -603,7 +601,7 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
if (isRotate) { if (isRotate) {
interaction.handleElementRotate({ interaction.handleElementRotate({
elements: this.selection.selectedElements, elements,
event: e, event: e,
onRotateStart: () => { onRotateStart: () => {
this._mode = 'rotate'; this._mode = 'rotate';
@@ -622,7 +620,7 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
}); });
} else { } else {
interaction.handleElementResize({ interaction.handleElementResize({
elements: this.selection.selectedElements, elements,
handle, handle,
event: e, event: e,
onResizeStart: () => { onResizeStart: () => {
@@ -632,12 +630,19 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
if (lockRatio) { if (lockRatio) {
this._scaleDirection = handle; this._scaleDirection = handle;
this._scalePercent = `${Math.round(scaleX * 100)}%`; this._scalePercent = `${Math.round(scaleX * 100)}%`;
this._mode = 'scale';
} }
if (exceed) { if (exceed) {
this._isWidthLimit = exceed.w; this._isWidthLimit = exceed.w;
this._isHeightLimit = exceed.h; this._isHeightLimit = exceed.h;
} }
this._updateCursor({
type: 'resize',
angle: elements.length > 1 ? 0 : (elements[0]?.rotate ?? 0),
handle,
});
}, },
onResizeEnd: () => { onResizeEnd: () => {
this._mode = 'resize'; this._mode = 'resize';
@@ -647,14 +652,11 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
} }
}, },
option => { option => {
if (option) { return this._updateCursor({
this._updateCursor({ ...option,
...option, angle: elements.length > 1 ? 0 : (elements[0]?.rotate ?? 0),
angle: elements.length > 1 ? 0 : (elements[0]?.rotate ?? 0), pure: true,
}); });
} else {
this._updateCursor();
}
} }
) )
); );
@@ -784,6 +786,12 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
`; `;
} }
@state()
private accessor _allowedHandles: {
rotatable: boolean;
resizeHandles: ResizeHandle[];
} | null = null;
@state() @state()
private accessor _isHeightLimit = false; private accessor _isHeightLimit = false;

View File

@@ -1,6 +1,7 @@
import type { ResizeHandle } from '@blocksuite/std/gfx'; import type { ResizeHandle } from '@blocksuite/std/gfx';
import { html, nothing } from 'lit'; import { html, nothing } from 'lit';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
export enum HandleDirection { export enum HandleDirection {
Bottom = 'bottom', Bottom = 'bottom',
@@ -17,36 +18,28 @@ function ResizeHandleRenderer(
handle: ResizeHandle, handle: ResizeHandle,
rotatable: boolean, rotatable: boolean,
onPointerDown?: (e: PointerEvent, direction: ResizeHandle) => void, onPointerDown?: (e: PointerEvent, direction: ResizeHandle) => void,
updateCursor?: (options?: { getCursor?: (options: {
type: 'resize' | 'rotate'; type: 'resize' | 'rotate';
handle: ResizeHandle; handle: ResizeHandle;
}) => void }) => string
) { ) {
const handlerPointerDown = (e: PointerEvent) => { const handlerPointerDown = (e: PointerEvent) => {
e.stopPropagation(); e.stopPropagation();
onPointerDown && onPointerDown(e, handle); onPointerDown && onPointerDown(e, handle);
}; };
const pointerEnter = (type: 'resize' | 'rotate') => (e: PointerEvent) => {
e.stopPropagation();
if (e.buttons === 1 || !updateCursor) return;
updateCursor({ type, handle });
};
const pointerLeave = (e: PointerEvent) => {
e.stopPropagation();
if (e.buttons === 1 || !updateCursor) return;
updateCursor();
};
const rotationTpl = const rotationTpl =
handle.length > 6 && rotatable handle.length > 6 && rotatable
? html`<div ? html`<div
class="rotate" class="rotate"
@pointerover=${pointerEnter('rotate')} style=${styleMap({
@pointerout=${pointerLeave} cursor: getCursor
? getCursor({
type: 'rotate',
handle,
})
: 'default',
})}
></div>` ></div>`
: nothing; : nothing;
@@ -58,8 +51,14 @@ function ResizeHandleRenderer(
${rotationTpl} ${rotationTpl}
<div <div
class="resize transparent-handle" class="resize transparent-handle"
@pointerover=${pointerEnter('resize')} style=${styleMap({
@pointerout=${pointerLeave} cursor: getCursor
? getCursor({
type: 'resize',
handle,
})
: 'default',
})}
></div> ></div>
</div>`; </div>`;
} }
@@ -79,17 +78,17 @@ export function RenderResizeHandles(
resizeHandles: ResizeHandle[], resizeHandles: ResizeHandle[],
rotatable: boolean, rotatable: boolean,
onPointerDown: (e: PointerEvent, direction: ResizeHandle) => void, onPointerDown: (e: PointerEvent, direction: ResizeHandle) => void,
updateCursor?: (options?: { getCursor?: (options: {
type: 'resize' | 'rotate'; type: 'resize' | 'rotate';
handle: ResizeHandle; handle: ResizeHandle;
}) => void }) => string
) { ) {
return html` return html`
${repeat( ${repeat(
resizeHandles, resizeHandles,
handle => handle, handle => handle,
handle => handle =>
ResizeHandleRenderer(handle, rotatable, onPointerDown, updateCursor) ResizeHandleRenderer(handle, rotatable, onPointerDown, getCursor)
)} )}
`; `;
} }