Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add additional logic for auto-scrolling workspace #749

Merged
merged 3 commits into from
Jun 10, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
316 changes: 265 additions & 51 deletions plugins/scroll-options/src/ScrollBlockDragger.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
import * as Blockly from 'blockly/core';
import {AutoScroll} from './AutoScroll';

/**
* @typedef {!Object<string, !Array<!Blockly.utils.Coordinate>>}
*/
let CandidateScrolls;

/**
* A block dragger that adds the functionality for a block to be moved while
* someone is dragging it.
Expand All @@ -34,7 +39,51 @@ export class ScrollBlockDragger extends Blockly.BlockDragger {
* @protected
*/
this.dragDelta_ = new Blockly.utils.Coordinate(0, 0);

// TODO(maribethb): Use `isMoveable` etc. to get this list
/**
* Possible directions the workspace could be scrolled.
* @type {!Array<string>}
* @protected
*/
this.scrollDirections_ = ['top', 'bottom', 'left', 'right'];

/**
* Configuration options for the scroll-options settings.
* @type {!Object<string, number>}
* @protected
*/
this.options_ = {
// Pixels per ms to scroll based on how far the block is from the edge of
// the viewport.
slowBlockSpeed: 0.28,
fastBlockSpeed: 1.4,
// Distance in workspace units that the edge of the block is from the edge
// of the viewport before the corresponding scroll speed will be used. Can
// be negative to start scrolling before the block extends over the edge.
slowBlockStartDistance: 0,
// Must be larger than slowBlockStartDistance.
fastBlockStartDistance: 50,
// If a block takes up this percentage of the viewport or more, it will be
// considered oversized. Rather than using the block edge, we use the
// mouse cursor plus the given margin size to activate block-based
// scrolling.
oversizeBlockThreshold: 0.85,
// A bigger value will cause the workspace to scroll sooner, i.e., the
// mouse can be further inward from the edge when scrolling begins.
oversizeBlockMargin: 15,
// Pixels per ms to scroll based on how far the mouse is from the edge of
// the viewport.
slowMouseSpeed: 0.5,
fastMouseSpeed: 1.6,
// Distance in workspace units that the mouse is from the edge of the
// viewport before the corresponding scroll speed will be used. Can be
// negative to start scrolling before the mouse extends over the edge.
slowMouseStartDistance: 0,
fastMouseStartDistance: 35,
};
}

