/** * @license * Copyright 2019 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Objects representing a single row on a rendered block and all * of its subcomponents. * @author fenichel@google.com (Rachel Fenichel) */ goog.provide('Blockly.blockRendering.BottomRow'); goog.provide('Blockly.blockRendering.InputRow'); goog.provide('Blockly.blockRendering.Row'); goog.provide('Blockly.blockRendering.SpacerRow'); goog.provide('Blockly.blockRendering.TopRow'); goog.require('Blockly.blockRendering.InputConnection'); goog.require('Blockly.blockRendering.InRowSpacer'); goog.require('Blockly.blockRendering.Measurable'); goog.require('Blockly.blockRendering.NextConnection'); goog.require('Blockly.blockRendering.PreviousConnection'); goog.require('Blockly.blockRendering.Types'); goog.require('Blockly.utils.object'); goog.requireType('Blockly.blockRendering.ConstantProvider'); goog.requireType('Blockly.BlockSvg'); /** * An object representing a single row on a rendered block and all of its * subcomponents. * @param {!Blockly.blockRendering.ConstantProvider} constants The rendering * constants provider. * @package * @constructor */ Blockly.blockRendering.Row = function(constants) { /** * The type of this rendering object. * @package * @type {number} */ this.type = Blockly.blockRendering.Types.ROW; /** * An array of elements contained in this row. * @package * @type {!Array} */ this.elements = []; /** * The height of the row. * @package * @type {number} */ this.height = 0; /** * The width of the row, from the left edge of the block to the right. * Does not include child blocks unless they are inline. * @package * @type {number} */ this.width = 0; /** * The minimum height of the row. * @package * @type {number} */ this.minHeight = 0; /** * The minimum width of the row, from the left edge of the block to the right. * Does not include child blocks unless they are inline. * @package * @type {number} */ this.minWidth = 0; /** * The width of the row, from the left edge of the block to the edge of the * block or any connected child blocks. * @package * @type {number} */ this.widthWithConnectedBlocks = 0; /** * The Y position of the row relative to the origin of the block's svg group. * @package * @type {number} */ this.yPos = 0; /** * The X position of the row relative to the origin of the block's svg group. * @package * @type {number} */ this.xPos = 0; /** * Whether the row has any external inputs. * @package * @type {boolean} */ this.hasExternalInput = false; /** * Whether the row has any statement inputs. * @package * @type {boolean} */ this.hasStatement = false; /** * Whether the row has any inline inputs. * @package * @type {boolean} */ this.hasInlineInput = false; /** * Whether the row has any dummy inputs. * @package * @type {boolean} */ this.hasDummyInput = false; /** * Whether the row has a jagged edge. * @package * @type {boolean} */ this.hasJaggedEdge = false; /** * The renderer's constant provider. * @type {!Blockly.blockRendering.ConstantProvider} * @protected */ this.constants_ = constants; this.notchOffset = this.constants_.NOTCH_OFFSET_LEFT; /** * Alignment of the row. * @package * @type {?number} */ this.align = null; }; /** * Inspect all subcomponents and populate all size properties on the row. * @package */ Blockly.blockRendering.Row.prototype.measure = function() { throw Error('Unexpected attempt to measure a base Row.'); }; /** * Get the last input on this row, if it has one. * @return {Blockly.blockRendering.InputConnection} The last input on the row, * or null. * @package */ // TODO: Consider moving this to InputRow, if possible. Blockly.blockRendering.Row.prototype.getLastInput = function() { for (var i = this.elements.length - 1, elem; (elem = this.elements[i]); i--) { if (Blockly.blockRendering.Types.isInput(elem)) { return /** @type {Blockly.blockRendering.InputConnection} */ (elem); } } return null; }; /** * Determines whether this row should start with an element spacer. * @return {boolean} Whether the row should start with a spacer. * @package */ Blockly.blockRendering.Row.prototype.startsWithElemSpacer = function() { return true; }; /** * Determines whether this row should end with an element spacer. * @return {boolean} Whether the row should end with a spacer. * @package */ Blockly.blockRendering.Row.prototype.endsWithElemSpacer = function() { return true; }; /** * Convenience method to get the first spacer element on this row. * @return {Blockly.blockRendering.InRowSpacer} The first spacer element on * this row. * @package */ Blockly.blockRendering.Row.prototype.getFirstSpacer = function() { for (var i = 0, elem; (elem = this.elements[i]); i++) { if (Blockly.blockRendering.Types.isSpacer(elem)) { return /** @type {Blockly.blockRendering.InRowSpacer} */ (elem); } } return null; }; /** * Convenience method to get the last spacer element on this row. * @return {Blockly.blockRendering.InRowSpacer} The last spacer element on * this row. * @package */ Blockly.blockRendering.Row.prototype.getLastSpacer = function() { for (var i = this.elements.length - 1, elem; (elem = this.elements[i]); i--) { if (Blockly.blockRendering.Types.isSpacer(elem)) { return /** @type {Blockly.blockRendering.InRowSpacer} */ (elem); } } return null; }; /** * An object containing information about what elements are in the top row of a * block as well as sizing information for the top row. * Elements in a top row can consist of corners, hats, spacers, and previous * connections. * After this constructor is called, the row will contain all non-spacer * elements it needs. * @param {!Blockly.blockRendering.ConstantProvider} constants The rendering * constants provider. * @package * @constructor * @extends {Blockly.blockRendering.Row} */ Blockly.blockRendering.TopRow = function(constants) { Blockly.blockRendering.TopRow.superClass_.constructor.call(this, constants); this.type |= Blockly.blockRendering.Types.TOP_ROW; /** * The starting point for drawing the row, in the y direction. * This allows us to draw hats and similar shapes that don't start at the * origin. Must be non-negative (see #2820). * @package * @type {number} */ this.capline = 0; /** * How much the row extends up above its capline. * @type {number} */ this.ascenderHeight = 0; /** * Whether the block has a previous connection. * @package * @type {boolean} */ this.hasPreviousConnection = false; /** * The previous connection on the block, if any. * @type {Blockly.blockRendering.PreviousConnection} */ this.connection = null; }; Blockly.utils.object.inherits(Blockly.blockRendering.TopRow, Blockly.blockRendering.Row); /** * Returns whether or not the top row has a left square corner. * @param {!Blockly.BlockSvg} block The block whose top row this represents. * @return {boolean} Whether or not the top row has a left square corner. */ Blockly.blockRendering.TopRow.prototype.hasLeftSquareCorner = function(block) { var hasHat = (block.hat ? block.hat === 'cap' : this.constants_.ADD_START_HATS) && !block.outputConnection && !block.previousConnection; var prevBlock = block.getPreviousBlock(); return !!block.outputConnection || hasHat || (prevBlock ? prevBlock.getNextBlock() == block : false); }; /** * Returns whether or not the top row has a right square corner. * @param {!Blockly.BlockSvg} _block The block whose top row this represents. * @return {boolean} Whether or not the top row has a right square corner. */ Blockly.blockRendering.TopRow.prototype.hasRightSquareCorner = function( _block) { return true; }; /** * @override */ Blockly.blockRendering.TopRow.prototype.measure = function() { var height = 0; var width = 0; var ascenderHeight = 0; for (var e = 0, elem; (elem = this.elements[e]); e++) { width += elem.width; if (!(Blockly.blockRendering.Types.isSpacer(elem))) { if (Blockly.blockRendering.Types.isHat(elem)) { ascenderHeight = Math.max(ascenderHeight, elem.ascenderHeight); } else { height = Math.max(height, elem.height); } } } this.width = Math.max(this.minWidth, width); this.height = Math.max(this.minHeight, height) + ascenderHeight; this.ascenderHeight = ascenderHeight; this.capline = this.ascenderHeight; this.widthWithConnectedBlocks = this.width; }; /** * @override */ Blockly.blockRendering.TopRow.prototype.startsWithElemSpacer = function() { return false; }; /** * @override */ Blockly.blockRendering.TopRow.prototype.endsWithElemSpacer = function() { return false; }; /** * An object containing information about what elements are in the bottom row of * a block as well as spacing information for the top row. * Elements in a bottom row can consist of corners, spacers and next * connections. * @param {!Blockly.blockRendering.ConstantProvider} constants The rendering * constants provider. * @package * @constructor * @extends {Blockly.blockRendering.Row} */ Blockly.blockRendering.BottomRow = function(constants) { Blockly.blockRendering.BottomRow.superClass_.constructor.call(this, constants); this.type |= Blockly.blockRendering.Types.BOTTOM_ROW; /** * Whether this row has a next connection. * @package * @type {boolean} */ this.hasNextConnection = false; /** * The next connection on the row, if any. * @package * @type {Blockly.blockRendering.NextConnection} */ this.connection = null; /** * The amount that the bottom of the block extends below the horizontal edge, * e.g. because of a next connection. Must be non-negative (see #2820). * @package * @type {number} */ this.descenderHeight = 0; /** * The Y position of the bottom edge of the block, relative to the origin * of the block rendering. * @type {number} */ this.baseline = 0; }; Blockly.utils.object.inherits(Blockly.blockRendering.BottomRow, Blockly.blockRendering.Row); /** * Returns whether or not the bottom row has a left square corner. * @param {!Blockly.BlockSvg} block The block whose bottom row this represents. * @return {boolean} Whether or not the bottom row has a left square corner. */ Blockly.blockRendering.BottomRow.prototype.hasLeftSquareCorner = function( block) { return !!block.outputConnection || !!block.getNextBlock(); }; /** * Returns whether or not the bottom row has a right square corner. * @param {!Blockly.BlockSvg} _block The block whose bottom row this represents. * @return {boolean} Whether or not the bottom row has a right square corner. */ Blockly.blockRendering.BottomRow.prototype.hasRightSquareCorner = function( _block) { return true; }; /** * @override */ Blockly.blockRendering.BottomRow.prototype.measure = function() { var height = 0; var width = 0; var descenderHeight = 0; for (var e = 0, elem; (elem = this.elements[e]); e++) { width += elem.width; if (!(Blockly.blockRendering.Types.isSpacer(elem))) { // Note: this assumes that next connections have *only* descenderHeight, // with no height above the baseline. if (Blockly.blockRendering.Types.isNextConnection(elem)) { descenderHeight = Math.max(descenderHeight, elem.height); } else { height = Math.max(height, elem.height); } } } this.width = Math.max(this.minWidth, width); this.height = Math.max(this.minHeight, height) + descenderHeight; this.descenderHeight = descenderHeight; this.widthWithConnectedBlocks = this.width; }; /** * @override */ Blockly.blockRendering.BottomRow.prototype.startsWithElemSpacer = function() { return false; }; /** * @override */ Blockly.blockRendering.BottomRow.prototype.endsWithElemSpacer = function() { return false; }; /** * An object containing information about a spacer between two rows. * @param {!Blockly.blockRendering.ConstantProvider} constants The rendering * constants provider. * @param {number} height The height of the spacer. * @param {number} width The width of the spacer. * @package * @constructor * @extends {Blockly.blockRendering.Row} */ Blockly.blockRendering.SpacerRow = function(constants, height, width) { Blockly.blockRendering.SpacerRow.superClass_.constructor.call(this, constants); this.type |= Blockly.blockRendering.Types.SPACER | Blockly.blockRendering.Types.BETWEEN_ROW_SPACER; this.width = width; this.height = height; this.followsStatement = false; this.widthWithConnectedBlocks = 0; this.elements = [ new Blockly.blockRendering.InRowSpacer(this.constants_, width)]; }; Blockly.utils.object.inherits(Blockly.blockRendering.SpacerRow, Blockly.blockRendering.Row); /** * @override */ Blockly.blockRendering.SpacerRow.prototype.measure = function() { // NOP. Width and height were set at creation. }; /** * An object containing information about a row that holds one or more inputs. * @param {!Blockly.blockRendering.ConstantProvider} constants The rendering * constants provider. * @package * @constructor * @extends {Blockly.blockRendering.Row} */ Blockly.blockRendering.InputRow = function(constants) { Blockly.blockRendering.InputRow.superClass_.constructor.call(this, constants); this.type |= Blockly.blockRendering.Types.INPUT_ROW; /** * The total width of all blocks connected to this row. * @type {number} * @package */ this.connectedBlockWidths = 0; }; Blockly.utils.object.inherits(Blockly.blockRendering.InputRow, Blockly.blockRendering.Row); /** * Inspect all subcomponents and populate all size properties on the row. * @package */ Blockly.blockRendering.InputRow.prototype.measure = function() { this.width = this.minWidth; this.height = this.minHeight; var connectedBlockWidths = 0; for (var e = 0, elem; (elem = this.elements[e]); e++) { this.width += elem.width; if (Blockly.blockRendering.Types.isInput(elem)) { if (Blockly.blockRendering.Types.isStatementInput(elem)) { connectedBlockWidths += elem.connectedBlockWidth; } else if (Blockly.blockRendering.Types.isExternalInput(elem) && elem.connectedBlockWidth != 0) { connectedBlockWidths += (elem.connectedBlockWidth - elem.connectionWidth); } } if (!(Blockly.blockRendering.Types.isSpacer(elem))) { this.height = Math.max(this.height, elem.height); } } this.connectedBlockWidths = connectedBlockWidths; this.widthWithConnectedBlocks = this.width + connectedBlockWidths; }; /** * @override */ Blockly.blockRendering.InputRow.prototype.endsWithElemSpacer = function() { return !this.hasExternalInput && !this.hasStatement; };