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

feat: Add inflaters for flyout labels and buttons. #8593

Merged
merged 5 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
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
63 changes: 63 additions & 0 deletions core/button_flyout_inflater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
gonfunko marked this conversation as resolved.
Show resolved Hide resolved
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import {FlyoutButton} from './flyout_button.js';
import {ButtonOrLabelInfo} from './utils/toolbox.js';
import * as registry from './registry.js';

/**
* Class responsible for creating buttons for flyouts.
*/
export class ButtonFlyoutInflater implements IFlyoutInflater {
/**
* Inflates a flyout button from the given state and adds it to the flyout.
*
* @param state A JSON representation of a flyout button.
* @param flyoutWorkspace The workspace to create the button on.
* @returns A newly created FlyoutButton.
*/
load(state: Object, flyoutWorkspace: WorkspaceSvg): IBoundedElement {
const button = new FlyoutButton(
flyoutWorkspace,
flyoutWorkspace.targetWorkspace!,
state as ButtonOrLabelInfo,
false,
);
button.show();
return button;
}

/**
* Returns the amount of space that should follow this button.
*
* @param state A JSON representation of a flyout button.
* @param defaultGap The default spacing for flyout items.
* @returns The amount of space that should follow this button.
*/
gapForElement(state: Object, defaultGap: number): number {
return defaultGap;
}

/**
* Disposes of the given button.
*
* @param element The flyout button to dispose of.
*/
disposeElement(element: IBoundedElement): void {
if (element instanceof FlyoutButton) {
element.dispose();
}
}
}

registry.register(
registry.Type.FLYOUT_INFLATER,
'button',
ButtonFlyoutInflater,
);
85 changes: 66 additions & 19 deletions core/flyout_button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@ import * as style from './utils/style.js';
import {Svg} from './utils/svg.js';
import type * as toolbox from './utils/toolbox.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import type {IASTNodeLocationSvg} from './blockly.js';
import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js';
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {IRenderedElement} from './interfaces/i_rendered_element.js';
import {Rect} from './utils/rect.js';

