diff --git a/packages/blockly/core/bubbles/mini_workspace_bubble.ts b/packages/blockly/core/bubbles/mini_workspace_bubble.ts index 89d5cfe4c38..40ce5ec3ff5 100644 --- a/packages/blockly/core/bubbles/mini_workspace_bubble.ts +++ b/packages/blockly/core/bubbles/mini_workspace_bubble.ts @@ -191,8 +191,10 @@ export class MiniWorkspaceBubble extends Bubble { /** * Updates the size of this bubble to account for the size of the * mini workspace. + * + * @internal */ - private updateBubbleSize() { + updateBubbleSize() { if (this.miniWorkspace.isDragging() && !KeyboardMover.mover.isMoving()) { return; } diff --git a/packages/blockly/core/bubbles/text_bubble.ts b/packages/blockly/core/bubbles/text_bubble.ts index 2b243694da9..63e0aad98a5 100644 --- a/packages/blockly/core/bubbles/text_bubble.ts +++ b/packages/blockly/core/bubbles/text_bubble.ts @@ -101,8 +101,12 @@ export class TextBubble extends Bubble { } } - /** Updates the size of this bubble to account for the size of the text. */ - private updateBubbleSize() { + /** + * Updates the size of this bubble to account for the size of the text. + * + * @internal + */ + updateBubbleSize() { const bbox = this.paragraph.getBBox(); this.setSize( new Size( diff --git a/packages/blockly/core/field.ts b/packages/blockly/core/field.ts index 2acd110c5fe..ed4a579b510 100644 --- a/packages/blockly/core/field.ts +++ b/packages/blockly/core/field.ts @@ -974,6 +974,10 @@ export abstract class Field if (this.textElement_) { contentWidth = dom.getTextWidth(this.textElement_); totalWidth += contentWidth; + totalHeight = Math.max( + totalHeight, + this.textElement_.getBoundingClientRect().height, + ); } if (this.borderRect_) { totalHeight = Math.max(totalHeight, constants!.FIELD_BORDER_RECT_HEIGHT); diff --git a/packages/blockly/core/flyout_base.ts b/packages/blockly/core/flyout_base.ts index 98121142368..38aecb0bda3 100644 --- a/packages/blockly/core/flyout_base.ts +++ b/packages/blockly/core/flyout_base.ts @@ -628,13 +628,6 @@ export abstract class Flyout this.setContents(flyoutInfo); - this.layout_(flyoutInfo); - - if (this.horizontalLayout) { - this.height_ = 0; - } else { - this.width_ = 0; - } this.reflow(); this.updateAriaContext(); eventUtils.setRecordUndo(true); @@ -829,6 +822,12 @@ export abstract class Flyout * Reflow flyout contents. */ reflow() { + this.layout_(this.getContents()); + if (this.horizontalLayout) { + this.height_ = 0; + } else { + this.width_ = 0; + } this.inhibitReflowWrapper = true; this.reflowInternal_(); this.inhibitReflowWrapper = false; diff --git a/packages/blockly/core/flyout_horizontal.ts b/packages/blockly/core/flyout_horizontal.ts index abba3e60549..2bcb5ccc922 100644 --- a/packages/blockly/core/flyout_horizontal.ts +++ b/packages/blockly/core/flyout_horizontal.ts @@ -259,13 +259,12 @@ export class HorizontalFlyout extends Flyout { let cursorX = margin + this.tabWidth_; const cursorY = margin; if (this.RTL) { - contents = contents.reverse(); + contents = [...contents].reverse(); } for (const item of contents) { - const rect = item.getElement().getBoundingRectangle(); - const moveX = this.RTL ? cursorX + rect.getWidth() : cursorX; - item.getElement().moveBy(moveX, cursorY); + const origin = item.getElement().getBoundingRectangle(); + item.getElement().moveBy(cursorX - origin.left, cursorY - origin.top); cursorX += item.getElement().getBoundingRectangle().getWidth(); } } diff --git a/packages/blockly/core/flyout_vertical.ts b/packages/blockly/core/flyout_vertical.ts index 97606e804ed..5e55bb88dd1 100644 --- a/packages/blockly/core/flyout_vertical.ts +++ b/packages/blockly/core/flyout_vertical.ts @@ -229,7 +229,10 @@ export class VerticalFlyout extends Flyout { let cursorY = margin; for (const item of contents) { - item.getElement().moveBy(cursorX, cursorY); + const origin = item.getElement().getBoundingRectangle(); + const deltaX = cursorX - origin.left; + const deltaY = cursorY - origin.top; + item.getElement().moveBy(deltaX, deltaY); cursorY += item.getElement().getBoundingRectangle().getHeight(); } } diff --git a/packages/blockly/core/renderers/common/constants.ts b/packages/blockly/core/renderers/common/constants.ts index 01d75d953e1..d824886c6a8 100644 --- a/packages/blockly/core/renderers/common/constants.ts +++ b/packages/blockly/core/renderers/common/constants.ts @@ -116,7 +116,10 @@ export function isNotch(shape: Shape): shape is Notch { ); } -const injectionSites = new Map>(); +const injectionSites = new Map< + string, + WeakMap +>(); /** * An object that provides constants for rendering blocks. @@ -1126,11 +1129,15 @@ export class ConstantProvider { */ protected injectCSS_(root: Document | ShadowRoot, selector: string) { if (typeof window === 'undefined') return; - if (injectionSites.get(selector)?.has(root)) return; - const styleEl = document.createElement('style'); + const styleEl = + injectionSites.get(selector)?.get(root) ?? + document.createElement('style'); + const styles = this.getCSS_(selector).join('\n'); + if (styleEl.textContent === styles) return; + styleEl.className = 'blockly-renderer-style'; - styleEl.textContent = this.getCSS_(selector).join('\n'); + styleEl.textContent = styles; // See css.ts inject() for the rationale on prepending and shadow root // handling. (typeof globalThis.ShadowRoot !== 'undefined' && root instanceof ShadowRoot @@ -1139,8 +1146,9 @@ export class ConstantProvider { ).prepend(styleEl); const sitesForSelector = - injectionSites.get(selector) ?? new WeakSet(); - sitesForSelector.add(root); + injectionSites.get(selector) ?? + new WeakMap(); + sitesForSelector.set(root, styleEl); injectionSites.set(selector, sitesForSelector); } diff --git a/packages/blockly/core/utils/dom.ts b/packages/blockly/core/utils/dom.ts index 82c063ce5a6..323710b2425 100644 --- a/packages/blockly/core/utils/dom.ts +++ b/packages/blockly/core/utils/dom.ts @@ -227,6 +227,8 @@ export function getTextWidth(textElement: SVGTextElement): number { style.fontSize, style.fontWeight, style.fontFamily, + style.letterSpacing, + style.wordSpacing, ); // Cache the computed width and return. @@ -245,6 +247,8 @@ export function getTextWidth(textElement: SVGTextElement): number { * @param fontSize The font size to use. * @param fontWeight The font weight to use. * @param fontFamily The font family to use. + * @param letterSpacing The letter spacing to use. + * @param wordSpacing The word spacing to use. * @returns Width of element. */ export function getFastTextWidth( @@ -252,12 +256,16 @@ export function getFastTextWidth( fontSize: number, fontWeight: string, fontFamily: string, + letterSpacing?: string, + wordSpacing?: string, ): number { return getFastTextWidthWithSizeString( textElement, fontSize + 'pt', fontWeight, fontFamily, + letterSpacing, + wordSpacing, ); } @@ -272,6 +280,8 @@ export function getFastTextWidth( * @param fontSize The font size to use. * @param fontWeight The font weight to use. * @param fontFamily The font family to use. + * @param letterSpacing The letter spacing to use. + * @param wordSpacing The word spacing to use. * @returns Width of element. */ export function getFastTextWidthWithSizeString( @@ -279,9 +289,14 @@ export function getFastTextWidthWithSizeString( fontSize: string, fontWeight: string, fontFamily: string, + letterSpacing?: string, + wordSpacing?: string, ): number { const text = textElement.textContent; - const key = text + '\n' + textElement.className.baseVal; + const key = + text + + '\n' + + `${fontSize}-${fontWeight}-${fontFamily}-${letterSpacing}-${wordSpacing}`; let width; // Return the cached width if it exists. @@ -308,6 +323,11 @@ export function getFastTextWidthWithSizeString( if (text && canvasContext) { // Set the desired font size and family. canvasContext.font = fontWeight + ' ' + fontSize + ' ' + fontFamily; + // Canvas doesn't understand 'normal', so map to 0px in that case (or when not specified) + canvasContext.letterSpacing = + letterSpacing && letterSpacing !== 'normal' ? letterSpacing : '0px'; + canvasContext.wordSpacing = + wordSpacing && wordSpacing !== 'normal' ? wordSpacing : '0px'; width = Math.ceil(canvasContext.measureText(text).width); } else { width = 0; diff --git a/packages/blockly/core/workspace_svg.ts b/packages/blockly/core/workspace_svg.ts index c99575e6289..a078ae48150 100644 --- a/packages/blockly/core/workspace_svg.ts +++ b/packages/blockly/core/workspace_svg.ts @@ -15,6 +15,8 @@ import type {Block} from './block.js'; import type {BlockSvg} from './block_svg.js'; import type {BlocklyOptions} from './blockly_options.js'; import * as browserEvents from './browser_events.js'; +import {MiniWorkspaceBubble} from './bubbles/mini_workspace_bubble.js'; +import {TextBubble} from './bubbles/text_bubble.js'; import {TextInputBubble} from './bubbles/textinput_bubble.js'; import {COMMENT_COLLAPSE_BAR_BUTTON_FOCUS_IDENTIFIER} from './comments/collapse_comment_bar_button.js'; import {COMMENT_EDITOR_FOCUS_IDENTIFIER} from './comments/comment_editor.js'; @@ -37,6 +39,7 @@ import {getFocusManager} from './focus_manager.js'; import {Gesture} from './gesture.js'; import {Grid} from './grid.js'; import * as hints from './hints.js'; +import {IconType} from './icons/icon_types.js'; import {isAutoHideable} from './interfaces/i_autohideable.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import type {IComponent} from './interfaces/i_component.js'; @@ -58,6 +61,7 @@ import {Msg} from './msg.js'; import {Options} from './options.js'; import * as Procedures from './procedures.js'; import * as registry from './registry.js'; +import * as renderManagement from './render_management.js'; import * as blockRendering from './renderers/common/block_rendering.js'; import type {Renderer} from './renderers/common/renderer.js'; import type {ScrollbarPair} from './scrollbar_pair.js'; @@ -527,10 +531,35 @@ export class WorkspaceSvg ); } + const stylishBlocks: BlockSvg[] = []; + + this.getAllBlocks(false).forEach((block) => { + if (block.getStyleName()) { + stylishBlocks.push(block); + } + // Clear cached field sizes and rerender the block. + block.markDirty(); + block.queueRender(); + + const warningIcon = block.getIcon(IconType.WARNING); + if (warningIcon?.bubbleIsVisible()) { + const bubble = warningIcon.getBubble(); + if (bubble instanceof TextBubble) { + bubble.updateBubbleSize(); + } + } + + const mutatorIcon = block.getIcon(IconType.MUTATOR); + if (mutatorIcon?.bubbleIsVisible()) { + const bubble = mutatorIcon.getBubble(); + if (bubble instanceof MiniWorkspaceBubble) { + bubble.updateBubbleSize(); + } + } + }); + // Update all blocks in workspace that have a style name. - this.updateBlockStyles( - this.getAllBlocks(false).filter((block) => !!block.getStyleName()), - ); + this.updateBlockStyles(stylishBlocks); // Update current toolbox selection. this.refreshToolboxSelection(); @@ -543,6 +572,15 @@ export class WorkspaceSvg this.setVisible(true); } + // If this workspace is inside a flyout, reflow the flyout to accommodate + // changes in block dimensions based on theme text size. + if (this.isFlyout && !this.targetWorkspace?.getToolbox()) { + renderManagement.finishQueuedRenders().then(() => { + const flyout = this.targetWorkspace?.getFlyout(); + flyout?.reflow(); + }); + } + const event = new (eventUtils.get(EventType.THEME_CHANGE))( this.getTheme().name, this.id,