feat: mini workspace bubble (#7096)

* feat: add properly sizing mini workspace bubble

* chore: add properly handling workspace options

* fix: various sizing and option bugs

* fix: code related to dragging

* fix: remove adding flyout change listener

* chore: add docs

* fix: build

* fix: PR comments'

* chore: PR comments
This commit is contained in:
Beka Westberg
2023-05-19 15:36:34 -07:00
committed by GitHub
parent 4d2201a427
commit 83c6c73817
6 changed files with 246 additions and 14 deletions

View File

@@ -18,28 +18,31 @@ import {WorkspaceSvg} from '../workspace_svg.js';
export abstract class Bubble implements IBubble {
/** The width of the border around the bubble. */
static BORDER_WIDTH = 6;
static readonly BORDER_WIDTH = 6;
/** Double the width of the border around the bubble. */
static readonly DOUBLE_BORDER = this.BORDER_WIDTH * 2;
/** The minimum size the bubble can have. */
static MIN_SIZE = this.BORDER_WIDTH * 2;
static readonly MIN_SIZE = this.DOUBLE_BORDER;
/**
* The thickness of the base of the tail in relation to the size of the
* bubble. Higher numbers result in thinner tails.
*/
static TAIL_THICKNESS = 1;
static readonly TAIL_THICKNESS = 1;
/** The number of degrees that the tail bends counter-clockwise. */
static TAIL_ANGLE = 20;
static readonly TAIL_ANGLE = 20;
/**
* The sharpness of the tail's bend. Higher numbers result in smoother
* tails.
*/
static TAIL_BEND = 4;
static readonly TAIL_BEND = 4;
/** Distance between arrow point and anchor point. */
static ANCHOR_RADIUS = 8;
static readonly ANCHOR_RADIUS = 8;
/** The SVG group containing all parts of the bubble. */
private svgRoot: SVGGElement;
@@ -548,11 +551,8 @@ export abstract class Bubble implements IBubble {
}
/**
* Move this bubble during a drag, taking into account whether or not there is
* a drag surface.
* Move this bubble during a drag.
*
* @param dragSurface The surface that carries rendered items during a drag,
* or null if no drag surface is in use.
* @param newLoc The location to translate to, in workspace coordinates.
* @internal
*/

View File

@@ -0,0 +1,224 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {Abstract as AbstractEvent} from '../events/events_abstract.js';
import type {BlocklyOptions} from '../blockly_options.js';
import {Bubble} from './bubble.js';
import {Coordinate} from '../utils/coordinate.js';
import * as dom from '../utils/dom.js';
import {Options} from '../options.js';
import {Svg} from '../utils/svg.js';
import type {Rect} from '../utils/rect.js';
import {Size} from '../utils/size.js';
import type {WorkspaceSvg} from '../workspace_svg.js';
export class MiniWorkspaceBubble extends Bubble {
/**
* The minimum amount of change to the mini workspace view to trigger
* resizing the bubble.
*/
private static readonly MINIMUM_VIEW_CHANGE = 10;
/**
* An arbitrary margin of whitespace to put around the blocks in the
* workspace.
*/
private static readonly MARGIN = Bubble.DOUBLE_BORDER * 3;
/** The root svg element containing the workspace. */
private svgDialog: SVGElement;
/** The workspace that gets shown within this bubble. */
private miniWorkspace: WorkspaceSvg;
/**
* Should this bubble automatically reposition itself when it resizes?
* Becomes false after this bubble is first dragged.
*/
private autoLayout = true;
/** @internal */
constructor(
workspaceOptions: BlocklyOptions,
protected readonly workspace: WorkspaceSvg,
protected anchor: Coordinate,
protected ownerRect?: Rect
) {
super(workspace, anchor, ownerRect);
const options = new Options(workspaceOptions);
this.validateWorkspaceOptions(options);
this.svgDialog = dom.createSvgElement(
Svg.SVG,
{
'x': Bubble.BORDER_WIDTH,
'y': Bubble.BORDER_WIDTH,
},
this.contentContainer
);
workspaceOptions.parentWorkspace = this.workspace;
this.miniWorkspace = this.newWorkspaceSvg(new Options(workspaceOptions));
const background = this.miniWorkspace.createDom('blocklyMutatorBackground');
this.svgDialog.appendChild(background);
if (options.languageTree) {
background.insertBefore(
this.miniWorkspace.addFlyout(Svg.G),
this.miniWorkspace.getCanvas()
);
const flyout = this.miniWorkspace.getFlyout();
flyout?.init(this.miniWorkspace);
flyout?.show(options.languageTree);
}
this.miniWorkspace.addChangeListener(this.updateBubbleSize.bind(this));
this.miniWorkspace
.getFlyout()
?.getWorkspace()
?.addChangeListener(this.updateBubbleSize.bind(this));
this.updateBubbleSize();
}
dispose() {
this.miniWorkspace.dispose();
super.dispose();
}
/** @internal */
getWorkspace(): WorkspaceSvg {
return this.miniWorkspace;
}
/** Adds a change listener to the mini workspace. */
addWorkspaceChangeListener(listener: (e: AbstractEvent) => void) {
this.miniWorkspace.addChangeListener(listener);
}
/**
* Validates the workspace options to make sure folks aren't trying to
* enable things the miniworkspace doesn't support.
*/
private validateWorkspaceOptions(options: Options) {
if (options.hasCategories) {
throw new Error(
'The miniworkspace bubble does not support toolboxes with categories'
);
}
if (options.hasTrashcan) {
throw new Error('The miniworkspace bubble does not support trashcans');
}
if (
options.zoomOptions.controls ||
options.zoomOptions.wheel ||
options.zoomOptions.pinch
) {
throw new Error('The miniworkspace bubble does not support zooming');
}
if (
options.moveOptions.scrollbars ||
options.moveOptions.wheel ||
options.moveOptions.drag
) {
throw new Error(
'The miniworkspace bubble does not scrolling/moving the workspace'
);
}
if (options.horizontalLayout) {
throw new Error(
'The miniworkspace bubble does not support horizontal layouts'
);
}
}
/**
* Updates the size of this bubble to account for the size of the
* mini workspace.
*/
private updateBubbleSize() {
if (this.miniWorkspace.isDragging()) return;
const currSize = this.getSize();
const newSize = this.calculateWorkspaceSize();
if (
Math.abs(currSize.width - newSize.width) <
MiniWorkspaceBubble.MINIMUM_VIEW_CHANGE &&
Math.abs(currSize.height - newSize.height) <
MiniWorkspaceBubble.MINIMUM_VIEW_CHANGE
) {
// Only resize if the size has noticeably changed.
return;
}
this.svgDialog.setAttribute('width', `${newSize.width}px`);
this.svgDialog.setAttribute('height', `${newSize.height}px`);
this.miniWorkspace.setCachedParentSvgSize(newSize.width, newSize.height);
if (this.miniWorkspace.RTL) {
// Scroll the workspace to always left-align.
this.miniWorkspace
.getCanvas()
.setAttribute('transform', `translate(${newSize.width}, 0)`);
}
this.setSize(
new Size(
newSize.width + Bubble.DOUBLE_BORDER,
newSize.height + Bubble.DOUBLE_BORDER
),
this.autoLayout
);
this.miniWorkspace.resize();
this.miniWorkspace.recordDragTargets();
}
/**
* Calculates the size of the mini workspace for use in resizing the bubble.
*/
private calculateWorkspaceSize(): Size {
// TODO (#7104): Clean this up to be more readable and unified for RTL
// vs LTR.
const canvas = this.miniWorkspace.getCanvas();
const workspaceSize = canvas.getBBox();
let width = workspaceSize.width + workspaceSize.x;
let height = workspaceSize.height + MiniWorkspaceBubble.MARGIN;
const flyout = this.miniWorkspace.getFlyout();
if (flyout) {
const flyoutScrollMetrics = flyout
.getWorkspace()
.getMetricsManager()
.getScrollMetrics();
height = Math.max(height, flyoutScrollMetrics.height + 20);
width += flyout.getWidth();
}
if (this.miniWorkspace.RTL) {
width = -workspaceSize.x;
}
width += MiniWorkspaceBubble.MARGIN;
return new Size(width, height);
}
/**
* Move this bubble during a drag.
*
* @param newLoc The location to translate to, in workspace coordinates.
* @internal
*/
moveDuringDrag(newLoc: Coordinate): void {
super.moveDuringDrag(newLoc);
this.autoLayout = false;
}
/** @internal */
moveTo(x: number, y: number): void {
super.moveTo(x, y);
this.miniWorkspace.recordDragTargets();
}
/** @internal */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
newWorkspaceSvg(options: Options): WorkspaceSvg {
throw new Error(
'The implementation of newWorkspaceSvg should be ' +
'monkey-patched in by blockly.ts'
);
}
}