From 57749e6eb892dd92e816eb311c67d4dcaab3c1fa Mon Sep 17 00:00:00 2001 From: Monica Kozbial Date: Mon, 1 Mar 2021 12:20:12 -0800 Subject: [PATCH] Updating bump logic to support single-direction scrollbars (#4652) * Updating bump logic to support single-direction scrollbars --- core/events/events.js | 19 +- core/flyout_horizontal.js | 19 +- core/flyout_vertical.js | 23 +- core/inject.js | 279 ++++++++------- core/interfaces/i_bounded_element.js | 7 + core/interfaces/i_metrics_manager.js | 49 +-- core/metrics_manager.js | 241 ++++++++----- core/mutator.js | 5 + core/scrollbar.js | 112 +++---- core/toolbox/toolbox.js | 5 +- core/tooltip.js | 2 +- core/trashcan.js | 5 +- core/utils/metrics.js | 24 ++ core/workspace_dragger.js | 5 +- core/workspace_svg.js | 90 ++--- tests/mocha/metrics_test.js | 484 ++++++++++++++++++--------- 16 files changed, 827 insertions(+), 542 deletions(-) diff --git a/core/events/events.js b/core/events/events.js index 70b1365c6..56c1b370d 100644 --- a/core/events/events.js +++ b/core/events/events.js @@ -200,11 +200,22 @@ Blockly.Events.COMMENT_MOVE = 'comment_move'; Blockly.Events.FINISHED_LOADING = 'finished_loading'; /** - * List of events that cause objects to be bumped back into the visible - * portion of the workspace (only used for non-movable workspaces). + * Type of events that cause objects to be bumped back into the visible + * portion of the workspace. * - * Not to be confused with bumping so that disconnected connections to do - * not appear connected. + * Not to be confused with bumping so that disconnected connections do not + * appear connected. + * @typedef {!Blockly.Events.BlockCreate|!Blockly.Events.BlockMove| + * !Blockly.Events.CommentCreate|!Blockly.Events.CommentMove} + */ +Blockly.Events.BumpEvent; + +/** + * List of events that cause objects to be bumped back into the visible + * portion of the workspace. + * + * Not to be confused with bumping so that disconnected connections do not + * appear connected. * @const */ Blockly.Events.BUMP_EVENTS = [ diff --git a/core/flyout_horizontal.js b/core/flyout_horizontal.js index af1fefac1..0b54ba137 100644 --- a/core/flyout_horizontal.js +++ b/core/flyout_horizontal.js @@ -48,11 +48,15 @@ Blockly.utils.object.inherits(Blockly.HorizontalFlyout, Blockly.Flyout); * .viewWidth: Width of the visible rectangle, * .contentHeight: Height of the contents, * .contentWidth: Width of the contents, + * .scrollHeight: Height of the scroll area, + * .scrollWidth: Width of the scroll area, * .viewTop: Offset of top edge of visible rectangle from parent, * .contentTop: Offset of the top-most content from the y=0 coordinate, + * .scrollTop: Offset of the scroll area top from the y=0 coordinate, * .absoluteTop: Top-edge of view. * .viewLeft: Offset of the left edge of visible rectangle from parent, * .contentLeft: Offset of the left-most content from the x=0 coordinate, + * .scrollLeft: Offset of the scroll area left from the x=0 coordinate, * .absoluteLeft: Left-edge of view. * @return {Blockly.utils.Metrics} Contains size and position metrics of the * flyout. @@ -83,11 +87,16 @@ Blockly.HorizontalFlyout.prototype.getMetrics_ = function() { var viewWidth = this.width_ - 2 * this.SCROLLBAR_PADDING; var metrics = { - contentHeight: (optionBox.height + 2 * this.MARGIN) * this.workspace_.scale, - contentWidth: (optionBox.width + 2 * this.MARGIN) * this.workspace_.scale, + contentHeight: optionBox.height * this.workspace_.scale, + contentWidth: optionBox.width * this.workspace_.scale, contentTop: 0, contentLeft: 0, + scrollHeight: (optionBox.height + 2 * this.MARGIN) * this.workspace_.scale, + scrollWidth: (optionBox.width + 2 * this.MARGIN) * this.workspace_.scale, + scrollTop: 0, + scrollLeft: 0, + viewHeight: viewHeight, viewWidth: viewWidth, viewTop: -this.workspace_.scrollY, @@ -115,8 +124,8 @@ Blockly.HorizontalFlyout.prototype.setMetrics_ = function(xyRatio) { if (typeof xyRatio.x == 'number') { this.workspace_.scrollX = - -(metrics.contentLeft + - (metrics.contentWidth - metrics.viewWidth) * xyRatio.x); + -(metrics.scrollLeft + + (metrics.scrollWidth - metrics.viewWidth) * xyRatio.x); } this.workspace_.translate(this.workspace_.scrollX + metrics.absoluteLeft, @@ -267,7 +276,7 @@ Blockly.HorizontalFlyout.prototype.wheel_ = function(e) { if (delta) { var metrics = this.getMetrics_(); var pos = metrics.viewLeft + delta; - var limit = metrics.contentWidth - metrics.viewWidth; + var limit = metrics.scrollWidth - metrics.viewWidth; pos = Math.min(pos, limit); pos = Math.max(pos, 0); this.workspace_.scrollbar.setX(pos); diff --git a/core/flyout_vertical.js b/core/flyout_vertical.js index f42856a60..38a3ee743 100644 --- a/core/flyout_vertical.js +++ b/core/flyout_vertical.js @@ -55,9 +55,11 @@ Blockly.VerticalFlyout.registryName = 'verticalFlyout'; * .contentWidth: Width of the contents, * .viewTop: Offset of top edge of visible rectangle from parent, * .contentTop: Offset of the top-most content from the y=0 coordinate, + * .scrollTop: Offset of the scroll area top from the y=0 coordinate, * .absoluteTop: Top-edge of view. * .viewLeft: Offset of the left edge of visible rectangle from parent, * .contentLeft: Offset of the left-most content from the x=0 coordinate, + * .scrollLeft: Offset of the scroll area left from the x=0 coordinate, * .absoluteLeft: Left-edge of view. * @return {Blockly.utils.Metrics} Contains size and position metrics of the * flyout. @@ -87,10 +89,15 @@ Blockly.VerticalFlyout.prototype.getMetrics_ = function() { } var metrics = { - contentHeight: optionBox.height * this.workspace_.scale + 2 * this.MARGIN, - contentWidth: optionBox.width * this.workspace_.scale + 2 * this.MARGIN, - contentTop: optionBox.y - this.MARGIN, - contentLeft: optionBox.x - this.MARGIN, + contentHeight: optionBox.height * this.workspace_.scale, + contentWidth: optionBox.width * this.workspace_.scale, + contentTop: optionBox.y, + contentLeft: optionBox.x, + + scrollHeight: (optionBox.height + 2 * this.MARGIN) * this.workspace_.scale, + scrollWidth: (optionBox.width + 2 * this.MARGIN) * this.workspace_.scale, + scrollTop: optionBox.y - this.MARGIN, + scrollLeft: optionBox.x - this.MARGIN, viewHeight: viewHeight, viewWidth: viewWidth, @@ -118,8 +125,8 @@ Blockly.VerticalFlyout.prototype.setMetrics_ = function(xyRatio) { } if (typeof xyRatio.y == 'number') { this.workspace_.scrollY = - -(metrics.contentTop + - (metrics.contentHeight - metrics.viewHeight) * xyRatio.y); + -(metrics.scrollTop + + (metrics.scrollHeight - metrics.viewHeight) * xyRatio.y); } this.workspace_.translate(this.workspace_.scrollX + metrics.absoluteLeft, this.workspace_.scrollY + metrics.absoluteTop); @@ -258,8 +265,8 @@ Blockly.VerticalFlyout.prototype.wheel_ = function(e) { if (scrollDelta.y) { var metrics = this.getMetrics_(); - var pos = (metrics.viewTop - metrics.contentTop) + scrollDelta.y; - var limit = metrics.contentHeight - metrics.viewHeight; + var pos = (metrics.viewTop - metrics.scrollTop) + scrollDelta.y; + var limit = metrics.scrollHeight - metrics.viewHeight; pos = Math.min(pos, limit); pos = Math.max(pos, 0); this.workspace_.scrollbar.setY(pos); diff --git a/core/inject.js b/core/inject.js index e15f49f6c..e469e0651 100644 --- a/core/inject.js +++ b/core/inject.js @@ -181,147 +181,7 @@ Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface, // A null translation will also apply the correct initial scale. mainWorkspace.translate(0, 0); - if (!wsOptions.readOnly && !mainWorkspace.isMovable()) { - // Helper function for the workspaceChanged callback. - // TODO (#2300): Move metrics math back to the WorkspaceSvg. - var getWorkspaceMetrics = function() { - var workspaceMetrics = Object.create(null); - var defaultMetrics = mainWorkspace.getMetrics(); - var scale = mainWorkspace.scale; - - workspaceMetrics.RTL = mainWorkspace.RTL; - - // Get the view metrics in workspace units. - workspaceMetrics.viewLeft = defaultMetrics.viewLeft / scale; - workspaceMetrics.viewTop = defaultMetrics.viewTop / scale; - workspaceMetrics.viewRight = - (defaultMetrics.viewLeft + defaultMetrics.viewWidth) / scale; - workspaceMetrics.viewBottom = - (defaultMetrics.viewTop + defaultMetrics.viewHeight) / scale; - - // Get the exact content metrics (in workspace units), even if the - // content is bounded. - if (mainWorkspace.isContentBounded()) { - // Already in workspace units, no need to divide by scale. - var blocksBoundingBox = mainWorkspace.getBlocksBoundingBox(); - workspaceMetrics.contentLeft = blocksBoundingBox.left; - workspaceMetrics.contentTop = blocksBoundingBox.top; - workspaceMetrics.contentRight = blocksBoundingBox.right; - workspaceMetrics.contentBottom = blocksBoundingBox.bottom; - } else { - workspaceMetrics.contentLeft = defaultMetrics.contentLeft / scale; - workspaceMetrics.contentTop = defaultMetrics.contentTop / scale; - workspaceMetrics.contentRight = - (defaultMetrics.contentLeft + defaultMetrics.contentWidth) / scale; - workspaceMetrics.contentBottom = - (defaultMetrics.contentTop + defaultMetrics.contentHeight) / scale; - } - - return workspaceMetrics; - }; - - var getObjectMetrics = function(object) { - var objectMetrics = object.getBoundingRectangle(); - objectMetrics.height = objectMetrics.bottom - objectMetrics.top; - objectMetrics.width = objectMetrics.right - objectMetrics.left; - return objectMetrics; - }; - - var bumpObjects = function(e) { - // We always check isMovable_ again because the original - // "not movable" state of isMovable_ could have been changed. - if (!mainWorkspace.isDragging() && !mainWorkspace.isMovable() && - (Blockly.Events.BUMP_EVENTS.indexOf(e.type) != -1)) { - var metrics = getWorkspaceMetrics(); - if (metrics.contentTop < metrics.viewTop || - metrics.contentBottom > metrics.viewBottom || - metrics.contentLeft < metrics.viewLeft || - metrics.contentRight > metrics.viewRight) { - - // Handle undo. - var oldGroup = null; - if (e) { - oldGroup = Blockly.Events.getGroup(); - Blockly.Events.setGroup(e.group); - } - - switch (e.type) { - case Blockly.Events.BLOCK_CREATE: - case Blockly.Events.BLOCK_MOVE: - var object = mainWorkspace.getBlockById(e.blockId); - if (object) { - object = object.getRootBlock(); - } - break; - case Blockly.Events.COMMENT_CREATE: - case Blockly.Events.COMMENT_MOVE: - var object = mainWorkspace.getCommentById(e.commentId); - break; - } - if (object) { - var objectMetrics = getObjectMetrics(object); - - // The idea is to find the region of valid coordinates for the top - // left corner of the object, and then clamp the object's - // top left corner within that region. - - // The top of the object should always be at or below the top of - // the workspace. - var topClamp = metrics.viewTop; - // The top of the object should ideally be positioned so that - // the bottom of the object is not below the bottom of the - // workspace. - var bottomClamp = metrics.viewBottom - objectMetrics.height; - // If the object is taller than the workspace we want to - // top-align the block, which means setting the bottom clamp to - // match. - bottomClamp = Math.max(topClamp, bottomClamp); - - var newYPosition = Blockly.utils.math.clamp( - topClamp, objectMetrics.top, bottomClamp); - var deltaY = newYPosition - objectMetrics.top; - - // Note: Even in RTL mode the "anchor" of the object is the - // top-left corner of the object. - - // The left edge of the object should ideally be positioned at - // or to the right of the left edge of the workspace. - var leftClamp = metrics.viewLeft; - // The left edge of the object should ideally be positioned so - // that the right of the object is not outside the workspace bounds. - var rightClamp = metrics.viewRight - objectMetrics.width; - if (metrics.RTL) { - // If the object is wider than the workspace and we're in RTL - // mode we want to right-align the block, which means setting - // the left clamp to match. - leftClamp = Math.min(rightClamp, leftClamp); - } else { - // If the object is wider than the workspace and we're in LTR - // mode we want to left-align the block, which means setting - // the right clamp to match. - rightClamp = Math.max(leftClamp, rightClamp); - } - - var newXPosition = Blockly.utils.math.clamp( - leftClamp, objectMetrics.left, rightClamp); - var deltaX = newXPosition - objectMetrics.left; - - object.moveBy(deltaX, deltaY); - } - if (e) { - if (!e.group && object) { - console.warn('Moved object in bounds but there was no' + - ' event group. This may break undo.'); - } - if (oldGroup !== null) { - Blockly.Events.setGroup(oldGroup); - } - } - } - } - }; - mainWorkspace.addChangeListener(bumpObjects); - } + mainWorkspace.addChangeListener(Blockly.bumpIntoBoundsHandler_(mainWorkspace)); // The SVG is now fully assembled. Blockly.svgResize(mainWorkspace); @@ -331,6 +191,142 @@ Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface, return mainWorkspace; }; +/** + * Extracts the object from the given event. + * @param {!Blockly.WorkspaceSvg} workspace The workspace the event originated + * from. + * @param {!Blockly.Events.BumpEvent} e An event containing an object. + * @return {?Blockly.BlockSvg|?Blockly.WorkspaceCommentSvg} The extracted + * object. + * @private + */ +Blockly.extractObjectFromEvent_ = function(workspace, e) { + var object = null; + switch (e.type) { + case Blockly.Events.BLOCK_CREATE: + case Blockly.Events.BLOCK_MOVE: + object = workspace.getBlockById(e.blockId); + if (object) { + object = object.getRootBlock(); + } + break; + case Blockly.Events.COMMENT_CREATE: + case Blockly.Events.COMMENT_MOVE: + object = workspace.getCommentById(e.commentId); + break; + } + return object; +}; + +/** + * Bumps the top objects in the given workspace into bounds. + * @param {!Blockly.WorkspaceSvg} workspace The workspace. + * @private + */ +Blockly.bumpTopObjectsIntoBounds_ = function(workspace) { + var metricsManager = workspace.getMetricsManager(); + if (!metricsManager.hasFixedEdges() || workspace.isDragging()) { + return; + } + + var scrollMetricsInWsCoords = metricsManager.getScrollMetrics(true); + var topBlocks = workspace.getTopBoundedElements(); + for (var i = 0, block; (block = topBlocks[i]); i++) { + Blockly.bumpObjectIntoBounds_( + workspace, scrollMetricsInWsCoords, block); + } +}; + +/** + * Creates a handler for bumping objects when they cross fixed bounds. + * @param {!Blockly.WorkspaceSvg} workspace The workspace to handle. + * @return {function(Blockly.Events.Abstract)} The event handler. + * @private + */ +Blockly.bumpIntoBoundsHandler_ = function(workspace) { + return function(e) { + var metricsManager = workspace.getMetricsManager(); + if (!metricsManager.hasFixedEdges() || workspace.isDragging() || + Blockly.Events.BUMP_EVENTS.indexOf(e.type) === -1) { + return; + } + + var scrollMetricsInWsCoords = metricsManager.getScrollMetrics(true); + + // Triggered by move/create event + var object = Blockly.extractObjectFromEvent_(workspace, e); + if (!object) { + return; + } + // Handle undo. + var oldGroup = Blockly.Events.getGroup(); + Blockly.Events.setGroup(e.group); + + var wasBumped = Blockly.bumpObjectIntoBounds_( + workspace, scrollMetricsInWsCoords, + /** @type {!Blockly.IBoundedElement} */ (object)); + + if (wasBumped && !e.group) { + console.warn('Moved object in bounds but there was no' + + ' event group. This may break undo.'); + } + if (oldGroup !== null) { + Blockly.Events.setGroup(oldGroup); + } + }; +}; + +/** + * Bumps the given object that has passed out of bounds. + * @param {!Blockly.WorkspaceSvg} workspace The workspace containing the object. + * @param {!Blockly.MetricsManager.ContainerRegion} scrollMetrics Scroll metrics + * in workspace coordinates. + * @param {!Blockly.IBoundedElement} object The object to bump. + * @return {boolean} True if block was bumped. + * @private + */ +Blockly.bumpObjectIntoBounds_ = function(workspace, scrollMetrics, object) { + // Compute new top/left position for object. + var objectMetrics = object.getBoundingRectangle(); + var height = objectMetrics.bottom - objectMetrics.top; + var width = objectMetrics.right - objectMetrics.left; + + var topClamp = scrollMetrics.top; + var scrollMetricsBottom = scrollMetrics.top + scrollMetrics.height; + var bottomClamp = scrollMetricsBottom - height; + // If the object is taller than the workspace we want to + // top-align the block + var newYPosition = + Blockly.utils.math.clamp(topClamp, objectMetrics.top, bottomClamp); + var deltaY = newYPosition - objectMetrics.top; + + // Note: Even in RTL mode the "anchor" of the object is the + // top-left corner of the object. + var leftClamp = scrollMetrics.left; + var scrollMetricsRight = scrollMetrics.left + scrollMetrics.width; + var rightClamp = scrollMetricsRight - width; + if (workspace.RTL) { + // If the object is wider than the workspace and we're in RTL + // mode we want to right-align the block, which means setting + // the left clamp to match. + leftClamp = Math.min(rightClamp, leftClamp); + } else { + // If the object is wider than the workspace and we're in LTR + // mode we want to left-align the block, which means setting + // the right clamp to match. + rightClamp = Math.max(leftClamp, rightClamp); + } + var newXPosition = + Blockly.utils.math.clamp(leftClamp, objectMetrics.left, rightClamp); + var deltaX = newXPosition - objectMetrics.left; + + if (deltaX || deltaY) { + object.moveBy(deltaX, deltaY); + return true; + } + return false; +}; + /** * Initialize Blockly with various handlers. * @param {!Blockly.WorkspaceSvg} mainWorkspace Newly created main workspace. @@ -353,6 +349,7 @@ Blockly.init_ = function(mainWorkspace) { Blockly.browserEvents.conditionalBind(window, 'resize', null, function() { Blockly.hideChaff(true); Blockly.svgResize(mainWorkspace); + Blockly.bumpTopObjectsIntoBounds_(mainWorkspace); }); mainWorkspace.setResizeHandlerWrapper(workspaceResizeHandler); diff --git a/core/interfaces/i_bounded_element.js b/core/interfaces/i_bounded_element.js index 34b72fb32..bc5ffeee6 100644 --- a/core/interfaces/i_bounded_element.js +++ b/core/interfaces/i_bounded_element.js @@ -29,3 +29,10 @@ Blockly.IBoundedElement = function() {}; * @return {!Blockly.utils.Rect} Object with coordinates of the bounded element. */ Blockly.IBoundedElement.prototype.getBoundingRectangle; + +/** + * Move the element by a relative offset. + * @param {number} dx Horizontal offset in workspace units. + * @param {number} dy Vertical offset in workspace units. + */ +Blockly.IBoundedElement.prototype.moveBy; diff --git a/core/interfaces/i_metrics_manager.js b/core/interfaces/i_metrics_manager.js index b3d267f2f..6164dd58f 100644 --- a/core/interfaces/i_metrics_manager.js +++ b/core/interfaces/i_metrics_manager.js @@ -28,30 +28,26 @@ goog.requireType('Blockly.utils.toolbox'); Blockly.IMetricsManager = function() {}; /** - * Gets the width and the height of the flyout on the workspace in pixel - * coordinates. - * @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics An object - * containing height and width attributes in CSS pixels. Together they - * specify the size of the visible workspace, not including areas covered up - * by the toolbox. - * @return {!Blockly.MetricsManager.ContainerRegion} The dimensions of the - * contents of the given workspace, as an object containing - * - height and width in pixels - * - left and top in pixels relative to the workspace origin. - * @protected + * Returns whether the scroll area has fixed edges. + * @return {boolean} Whether the scroll area has fixed edges. + * @package */ -Blockly.IMetricsManager.prototype.getContentDimensionsBounded_; +Blockly.IMetricsManager.prototype.hasFixedEdges; /** - * Gets the bounding box for all workspace contents, in pixel coordinates. - * @return {!Blockly.MetricsManager.ContainerRegion} The dimensions of the - * contents of the given workspace in pixel coordinates, as an object - * containing - * - height and width in pixels - * - left and top in pixels relative to the workspace origin. - * @protected + * Returns the metrics for the scroll area of the workspace. + * @param {boolean=} opt_getWorkspaceCoordinates True to get the scroll metrics + * in workspace coordinates, false to get them in pixel coordinates. + * @param {!Blockly.MetricsManager.ContainerRegion=} opt_viewMetrics The view + * metrics if they have been previously computed. Passing in null may cause + * the view metrics to be computed again, if it is needed. + * @param {!Blockly.MetricsManager.ContainerRegion=} opt_contentMetrics The + * content metrics if they have been previously computed. Passing in null + * may cause the content metrics to be computed again, if it is needed. + * @return {!Blockly.MetricsManager.ContainerRegion} The metrics for the scroll + * container */ -Blockly.IMetricsManager.prototype.getContentDimensionsExact_; +Blockly.IMetricsManager.prototype.getScrollMetrics; /** * Gets the width and the height of the flyout on the workspace in pixel @@ -107,19 +103,10 @@ Blockly.IMetricsManager.prototype.getViewMetrics; /** * Gets content metrics in either pixel or workspace coordinates. - * - * This can mean two things: - * If the workspace has a fixed width and height then the content - * area is rectangle around all the top bounded elements on the workspace - * (workspace comments and blocks). - * - * If the workspace does not have a fixed width and height then it is the - * metrics of the area that content can be placed. + * The content area is a rectangle around all the top bounded elements on the + * workspace (workspace comments and blocks). * @param {boolean=} opt_getWorkspaceCoordinates True to get the content metrics * in workspace coordinates, false to get them in pixel coordinates. - * @param {!Blockly.MetricsManager.ContainerRegion=} opt_viewMetrics The view - * metrics if they have been previously computed. Passing in null may cause - * the view metrics to be computed again, if it is needed. * @return {!Blockly.MetricsManager.ContainerRegion} The * metrics for the content container. * @public diff --git a/core/metrics_manager.js b/core/metrics_manager.js index 9ca3fed0c..7f6b106e6 100644 --- a/core/metrics_manager.js +++ b/core/metrics_manager.js @@ -71,6 +71,17 @@ Blockly.MetricsManager.AbsoluteMetrics; */ Blockly.MetricsManager.ContainerRegion; +/** + * Describes fixed edges of the workspace. + * @typedef {{ + * top: (number|undefined), + * bottom: (number|undefined), + * left: (number|undefined), + * right: (number|undefined) + * }} + */ +Blockly.MetricsManager.FixedEdges; + /** * Gets the dimensions of the given workspace component, in pixel coordinates. * @param {?Blockly.IToolbox|?Blockly.IFlyout} elem The element to get the @@ -90,66 +101,6 @@ Blockly.MetricsManager.prototype.getDimensionsPx_ = function(elem) { return new Blockly.utils.Size(width, height); }; -/** - * Calculates the size of a scrollable workspace, which should include - * room for a half screen border around the workspace contents. In pixel - * coordinates. - * @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics An object - * containing height and width attributes in CSS pixels. Together they - * specify the size of the visible workspace, not including areas covered up - * by the toolbox. - * @return {!Blockly.MetricsManager.ContainerRegion} The dimensions of the - * contents of the given workspace, as an object containing - * - height and width in pixels - * - left and top in pixels relative to the workspace origin. - * @protected - */ -Blockly.MetricsManager.prototype.getContentDimensionsBounded_ = function( - viewMetrics) { - var content = this.getContentDimensionsExact_(); - var contentRight = content.left + content.width; - var contentBottom = content.top + content.height; - - // View height and width are both in pixels, and are the same as the SVG size. - var viewWidth = viewMetrics.width; - var viewHeight = viewMetrics.height; - var halfWidth = viewWidth / 2; - var halfHeight = viewHeight / 2; - - // Add a border around the content that is at least half a screen wide. - // Ensure border is wide enough that blocks can scroll over entire screen. - var left = Math.min(content.left - halfWidth, contentRight - viewWidth); - var right = Math.max(contentRight + halfWidth, content.left + viewWidth); - - var top = Math.min(content.top - halfHeight, contentBottom - viewHeight); - var bottom = Math.max(contentBottom + halfHeight, content.top + viewHeight); - - return {left: left, top: top, height: bottom - top, width: right - left}; -}; - -/** - * Gets the bounding box for all workspace contents, in pixel coordinates. - * @return {!Blockly.MetricsManager.ContainerRegion} The dimensions of the - * contents of the given workspace in pixel coordinates, as an object - * containing - * - height and width in pixels - * - left and top in pixels relative to the workspace origin. - * @protected - */ -Blockly.MetricsManager.prototype.getContentDimensionsExact_ = function() { - // Block bounding box is in workspace coordinates. - var blockBox = this.workspace_.getBlocksBoundingBox(); - var scale = this.workspace_.scale; - - // Convert to pixels. - var top = blockBox.top * scale; - var bottom = blockBox.bottom * scale; - var left = blockBox.left * scale; - var right = blockBox.right * scale; - - return {top: top, left: left, width: right - left, height: bottom - top}; -}; - /** * Gets the width and the height of the flyout on the workspace in pixel * coordinates. Returns 0 for the width and height if the workspace has a @@ -280,40 +231,145 @@ Blockly.MetricsManager.prototype.getViewMetrics = function( /** * Gets content metrics in either pixel or workspace coordinates. - * - * This can mean two things: - * If the workspace has a fixed width and height then the content - * area is rectangle around all the top bounded elements on the workspace - * (workspace comments and blocks). - * - * If the workspace does not have a fixed width and height then it is the - * metrics of the area that content can be placed. This area is computed by - * getting the rectangle around the top bounded elements on the workspace and - * adding padding to all sides. + * The content area is a rectangle around all the top bounded elements on the + * workspace (workspace comments and blocks). * @param {boolean=} opt_getWorkspaceCoordinates True to get the content metrics * in workspace coordinates, false to get them in pixel coordinates. - * @param {!Blockly.MetricsManager.ContainerRegion=} opt_viewMetrics The view - * metrics if they have been previously computed. Not passing in view - * metrics may cause them to be computed again. * @return {!Blockly.MetricsManager.ContainerRegion} The * metrics for the content container. * @public */ Blockly.MetricsManager.prototype.getContentMetrics = function( - opt_getWorkspaceCoordinates, opt_viewMetrics) { - var scale = opt_getWorkspaceCoordinates ? this.workspace_.scale : 1; - var contentDimensions = null; - if (this.workspace_.isContentBounded()) { - opt_viewMetrics = opt_viewMetrics || this.getViewMetrics(false); - contentDimensions = this.getContentDimensionsBounded_(opt_viewMetrics); - } else { - contentDimensions = this.getContentDimensionsExact_(); - } + opt_getWorkspaceCoordinates) { + var scale = opt_getWorkspaceCoordinates ? 1 : this.workspace_.scale; + + // Block bounding box is in workspace coordinates. + var blockBox = this.workspace_.getBlocksBoundingBox(); + return { - height: contentDimensions.height / scale, - width: contentDimensions.width / scale, - top: contentDimensions.top / scale, - left: contentDimensions.left / scale, + height: (blockBox.bottom - blockBox.top) * scale, + width: (blockBox.right - blockBox.left) * scale, + top: blockBox.top * scale, + left: blockBox.left * scale, + }; +}; + +/** + * Returns whether the scroll area has fixed edges. + * @return {boolean} Whether the scroll area has fixed edges. + * @package + */ +Blockly.MetricsManager.prototype.hasFixedEdges = function() { + // This exists for optimization of bump logic. + return !this.workspace_.isMovableHorizontally() || + !this.workspace_.isMovableVertically(); +}; + +/** + * Computes the fixed edges of the scroll area. + * @param {!Blockly.MetricsManager.ContainerRegion=} opt_viewMetrics The view + * metrics if they have been previously computed. Passing in null may cause + * the view metrics to be computed again, if it is needed. + * @return {Blockly.MetricsManager.FixedEdges} The fixed edges of the scroll + * area. + * @protected + */ +Blockly.MetricsManager.prototype.getComputedFixedEdges_ = function( + opt_viewMetrics) { + if (!this.hasFixedEdges()) { + // Return early if there are no edges. + return {}; + } + + var hScrollEnabled = this.workspace_.isMovableHorizontally(); + var vScrollEnabled = this.workspace_.isMovableVertically(); + + var viewMetrics = opt_viewMetrics || this.getViewMetrics(false); + + var edges = {}; + if (!vScrollEnabled) { + edges.top = viewMetrics.top; + edges.bottom = viewMetrics.top + viewMetrics.height; + } + if (!hScrollEnabled) { + edges.left = viewMetrics.left; + edges.right = viewMetrics.left + viewMetrics.width; + } + return edges; +}; + +/** + * Returns the content area with added padding. + * @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics The view + * metrics. + * @param {!Blockly.MetricsManager.ContainerRegion} contentMetrics The content + * metrics. + * @return {{top: number, bottom: number, left: number, right: number}} The + * padded content area. + * @protected + */ +Blockly.MetricsManager.prototype.getPaddedContent_ = function( + viewMetrics, contentMetrics) { + var contentBottom = contentMetrics.top + contentMetrics.height; + var contentRight = contentMetrics.left + contentMetrics.width; + + var viewWidth = viewMetrics.width; + var viewHeight = viewMetrics.height; + var halfWidth = viewWidth / 2; + var halfHeight = viewHeight / 2; + + // Add a padding around the content that is at least half a screen wide. + // Ensure padding is wide enough that blocks can scroll over entire screen. + var top = + Math.min(contentMetrics.top - halfHeight, contentBottom - viewHeight); + var left = + Math.min(contentMetrics.left - halfWidth, contentRight - viewWidth); + var bottom = + Math.max(contentBottom + halfHeight, contentMetrics.top + viewHeight); + var right = + Math.max(contentRight + halfWidth, contentMetrics.left + viewWidth); + + return {top: top, bottom: bottom, left: left, right: right}; +}; + +/** + * Returns the metrics for the scroll area of the workspace. + * @param {boolean=} opt_getWorkspaceCoordinates True to get the scroll metrics + * in workspace coordinates, false to get them in pixel coordinates. + * @param {!Blockly.MetricsManager.ContainerRegion=} opt_viewMetrics The view + * metrics if they have been previously computed. Passing in null may cause + * the view metrics to be computed again, if it is needed. + * @param {!Blockly.MetricsManager.ContainerRegion=} opt_contentMetrics The + * content metrics if they have been previously computed. Passing in null + * may cause the content metrics to be computed again, if it is needed. + * @return {!Blockly.MetricsManager.ContainerRegion} The metrics for the scroll + * container + */ +Blockly.MetricsManager.prototype.getScrollMetrics = function( + opt_getWorkspaceCoordinates, opt_viewMetrics, opt_contentMetrics) { + var scale = opt_getWorkspaceCoordinates ? this.workspace_.scale : 1; + var viewMetrics = opt_viewMetrics || this.getViewMetrics(false); + var contentMetrics = opt_contentMetrics || this.getContentMetrics(); + var fixedEdges = this.getComputedFixedEdges_(viewMetrics); + + // Add padding around content + var paddedContent = this.getPaddedContent_(viewMetrics, contentMetrics); + + // Use combination of fixed bounds and padded content to make scroll area. + var top = fixedEdges.top !== undefined ? + fixedEdges.top : paddedContent.top; + var left = fixedEdges.left !== undefined ? + fixedEdges.left : paddedContent.left; + var bottom = fixedEdges.bottom !== undefined ? + fixedEdges.bottom : paddedContent.bottom; + var right = fixedEdges.right !== undefined ? + fixedEdges.right : paddedContent.right; + + return { + top: top / scale, + left: left / scale, + width: (right - left) / scale, + height: (bottom - top) / scale, }; }; @@ -325,6 +381,8 @@ Blockly.MetricsManager.prototype.getContentMetrics = function( * .viewWidth: Width of the visible portion of the workspace. * .contentHeight: Height of the content. * .contentWidth: Width of the content. + * .scrollHeight: Height of the scroll area. + * .scrollWidth: Width of the scroll area. * .svgHeight: Height of the Blockly div (the view + the toolbox, * simple or otherwise), * .svgWidth: Width of the Blockly div (the view + the toolbox, @@ -335,6 +393,8 @@ Blockly.MetricsManager.prototype.getContentMetrics = function( * the workspace origin. * .contentTop: Top-edge of the content, relative to the workspace origin. * .contentLeft: Left-edge of the content relative to the workspace origin. + * .scrollTop: Top-edge of the scroll area, relative to the workspace origin. + * .scrollLeft: Left-edge of the scroll area relative to the workspace origin. * .absoluteTop: Top-edge of the visible portion of the workspace, relative * to the blocklyDiv. * .absoluteLeft: Left-edge of the visible portion of the workspace, relative @@ -355,7 +415,9 @@ Blockly.MetricsManager.prototype.getMetrics = function() { var svgMetrics = this.getSvgMetrics(); var absoluteMetrics = this.getAbsoluteMetrics(); var viewMetrics = this.getViewMetrics(); - var contentMetrics = this.getContentMetrics(false, viewMetrics); + var contentMetrics = this.getContentMetrics(); + var scrollMetrics = + this.getScrollMetrics(false, viewMetrics, contentMetrics); return { contentHeight: contentMetrics.height, @@ -363,6 +425,11 @@ Blockly.MetricsManager.prototype.getMetrics = function() { contentTop: contentMetrics.top, contentLeft: contentMetrics.left, + scrollHeight: scrollMetrics.height, + scrollWidth: scrollMetrics.width, + scrollTop: scrollMetrics.top, + scrollLeft: scrollMetrics.left, + viewHeight: viewMetrics.height, viewWidth: viewMetrics.width, viewTop: viewMetrics.top, diff --git a/core/mutator.js b/core/mutator.js index 416f77870..0f31b2ad2 100644 --- a/core/mutator.js +++ b/core/mutator.js @@ -469,6 +469,11 @@ Blockly.Mutator.prototype.getFlyoutMetrics_ = function() { contentTop: unsupported, contentLeft: unsupported, + scrollHeight: unsupported, + scrollWidth: unsupported, + scrollTop: unsupported, + scrollLeft: unsupported, + viewHeight: this.workspaceHeight_, viewWidth: this.workspaceWidth_ - flyoutWidth, viewTop: unsupported, diff --git a/core/scrollbar.js b/core/scrollbar.js index 0046442f2..d722471be 100644 --- a/core/scrollbar.js +++ b/core/scrollbar.js @@ -128,15 +128,15 @@ Blockly.ScrollbarPair.prototype.resize = function() { } else { // Has the content been resized or moved? if (!this.oldHostMetrics_ || - this.oldHostMetrics_.contentWidth != hostMetrics.contentWidth || + this.oldHostMetrics_.scrollWidth != hostMetrics.scrollWidth || this.oldHostMetrics_.viewLeft != hostMetrics.viewLeft || - this.oldHostMetrics_.contentLeft != hostMetrics.contentLeft) { + this.oldHostMetrics_.scrollLeft != hostMetrics.scrollLeft) { resizeH = true; } if (!this.oldHostMetrics_ || - this.oldHostMetrics_.contentHeight != hostMetrics.contentHeight || + this.oldHostMetrics_.scrollHeight != hostMetrics.scrollHeight || this.oldHostMetrics_.viewTop != hostMetrics.viewTop || - this.oldHostMetrics_.contentTop != hostMetrics.contentTop) { + this.oldHostMetrics_.scrollTop != hostMetrics.scrollTop) { resizeV = true; } } @@ -174,7 +174,6 @@ Blockly.ScrollbarPair.prototype.resize = function() { this.oldHostMetrics_ = hostMetrics; }; - /** * Returns whether scrolling horizontally is enabled. * @return {boolean} True if horizontal scroll is enabled. @@ -428,12 +427,13 @@ Blockly.Scrollbar.prototype.origin_ = new Blockly.utils.Coordinate(0, 0); Blockly.Scrollbar.prototype.startDragMouse_ = 0; /** - * The size of the area within which the scrollbar handle can move, in CSS - * pixels (the size of the scrollbar background). + * The length of the scrollbars (including the handle and the background), in + * CSS pixels. This is equivalent to scrollbar background length and the area + * within which the scrollbar handle can move. * @type {number} * @private */ -Blockly.Scrollbar.prototype.scrollViewSize_ = 0; +Blockly.Scrollbar.prototype.scrollbarLength_ = 0; /** * The length of the scrollbar handle in CSS pixels. @@ -485,7 +485,7 @@ Blockly.Scrollbar.SCROLLBAR_MARGIN = 0.5; /** * @param {Blockly.utils.Metrics} first An object containing computed * measurements of a workspace. - * @param {Blockly.utils.Metrics} second Another object containing computed + * @param {?Blockly.utils.Metrics} second Another object containing computed * measurements of a workspace. * @return {boolean} Whether the two sets of metrics are equivalent. * @private @@ -501,10 +501,10 @@ Blockly.Scrollbar.metricsAreEquivalent_ = function(first, second) { first.viewTop != second.viewTop || first.absoluteTop != second.absoluteTop || first.absoluteLeft != second.absoluteLeft || - first.contentWidth != second.contentWidth || - first.contentHeight != second.contentHeight || - first.contentLeft != second.contentLeft || - first.contentTop != second.contentTop) { + first.scrollWidth != second.scrollWidth || + first.scrollHeight != second.scrollHeight || + first.scrollLeft != second.scrollLeft || + first.scrollTop != second.scrollTop) { return false; } @@ -541,11 +541,11 @@ Blockly.Scrollbar.prototype.dispose = function() { * @return {number} Constrained value, in CSS pixels. * @private */ -Blockly.Scrollbar.prototype.constrainLength_ = function(value) { +Blockly.Scrollbar.prototype.constrainHandleLength_ = function(value) { if (value <= 0 || isNaN(value)) { value = 0; } else { - value = Math.min(value, this.scrollViewSize_); + value = Math.min(value, this.scrollbarLength_); } return value; }; @@ -568,14 +568,14 @@ Blockly.Scrollbar.prototype.setHandleLength_ = function(newLength) { * @return {number} Constrained value, in CSS pixels. * @private */ -Blockly.Scrollbar.prototype.constrainPosition_ = function(value) { +Blockly.Scrollbar.prototype.constrainHandlePosition_ = function(value) { if (value <= 0 || isNaN(value)) { value = 0; } else { - // Handle length should never be greater than this.scrollViewSize_. - // If the viewSize is greater than or equal to the contentSize, the - // handleLength will end up equal to this.scrollViewSize_. - value = Math.min(value, this.scrollViewSize_ - this.handleLength_); + // Handle length should never be greater than this.scrollbarLength_. + // If the viewSize is greater than or equal to the scrollSize, the + // handleLength will end up equal to this.scrollbarLength_. + value = Math.min(value, this.scrollbarLength_ - this.handleLength_); } return value; }; @@ -596,10 +596,10 @@ Blockly.Scrollbar.prototype.setHandlePosition = function(newPosition) { * @param {number} newSize The new scrollbar background length in CSS pixels. * @private */ -Blockly.Scrollbar.prototype.setScrollViewSize_ = function(newSize) { - this.scrollViewSize_ = newSize; - this.outerSvg_.setAttribute(this.lengthAttribute_, this.scrollViewSize_); - this.svgBackground_.setAttribute(this.lengthAttribute_, this.scrollViewSize_); +Blockly.Scrollbar.prototype.setScrollbarLength_ = function(newSize) { + this.scrollbarLength_ = newSize; + this.outerSvg_.setAttribute(this.lengthAttribute_, this.scrollbarLength_); + this.svgBackground_.setAttribute(this.lengthAttribute_, this.scrollbarLength_); }; /** @@ -697,7 +697,7 @@ Blockly.Scrollbar.prototype.resizeViewHorizontal = function(hostMetrics) { // Shorten the scrollbar to make room for the corner square. viewSize -= Blockly.Scrollbar.scrollbarThickness; } - this.setScrollViewSize_(Math.max(0, viewSize)); + this.setScrollbarLength_(Math.max(0, viewSize)); var xCoordinate = hostMetrics.absoluteLeft + Blockly.Scrollbar.SCROLLBAR_MARGIN; @@ -722,10 +722,10 @@ Blockly.Scrollbar.prototype.resizeViewHorizontal = function(hostMetrics) { * the required dimensions, possibly fetched from the host object. */ Blockly.Scrollbar.prototype.resizeContentHorizontal = function(hostMetrics) { - if (hostMetrics.viewWidth >= hostMetrics.contentWidth) { - // viewWidth is often greater than contentWidth in flyouts and + if (hostMetrics.viewWidth >= hostMetrics.scrollWidth) { + // viewWidth is often greater than scrollWidth in flyouts and // non-scrollable workspaces. - this.setHandleLength_(this.scrollViewSize_); + this.setHandleLength_(this.scrollbarLength_); this.setHandlePosition(0); if (!this.pair_) { // The scrollbar isn't needed. @@ -741,27 +741,27 @@ Blockly.Scrollbar.prototype.resizeContentHorizontal = function(hostMetrics) { // Resize the handle. var handleLength = - this.scrollViewSize_ * hostMetrics.viewWidth / hostMetrics.contentWidth; - handleLength = this.constrainLength_(handleLength); + this.scrollbarLength_ * hostMetrics.viewWidth / hostMetrics.scrollWidth; + handleLength = this.constrainHandleLength_(handleLength); this.setHandleLength_(handleLength); // Compute the handle offset. // The position of the handle can be between: - // 0 and this.scrollViewSize_ - handleLength - // If viewLeft == contentLeft + // 0 and this.scrollbarLength_ - handleLength + // If viewLeft == scrollLeft // then the offset should be 0 - // If viewRight == contentRight - // then viewLeft = contentLeft + contentWidth - viewWidth + // If viewRight == scrollRight + // then viewLeft = scrollLeft + scrollWidth - viewWidth // then the offset should be max offset - var maxScrollDistance = hostMetrics.contentWidth - hostMetrics.viewWidth; - var contentDisplacement = hostMetrics.viewLeft - hostMetrics.contentLeft; + var maxScrollDistance = hostMetrics.scrollWidth - hostMetrics.viewWidth; + var contentDisplacement = hostMetrics.viewLeft - hostMetrics.scrollLeft; // Percent of content to the left of our current position. var offsetRatio = contentDisplacement / maxScrollDistance; // Area available to scroll * percent to the left - var maxHandleOffset = this.scrollViewSize_ - this.handleLength_; + var maxHandleOffset = this.scrollbarLength_ - this.handleLength_; var handleOffset = maxHandleOffset * offsetRatio; - handleOffset = this.constrainPosition_(handleOffset); + handleOffset = this.constrainHandlePosition_(handleOffset); this.setHandlePosition(handleOffset); // Compute ratio (for use with set calls, which pass in content displacement). @@ -794,7 +794,7 @@ Blockly.Scrollbar.prototype.resizeViewVertical = function(hostMetrics) { // Shorten the scrollbar to make room for the corner square. viewSize -= Blockly.Scrollbar.scrollbarThickness; } - this.setScrollViewSize_(Math.max(0, viewSize)); + this.setScrollbarLength_(Math.max(0, viewSize)); var xCoordinate = this.workspace_.RTL ? hostMetrics.absoluteLeft + Blockly.Scrollbar.SCROLLBAR_MARGIN : @@ -817,10 +817,10 @@ Blockly.Scrollbar.prototype.resizeViewVertical = function(hostMetrics) { * the required dimensions, possibly fetched from the host object. */ Blockly.Scrollbar.prototype.resizeContentVertical = function(hostMetrics) { - if (hostMetrics.viewHeight >= hostMetrics.contentHeight) { - // viewHeight is often greater than contentHeight in flyouts and + if (hostMetrics.viewHeight >= hostMetrics.scrollHeight) { + // viewHeight is often greater than scrollHeight in flyouts and // non-scrollable workspaces. - this.setHandleLength_(this.scrollViewSize_); + this.setHandleLength_(this.scrollbarLength_); this.setHandlePosition(0); if (!this.pair_) { // The scrollbar isn't needed. @@ -836,27 +836,27 @@ Blockly.Scrollbar.prototype.resizeContentVertical = function(hostMetrics) { // Resize the handle. var handleLength = - this.scrollViewSize_ * hostMetrics.viewHeight / hostMetrics.contentHeight; - handleLength = this.constrainLength_(handleLength); + this.scrollbarLength_ * hostMetrics.viewHeight / hostMetrics.scrollHeight; + handleLength = this.constrainHandleLength_(handleLength); this.setHandleLength_(handleLength); // Compute the handle offset. // The position of the handle can be between: - // 0 and this.scrollViewSize_ - handleLength - // If viewTop == contentTop + // 0 and this.scrollbarLength_ - handleLength + // If viewTop == scrollTop // then the offset should be 0 - // If viewBottom == contentBottom - // then viewTop = contentTop + contentHeight - viewHeight + // If viewBottom == scrollBottom + // then viewTop = scrollTop + scrollHeight - viewHeight // then the offset should be max offset - var maxScrollDistance = hostMetrics.contentHeight - hostMetrics.viewHeight; - var contentDisplacement = hostMetrics.viewTop - hostMetrics.contentTop; + var maxScrollDistance = hostMetrics.scrollHeight - hostMetrics.viewHeight; + var contentDisplacement = hostMetrics.viewTop - hostMetrics.scrollTop; // Percent of content to the left of our current position. var offsetRatio = contentDisplacement / maxScrollDistance; // Area available to scroll * percent to the left - var maxHandleOffset = this.scrollViewSize_ - this.handleLength_; + var maxHandleOffset = this.scrollbarLength_ - this.handleLength_; var handleOffset = maxHandleOffset * offsetRatio; - handleOffset = this.constrainPosition_(handleOffset); + handleOffset = this.constrainHandlePosition_(handleOffset); this.setHandlePosition(handleOffset); // Compute ratio (for use with set calls, which pass in content displacement). @@ -1005,7 +1005,7 @@ Blockly.Scrollbar.prototype.onMouseDownBar_ = function(e) { handlePosition += pageLength; } - this.setHandlePosition(this.constrainPosition_(handlePosition)); + this.setHandlePosition(this.constrainHandlePosition_(handlePosition)); this.updateMetrics_(); e.stopPropagation(); @@ -1055,7 +1055,7 @@ Blockly.Scrollbar.prototype.onMouseMoveHandle_ = function(e) { var mouseDelta = currentMouse - this.startDragMouse_; var handlePosition = this.startDragHandle + mouseDelta; // Position the bar. - this.setHandlePosition(this.constrainPosition_(handlePosition)); + this.setHandlePosition(this.constrainHandlePosition_(handlePosition)); this.updateMetrics_(); }; @@ -1093,7 +1093,7 @@ Blockly.Scrollbar.prototype.cleanUp_ = function() { * @protected */ Blockly.Scrollbar.prototype.getRatio_ = function() { - var scrollHandleRange = this.scrollViewSize_ - this.handleLength_; + var scrollHandleRange = this.scrollbarLength_ - this.handleLength_; var ratio = this.handlePosition_ / scrollHandleRange; if (isNaN(ratio)) { ratio = 0; @@ -1125,7 +1125,7 @@ Blockly.Scrollbar.prototype.updateMetrics_ = function() { * Defaults to true. */ Blockly.Scrollbar.prototype.set = function(value, updateMetrics) { - this.setHandlePosition(this.constrainPosition_(value * this.ratio)); + this.setHandlePosition(this.constrainHandlePosition_(value * this.ratio)); if (updateMetrics || updateMetrics === undefined) { this.updateMetrics_(); } diff --git a/core/toolbox/toolbox.js b/core/toolbox/toolbox.js index 272ebb8a0..ab81cf4cf 100644 --- a/core/toolbox/toolbox.js +++ b/core/toolbox/toolbox.js @@ -343,7 +343,10 @@ Blockly.Toolbox.prototype.createFlyout_ = function() { 'oneBasedIndex': workspace.options.oneBasedIndex, 'horizontalLayout': workspace.horizontalLayout, 'renderer': workspace.options.renderer, - 'rendererOverrides': workspace.options.rendererOverrides + 'rendererOverrides': workspace.options.rendererOverrides, + 'move': { + 'scrollbars': true, + } })); // Options takes in either 'end' or 'start'. This has already been parsed to // be either 0 or 1, so set it after. diff --git a/core/tooltip.js b/core/tooltip.js index a07884b95..95b45ab16 100644 --- a/core/tooltip.js +++ b/core/tooltip.js @@ -351,7 +351,7 @@ Blockly.Tooltip.show_ = function() { div.appendChild(document.createTextNode(lines[i])); Blockly.Tooltip.DIV.appendChild(div); } - var rtl = Blockly.Tooltip.element_.RTL; + var rtl = /** @type {{RTL: boolean}} */ (Blockly.Tooltip.element_).RTL; var windowWidth = document.documentElement.clientWidth; var windowHeight = document.documentElement.clientHeight; // Display the tooltip. diff --git a/core/trashcan.js b/core/trashcan.js index e47571145..9eb0901b5 100644 --- a/core/trashcan.js +++ b/core/trashcan.js @@ -69,7 +69,10 @@ Blockly.Trashcan = function(workspace) { 'rtl': this.workspace_.RTL, 'oneBasedIndex': this.workspace_.options.oneBasedIndex, 'renderer': this.workspace_.options.renderer, - 'rendererOverrides': this.workspace_.options.rendererOverrides + 'rendererOverrides': this.workspace_.options.rendererOverrides, + 'move': { + 'scrollbars': true, + } })); // Create vertical or horizontal flyout. if (this.workspace_.horizontalLayout) { diff --git a/core/utils/metrics.js b/core/utils/metrics.js index 274c382c6..cf982d241 100644 --- a/core/utils/metrics.js +++ b/core/utils/metrics.js @@ -42,6 +42,18 @@ Blockly.utils.Metrics.prototype.contentHeight; */ Blockly.utils.Metrics.prototype.contentWidth; +/** + * Height of the scroll area. + * @type {number} + */ +Blockly.utils.Metrics.prototype.scrollHeight; + +/** + * Width of the scroll area. + * @type {number} + */ +Blockly.utils.Metrics.prototype.scrollWidth; + /** * Top-edge of the visible portion of the workspace, relative to the workspace * origin. @@ -68,6 +80,18 @@ Blockly.utils.Metrics.prototype.contentTop; */ Blockly.utils.Metrics.prototype.contentLeft; +/** + * Top-edge of the scroll area, relative to the workspace origin. + * @type {number} + */ +Blockly.utils.Metrics.prototype.scrollTop; + +/** + * Left-edge of the scroll area relative to the workspace origin. + * @type {number} + */ +Blockly.utils.Metrics.prototype.scrollLeft; + /** * Top-edge of the visible portion of the workspace, relative to the blocklyDiv. * @type {number} diff --git a/core/workspace_dragger.js b/core/workspace_dragger.js index ea47b02bf..3ad07a20e 100644 --- a/core/workspace_dragger.js +++ b/core/workspace_dragger.js @@ -38,15 +38,14 @@ Blockly.WorkspaceDragger = function(workspace) { * @type {boolean} * @private */ - this.horizontalScrollEnabled_ = - this.workspace_.scrollbar.canScrollHorizontally(); + this.horizontalScrollEnabled_ = this.workspace_.isMovableHorizontally(); /** * Whether vertical scroll is enabled. * @type {boolean} * @private */ - this.verticalScrollEnabled_ = this.workspace_.scrollbar.canScrollVertically(); + this.verticalScrollEnabled_ = this.workspace_.isMovableVertically(); /** * The scroll position of the workspace at the beginning of the drag. diff --git a/core/workspace_svg.js b/core/workspace_svg.js index e86162a56..4e7231bfa 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -986,7 +986,10 @@ Blockly.WorkspaceSvg.prototype.addFlyout = function(tagName) { 'oneBasedIndex': this.options.oneBasedIndex, 'horizontalLayout': this.horizontalLayout, 'renderer': this.options.renderer, - 'rendererOverrides': this.options.rendererOverrides + 'rendererOverrides': this.options.rendererOverrides, + 'move': { + 'scrollbars': true, + } })); workspaceOptions.toolboxPosition = this.options.toolboxPosition; if (this.horizontalLayout) { @@ -1157,11 +1160,11 @@ Blockly.WorkspaceSvg.prototype.maybeFireViewportChangeEvent = function() { // negligible changes in viewport top/left. return; } + var event = new (Blockly.Events.get(Blockly.Events.VIEWPORT_CHANGE))(top, + left, scale, this.id); this.oldScale_ = scale; this.oldTop_ = top; this.oldLeft_ = left; - var event = new (Blockly.Events.get(Blockly.Events.VIEWPORT_CHANGE))(top, - left, scale, this.id); Blockly.Events.fire(event); }; @@ -1629,22 +1632,6 @@ Blockly.WorkspaceSvg.prototype.isDraggable = function() { return this.options.moveOptions && this.options.moveOptions.drag; }; -/** - * Should the workspace have bounded content? Used to tell if the - * workspace's content should be sized so that it can move (bounded) or not - * (exact sizing). - * @return {boolean} True if the workspace should be bounded, false otherwise. - * @package - */ -Blockly.WorkspaceSvg.prototype.isContentBounded = function() { - return (this.options.moveOptions && this.options.moveOptions.scrollbars) || - (this.options.moveOptions && this.options.moveOptions.wheel) || - (this.options.moveOptions && this.options.moveOptions.drag) || - (this.options.zoomOptions && this.options.zoomOptions.controls) || - (this.options.zoomOptions && this.options.zoomOptions.wheel) || - (this.options.zoomOptions && this.options.zoomOptions.pinch); -}; - /** * Is this workspace movable? * @@ -1663,6 +1650,28 @@ Blockly.WorkspaceSvg.prototype.isMovable = function() { (this.options.zoomOptions && this.options.zoomOptions.pinch); }; +/** + * Is this workspace movable horizontally? + * @return {boolean} True if the workspace is movable horizontally, false + * otherwise. + */ +Blockly.WorkspaceSvg.prototype.isMovableHorizontally = function() { + var hasScrollbars = !!this.scrollbar; + return this.isMovable() && (!hasScrollbars || + (hasScrollbars && this.scrollbar.canScrollHorizontally())); +}; + +/** + * Is this workspace movable vertically? + * @return {boolean} True if the workspace is movable vertically, false + * otherwise. + */ +Blockly.WorkspaceSvg.prototype.isMovableVertically = function() { + var hasScrollbars = !!this.scrollbar; + return this.isMovable() && (!hasScrollbars || + (hasScrollbars && this.scrollbar.canScrollVertically())); +}; + /** * Handle a mouse-wheel on SVG drawing surface. * @param {!Event} e Mouse wheel event. @@ -2032,12 +2041,12 @@ Blockly.WorkspaceSvg.prototype.scrollCenter = function() { } var metrics = this.getMetrics(); - var x = (metrics.contentWidth - metrics.viewWidth) / 2; - var y = (metrics.contentHeight - metrics.viewHeight) / 2; + var x = (metrics.scrollWidth - metrics.viewWidth) / 2; + var y = (metrics.scrollHeight - metrics.viewHeight) / 2; // Convert from workspace directions to canvas directions. - x = -x - metrics.contentLeft; - y = -y - metrics.contentTop; + x = -x - metrics.scrollLeft; + y = -y - metrics.scrollTop; this.scroll(x, y); }; @@ -2124,10 +2133,11 @@ Blockly.WorkspaceSvg.prototype.setScale = function(newScale) { // zoom correctly without scrollbars, but scroll does not resize the // scrollbars so we have to call resizeView/resizeContent as well. var metrics = this.getMetrics(); - // The scroll values and the view values are additive inverses of - // each other, so when we subtract from one we have to add to the other. + this.scrollX -= metrics.absoluteLeft; this.scrollY -= metrics.absoluteTop; + // // The scroll values and the view values are additive inverses of + // // each other, so when we subtract from one we have to add to the other. metrics.viewLeft += metrics.absoluteLeft; metrics.viewTop += metrics.absoluteTop; @@ -2167,22 +2177,18 @@ Blockly.WorkspaceSvg.prototype.scroll = function(x, y) { // Keep scrolling within the bounds of the content. var metrics = this.getMetrics(); - // This is the offset of the top-left corner of the view from the - // workspace origin when the view is "seeing" the bottom-right corner of - // the content. - var maxOffsetOfViewFromOriginX = metrics.contentWidth + metrics.contentLeft - - metrics.viewWidth; - var maxOffsetOfViewFromOriginY = metrics.contentHeight + metrics.contentTop - - metrics.viewHeight; // Canvas coordinates (aka scroll coordinates) have inverse directionality // to workspace coordinates so we have to inverse them. - x = Math.min(x, -metrics.contentLeft); - y = Math.min(y, -metrics.contentTop); - x = Math.max(x, -maxOffsetOfViewFromOriginX); - y = Math.max(y, -maxOffsetOfViewFromOriginY); - + x = Math.min(x, -metrics.scrollLeft); + y = Math.min(y, -metrics.scrollTop); + var maxXScroll = metrics.scrollLeft + metrics.scrollWidth - metrics.viewWidth; + var maxYScroll = + metrics.scrollTop + metrics.scrollHeight - metrics.viewHeight; + x = Math.max(x, -maxXScroll); + y = Math.max(y, -maxYScroll); this.scrollX = x; this.scrollY = y; + if (this.scrollbar) { // The content position (displacement from the content's top-left to the // origin) plus the scroll position (displacement from the view's top-left @@ -2191,7 +2197,7 @@ Blockly.WorkspaceSvg.prototype.scroll = function(x, y) { // the content's top-left to the view's top-left, matching the // directionality of the scrollbars. this.scrollbar.set( - -(x + metrics.contentLeft), -(y + metrics.contentTop), false); + -(x + metrics.scrollLeft), -(y + metrics.scrollTop), false); } // We have to shift the translation so that when the canvas is at 0, 0 the // workspace origin is not underneath the toolbox. @@ -2212,13 +2218,13 @@ Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_ = function(xyRatio) { if (typeof xyRatio.x == 'number') { this.scrollX = - -(metrics.contentLeft + - (metrics.contentWidth - metrics.viewWidth) * xyRatio.x); + -(metrics.scrollLeft + + (metrics.scrollWidth - metrics.viewWidth) * xyRatio.x); } if (typeof xyRatio.y == 'number') { this.scrollY = - -(metrics.contentTop + - (metrics.contentHeight - metrics.viewHeight) * xyRatio.y); + -(metrics.scrollTop + + (metrics.scrollHeight - metrics.viewHeight) * xyRatio.y); } // We have to shift the translation so that when the canvas is at 0, 0 the // workspace origin is not underneath the toolbox. diff --git a/tests/mocha/metrics_test.js b/tests/mocha/metrics_test.js index 68e50f3ea..bdde32dff 100644 --- a/tests/mocha/metrics_test.js +++ b/tests/mocha/metrics_test.js @@ -32,7 +32,8 @@ suite('Metrics', function() { scale: scale, scrollX: SCROLL_X, scrollY: SCROLL_Y, - isContentBounded: function() {} + isMovableHorizontally: function() { return true; }, + isMovableVertically: function() { return true; } }; } @@ -43,125 +44,6 @@ suite('Metrics', function() { sharedTestTeardown.call(this); }); - suite('getContentDimensionsExact_', function() { - test('Empty', function() { - var ws = makeMockWs(1, 0, 0, 0, 0); - var metricsManager = new Blockly.MetricsManager(ws); - var defaultZoom = metricsManager.getContentDimensionsExact_(ws); - assertDimensionsMatch(defaultZoom, 0, 0, 0, 0); - }); - test('Empty zoom in', function() { - var ws = makeMockWs(2, 0, 0, 0, 0); - var metricsManager = new Blockly.MetricsManager(ws); - var zoomIn = metricsManager.getContentDimensionsExact_(ws); - assertDimensionsMatch(zoomIn, 0, 0, 0, 0); - }); - test('Empty zoom out', function() { - var ws = makeMockWs(.5, 0, 0, 0, 0); - var metricsManager = new Blockly.MetricsManager(ws); - var zoomOut = metricsManager.getContentDimensionsExact_(ws); - assertDimensionsMatch(zoomOut, 0, 0, 0, 0); - }); - test('Non empty at origin', function() { - var ws = makeMockWs(1, 0, 0, 100, 100); - var metricsManager = new Blockly.MetricsManager(ws); - var defaultZoom = metricsManager.getContentDimensionsExact_(ws); - // Pixel and ws units are the same at default zoom. - assertDimensionsMatch(defaultZoom, 0, 0, 100, 100); - }); - test('Non empty at origin zoom in', function() { - var ws = makeMockWs(2, 0, 0, 100, 100); - var metricsManager = new Blockly.MetricsManager(ws); - var zoomIn = metricsManager.getContentDimensionsExact_(ws); - // 1 ws unit = 2 pixels at this zoom level. - assertDimensionsMatch(zoomIn, 0, 0, 200, 200); - }); - test('Non empty at origin zoom out', function() { - var ws = makeMockWs(.5, 0, 0, 100, 100); - var metricsManager = new Blockly.MetricsManager(ws); - var zoomOut = metricsManager.getContentDimensionsExact_(ws); - // 1 ws unit = 0.5 pixels at this zoom level. - assertDimensionsMatch(zoomOut, 0, 0, 50, 50); - }); - test('Non empty positive origin', function() { - var ws = makeMockWs(1, 10, 10, 100, 100); - var metricsManager = new Blockly.MetricsManager(ws); - var defaultZoom = metricsManager.getContentDimensionsExact_(ws); - // Pixel and ws units are the same at default zoom. - assertDimensionsMatch(defaultZoom, 10, 10, 100, 100); - }); - test('Non empty positive origin zoom in', function() { - var ws = makeMockWs(2, 10, 10, 100, 100); - var metricsManager = new Blockly.MetricsManager(ws); - var zoomIn = metricsManager.getContentDimensionsExact_(ws); - // 1 ws unit = 2 pixels at this zoom level. - assertDimensionsMatch(zoomIn, 20, 20, 200, 200); - }); - test('Non empty positive origin zoom out', function() { - var ws = makeMockWs(.5, 10, 10, 100, 100); - var metricsManager = new Blockly.MetricsManager(ws); - var zoomOut = metricsManager.getContentDimensionsExact_(ws); - // 1 ws unit = 0.5 pixels at this zoom level. - assertDimensionsMatch(zoomOut, 5, 5, 50, 50); - }); - test('Non empty negative origin', function() { - var ws = makeMockWs(1, -10, -10, 100, 100); - var metricsManager = new Blockly.MetricsManager(ws); - var defaultZoom = metricsManager.getContentDimensionsExact_(ws); - // Pixel and ws units are the same at default zoom. - assertDimensionsMatch(defaultZoom, -10, -10, 100, 100); - }); - test('Non empty negative origin zoom in', function() { - var ws = makeMockWs(2, -10, -10, 100, 100); - var metricsManager = new Blockly.MetricsManager(ws); - var zoomIn = metricsManager.getContentDimensionsExact_(ws); - // 1 ws unit = 2 pixels at this zoom level. - assertDimensionsMatch(zoomIn, -20, -20, 200, 200); - }); - test('Non empty negative origin zoom out', function() { - var ws = makeMockWs(.5, -10, -10, 100, 100); - var metricsManager = new Blockly.MetricsManager(ws); - var zoomOut = metricsManager.getContentDimensionsExact_(ws); - // 1 ws unit = 0.5 pixels at this zoom level. - assertDimensionsMatch(zoomOut, -5, -5, 50, 50); - }); - }); - - suite('getContentDimensionsBounded_', function() { - setup(function() { - this.ws = makeMockWs(1, 0, 0, 0, 0); - this.metricsManager = new Blockly.MetricsManager(this.ws); - this.contentDimensionsStub = - sinon.stub(this.metricsManager, 'getContentDimensionsExact_'); - }); - test('Empty workspace', function() { - // The location of the viewport. - var mockViewMetrics = {top: 0, left: 0, width: 200, height: 200}; - // The bounding box around the blocks on the screen. - var mockContentDimensions = {top: 0, left: 0, width: 0, height: 0}; - this.contentDimensionsStub.returns(mockContentDimensions); - - var contentMetrics = - this.metricsManager.getContentDimensionsBounded_(mockViewMetrics); - - // Should add half the view width to all sides. - assertDimensionsMatch(contentMetrics, -200, -200, 400, 400); - }); - test('Non empty workspace', function() { - // The location of the viewport. - var mockViewMetrics = {top: 0, left: 0, width: 200, height: 200}; - // The bounding box around the blocks on the screen. - var mockContentDimensions = {top: 100, left: 100, width: 50, height: 50}; - this.contentDimensionsStub.returns(mockContentDimensions); - - var contentMetrics = - this.metricsManager.getContentDimensionsBounded_(mockViewMetrics); - - // Should add half of the view width to all sides. - assertDimensionsMatch(contentMetrics, -50, -50, 350, 350); - }); - }); - suite('getAbsoluteMetrics', function() { setup(function() { this.ws = makeMockWs(1, 0, 0, 0, 0); @@ -296,54 +178,332 @@ suite('Metrics', function() { }); suite('getContentMetrics', function() { - setup(function() { - this.ws = makeMockWs(1, 0, 0, 0, 0); - this.metricsManager = new Blockly.MetricsManager(this.ws); - this.viewMetricsStub = sinon.stub(this.metricsManager, 'getViewMetrics'); - this.isContentBoundedStub = - sinon.stub(this.metricsManager.workspace_, 'isContentBounded'); - this.getBoundedMetricsStub = - sinon.stub(this.metricsManager, 'getContentDimensionsBounded_'); - this.getExactMetricsStub = - sinon.stub(this.metricsManager, 'getContentDimensionsExact_'); + test('Empty in ws coordinates', function() { + var ws = makeMockWs(1, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(true); + assertDimensionsMatch(contentMetrics, 0, 0, 0, 0); }); - test('Content Dimensions in pixel coordinates bounded ws', function() { - this.isContentBoundedStub.returns(true); - this.getBoundedMetricsStub.returns( - {height: 100, width: 100, left: 100, top: 100}); - - var contentMetrics = this.metricsManager.getContentMetrics(false); - - // Should return what getContentDimensionsBounded_ returns. - assertDimensionsMatch(contentMetrics, 100, 100, 100, 100); - sinon.assert.calledOnce(this.getBoundedMetricsStub); - sinon.assert.calledOnce(this.viewMetricsStub); + test('Empty zoom-in in ws coordinates', function() { + var ws = makeMockWs(2, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(true); + assertDimensionsMatch(contentMetrics, 0, 0, 0, 0); }); - test('Content Dimensions in pixel coordinates exact ws', function() { - this.isContentBoundedStub.returns(false); - this.getExactMetricsStub.returns( - {height: 100, width: 100, left: 100, top: 100}); - - var contentMetrics = this.metricsManager.getContentMetrics(false); - - // Should return what getContentDimensionsExact_ returns. - assertDimensionsMatch(contentMetrics, 100, 100, 100, 100); - sinon.assert.calledOnce(this.getExactMetricsStub); - sinon.assert.notCalled(this.viewMetricsStub); + test('Empty zoom-out in ws coordinates', function() { + var ws = makeMockWs(.5, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(true); + assertDimensionsMatch(contentMetrics, 0, 0, 0, 0); }); - test('Content Dimensions in ws coordinates bounded ws', function() { - var getWorkspaceCoordinates = true; - this.ws.scale = 2; - this.isContentBoundedStub.returns(true); - this.getBoundedMetricsStub.returns( - {height: 100, width: 100, left: 100, top: 100}); + test('Non empty at origin ws coordinates', function() { + var ws = makeMockWs(1, 0, 0, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(true); + assertDimensionsMatch(contentMetrics, 0, 0, 100, 100); + }); + test('Non empty at origin zoom-in ws coordinates', function() { + var ws = makeMockWs(2, 0, 0, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(true); + assertDimensionsMatch(contentMetrics, 0, 0, 100, 100); + }); + test('Non empty at origin zoom-out ws coordinates', function() { + var ws = makeMockWs(.5, 0, 0, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(true); + assertDimensionsMatch(contentMetrics, 0, 0, 100, 100); + }); + test('Non empty positive origin ws coordinates', function() { + var ws = makeMockWs(1, 10, 10, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(true); + assertDimensionsMatch(contentMetrics, 10, 10, 100, 100); + }); + test('Non empty positive origin zoom-in ws coordinates', function() { + var ws = makeMockWs(2, 10, 10, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(true); + // 1 ws unit = 2 pixels at this zoom level. + assertDimensionsMatch(contentMetrics, 10, 10, 100, 100); + }); + test('Non empty positive origin zoom-out ws coordinates', function() { + var ws = makeMockWs(.5, 10, 10, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(true); + // 1 ws unit = 0.5 pixels at this zoom level. + assertDimensionsMatch(contentMetrics, 10, 10, 100, 100); + }); + test('Non empty negative origin ws coordinates', function() { + var ws = makeMockWs(1, -10, -10, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(true); + // Pixel and ws units are the same at default zoom. + assertDimensionsMatch(contentMetrics, -10, -10, 100, 100); + }); + test('Non empty negative origin zoom-in ws coordinates', function() { + var ws = makeMockWs(2, -10, -10, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(true); + assertDimensionsMatch(contentMetrics, -10, -10, 100, 100); + }); + test('Non empty negative origin zoom-out ws coordinates', function() { + var ws = makeMockWs(.5, -10, -10, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(true); + assertDimensionsMatch(contentMetrics, -10, -10, 100, 100); + }); + test('Empty in pixel coordinates', function() { + var ws = makeMockWs(1, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(false); + assertDimensionsMatch(contentMetrics, 0, 0, 0, 0); + }); + test('Empty zoom-in in pixel coordinates', function() { + var ws = makeMockWs(2, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(false); + assertDimensionsMatch(contentMetrics, 0, 0, 0, 0); + }); + test('Empty zoom-out in pixel coordinates', function() { + var ws = makeMockWs(.5, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(false); + assertDimensionsMatch(contentMetrics, 0, 0, 0, 0); + }); + test('Non empty at origin pixel coordinates', function() { + var ws = makeMockWs(1, 0, 0, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(false); + // Pixel and ws units are the same at default zoom. + assertDimensionsMatch(contentMetrics, 0, 0, 100, 100); + }); + test('Non empty at origin zoom-in pixel coordinates', function() { + var ws = makeMockWs(2, 0, 0, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(false); + // 1 ws unit = 2 pixels at this zoom level. + assertDimensionsMatch(contentMetrics, 0, 0, 200, 200); + }); + test('Non empty at origin zoom-out pixel coordinates', function() { + var ws = makeMockWs(.5, 0, 0, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(false); + // 1 ws unit = 0.5 pixels at this zoom level. + assertDimensionsMatch(contentMetrics, 0, 0, 50, 50); + }); + test('Non empty positive origin pixel coordinates', function() { + var ws = makeMockWs(1, 10, 10, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(false); + // Pixel and ws units are the same at default zoom. + assertDimensionsMatch(contentMetrics, 10, 10, 100, 100); + }); + test('Non empty positive origin zoom-in pixel coordinates', function() { + var ws = makeMockWs(2, 10, 10, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(false); + // 1 ws unit = 2 pixels at this zoom level. + assertDimensionsMatch(contentMetrics, 20, 20, 200, 200); + }); + test('Non empty positive origin zoom-out pixel coordinates', function() { + var ws = makeMockWs(.5, 10, 10, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(false); + // 1 ws unit = 0.5 pixels at this zoom level. + assertDimensionsMatch(contentMetrics, 5, 5, 50, 50); + }); + test('Non empty negative origin pixel coordinates', function() { + var ws = makeMockWs(1, -10, -10, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(false); + // Pixel and ws units are the same at default zoom. + assertDimensionsMatch(contentMetrics, -10, -10, 100, 100); + }); + test('Non empty negative origin zoom-in pixel coordinates', function() { + var ws = makeMockWs(2, -10, -10, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(false); + // 1 ws unit = 2 pixels at this zoom level. + assertDimensionsMatch(contentMetrics, -20, -20, 200, 200); + }); + test('Non empty negative origin zoom-out pixel coordinates', function() { + var ws = makeMockWs(.5, -10, -10, 100, 100); + var metricsManager = new Blockly.MetricsManager(ws); + var contentMetrics = metricsManager.getContentMetrics(false); + // 1 ws unit = 0.5 pixels at this zoom level. + assertDimensionsMatch(contentMetrics, -5, -5, 50, 50); + }); + }); + + suite('getScrollMetrics', function() { + test('Empty workspace in ws coordinates', function() { + var ws = makeMockWs(1, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + // The location of the viewport. + var mockViewMetrics = {top: 0, left: 0, width: 200, height: 200}; + // The bounding box around the blocks on the screen. + var mockContentMetrics = {top: 0, left: 0, width: 0, height: 0}; var contentMetrics = - this.metricsManager.getContentMetrics(getWorkspaceCoordinates); + metricsManager.getScrollMetrics(true, mockViewMetrics, mockContentMetrics); - assertDimensionsMatch(contentMetrics, 50, 50, 50, 50); - sinon.assert.calledOnce(this.getBoundedMetricsStub); - sinon.assert.calledOnce(this.viewMetricsStub); + // Should add half the view width to all sides. + assertDimensionsMatch(contentMetrics, -200, -200, 400, 400); + }); + test('Empty workspace zoom-in in ws coordinates', function() { + var ws = makeMockWs(2, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + // The location of the viewport. + var mockViewMetrics = {top: 0, left: 0, width: 200, height: 200}; + // The bounding box around the blocks on the screen. + var mockContentMetrics = {top: 0, left: 0, width: 0, height: 0}; + + var contentMetrics = + metricsManager.getScrollMetrics(true, mockViewMetrics, mockContentMetrics); + + // Should add half the view width to all sides. + assertDimensionsMatch(contentMetrics, -100, -100, 200, 200); + }); + test('Empty workspace zoom-out in ws coordinates', function() { + var ws = makeMockWs(0.5, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + // The location of the viewport. + var mockViewMetrics = {top: 0, left: 0, width: 200, height: 200}; + // The bounding box around the blocks on the screen. + var mockContentMetrics = {top: 0, left: 0, width: 0, height: 0}; + + var contentMetrics = + metricsManager.getScrollMetrics(true, mockViewMetrics, mockContentMetrics); + + // Should add half the view width to all sides. + assertDimensionsMatch(contentMetrics, -400, -400, 800, 800); + }); + test('Non empty workspace in ws coordinates', function() { + var ws = makeMockWs(1, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + // The location of the viewport. + var mockViewMetrics = {top: 0, left: 0, width: 200, height: 200}; + // The bounding box around the blocks on the screen. + var mockContentMetrics = {top: 100, left: 100, width: 50, height: 50}; + + var contentMetrics = + metricsManager.getScrollMetrics(true, mockViewMetrics, mockContentMetrics); + + // Should add half of the view width to all sides. + assertDimensionsMatch(contentMetrics, -50, -50, 350, 350); + }); + test('Non empty workspace zoom-in in ws coordinates', function() { + var ws = makeMockWs(2, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + // The location of the viewport. + var mockViewMetrics = {top: 0, left: 0, width: 200, height: 200}; + // The bounding box around the blocks on the screen. + var mockContentMetrics = {top: 100, left: 100, width: 50, height: 50}; + + var contentMetrics = + metricsManager.getScrollMetrics(true, mockViewMetrics, mockContentMetrics); + + // Should add half of the view width to all sides. + assertDimensionsMatch(contentMetrics, -25, -25, 175, 175); + }); + test('Non empty workspace zoom-out in ws coordinates', function() { + var ws = makeMockWs(0.5, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + // The location of the viewport. + var mockViewMetrics = {top: 0, left: 0, width: 200, height: 200}; + // The bounding box around the blocks on the screen. + var mockContentMetrics = {top: 100, left: 100, width: 50, height: 50}; + + var contentMetrics = + metricsManager.getScrollMetrics(true, mockViewMetrics, mockContentMetrics); + + // Should add half of the view width to all sides. + assertDimensionsMatch(contentMetrics, -100, -100, 700, 700); + }); + test('Empty workspace in pixel coordinates', function() { + var ws = makeMockWs(1, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + // The location of the viewport. + var mockViewMetrics = {top: 0, left: 0, width: 200, height: 200}; + // The bounding box around the blocks on the screen. + var mockContentMetrics = {top: 0, left: 0, width: 0, height: 0}; + + var contentMetrics = + metricsManager.getScrollMetrics(false, mockViewMetrics, mockContentMetrics); + + // Should add half the view width to all sides. + assertDimensionsMatch(contentMetrics, -200, -200, 400, 400); + }); + test('Empty workspace zoom-in in pixel coordinates', function() { + var ws = makeMockWs(2, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + // The location of the viewport. + var mockViewMetrics = {top: 0, left: 0, width: 200, height: 200}; + // The bounding box around the blocks on the screen. + var mockContentMetrics = {top: 0, left: 0, width: 0, height: 0}; + + var contentMetrics = + metricsManager.getScrollMetrics(false, mockViewMetrics, mockContentMetrics); + + // Should add half the view width to all sides. + assertDimensionsMatch(contentMetrics, -200, -200, 400, 400); + }); + test('Empty workspace zoom-out in pixel coordinates', function() { + var ws = makeMockWs(0.5, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + // The location of the viewport. + var mockViewMetrics = {top: 0, left: 0, width: 200, height: 200}; + // The bounding box around the blocks on the screen. + var mockContentMetrics = {top: 0, left: 0, width: 0, height: 0}; + + var contentMetrics = + metricsManager.getScrollMetrics(false, mockViewMetrics, mockContentMetrics); + + // Should add half the view width to all sides. + assertDimensionsMatch(contentMetrics, -200, -200, 400, 400); + }); + test('Non empty workspace in pixel coordinates', function() { + var ws = makeMockWs(1, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + // The location of the viewport. + var mockViewMetrics = {top: 0, left: 0, width: 200, height: 200}; + // The bounding box around the blocks on the screen. + var mockContentMetrics = {top: 100, left: 100, width: 50, height: 50}; + + var contentMetrics = + metricsManager.getScrollMetrics(false, mockViewMetrics, mockContentMetrics); + + // Should add half of the view width to all sides. + assertDimensionsMatch(contentMetrics, -50, -50, 350, 350); + }); + test('Non empty workspace zoom-in in pixel coordinates', function() { + var ws = makeMockWs(2, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + // The location of the viewport. + var mockViewMetrics = {top: 0, left: 0, width: 200, height: 200}; + // The bounding box around the blocks on the screen. + var mockContentMetrics = {top: 100, left: 100, width: 50, height: 50}; + + var contentMetrics = + metricsManager.getScrollMetrics(false, mockViewMetrics, mockContentMetrics); + + // Should add half of the view width to all sides. + assertDimensionsMatch(contentMetrics, -50, -50, 350, 350); + }); + test('Non empty workspace zoom-out in pixel coordinates', function() { + var ws = makeMockWs(0.5, 0, 0, 0, 0); + var metricsManager = new Blockly.MetricsManager(ws); + // The location of the viewport. + var mockViewMetrics = {top: 0, left: 0, width: 200, height: 200}; + // The bounding box around the blocks on the screen. + var mockContentMetrics = {top: 100, left: 100, width: 50, height: 50}; + + var contentMetrics = + metricsManager.getScrollMetrics(false, mockViewMetrics, mockContentMetrics); + + // Should add half of the view width to all sides. + assertDimensionsMatch(contentMetrics, -50, -50, 350, 350); }); }); });