/**
* Class for a button or label in the flyout.
*/
export class FlyoutButton implements IASTNodeLocationSvg {
export class FlyoutButton
implements IASTNodeLocationSvg, IBoundedElement, IRenderedElement
{
/** The horizontal margin around the text in the button. */
static TEXT_MARGIN_X = 5;

Expand All @@ -41,7 +46,8 @@ export class FlyoutButton implements IASTNodeLocationSvg {
private readonly cssClass: string | null;

/** Mouse up event data. */
private onMouseUpWrapper: browserEvents.Data | null = null;
private onMouseDownWrapper: browserEvents.Data;
private onMouseUpWrapper: browserEvents.Data;
info: toolbox.ButtonOrLabelInfo;

/** The width of the button's rect. */
Expand All @@ -51,7 +57,7 @@ export class FlyoutButton implements IASTNodeLocationSvg {
height = 0;

/** The root SVG group for the button or label. */
private svgGroup: SVGGElement | null = null;
private svgGroup: SVGGElement;

/** The SVG element with the text of the label or button. */
private svgText: SVGTextElement | null = null;
Expand Down Expand Up @@ -92,14 +98,6 @@ export class FlyoutButton implements IASTNodeLocationSvg {

/** The JSON specifying the label / button. */
this.info = json;
}

/**
* Create the button elements.
*
* @returns The button's SVG group.
*/
createDom(): SVGElement {
let cssClass = this.isFlyoutLabel
? 'blocklyFlyoutLabel'
: 'blocklyFlyoutButton';
Expand Down Expand Up @@ -198,15 +196,24 @@ export class FlyoutButton implements IASTNodeLocationSvg {

this.updateTransform();

// AnyDuringMigration because: Argument of type 'SVGGElement | null' is not
// assignable to parameter of type 'EventTarget'.
this.onMouseDownWrapper = browserEvents.conditionalBind(
this.svgGroup,
'pointerdown',
this,
this.onMouseDown,
);
this.onMouseUpWrapper = browserEvents.conditionalBind(
this.svgGroup as AnyDuringMigration,
this.svgGroup,
'pointerup',
this,
this.onMouseUp,
);
return this.svgGroup!;
}

createDom(): SVGElement {
// No-op, now handled in constructor. Will be removed in followup refactor
// PR that updates the flyout classes to use inflaters.
return this.svgGroup;
}

/** Correctly position the flyout button and make it visible. */
Expand Down Expand Up @@ -235,6 +242,17 @@ export class FlyoutButton implements IASTNodeLocationSvg {
this.updateTransform();
}

/**
* Move the element by a relative offset.
*
* @param dx Horizontal offset in workspace units.
* @param dy Vertical offset in workspace units.
* @param _reason Why is this move happening? 'user', 'bump', 'snap'...
*/
moveBy(dx: number, dy: number, _reason?: string[]) {
this.moveTo(this.position.x + dx, this.position.y + dy);
}

/** @returns Whether or not the button is a label. */
isLabel(): boolean {
return this.isFlyoutLabel;
Expand All @@ -250,6 +268,21 @@ export class FlyoutButton implements IASTNodeLocationSvg {
return this.position;
}

/**
* Returns the coordinates of a bounded element describing the dimensions of
* the element. Coordinate system: workspace coordinates.
*
* @returns Object with coordinates of the bounded element.
*/
getBoundingRectangle() {
return new Rect(
this.position.y,
this.position.y + this.height,
this.position.x,
this.position.x + this.width,
);
}

/** @returns Text of the button. */
getButtonText(): string {
return this.text;
Expand All @@ -275,9 +308,8 @@ export class FlyoutButton implements IASTNodeLocationSvg {

/** Dispose of this button. */
dispose() {
if (this.onMouseUpWrapper) {
browserEvents.unbind(this.onMouseUpWrapper);
}
browserEvents.unbind(this.onMouseDownWrapper);
browserEvents.unbind(this.onMouseUpWrapper);
if (this.svgGroup) {
dom.removeNode(this.svgGroup);
}
Expand Down Expand Up @@ -342,6 +374,21 @@ export class FlyoutButton implements IASTNodeLocationSvg {
}
}
}

private onMouseDown(e: PointerEvent) {
const gesture = this.targetWorkspace.getGesture(e);
const flyout = this.targetWorkspace.getFlyout();
if (gesture && flyout) {
gesture.handleFlyoutStart(e, flyout);
}
}

/**
* @returns The root SVG element of this rendered element.
*/
getSvgRoot() {
return this.svgGroup;
}
}

/** CSS for buttons and labels. See css.js for use. */
Expand Down
59 changes: 59 additions & 0 deletions core/label_flyout_inflater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import {FlyoutButton} from './flyout_button.js';
import {ButtonOrLabelInfo} from './utils/toolbox.js';
import * as registry from './registry.js';

/**
* Class responsible for creating labels for flyouts.
*/
export class LabelFlyoutInflater implements IFlyoutInflater {
/**
* Inflates a flyout label from the given state and adds it to the flyout.
*
* @param state A JSON representation of a flyout label.
* @param flyoutWorkspace The workspace to create the label on.
* @returns A FlyoutButton configured as a label.
*/
load(state: Object, flyoutWorkspace: WorkspaceSvg): IBoundedElement {
const label = new FlyoutButton(
flyoutWorkspace,
flyoutWorkspace.targetWorkspace!,
state as ButtonOrLabelInfo,
true,
);
label.show();
return label;
}

/**
* Returns the amount of space that should follow this label.
*
* @param state A JSON representation of a flyout label.
* @param defaultGap The default spacing for flyout items.
* @returns The amount of space that should follow this label.
*/
gapForElement(state: Object, defaultGap: number): number {
return defaultGap;
}

/**
* Disposes of the given label.
*
* @param element The flyout label to dispose of.
*/
disposeElement(element: IBoundedElement): void {
if (element instanceof FlyoutButton) {
element.dispose();
}
}
}

registry.register(registry.Type.FLYOUT_INFLATER, 'label', LabelFlyoutInflater);