/**
* Updates the location of the block that is being dragged.
* @param {number} deltaX Horizontal offset in pixel units.
Expand Down Expand Up @@ -86,11 +135,7 @@ export class ScrollBlockDragger extends Blockly.BlockDragger {
super.drag(e, totalDelta);
this.dragDelta_ = currentDragDeltaXY;

// Calculate the location the block is being dragged to, in ws units.
// This same calculation is done in super.drag().
const deltaPx = this.pixelsToWorkspaceUnits_(totalDelta);
const newLoc = Blockly.utils.Coordinate.sum(this.startXY_, deltaPx);
this.scrollWorkspaceWhileDragging(newLoc);
this.scrollWorkspaceWhileDragging_(e);
}

/**
Expand All @@ -99,7 +144,7 @@ export class ScrollBlockDragger extends Blockly.BlockDragger {
* @override
*/
endDrag(e, currentDragDeltaXY) {
// We can not override this method similar to the others because we call
// We cannot override this method similar to the others because we call
// drag here with the passed in value.
// Make sure internal state is fresh.
this.drag(e, currentDragDeltaXY);
Expand Down Expand Up @@ -140,61 +185,52 @@ export class ScrollBlockDragger extends Blockly.BlockDragger {
* The workspace will not resize as the block is dragged. The workspace should
* appear to move out from under the block, i.e., the block should stay under
* the user's mouse.
* @param {!Blockly.utils.Coordinate} newLoc New coordinate the block is being
* dragged to.
* @param {!Event} e The mouse/touch event for the drag.
* @protected
*/
scrollWorkspaceWhileDragging(newLoc) {
const SCROLL_DIRECTION_VECTORS = {
scrollWorkspaceWhileDragging_(e) {
/**
* Unit vector for each direction that could be scrolled. This vector will
* be scaled to get the calculated velocity in each direction. Must be a
* dict because the properties are accessed based on the members of
* `this.scrollDirections_`.
* @dict
* @private
*/
this.SCROLL_DIRECTION_VECTORS_ = {
top: new Blockly.utils.Coordinate(0, 1),
bottom: new Blockly.utils.Coordinate(0, -1),
left: new Blockly.utils.Coordinate(1, 0),
right: new Blockly.utils.Coordinate(-1, 0),
};
// TODO(maribethb): I just made this up, pick a better one
// and make this configurable.
const SCROLL_SPEED = 0.4;
const mouse = Blockly.utils.screenToWsCoordinates(
this.workspace_, new Blockly.utils.Coordinate(e.clientX, e.clientY));

const candidateScrolls = [];
let overallScrollVector = new Blockly.utils.Coordinate(0, 0);
/**
* List of possible scrolls in each direction. This will be modified in
* place. Must be a dict because the properties are accessed based on the
* members of `this.scrollDirections_`.
* @dict
* @type {!CandidateScrolls}
*/
const candidateScrolls = {
top: [],
bottom: [],
left: [],
right: [],
};

// Get ViewMetrics in workspace coordinates.
const metrics = this.workspace_.getMetricsManager().getViewMetrics(true);

// TODO(maribethb): Add fancier logic based on how far out of bounds the
// block is held.

// See Blockly.MetricsManager for more information on the metrics used.
// In particular, it uses workspace coordinates where the top and left
// of the workspace are negative.
// More than one scroll vector may apply, for example if the block is
// dragged to a corner.
if (newLoc.y < metrics.top) {
const scrollVector = SCROLL_DIRECTION_VECTORS['top'].scale(SCROLL_SPEED);
candidateScrolls.push(scrollVector);
}
if (newLoc.y > metrics.top + metrics.height) {
const scrollVector =
SCROLL_DIRECTION_VECTORS['bottom'].scale(SCROLL_SPEED);
candidateScrolls.push(scrollVector);
}
if (newLoc.x < metrics.left) {
const scrollVector = SCROLL_DIRECTION_VECTORS['left'].scale(SCROLL_SPEED);
candidateScrolls.push(scrollVector);
}
if (newLoc.x > metrics.left + metrics.width) {
const scrollVector =
SCROLL_DIRECTION_VECTORS['right'].scale(SCROLL_SPEED);
candidateScrolls.push(scrollVector);
}
const viewMetrics =
this.workspace_.getMetricsManager().getViewMetrics(true);

// Get the overall scroll direction vector (could scroll diagonally).
// Note: code.org reduces down to just one vector per direction from
// all the possible ones they generate. Currently we just have one per
// direction so we don't need to do anything else.
candidateScrolls.forEach(function(scroll) {
overallScrollVector =
Blockly.utils.Coordinate.sum(overallScrollVector, scroll);
});
// Get possible scroll velocities based on the location of both the block
// and the mouse.

this.computeBlockCandidateScrolls_(candidateScrolls, viewMetrics, mouse);
this.computeMouseCandidateScrolls_(candidateScrolls, viewMetrics, mouse);
// Calculate the final scroll vector we should actually use.
const overallScrollVector = this.getOverallScrollVector_(candidateScrolls);

// If the workspace should not be scrolled any longer, cancel the
// autoscroll.
Expand All @@ -204,11 +240,189 @@ export class ScrollBlockDragger extends Blockly.BlockDragger {
return;
}

// Update the autoscroll or start a new one.
this.activeAutoScroll_ =
this.activeAutoScroll_ || new AutoScroll(this.workspace_);
this.activeAutoScroll_.updateProperties(overallScrollVector);
}

/**
* There could be multiple candidate scrolls for each direction, such as one
* for block position and one for mouse position. We should first find the
* fastest scroll in each direction. Then, we sum those to find the overall
* scroll vector.
*
* For example, we may have a fast block scroll and a slow
* mouse scroll candidate in both the top and left directions. First, we
* reduce to only the fast block scroll. Then, we sum the vectors in each
* direction to get a resulting fast scroll in a diagonal direction to the top
* left.
* @param {!CandidateScrolls} candidateScrolls Existing lists of candidate
* scrolls. Will be modified in place.
* @return {!Blockly.utils.Coordinate} Overall scroll vector.
* @protected
*/
getOverallScrollVector_(candidateScrolls) {
let overallScrollVector = new Blockly.utils.Coordinate(0, 0);
for (const direction of this.scrollDirections_) {
const fastestScroll =
candidateScrolls[direction].reduce((fastest, current) => {
if (!fastest) {
return current;
}
return Blockly.utils.Coordinate.magnitude(fastest) >
Blockly.utils.Coordinate.magnitude(current) ?
fastest :
current;
}, new Blockly.utils.Coordinate(0, 0)); // Initial value
overallScrollVector =
Blockly.utils.Coordinate.sum(overallScrollVector, fastestScroll);
}
return overallScrollVector;
}

/**
* Gets the candidate scrolls based on the position of the block on the
* workspace. If the block is near/over the edge, a candidate scroll will be
* added based on the options provided.
*
* This method can be overridden to further customize behavior, e.g. To add a
* third speed option.
* @param {!CandidateScrolls} candidateScrolls Existing list of candidate
* scrolls. Will be modified in place.
* @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics View metrics
* for the workspace.
* @param {!Blockly.utils.Coordinate} mouse Mouse coordinates.
* @protected
*/
computeBlockCandidateScrolls_(candidateScrolls, viewMetrics, mouse) {
const blockOverflows = this.getBlockBoundsOverflows_(viewMetrics, mouse);
for (const direction of this.scrollDirections_) {
const overflow = blockOverflows[direction];
if (overflow > this.options_.slowBlockStartDistance) {
const speed = overflow > this.options_.fastBlockStartDistance ?
this.options_.fastBlockSpeed :
this.options_.slowBlockSpeed;
const scrollVector =
this.SCROLL_DIRECTION_VECTORS_[direction].scale(speed);
candidateScrolls[direction].push(scrollVector);
}
}
}

/**
* Gets the candidate scrolls based on the position of the mouse cursor
* relative to the workspace. If the mouse is near/over the edge, a candidate
* scroll will be added based on the options provided.
*
* This method can be overridden to further customize behavior, e.g. To add a
* third speed option.
* @param {!CandidateScrolls} candidateScrolls Existing list of candidate
* scrolls. Will be modified in place.
* @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics View metrics
* for the workspace.
* @param {!Blockly.utils.Coordinate} mouse Mouse coordinates.
* @protected
*/
computeMouseCandidateScrolls_(candidateScrolls, viewMetrics, mouse) {
const mouseOverflows = this.getMouseOverflows_(viewMetrics, mouse);
for (const direction of this.scrollDirections_) {
const overflow = mouseOverflows[direction];
if (overflow > this.options_.slowMouseStartDistance) {
const speed = overflow > this.options_.fastMouseStartDistance ?
this.options_.fastMouseSpeed :
this.options_.slowMouseSpeed;
const scrollVector =
this.SCROLL_DIRECTION_VECTORS_[direction].scale(speed);
candidateScrolls[direction].push(scrollVector);
}
}
}

/**
* Gets the amount of overflow of a box relative to the workspace viewport.
*
* The value for each direction will be how far the given block edge is from
* the given edge of the viewport. If the block edge is outside the viewport,
* the value will be positive. If the block edge is inside the viewport, the
* value will be negative.
*
* This method also checks for oversized blocks. If the block is very large
* relative to the viewport size, then we will actually use a small zone
* around the cursor, rather than the edge of the block, to calculate the
* overflow values. This calculation is done independently in both the
* horizontal and vertical directions. These values can be configured in the
* options for the plugin.
*
* @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics View metrics
* for the workspace.
* @param {!Blockly.utils.Coordinate} mouse Mouse coordinates.
* @return {!Object<string, number>} An object describing the amount of
* overflow in each direction.
* @protected
*/
getBlockBoundsOverflows_(viewMetrics, mouse) {
const blockBounds = this.draggingBlock_.getBoundingRectangle();

// Handle large blocks. If the block is nearly as tall as the viewport,
// use a margin around the cursor rather than the height of the block.
const blockHeight = blockBounds.bottom - blockBounds.top;
if (blockHeight >
viewMetrics.height * this.options_.oversizeBlockThreshold) {
blockBounds.top = Math.max(
blockBounds.top, mouse.y - this.options_.oversizeBlockMargin);
blockBounds.bottom = Math.min(
blockBounds.bottom, mouse.y + this.options_.oversizeBlockMargin);
}

// Same logic, but for block width.
const blockWidth = blockBounds.right - blockBounds.left;
if (blockWidth > viewMetrics.width * this.options_.oversizeBlockThreshold) {
blockBounds.left = Math.max(
blockBounds.left, mouse.x - this.options_.oversizeBlockMargin);
blockBounds.right = Math.min(
blockBounds.right, mouse.x + this.options_.oversizeBlockMargin);
}

// The coordinate system is negative in the top and left directions, and
// positive in the bottom and right directions. Therefore, the direction of
// the comparison must be switched for bottom and right.
return {
top: viewMetrics.top - blockBounds.top,
bottom: -(viewMetrics.top + viewMetrics.height - blockBounds.bottom),
left: viewMetrics.left - blockBounds.left,
right: -(viewMetrics.left + viewMetrics.width - blockBounds.right),
};
}

/**
* Gets the amount of overflow of the mouse coordinates relative to the
* viewport.
*
* The value for each direction will be how far the pointer is from
* the given edge of the viewport. If the pointer is outside the viewport,
* the value will be positive. If the pointer is inside the viewport, the
* value will be negative.
*
* @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics View metrics
* for the workspace.
* @param {!Blockly.utils.Coordinate} mouse Mouse coordinates.
* @return {!Object<string, number>} An object describing the amount of
* overflow in each direction.
* @protected
*/
getMouseOverflows_(viewMetrics, mouse) {
// The coordinate system is negative in the top and left directions, and
// positive in the bottom and right directions. Therefore, the direction of
// the comparison must be switched for bottom and right.
return {
top: viewMetrics.top - mouse.y,
bottom: -(viewMetrics.top + viewMetrics.height - mouse.y),
left: viewMetrics.left - mouse.x,
right: -(viewMetrics.left + viewMetrics.width - mouse.x),
};
}

/**
* Cancel any AutoScroll. This must be called when there is no need to scroll
* further, e.g., when no longer dragging near the edge of the workspace, or
Expand Down