From cdb1215d95e14af274d5ac2c9060c16fafc60ffc Mon Sep 17 00:00:00 2001 From: Maribeth Bottorff Date: Thu, 9 Mar 2023 13:43:12 -0800 Subject: [PATCH] revert: "refactor: Remove block and workspace drag surfaces (#6758)" (#6888) This reverts commit 332c0fd2f2685f2f0690692f8a21c76693571568. --- core/block_drag_surface.ts | 245 +++++++++++++++++++++++++++++ core/block_dragger.ts | 15 +- core/block_svg.ts | 80 +++++++++- core/blockly.ts | 4 + core/bubble.ts | 14 +- core/bubble_dragger.ts | 21 ++- core/css.ts | 38 ++++- core/inject.ts | 20 ++- core/interfaces/i_block_dragger.ts | 2 +- core/interfaces/i_bubble.ts | 9 +- core/scrollbar.ts | 7 + core/workspace_comment_svg.ts | 62 +++++++- core/workspace_drag_surface_svg.ts | 176 +++++++++++++++++++++ core/workspace_dragger.ts | 5 + core/workspace_svg.ts | 123 ++++++++++++++- 15 files changed, 791 insertions(+), 30 deletions(-) create mode 100644 core/block_drag_surface.ts create mode 100644 core/workspace_drag_surface_svg.ts diff --git a/core/block_drag_surface.ts b/core/block_drag_surface.ts new file mode 100644 index 000000000..909aa76bf --- /dev/null +++ b/core/block_drag_surface.ts @@ -0,0 +1,245 @@ +/** + * @license + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * A class that manages a surface for dragging blocks. When a + * block drag is started, we move the block (and children) to a separate DOM + * element that we move around using translate3d. At the end of the drag, the + * blocks are put back in into the SVG they came from. This helps + * performance by avoiding repainting the entire SVG on every mouse move + * while dragging blocks. + * + * @class + */ +import * as goog from '../closure/goog/goog.js'; +goog.declareModuleId('Blockly.BlockDragSurfaceSvg'); + +import {Coordinate} from './utils/coordinate.js'; +import * as deprecation from './utils/deprecation.js'; +import * as dom from './utils/dom.js'; +import {Svg} from './utils/svg.js'; +import * as svgMath from './utils/svg_math.js'; + + +/** + * Class for a drag surface for the currently dragged block. This is a separate + * SVG that contains only the currently moving block, or nothing. + * + * @alias Blockly.BlockDragSurfaceSvg + */ +export class BlockDragSurfaceSvg { + /** + * The root element of the drag surface. + */ + private svg: SVGElement; + + /** + * This is where blocks live while they are being dragged if the drag + * surface is enabled. + */ + private dragGroup: SVGElement; + + /** + * Cached value for the scale of the drag surface. + * Used to set/get the correct translation during and after a drag. + */ + private scale = 1; + + /** + * Cached value for the translation of the drag surface. + * This translation is in pixel units, because the scale is applied to the + * drag group rather than the top-level SVG. + */ + private surfaceXY = new Coordinate(0, 0); + + /** + * Cached value for the translation of the child drag surface in pixel + * units. Since the child drag surface tracks the translation of the + * workspace this is ultimately the translation of the workspace. + */ + private readonly childSurfaceXY = new Coordinate(0, 0); + + /** @param container Containing element. */ + constructor(private readonly container: Element) { + this.svg = dom.createSvgElement( + Svg.SVG, { + 'xmlns': dom.SVG_NS, + 'xmlns:html': dom.HTML_NS, + 'xmlns:xlink': dom.XLINK_NS, + 'version': '1.1', + 'class': 'blocklyBlockDragSurface', + }, + this.container); + + this.dragGroup = dom.createSvgElement(Svg.G, {}, this.svg); + } + + /** + * Create the drag surface and inject it into the container. + * + * @deprecated The DOM is automatically created by the constructor. + */ + createDom() { + // No alternative provided, because now the dom is just automatically + // created in the constructor now. + deprecation.warn('BlockDragSurfaceSvg createDom', 'June 2022', 'June 2023'); + } + + /** + * Set the SVG blocks on the drag surface's group and show the surface. + * Only one block group should be on the drag surface at a time. + * + * @param blocks Block or group of blocks to place on the drag surface. + */ + setBlocksAndShow(blocks: SVGElement) { + if (this.dragGroup.childNodes.length) { + throw Error('Already dragging a block.'); + } + // appendChild removes the blocks from the previous parent + this.dragGroup.appendChild(blocks); + this.svg.style.display = 'block'; + this.surfaceXY = new Coordinate(0, 0); + } + + /** + * Translate and scale the entire drag surface group to the given position, to + * keep in sync with the workspace. + * + * @param x X translation in pixel coordinates. + * @param y Y translation in pixel coordinates. + * @param scale Scale of the group. + */ + translateAndScaleGroup(x: number, y: number, scale: number) { + this.scale = scale; + // Make sure the svg exists on a pixel boundary so that it is not fuzzy. + const roundX = Math.round(x); + const roundY = Math.round(y); + this.childSurfaceXY.x = roundX; + this.childSurfaceXY.y = roundY; + this.dragGroup.setAttribute( + 'transform', + 'translate(' + roundX + ',' + roundY + ') scale(' + scale + ')'); + } + + /** + * Translate the drag surface's SVG based on its internal state. + * + * @internal + */ + translateSurfaceInternal_() { + // Make sure the svg exists on a pixel boundary so that it is not fuzzy. + const x = Math.round(this.surfaceXY.x); + const y = Math.round(this.surfaceXY.y); + this.svg.style.display = 'block'; + dom.setCssTransform(this.svg, 'translate3d(' + x + 'px, ' + y + 'px, 0)'); + } + + /** + * Translates the entire surface by a relative offset. + * + * @param deltaX Horizontal offset in pixel units. + * @param deltaY Vertical offset in pixel units. + */ + translateBy(deltaX: number, deltaY: number) { + const x = this.surfaceXY.x + deltaX; + const y = this.surfaceXY.y + deltaY; + this.surfaceXY = new Coordinate(x, y); + this.translateSurfaceInternal_(); + } + + /** + * Translate the entire drag surface during a drag. + * We translate the drag surface instead of the blocks inside the surface + * so that the browser avoids repainting the SVG. + * Because of this, the drag coordinates must be adjusted by scale. + * + * @param x X translation for the entire surface. + * @param y Y translation for the entire surface. + */ + translateSurface(x: number, y: number) { + this.surfaceXY = new Coordinate(x * this.scale, y * this.scale); + this.translateSurfaceInternal_(); + } + + /** + * Reports the surface translation in scaled workspace coordinates. + * Use this when finishing a drag to return blocks to the correct position. + * + * @returns Current translation of the surface. + */ + getSurfaceTranslation(): Coordinate { + const xy = svgMath.getRelativeXY(this.svg); + return new Coordinate(xy.x / this.scale, xy.y / this.scale); + } + + /** + * Provide a reference to the drag group (primarily for + * BlockSvg.getRelativeToSurfaceXY). + * + * @returns Drag surface group element. + */ + getGroup(): SVGElement { + return this.dragGroup; + } + + /** + * Returns the SVG drag surface. + * + * @returns The SVG drag surface. + */ + getSvgRoot(): SVGElement { + return this.svg; + } + + /** + * Get the current blocks on the drag surface, if any (primarily + * for BlockSvg.getRelativeToSurfaceXY). + * + * @returns Drag surface block DOM element, or null if no blocks exist. + */ + getCurrentBlock(): Element|null { + return this.dragGroup.firstChild as Element; + } + + /** + * Gets the translation of the child block surface + * This surface is in charge of keeping track of how much the workspace has + * moved. + * + * @returns The amount the workspace has been moved. + */ + getWsTranslation(): Coordinate { + // Returning a copy so the coordinate can not be changed outside this class. + return this.childSurfaceXY.clone(); + } + + /** + * Clear the group and hide the surface; move the blocks off onto the provided + * element. + * If the block is being deleted it doesn't need to go back to the original + * surface, since it would be removed immediately during dispose. + * + * @param opt_newSurface Surface the dragging blocks should be moved to, or + * null if the blocks should be removed from this surface without being + * moved to a different surface. + */ + clearAndHide(opt_newSurface?: Element) { + const currentBlockElement = this.getCurrentBlock(); + if (currentBlockElement) { + if (opt_newSurface) { + // appendChild removes the node from this.dragGroup + opt_newSurface.appendChild(currentBlockElement); + } else { + this.dragGroup.removeChild(currentBlockElement); + } + } + this.svg.style.display = 'none'; + if (this.dragGroup.childNodes.length) { + throw Error('Drag group was not cleared.'); + } + this.surfaceXY = new Coordinate(0, 0); + } +} diff --git a/core/block_dragger.ts b/core/block_dragger.ts index 5ace7d6b0..5809f7484 100644 --- a/core/block_dragger.ts +++ b/core/block_dragger.ts @@ -92,7 +92,7 @@ export class BlockDragger implements IBlockDragger { } /** - * Start dragging a block. + * Start dragging a block. This includes moving it to the drag surface. * * @param currentDragDeltaXY How far the pointer has moved from the position * at mouse down, in pixel units. @@ -122,6 +122,10 @@ export class BlockDragger implements IBlockDragger { this.disconnectBlock_(healStack, currentDragDeltaXY); } this.draggingBlock_.setDragging(true); + // For future consideration: we may be able to put moveToDragSurface inside + // the block dragger, which would also let the block not track the block + // drag surface. + this.draggingBlock_.moveToDragSurface(); } /** @@ -215,11 +219,16 @@ export class BlockDragger implements IBlockDragger { const preventMove = !!this.dragTarget_ && this.dragTarget_.shouldPreventMove(this.draggingBlock_); + let newLoc: Coordinate; let delta: Coordinate|null = null; - if (!preventMove) { + if (preventMove) { + newLoc = this.startXY_; + } else { const newValues = this.getNewLocationAfterDrag_(currentDragDeltaXY); delta = newValues.delta; + newLoc = newValues.newLocation; } + this.draggingBlock_.moveOffDragSurface(newLoc); if (this.dragTarget_) { this.dragTarget_.onDrop(this.draggingBlock_); @@ -427,8 +436,6 @@ function initIconData(block: BlockSvg): IconPositionData[] { for (let i = 0, descendant; descendant = descendants[i]; i++) { const icons = descendant.getIcons(); for (let j = 0; j < icons.length; j++) { - // Only bother to track icons whose bubble is visible. - if (!icons[j].isVisible()) continue; const data = { // Coordinate with x and y properties (workspace // coordinates). diff --git a/core/block_svg.ts b/core/block_svg.ts index ee21986f8..befb621fd 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -140,6 +140,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg, override nextConnection!: RenderedConnection; // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. override previousConnection!: RenderedConnection; + private readonly useDragSurface_: boolean; private translation = ''; @@ -178,6 +179,12 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg, this.pathObject = workspace.getRenderer().makePathObject(this.svgGroup_, this.style); + /** + * Whether to move the block to the drag surface when it is dragged. + * True if it should move, false if it should be translated directly. + */ + this.useDragSurface_ = !!workspace.getBlockDragSurface(); + const svgPath = this.pathObject.svgPath; (svgPath as any).tooltip = this; Tooltip.bindMouseEvents(svgPath); @@ -351,6 +358,10 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg, let x = 0; let y = 0; + const dragSurfaceGroup = this.useDragSurface_ ? + this.workspace.getBlockDragSurface()!.getGroup() : + null; + let element: SVGElement = this.getSvgRoot(); if (element) { do { @@ -358,8 +369,19 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg, const xy = svgMath.getRelativeXY(element); x += xy.x; y += xy.y; + // If this element is the current element on the drag surface, include + // the translation of the drag surface itself. + if (this.useDragSurface_ && + this.workspace.getBlockDragSurface()!.getCurrentBlock() === + element) { + const surfaceTranslation = + this.workspace.getBlockDragSurface()!.getSurfaceTranslation(); + x += surfaceTranslation.x; + y += surfaceTranslation.y; + } element = element.parentNode as SVGElement; - } while (element && element !== this.workspace.getCanvas()); + } while (element && element !== this.workspace.getCanvas() && + element !== dragSurfaceGroup); } return new Coordinate(x, y); } @@ -411,6 +433,31 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg, return this.translation; } + /** + * Move this block to its workspace's drag surface, accounting for + * positioning. Generally should be called at the same time as + * setDragging_(true). Does nothing if useDragSurface_ is false. + * + * @internal + */ + moveToDragSurface() { + if (!this.useDragSurface_) { + return; + } + // The translation for drag surface blocks, + // is equal to the current relative-to-surface position, + // to keep the position in sync as it move on/off the surface. + // This is in workspace coordinates. + const xy = this.getRelativeToSurfaceXY(); + this.clearTransformAttributes_(); + this.workspace.getBlockDragSurface()!.translateSurface(xy.x, xy.y); + // Execute the move on the top-level SVG component + const svg = this.getSvgRoot(); + if (svg) { + this.workspace.getBlockDragSurface()!.setBlocksAndShow(svg); + } + } + /** * Move a block to a position. * @@ -422,15 +469,40 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg, } /** - * Move this block during a drag. + * Move this block back to the workspace block canvas. + * Generally should be called at the same time as setDragging_(false). + * Does nothing if useDragSurface_ is false. + * + * @param newXY The position the block should take on on the workspace canvas, + * in workspace coordinates. + * @internal + */ + moveOffDragSurface(newXY: Coordinate) { + if (!this.useDragSurface_) { + return; + } + // Translate to current position, turning off 3d. + this.translate(newXY.x, newXY.y); + this.workspace.getBlockDragSurface()!.clearAndHide( + this.workspace.getCanvas()); + } + + /** + * Move this block during a drag, taking into account whether we are using a + * drag surface to translate blocks. * This block must be a top-level block. * * @param newLoc The location to translate to, in workspace coordinates. * @internal */ moveDuringDrag(newLoc: Coordinate) { - this.translate(newLoc.x, newLoc.y); - this.getSvgRoot().setAttribute('transform', this.getTranslation()); + if (this.useDragSurface_) { + this.workspace.getBlockDragSurface()!.translateSurface( + newLoc.x, newLoc.y); + } else { + this.translate(newLoc.x, newLoc.y); + this.getSvgRoot().setAttribute('transform', this.getTranslation()); + } } /** diff --git a/core/blockly.ts b/core/blockly.ts index d8a030893..da5dc651d 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -25,6 +25,7 @@ import './events/events_var_create.js'; import {Block} from './block.js'; import * as blockAnimations from './block_animations.js'; +import {BlockDragSurfaceSvg} from './block_drag_surface.js'; import {BlockDragger} from './block_dragger.js'; import {BlockSvg} from './block_svg.js'; import {BlocklyOptions} from './blockly_options.js'; @@ -161,6 +162,7 @@ import {Workspace} from './workspace.js'; import {WorkspaceAudio} from './workspace_audio.js'; import {WorkspaceComment} from './workspace_comment.js'; import {WorkspaceCommentSvg} from './workspace_comment_svg.js'; +import {WorkspaceDragSurfaceSvg} from './workspace_drag_surface_svg.js'; import {WorkspaceDragger} from './workspace_dragger.js'; import {resizeSvgContents as realResizeSvgContents, WorkspaceSvg} from './workspace_svg.js'; import * as Xml from './xml.js'; @@ -591,6 +593,7 @@ export {BasicCursor}; export {Block}; export {BlocklyOptions}; export {BlockDragger}; +export {BlockDragSurfaceSvg}; export {BlockSvg}; export {Blocks}; export {Bubble}; @@ -732,6 +735,7 @@ export {Workspace}; export {WorkspaceAudio}; export {WorkspaceComment}; export {WorkspaceCommentSvg}; +export {WorkspaceDragSurfaceSvg}; export {WorkspaceDragger}; export {WorkspaceSvg}; export {ZoomControls}; diff --git a/core/bubble.ts b/core/bubble.ts index 3107af725..5036b4b25 100644 --- a/core/bubble.ts +++ b/core/bubble.ts @@ -12,6 +12,7 @@ import * as goog from '../closure/goog/goog.js'; goog.declareModuleId('Blockly.Bubble'); +import type {BlockDragSurfaceSvg} from './block_drag_surface.js'; import type {BlockSvg} from './block_svg.js'; import * as browserEvents from './browser_events.js'; import type {IBubble} from './interfaces/i_bubble.js'; @@ -782,13 +783,20 @@ export class Bubble implements IBubble { } /** - * Move this bubble during a drag. + * Move this bubble during a drag, taking into account whether or not there is + * a drag surface. * + * @param dragSurface The surface that carries rendered items during a drag, + * or null if no drag surface is in use. * @param newLoc The location to translate to, in workspace coordinates. * @internal */ - moveDuringDrag(newLoc: Coordinate) { - this.moveTo(newLoc.x, newLoc.y); + moveDuringDrag(dragSurface: BlockDragSurfaceSvg, newLoc: Coordinate) { + if (dragSurface) { + dragSurface.translateSurface(newLoc.x, newLoc.y); + } else { + this.moveTo(newLoc.x, newLoc.y); + } if (this.workspace_.RTL) { this.relativeLeft = this.anchorXY.x - newLoc.x - this.width; } else { diff --git a/core/bubble_dragger.ts b/core/bubble_dragger.ts index 45a0ba430..f960e399f 100644 --- a/core/bubble_dragger.ts +++ b/core/bubble_dragger.ts @@ -12,6 +12,7 @@ import * as goog from '../closure/goog/goog.js'; goog.declareModuleId('Blockly.BubbleDragger'); +import type {BlockDragSurfaceSvg} from './block_drag_surface.js'; import {ComponentManager} from './component_manager.js'; import type {CommentMove} from './events/events_comment_move.js'; import * as eventUtils from './events/utils.js'; @@ -35,6 +36,7 @@ export class BubbleDragger { /** Whether the bubble would be deleted if dropped immediately. */ private wouldDeleteBubble_ = false; private readonly startXY_: Coordinate; + private dragSurface_: BlockDragSurfaceSvg|null; /** * @param bubble The item on the bubble canvas to drag. @@ -46,10 +48,16 @@ export class BubbleDragger { * beginning of the drag, in workspace coordinates. */ this.startXY_ = this.bubble.getRelativeToSurfaceXY(); + + /** + * The drag surface to move bubbles to during a drag, or null if none should + * be used. Block dragging and bubble dragging use the same surface. + */ + this.dragSurface_ = workspace.getBlockDragSurface(); } /** - * Start dragging a bubble. + * Start dragging a bubble. This includes moving it to the drag surface. * * @internal */ @@ -60,6 +68,12 @@ export class BubbleDragger { this.workspace.setResizesEnabled(false); this.bubble.setAutoLayout(false); + if (this.dragSurface_) { + this.bubble.moveTo(0, 0); + this.dragSurface_.translateSurface(this.startXY_.x, this.startXY_.y); + // Execute the move on the top-level SVG component. + this.dragSurface_.setBlocksAndShow(this.bubble.getSvgRoot()); + } this.bubble.setDragging && this.bubble.setDragging(true); } @@ -76,7 +90,7 @@ export class BubbleDragger { dragBubble(e: PointerEvent, currentDragDeltaXY: Coordinate) { const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); const newLoc = Coordinate.sum(this.startXY_, delta); - this.bubble.moveDuringDrag(newLoc); + this.bubble.moveDuringDrag(this.dragSurface_, newLoc); const oldDragTarget = this.dragTarget_; this.dragTarget_ = this.workspace.getDragTarget(e); @@ -156,6 +170,9 @@ export class BubbleDragger { this.bubble.dispose(); } else { // Put everything back onto the bubble canvas. + if (this.dragSurface_) { + this.dragSurface_.clearAndHide(this.workspace.getBubbleCanvas()); + } if (this.bubble.setDragging) { this.bubble.setDragging(false); } diff --git a/core/css.ts b/core/css.ts index 2071a28a5..ad96f2627 100644 --- a/core/css.ts +++ b/core/css.ts @@ -94,6 +94,31 @@ let content = ` -webkit-user-select: none; } +.blocklyWsDragSurface { + display: none; + position: absolute; + top: 0; + left: 0; +} + +/* Added as a separate rule with multiple classes to make it more specific + than a bootstrap rule that selects svg:root. See issue #1275 for context. +*/ +.blocklyWsDragSurface.blocklyOverflowVisible { + overflow: visible; +} + +.blocklyBlockDragSurface { + display: none; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: visible !important; + z-index: 50; /* Display below toolbox, but above everything else. */ +} + .blocklyBlockCanvas.blocklyCanvasTransitioning, .blocklyBubbleCanvas.blocklyCanvasTransitioning { transition: transform .5s; @@ -229,6 +254,16 @@ let content = ` cursor: -webkit-grabbing; } +/* Change the cursor on the whole drag surface in case the mouse gets + ahead of block during a drag. This way the cursor is still a closed hand. + */ +.blocklyBlockDragSurface .blocklyDraggable { + /* backup for browsers (e.g. IE11) that don't support grabbing */ + cursor: url("<<>>/handclosed.cur"), auto; + cursor: grabbing; + cursor: -webkit-grabbing; +} + .blocklyDragging.blocklyDraggingDelete { cursor: url("<<>>/handdelete.cur"), auto; } @@ -281,7 +316,8 @@ let content = ` Don't allow users to select text. It gets annoying when trying to drag a block and selected text moves instead. */ -.blocklySvg text { +.blocklySvg text, +.blocklyBlockDragSurface text { user-select: none; -ms-user-select: none; -webkit-user-select: none; diff --git a/core/inject.ts b/core/inject.ts index dcf62ac0c..ad4162546 100644 --- a/core/inject.ts +++ b/core/inject.ts @@ -12,6 +12,7 @@ import * as goog from '../closure/goog/goog.js'; goog.declareModuleId('Blockly.inject'); +import {BlockDragSurfaceSvg} from './block_drag_surface.js'; import type {BlocklyOptions} from './blockly_options.js'; import * as browserEvents from './browser_events.js'; import * as bumpObjects from './bump_objects.js'; @@ -30,6 +31,7 @@ import * as dom from './utils/dom.js'; import {Svg} from './utils/svg.js'; import * as userAgent from './utils/useragent.js'; import * as WidgetDiv from './widgetdiv.js'; +import {WorkspaceDragSurfaceSvg} from './workspace_drag_surface_svg.js'; import {WorkspaceSvg} from './workspace_svg.js'; @@ -66,7 +68,14 @@ export function inject( (container as AnyDuringMigration).appendChild(subContainer); const svg = createDom(subContainer, options); - const workspace = createMainWorkspace(svg, options); + // Create surfaces for dragging things. These are optimizations + // so that the browser does not repaint during the drag. + const blockDragSurface = new BlockDragSurfaceSvg(subContainer); + + const workspaceDragSurface = new WorkspaceDragSurfaceSvg(subContainer); + + const workspace = + createMainWorkspace(svg, options, blockDragSurface, workspaceDragSurface); init(workspace); @@ -144,11 +153,16 @@ function createDom(container: Element, options: Options): SVGElement { * * @param svg SVG element with pattern defined. * @param options Dictionary of options. + * @param blockDragSurface Drag surface SVG for the blocks. + * @param workspaceDragSurface Drag surface SVG for the workspace. * @returns Newly created main workspace. */ -function createMainWorkspace(svg: SVGElement, options: Options): WorkspaceSvg { +function createMainWorkspace( + svg: SVGElement, options: Options, blockDragSurface: BlockDragSurfaceSvg, + workspaceDragSurface: WorkspaceDragSurfaceSvg): WorkspaceSvg { options.parentWorkspace = null; - const mainWorkspace = new WorkspaceSvg(options); + const mainWorkspace = + new WorkspaceSvg(options, blockDragSurface, workspaceDragSurface); const wsOptions = mainWorkspace.options; mainWorkspace.scale = wsOptions.zoomOptions.startScale; svg.appendChild(mainWorkspace.createDom('blocklyMainBackground')); diff --git a/core/interfaces/i_block_dragger.ts b/core/interfaces/i_block_dragger.ts index db6995c13..4bb95b27f 100644 --- a/core/interfaces/i_block_dragger.ts +++ b/core/interfaces/i_block_dragger.ts @@ -19,7 +19,7 @@ goog.declareModuleId('Blockly.IBlockDragger'); */ export interface IBlockDragger { /** - * Start dragging a block. + * Start dragging a block. This includes moving it to the drag surface. * * @param currentDragDeltaXY How far the pointer has moved from the position * at mouse down, in pixel units. diff --git a/core/interfaces/i_bubble.ts b/core/interfaces/i_bubble.ts index 467aae673..eb27fdcc8 100644 --- a/core/interfaces/i_bubble.ts +++ b/core/interfaces/i_bubble.ts @@ -11,6 +11,7 @@ */ import * as goog from '../../closure/goog/goog.js'; import type {Coordinate} from '../utils/coordinate.js'; +import type {BlockDragSurfaceSvg} from '../block_drag_surface.js'; goog.declareModuleId('Blockly.IBubble'); import type {IContextMenu} from './i_contextmenu.js'; @@ -53,11 +54,15 @@ export interface IBubble extends IDraggable, IContextMenu { setDragging(dragging: boolean): void; /** - * Move this bubble during a drag. + * Move this bubble during a drag, taking into account whether or not there is + * a drag surface. * + * @param dragSurface The surface that carries rendered items during a drag, + * or null if no drag surface is in use. * @param newLoc The location to translate to, in workspace coordinates. */ - moveDuringDrag(newLoc: Coordinate): void; + moveDuringDrag(dragSurface: BlockDragSurfaceSvg|null, newLoc: Coordinate): + void; /** * Move the bubble to the specified location in workspace coordinates. diff --git a/core/scrollbar.ts b/core/scrollbar.ts index 17e3d29bd..707b6d2b3 100644 --- a/core/scrollbar.ts +++ b/core/scrollbar.ts @@ -713,6 +713,11 @@ export class Scrollbar { // Look up the current translation and record it. this.startDragHandle = this.handlePosition; + // Tell the workspace to setup its drag surface since it is about to move. + // onMouseMoveHandle will call onScroll which actually tells the workspace + // to move. + this.workspace.setupDragSurface(); + // Record the current mouse position. this.startDragMouse = this.horizontal ? e.clientX : e.clientY; this.onMouseUpWrapper_ = browserEvents.conditionalBind( @@ -739,6 +744,8 @@ export class Scrollbar { /** Release the scrollbar handle and reset state accordingly. */ private onMouseUpHandle() { + // Tell the workspace to clean up now that the workspace is done moving. + this.workspace.resetDragSurface(); Touch.clearTouchIdentifier(); this.cleanUp(); } diff --git a/core/workspace_comment_svg.ts b/core/workspace_comment_svg.ts index 318c4ee77..2d45de046 100644 --- a/core/workspace_comment_svg.ts +++ b/core/workspace_comment_svg.ts @@ -15,6 +15,7 @@ goog.declareModuleId('Blockly.WorkspaceCommentSvg'); // Unused import preserved for side-effects. Remove if unneeded. import './events/events_selected.js'; +import type {BlockDragSurfaceSvg} from './block_drag_surface.js'; import * as browserEvents from './browser_events.js'; import * as common from './common.js'; // import * as ContextMenu from './contextmenu.js'; @@ -90,6 +91,7 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements /** Whether the comment is rendered onscreen and is a part of the DOM. */ private rendered_ = false; + private readonly useDragSurface_: boolean; /** * @param workspace The block's workspace. @@ -115,6 +117,12 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements }); this.svgGroup_.appendChild(this.svgRect_); + /** + * Whether to move the comment to the drag surface when it is dragged. + * True if it should move, false if it should be translated directly. + */ + this.useDragSurface_ = !!workspace.getBlockDragSurface(); + this.render(); } @@ -298,6 +306,10 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements let x = 0; let y = 0; + const dragSurfaceGroup = this.useDragSurface_ ? + this.workspace.getBlockDragSurface()!.getGroup() : + null; + let element: Node|null = this.getSvgRoot(); if (element) { do { @@ -305,9 +317,20 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements const xy = svgMath.getRelativeXY(element as Element); x += xy.x; y += xy.y; + // If this element is the current element on the drag surface, include + // the translation of the drag surface itself. + if (this.useDragSurface_ && + this.workspace.getBlockDragSurface()!.getCurrentBlock() === + element) { + const surfaceTranslation = + this.workspace.getBlockDragSurface()!.getSurfaceTranslation(); + x += surfaceTranslation.x; + y += surfaceTranslation.y; + } + element = element.parentNode; } while (element && element !== this.workspace.getBubbleCanvas() && - element !== null); + element !== dragSurfaceGroup); } this.xy_ = new Coordinate(x, y); return this.xy_; @@ -347,14 +370,43 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements } /** - * Move this comment during a drag. + * Move this comment to its workspace's drag surface, accounting for + * positioning. Generally should be called at the same time as + * setDragging(true). Does nothing if useDragSurface_ is false. * + * @internal + */ + moveToDragSurface() { + if (!this.useDragSurface_) { + return; + } + // The translation for drag surface blocks, + // is equal to the current relative-to-surface position, + // to keep the position in sync as it move on/off the surface. + // This is in workspace coordinates. + const xy = this.getRelativeToSurfaceXY(); + this.clearTransformAttributes_(); + this.workspace.getBlockDragSurface()!.translateSurface(xy.x, xy.y); + // Execute the move on the top-level SVG component + this.workspace.getBlockDragSurface()!.setBlocksAndShow(this.getSvgRoot()); + } + + /** + * Move this comment during a drag, taking into account whether we are using a + * drag surface to translate blocks. + * + * @param dragSurface The surface that carries rendered items during a drag, + * or null if no drag surface is in use. * @param newLoc The location to translate to, in workspace coordinates. * @internal */ - moveDuringDrag(newLoc: Coordinate) { - const translation = `translate(${newLoc.x}, ${newLoc.y})`; - this.getSvgRoot().setAttribute('transform', translation); + moveDuringDrag(dragSurface: BlockDragSurfaceSvg, newLoc: Coordinate) { + if (dragSurface) { + dragSurface.translateSurface(newLoc.x, newLoc.y); + } else { + const translation = `translate(${newLoc.x}, ${newLoc.y})`; + this.getSvgRoot().setAttribute('transform', translation); + } } /** diff --git a/core/workspace_drag_surface_svg.ts b/core/workspace_drag_surface_svg.ts new file mode 100644 index 000000000..c20678609 --- /dev/null +++ b/core/workspace_drag_surface_svg.ts @@ -0,0 +1,176 @@ +/** + * @license + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * An SVG that floats on top of the workspace. + * Blocks are moved into this SVG during a drag, improving performance. + * The entire SVG is translated using CSS translation instead of SVG so the + * blocks are never repainted during drag improving performance. + * + * @class + */ +import * as goog from '../closure/goog/goog.js'; +goog.declareModuleId('Blockly.WorkspaceDragSurfaceSvg'); + +import type {Coordinate} from './utils/coordinate.js'; +import * as dom from './utils/dom.js'; +import {Svg} from './utils/svg.js'; +import * as svgMath from './utils/svg_math.js'; + + +/** + * Blocks are moved into this SVG during a drag, improving performance. + * The entire SVG is translated using CSS transforms instead of SVG so the + * blocks are never repainted during drag improving performance. + * + * @alias Blockly.WorkspaceDragSurfaceSvg + */ +export class WorkspaceDragSurfaceSvg { + /** + * The SVG drag surface. Set once by WorkspaceDragSurfaceSvg.createDom. + */ + private SVG!: SVGElement; + + /** + * The element to insert the block canvas and bubble canvas after when it + * goes back in the DOM at the end of a drag. + */ + private previousSibling: Element|null = null; + + /** @param container Containing element. */ + constructor(private readonly container: Element) { + this.createDom(); + } + + /** Create the drag surface and inject it into the container. */ + createDom() { + if (this.SVG) { + return; // Already created. + } + /** + * Dom structure when the workspace is being dragged. If there is no drag in + * progress, the SVG is empty and display: none. + * + * + * /g> + * + */ + this.SVG = dom.createSvgElement(Svg.SVG, { + 'xmlns': dom.SVG_NS, + 'xmlns:html': dom.HTML_NS, + 'xmlns:xlink': dom.XLINK_NS, + 'version': '1.1', + 'class': 'blocklyWsDragSurface blocklyOverflowVisible', + }); + this.container.appendChild(this.SVG); + } + + /** + * Translate the entire drag surface during a drag. + * We translate the drag surface instead of the blocks inside the surface + * so that the browser avoids repainting the SVG. + * Because of this, the drag coordinates must be adjusted by scale. + * + * @param x X translation for the entire surface + * @param y Y translation for the entire surface + * @internal + */ + translateSurface(x: number, y: number) { + // Make sure the svg exists on a pixel boundary so that it is not fuzzy. + const fixedX = Math.round(x); + const fixedY = Math.round(y); + + this.SVG.style.display = 'block'; + dom.setCssTransform( + this.SVG, 'translate3d(' + fixedX + 'px, ' + fixedY + 'px, 0)'); + } + + /** + * Reports the surface translation in scaled workspace coordinates. + * Use this when finishing a drag to return blocks to the correct position. + * + * @returns Current translation of the surface + * @internal + */ + getSurfaceTranslation(): Coordinate { + return svgMath.getRelativeXY((this.SVG)); + } + + /** + * Move the blockCanvas and bubbleCanvas out of the surface SVG and on to + * newSurface. + * + * @param newSurface The element to put the drag surface contents into. + * @internal + */ + clearAndHide(newSurface: SVGElement) { + if (!newSurface) { + throw Error( + 'Couldn\'t clear and hide the drag surface: missing new surface.'); + } + const blockCanvas = this.SVG.childNodes[0] as Element; + const bubbleCanvas = this.SVG.childNodes[1] as Element; + if (!blockCanvas || !bubbleCanvas || + !(blockCanvas.classList.contains('blocklyBlockCanvas') || + !bubbleCanvas.classList.contains('blocklyBubbleCanvas'))) { + throw Error( + 'Couldn\'t clear and hide the drag surface. A node was missing.'); + } + + // If there is a previous sibling, put the blockCanvas back right + // afterwards, otherwise insert it as the first child node in newSurface. + if (this.previousSibling !== null) { + dom.insertAfter(blockCanvas, this.previousSibling); + } else { + newSurface.insertBefore(blockCanvas, newSurface.firstChild); + } + + // Reattach the bubble canvas after the blockCanvas. + dom.insertAfter(bubbleCanvas, blockCanvas); + // Hide the drag surface. + this.SVG.style.display = 'none'; + if (this.SVG.childNodes.length) { + throw Error('Drag surface was not cleared.'); + } + dom.setCssTransform(this.SVG, ''); + this.previousSibling = null; + } + + /** + * Set the SVG to have the block canvas and bubble canvas in it and then + * show the surface. + * + * @param blockCanvas The block canvas element from the + * workspace. + * @param bubbleCanvas The element that contains the + bubbles. + * @param previousSibling The element to insert the block canvas and + bubble canvas after when it goes back in the DOM at the end of a + drag. + * @param width The width of the workspace SVG element. + * @param height The height of the workspace SVG element. + * @param scale The scale of the workspace being dragged. + * @internal + */ + setContentsAndShow( + blockCanvas: SVGElement, bubbleCanvas: SVGElement, + previousSibling: Element, width: number, height: number, scale: number) { + if (this.SVG.childNodes.length) { + throw Error('Already dragging a block.'); + } + this.previousSibling = previousSibling; + // Make sure the blocks and bubble canvas are scaled appropriately. + blockCanvas.setAttribute( + 'transform', 'translate(0, 0) scale(' + scale + ')'); + bubbleCanvas.setAttribute( + 'transform', 'translate(0, 0) scale(' + scale + ')'); + this.SVG.setAttribute('width', String(width)); + this.SVG.setAttribute('height', String(height)); + this.SVG.appendChild(blockCanvas); + this.SVG.appendChild(bubbleCanvas); + this.SVG.style.display = 'block'; + } +} diff --git a/core/workspace_dragger.ts b/core/workspace_dragger.ts index 35f6e97f5..d3a50adc5 100644 --- a/core/workspace_dragger.ts +++ b/core/workspace_dragger.ts @@ -20,6 +20,9 @@ import type {WorkspaceSvg} from './workspace_svg.js'; /** * Class for a workspace dragger. It moves the workspace around when it is * being dragged by a mouse or touch. + * Note that the workspace itself manages whether or not it has a drag surface + * and how to do translations based on that. This simply passes the right + * commands based on events. */ export class WorkspaceDragger { private readonly horizontalScrollEnabled_: boolean; @@ -62,6 +65,7 @@ export class WorkspaceDragger { if (common.getSelected()) { common.getSelected()!.unselect(); } + this.workspace.setupDragSurface(); } /** @@ -74,6 +78,7 @@ export class WorkspaceDragger { endDrag(currentDragDeltaXY: Coordinate) { // Make sure everything is up to date. this.drag(currentDragDeltaXY); + this.workspace.resetDragSurface(); } /** diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index dfc287afa..72e0c3396 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -20,6 +20,7 @@ import './events/events_theme_change.js'; import './events/events_viewport.js'; import type {Block} from './block.js'; +import type {BlockDragSurfaceSvg} from './block_drag_surface.js'; import type {BlockSvg} from './block_svg.js'; import type {BlocklyOptions} from './blockly_options.js'; import * as browserEvents from './browser_events.js'; @@ -74,6 +75,7 @@ import {Workspace} from './workspace.js'; import {WorkspaceAudio} from './workspace_audio.js'; import {WorkspaceComment} from './workspace_comment.js'; import {WorkspaceCommentSvg} from './workspace_comment_svg.js'; +import type {WorkspaceDragSurfaceSvg} from './workspace_drag_surface_svg.js'; import * as Xml from './xml.js'; import {ZoomControls} from './zoom_controls.js'; import {ContextMenuOption} from './contextmenu_registry.js'; @@ -221,6 +223,26 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { */ currentGesture_: Gesture|null = null; + /** This workspace's surface for dragging blocks, if it exists. */ + private readonly blockDragSurface: BlockDragSurfaceSvg|null = null; + + /** This workspace's drag surface, if it exists. */ + private readonly workspaceDragSurface: WorkspaceDragSurfaceSvg|null = null; + + /** + * Whether to move workspace to the drag surface when it is dragged. + * True if it should move, false if it should be translated directly. + */ + private readonly useWorkspaceDragSurface; + + /** + * Whether the drag surface is actively in use. When true, calls to + * translate will translate the drag surface instead of the translating the + * workspace directly. + * This is set to true in setupDragSurface and to false in resetDragSurface. + */ + private isDragSurfaceActive = false; + /** * The first parent div with 'injectionDiv' in the name, or null if not set. * Access this with getInjectionDiv. @@ -314,8 +336,12 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { /** * @param options Dictionary of options. + * @param opt_blockDragSurface Drag surface for blocks. + * @param opt_wsDragSurface Drag surface for the workspace. */ - constructor(options: Options) { + constructor( + options: Options, opt_blockDragSurface?: BlockDragSurfaceSvg, + opt_wsDragSurface?: WorkspaceDragSurfaceSvg) { super(options); const MetricsManagerClass = registry.getClassFromOptions( @@ -335,6 +361,16 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { this.connectionDBList = ConnectionDB.init(this.connectionChecker); + if (opt_blockDragSurface) { + this.blockDragSurface = opt_blockDragSurface; + } + + if (opt_wsDragSurface) { + this.workspaceDragSurface = opt_wsDragSurface; + } + + this.useWorkspaceDragSurface = !!this.workspaceDragSurface; + /** * Object in charge of loading, storing, and playing audio for a workspace. */ @@ -1114,10 +1150,18 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * the Blockly div. */ translate(x: number, y: number) { - const translation = 'translate(' + x + ',' + y + ') ' + - 'scale(' + this.scale + ')'; - this.svgBlockCanvas_.setAttribute('transform', translation); - this.svgBubbleCanvas_.setAttribute('transform', translation); + if (this.useWorkspaceDragSurface && this.isDragSurfaceActive) { + this.workspaceDragSurface?.translateSurface(x, y); + } else { + const translation = 'translate(' + x + ',' + y + ') ' + + 'scale(' + this.scale + ')'; + this.svgBlockCanvas_.setAttribute('transform', translation); + this.svgBubbleCanvas_.setAttribute('transform', translation); + } + // Now update the block drag surface if we're using one. + if (this.blockDragSurface) { + this.blockDragSurface.translateAndScaleGroup(x, y, this.scale); + } // And update the grid if we're using one. if (this.grid) { this.grid.moveTo(x, y); @@ -1126,6 +1170,75 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { this.maybeFireViewportChangeEvent(); } + /** + * Called at the end of a workspace drag to take the contents + * out of the drag surface and put them back into the workspace SVG. + * Does nothing if the workspace drag surface is not enabled. + * + * @internal + */ + resetDragSurface() { + // Don't do anything if we aren't using a drag surface. + if (!this.useWorkspaceDragSurface) { + return; + } + + this.isDragSurfaceActive = false; + + const trans = this.workspaceDragSurface!.getSurfaceTranslation(); + this.workspaceDragSurface!.clearAndHide(this.svgGroup_); + const translation = 'translate(' + trans.x + ',' + trans.y + ') ' + + 'scale(' + this.scale + ')'; + this.svgBlockCanvas_.setAttribute('transform', translation); + this.svgBubbleCanvas_.setAttribute('transform', translation); + } + + /** + * Called at the beginning of a workspace drag to move contents of + * the workspace to the drag surface. + * Does nothing if the drag surface is not enabled. + * + * @internal + */ + setupDragSurface() { + // Don't do anything if we aren't using a drag surface. + if (!this.useWorkspaceDragSurface) { + return; + } + + // This can happen if the user starts a drag, mouses up outside of the + // document where the mouseup listener is registered (e.g. outside of an + // iframe) and then moves the mouse back in the workspace. On mobile and + // ff, we get the mouseup outside the frame. On chrome and safari desktop we + // do not. + if (this.isDragSurfaceActive) { + return; + } + + this.isDragSurfaceActive = true; + + // Figure out where we want to put the canvas back. The order + // in the is important because things are layered. + const previousElement = this.svgBlockCanvas_.previousSibling as Element; + const width = parseInt(this.getParentSvg().getAttribute('width') ?? '0'); + const height = parseInt(this.getParentSvg().getAttribute('height') ?? '0'); + const coord = svgMath.getRelativeXY(this.getCanvas()); + this.workspaceDragSurface!.setContentsAndShow( + this.getCanvas(), this.getBubbleCanvas(), previousElement, width, + height, this.scale); + this.workspaceDragSurface!.translateSurface(coord.x, coord.y); + } + + /** + * Gets the drag surface blocks are moved to when a drag is started. + * + * @returns This workspace's block drag surface, if one is in use. + * @internal + */ + getBlockDragSurface(): BlockDragSurfaceSvg|null { + return this.blockDragSurface; + } + /** * Returns the horizontal offset of the workspace. * Intended for LTR/RTL compatibility in XML.