From 0aedc30a4fd193a64314f7f6bd0336144d680eeb Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 15 Oct 2026 14:35:17 -0700 Subject: [PATCH 1/6] fix: Improve rendering of text in response to theme and CSS changes --- .../core/bubbles/mini_workspace_bubble.ts | 3 +- packages/blockly/core/bubbles/text_bubble.ts | 7 ++- packages/blockly/core/field.ts | 1 + packages/blockly/core/flyout_base.ts | 13 +++--- packages/blockly/core/flyout_horizontal.ts | 5 ++- packages/blockly/core/flyout_vertical.ts | 5 ++- .../core/renderers/common/constants.ts | 20 ++++++--- packages/blockly/core/utils/dom.ts | 22 +++++++++- packages/blockly/core/workspace_svg.ts | 44 +++++++++++++++++-- 9 files changed, 98 insertions(+), 22 deletions(-) diff --git a/packages/blockly/core/bubbles/mini_workspace_bubble.ts b/packages/blockly/core/bubbles/mini_workspace_bubble.ts index 89d5cfe4c38..fdbb5ccad08 100644 --- a/packages/blockly/core/bubbles/mini_workspace_bubble.ts +++ b/packages/blockly/core/bubbles/mini_workspace_bubble.ts @@ -191,8 +191,9 @@ 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..dcc870b2d86 100644 --- a/packages/blockly/core/bubbles/text_bubble.ts +++ b/packages/blockly/core/bubbles/text_bubble.ts @@ -101,8 +101,11 @@ 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..b209e66f4fb 100644 --- a/packages/blockly/core/field.ts +++ b/packages/blockly/core/field.ts @@ -974,6 +974,7 @@ export abstract class Field if (this.textElement_) { contentWidth = dom.getTextWidth(this.textElement_); totalWidth += contentWidth; + totalHeight = Math.max(totalHeight, this.textElement_.getBBox().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..c53e4fe71e3 100644 --- a/packages/blockly/core/flyout_horizontal.ts +++ b/packages/blockly/core/flyout_horizontal.ts @@ -265,7 +265,10 @@ export class HorizontalFlyout extends Flyout { 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(); + const deltaX = moveX - origin.left; + const deltaY = cursorY - origin.top; + item.getElement().moveBy(deltaX, deltaY); 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..0c370a28179 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, From c31c11ac8f6b6fb4c2fca42e636378ee67fe268f Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 15 Oct 2026 14:49:33 -0700 Subject: [PATCH 2/6] chore: Make the linter happy --- packages/blockly/core/bubbles/mini_workspace_bubble.ts | 1 + packages/blockly/core/bubbles/text_bubble.ts | 1 + packages/blockly/core/workspace_svg.ts | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/blockly/core/bubbles/mini_workspace_bubble.ts b/packages/blockly/core/bubbles/mini_workspace_bubble.ts index fdbb5ccad08..40ce5ec3ff5 100644 --- a/packages/blockly/core/bubbles/mini_workspace_bubble.ts +++ b/packages/blockly/core/bubbles/mini_workspace_bubble.ts @@ -191,6 +191,7 @@ export class MiniWorkspaceBubble extends Bubble { /** * Updates the size of this bubble to account for the size of the * mini workspace. + * * @internal */ updateBubbleSize() { diff --git a/packages/blockly/core/bubbles/text_bubble.ts b/packages/blockly/core/bubbles/text_bubble.ts index dcc870b2d86..63e0aad98a5 100644 --- a/packages/blockly/core/bubbles/text_bubble.ts +++ b/packages/blockly/core/bubbles/text_bubble.ts @@ -103,6 +103,7 @@ export class TextBubble extends Bubble { /** * Updates the size of this bubble to account for the size of the text. + * * @internal */ updateBubbleSize() { diff --git a/packages/blockly/core/workspace_svg.ts b/packages/blockly/core/workspace_svg.ts index 0c370a28179..a078ae48150 100644 --- a/packages/blockly/core/workspace_svg.ts +++ b/packages/blockly/core/workspace_svg.ts @@ -534,7 +534,7 @@ export class WorkspaceSvg const stylishBlocks: BlockSvg[] = []; this.getAllBlocks(false).forEach((block) => { - if (!!block.getStyleName()) { + if (block.getStyleName()) { stylishBlocks.push(block); } // Clear cached field sizes and rerender the block. From 55a96c2ed9512317bb96c4a22e053157d7e396b9 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 15 Oct 2026 15:14:10 -0700 Subject: [PATCH 3/6] fix: Avoid accidentally breaking tests --- packages/blockly/core/field.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/blockly/core/field.ts b/packages/blockly/core/field.ts index b209e66f4fb..ed4a579b510 100644 --- a/packages/blockly/core/field.ts +++ b/packages/blockly/core/field.ts @@ -974,7 +974,10 @@ export abstract class Field if (this.textElement_) { contentWidth = dom.getTextWidth(this.textElement_); totalWidth += contentWidth; - totalHeight = Math.max(totalHeight, this.textElement_.getBBox().height); + totalHeight = Math.max( + totalHeight, + this.textElement_.getBoundingClientRect().height, + ); } if (this.borderRect_) { totalHeight = Math.max(totalHeight, constants!.FIELD_BORDER_RECT_HEIGHT); From f4b2d0ed488235d5166441eba0e59f5b0deb4e22 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 19 Oct 2026 09:00:31 -0700 Subject: [PATCH 4/6] fix: Fix RTL horizontal flyout layout --- packages/blockly/core/flyout_horizontal.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/blockly/core/flyout_horizontal.ts b/packages/blockly/core/flyout_horizontal.ts index c53e4fe71e3..2bcb5ccc922 100644 --- a/packages/blockly/core/flyout_horizontal.ts +++ b/packages/blockly/core/flyout_horizontal.ts @@ -259,16 +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; const origin = item.getElement().getBoundingRectangle(); - const deltaX = moveX - origin.left; - const deltaY = cursorY - origin.top; - item.getElement().moveBy(deltaX, deltaY); + item.getElement().moveBy(cursorX - origin.left, cursorY - origin.top); cursorX += item.getElement().getBoundingRectangle().getWidth(); } } From f886f1dfbf14fbf7ce7c3fc187d5303524a14fd4 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 19 Oct 2026 10:19:35 -0700 Subject: [PATCH 5/6] refactor: Handle RTL reversal of flyout items once in `setContents()` --- packages/blockly/core/flyout_horizontal.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/blockly/core/flyout_horizontal.ts b/packages/blockly/core/flyout_horizontal.ts index 2bcb5ccc922..6f54f7798a3 100644 --- a/packages/blockly/core/flyout_horizontal.ts +++ b/packages/blockly/core/flyout_horizontal.ts @@ -248,6 +248,20 @@ export class HorizontalFlyout extends Flyout { e.stopPropagation(); } + /** + * Sets the items that will be displayed in this flyout. + * + * @param contents The new items to display in the flyout. + */ + override setContents(contents: FlyoutItem[]): void { + let newContents = [...contents]; + if (this.RTL) { + newContents.reverse(); + } + + super.setContents(newContents); + } + /** * Lay out the blocks in the flyout. * @@ -258,9 +272,6 @@ export class HorizontalFlyout extends Flyout { const margin = this.MARGIN; let cursorX = margin + this.tabWidth_; const cursorY = margin; - if (this.RTL) { - contents = [...contents].reverse(); - } for (const item of contents) { const origin = item.getElement().getBoundingRectangle(); From 44d0eeaf965e9d490b58f04f98152e6b3dec2104 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 19 Oct 2026 10:24:58 -0700 Subject: [PATCH 6/6] chore: Make the linter happy --- packages/blockly/core/flyout_horizontal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/blockly/core/flyout_horizontal.ts b/packages/blockly/core/flyout_horizontal.ts index 6f54f7798a3..767cdf5113b 100644 --- a/packages/blockly/core/flyout_horizontal.ts +++ b/packages/blockly/core/flyout_horizontal.ts @@ -254,7 +254,7 @@ export class HorizontalFlyout extends Flyout { * @param contents The new items to display in the flyout. */ override setContents(contents: FlyoutItem[]): void { - let newContents = [...contents]; + const newContents = [...contents]; if (this.RTL) { newContents.reverse(); }