diff --git a/blocks_vertical/procedures.js b/blocks_vertical/procedures.js index d0da81bff2..7fb54c9477 100644 --- a/blocks_vertical/procedures.js +++ b/blocks_vertical/procedures.js @@ -777,7 +777,11 @@ Blockly.Blocks["procedures_definition"] = { name: "custom_block", }, ], - extensions: ["colours_more", "shape_hat", "procedure_def_contextmenu"], + extensions: [ + "colours_more", + "shape_bowler_hat", + "procedure_def_contextmenu", + ], }); }, }; diff --git a/blocks_vertical/vertical_extensions.js b/blocks_vertical/vertical_extensions.js index 992623f5aa..89a046b9d3 100644 --- a/blocks_vertical/vertical_extensions.js +++ b/blocks_vertical/vertical_extensions.js @@ -96,6 +96,19 @@ VerticalExtensions.SHAPE_STATEMENT = function () { VerticalExtensions.SHAPE_HAT = function () { this.setInputsInline(true); this.setNextStatement(true, null); + this.hat = "cap"; +}; + +/** + * Extension to make a block be shaped as a bowler hat block, with rounded + * corners on both sides and no indentation for statement blocks. + * @this {Blockly.Block} + * @readonly + */ +VerticalExtensions.SHAPE_BOWLER_HAT = function () { + this.setInputsInline(true); + this.setNextStatement(true, null); + this.hat = "bowler"; }; /** @@ -259,6 +272,10 @@ VerticalExtensions.registerAll = function () { VerticalExtensions.SHAPE_STATEMENT ); Blockly.Extensions.register("shape_hat", VerticalExtensions.SHAPE_HAT); + Blockly.Extensions.register( + "shape_bowler_hat", + VerticalExtensions.SHAPE_BOWLER_HAT + ); Blockly.Extensions.register("shape_end", VerticalExtensions.SHAPE_END); // Output shapes and types are related. diff --git a/src/index.js b/src/index.js index 51001a69c6..4fe9ec8b27 100644 --- a/src/index.js +++ b/src/index.js @@ -26,6 +26,7 @@ import * as scratchBlocksUtils from "../core/scratch_blocks_utils.js"; import * as ScratchVariables from "./variables.js"; import "../core/css.js"; import "../core/field_vertical_separator.js"; +import "./renderer/renderer.js"; import { ContinuousToolbox, ContinuousFlyout, @@ -66,6 +67,8 @@ export { ScratchVariables }; export function inject(container, options) { Object.assign(options, { + renderer: "scratch", + theme: "zelos", plugins: { toolbox: ScratchContinuousToolbox, flyoutsVerticalToolbox: CheckableContinuousFlyout, diff --git a/src/renderer/bowler_hat.js b/src/renderer/bowler_hat.js new file mode 100644 index 0000000000..ba435cfd70 --- /dev/null +++ b/src/renderer/bowler_hat.js @@ -0,0 +1,17 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Blockly from "blockly/core"; + +export class BowlerHat extends Blockly.blockRendering.Hat { + constructor(constants) { + super(constants); + // Calculated dynamically by computeBounds_(). + this.width = 0; + this.height = 20; + this.ascenderHeight = this.height; + } +} diff --git a/src/renderer/drawer.js b/src/renderer/drawer.js new file mode 100644 index 0000000000..90b3cf3ff4 --- /dev/null +++ b/src/renderer/drawer.js @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Blockly from "blockly/core"; + +export class Drawer extends Blockly.zelos.Drawer { + drawStatementInput_(row) { + if (this.info_.isBowlerHatBlock()) { + // Bowler hat blocks have straight sides with no C-shape/indentation for + // statement blocks. + this.drawRightSideRow_(row); + this.positionStatementInputConnection_(row); + } else { + super.drawStatementInput_(row); + } + } + + drawRightSideRow_(row) { + if ( + this.info_.isBowlerHatBlock() && + Blockly.blockRendering.Types.isSpacer(row) + ) { + // Multi-row bowler hat blocks are not supported, this may need + // adjustment to do so. + Blockly.blockRendering.Drawer.prototype.drawRightSideRow_.call(this, row); + } else { + super.drawRightSideRow_(row); + } + } + + drawTop_() { + super.drawTop_(); + // This is a horrible hack, but the superclass' implementation of drawTop_() + // provides no way to cleanly override a hat's path without copying and + // pasting the entire implementation here. We know that there will only be + // one hat on a block, and its path is a known constant, so we just find and + // replace it with the desired bowler hat path here. + // If https://github.com/google/blockly/issues/7292 is resolved, this should + // be revisited. + if (this.info_.isBowlerHatBlock()) { + const capHatPath = this.constants_.START_HAT.path; + const bowlerHatPath = `a20,20 0 0,1 20,-20 l ${ + this.info_.width - 40 + } 0 a20,20 0 0,1 20,20`; + this.outlinePath_ = this.outlinePath_.replace(capHatPath, bowlerHatPath); + } + } +} diff --git a/src/renderer/render_info.js b/src/renderer/render_info.js new file mode 100644 index 0000000000..6ec561e6a6 --- /dev/null +++ b/src/renderer/render_info.js @@ -0,0 +1,89 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Blockly from "blockly/core"; +import { BowlerHat } from "./bowler_hat.js"; + +export class RenderInfo extends Blockly.zelos.RenderInfo { + populateTopRow_() { + if (this.isBowlerHatBlock()) { + const bowlerHat = new BowlerHat(this.constants_); + this.topRow.elements.push( + new Blockly.blockRendering.SquareCorner(this.constants_) + ); + this.topRow.elements.push(bowlerHat); + this.topRow.elements.push( + new Blockly.blockRendering.SquareCorner(this.constants_) + ); + this.topRow.minHeight = 0; + this.topRow.capline = bowlerHat.ascenderHeight; + } else { + super.populateTopRow_(); + } + } + + populateBottomRow_() { + super.populateBottomRow_(); + if (this.isBowlerHatBlock()) { + this.bottomRow.minHeight = this.constants_.MEDIUM_PADDING; + } + } + + computeBounds_() { + super.computeBounds_(); + if (this.isBowlerHatBlock()) { + // Resize the render info to the same width as the widest part of a + // bowler hat block. + const statementRow = this.rows.find((r) => r.hasStatement); + this.width = + statementRow.widthWithConnectedBlocks - + statementRow.elements.find((e) => + Blockly.blockRendering.Types.isInput(e) + ).width + + this.constants_.MEDIUM_PADDING; + + // The bowler hat's width is the same as the block's width, so it can't + // be derived from the constants like a normal hat and has to be set here. + const hat = this.topRow.elements.find((e) => + Blockly.blockRendering.Types.isHat(e) + ); + hat.width = this.width; + this.topRow.measure(true); + } + } + + getInRowSpacing_(prev, next) { + if ( + this.isBowlerHatBlock() && + ((prev && Blockly.blockRendering.Types.isHat(prev)) || + (next && Blockly.blockRendering.Types.isHat(next))) + ) { + // Bowler hat rows have no spacing/gaps, just the hat. + return 0; + } + + return super.getInRowSpacing_(prev, next); + } + + getSpacerRowHeight_(prev, next) { + if (this.isBowlerHatBlock() && prev === this.topRow) { + return 0; + } + + return super.getSpacerRowHeight_(prev, next); + } + + getElemCenterline_(row, elem) { + if (this.isBowlerHatBlock() && Blockly.blockRendering.Types.isField(elem)) { + return row.yPos + elem.height; + } + return super.getElemCenterline_(row, elem); + } + + isBowlerHatBlock() { + return this.block_.hat === "bowler"; + } +} diff --git a/src/renderer/renderer.js b/src/renderer/renderer.js new file mode 100644 index 0000000000..53f6a775bd --- /dev/null +++ b/src/renderer/renderer.js @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Blockly from "blockly/core"; +import { Drawer } from "./drawer.js"; +import { RenderInfo } from "./render_info.js"; + +export class ScratchRenderer extends Blockly.zelos.Renderer { + makeDrawer_(block, info) { + return new Drawer(block, info); + } + + makeRenderInfo_(block) { + return new RenderInfo(this, block); + } +} + +Blockly.blockRendering.register("scratch", ScratchRenderer);