From 613b5dcf4c8c8029c04c0bdacd2bf8c86a2f8e5a Mon Sep 17 00:00:00 2001 From: Utkarsh Patel Date: Sun, 17 May 2026 18:24:07 +0530 Subject: [PATCH] fix(text): improve overflow fit behavior --- src/graphic/Text.ts | 40 ++++++++++++++++++++++++++++----- src/graphic/helper/parseText.ts | 23 +++++++++++++++++-- test/text.html | 39 +++++++++++++++++++++++++++++++- 3 files changed, 94 insertions(+), 8 deletions(-) diff --git a/src/graphic/Text.ts b/src/graphic/Text.ts index 6d0e8a59..e1c7acfa 100644 --- a/src/graphic/Text.ts +++ b/src/graphic/Text.ts @@ -178,7 +178,7 @@ export interface TextStyleProps extends TextStylePropsPart { * truncate: truncate the text and show ellipsis * Do nothing if not set */ - overflow?: 'break' | 'breakAll' | 'truncate' | 'none' + overflow?: 'break' | 'breakAll' | 'truncate' | 'fit' | 'none' /** * Strategy when text lines exceeds textHeight. @@ -635,6 +635,23 @@ class ZRText extends Displayable implements GroupLike { subElStyle.font = textFont; setSeparateFont(subElStyle, style); + // Apply horizontal scaling when overflow is 'fit'. + // The scaleX transform stretches or compresses text glyphs to exactly fill the target width. + if (contentBlock.fitScaleX != null && contentBlock.fitScaleX !== 1) { + el.scaleX = contentBlock.fitScaleX; + el.scaleY = 1; + // Adjust the x-origin so that the text scales from the correct anchor point + // based on textAlign. For 'left' alignment, origin is the text x position. + // For 'center', the origin should be the center; for 'right', the right edge. + el.originX = subElStyle.x; + el.originY = subElStyle.y; + } + else { + // Reset to avoid stale transforms from previous updates + el.scaleX = 1; + el.scaleY = 1; + } + textY += lineHeight; // Always set tspan bounding rect to guarantee the consistency if users lays out based @@ -728,7 +745,7 @@ class ZRText extends Displayable implements GroupLike { leftIndex < tokenCount && (token = tokens[leftIndex], !token.align || token.align === 'left') ) { - this._placeToken(token, style, lineHeight, lineTop, lineXLeft, 'left', bgColorDrawn); + this._placeToken(token, style, lineHeight, lineTop, lineXLeft, 'left', bgColorDrawn, contentBlock.fitScaleX); remainedWidth -= token.width; lineXLeft += token.width; leftIndex++; @@ -738,7 +755,7 @@ class ZRText extends Displayable implements GroupLike { rightIndex >= 0 && (token = tokens[rightIndex], token.align === 'right') ) { - this._placeToken(token, style, lineHeight, lineTop, lineXRight, 'right', bgColorDrawn); + this._placeToken(token, style, lineHeight, lineTop, lineXRight, 'right', bgColorDrawn, contentBlock.fitScaleX); remainedWidth -= token.width; lineXRight -= token.width; rightIndex--; @@ -751,7 +768,7 @@ class ZRText extends Displayable implements GroupLike { // Consider width specified by user, use 'center' rather than 'left'. this._placeToken( token, style, lineHeight, lineTop, - lineXLeft + token.width / 2, 'center', bgColorDrawn + lineXLeft + token.width / 2, 'center', bgColorDrawn, contentBlock.fitScaleX ); lineXLeft += token.width; leftIndex++; @@ -768,7 +785,8 @@ class ZRText extends Displayable implements GroupLike { lineTop: number, x: number, textAlign: string, - parentBgColorDrawn: boolean + parentBgColorDrawn: boolean, + fitScaleX?: number ) { const tokenStyle = style.rich[token.styleName] || {}; tokenStyle.text = token.text; @@ -865,6 +883,18 @@ class ZRText extends Displayable implements GroupLike { subElStyle.fill = textFill; } + // Apply horizontal scaling when overflow is 'fit'. + if (fitScaleX != null && fitScaleX !== 1) { + el.scaleX = fitScaleX; + el.scaleY = 1; + el.originX = subElStyle.x; + el.originY = subElStyle.y; + } + else { + el.scaleX = 1; + el.scaleY = 1; + } + // NOTE: Should not call dirtyStyle after setBoundingRect. Or it will be cleared. el.setBoundingRect(tSpanCreateBoundingRect2( subElStyle, diff --git a/src/graphic/helper/parseText.ts b/src/graphic/helper/parseText.ts index 69c63a69..c442996f 100644 --- a/src/graphic/helper/parseText.ts +++ b/src/graphic/helper/parseText.ts @@ -208,6 +208,10 @@ export interface PlainTextContentBlock { // Be `true` if and only if the result text is modified due to overflow, due to // settings on either `overflow` or `lineOverflow` isTruncated: boolean + + // Horizontal scale factor when overflow is 'fit'. + // Text glyphs will be horizontally scaled by this factor to exactly fill the target width. + fitScaleX?: number } export function parsePlainText( @@ -302,6 +306,13 @@ export function parsePlainText( outerHeight += paddingV; outerWidth += paddingH; + // When overflow is 'fit', compute the horizontal scale factor to stretch/compress + // the text glyphs so they exactly fill the target width. No truncation or wrapping is applied. + let fitScaleX: number; + if (overflow === 'fit' && contentWidth > 0) { + fitScaleX = width / contentWidth; + } + return { lines: lines, height: height, @@ -312,7 +323,8 @@ export function parsePlainText( contentWidth: contentWidth, contentHeight: contentHeight, width: width, - isTruncated: isTruncated + isTruncated: isTruncated, + fitScaleX: fitScaleX }; } @@ -370,6 +382,8 @@ export class RichTextContentBlock { // Be `true` if and only if the result text is modified due to overflow, due to // settings on either `overflow` or `lineOverflow` isTruncated: boolean = false + // Horizontal scale factor when overflow is 'fit'. + fitScaleX?: number } type WrapInfo = { @@ -576,6 +590,12 @@ export function parseRichText( token.width = parseInt(percentWidth, 10) / 100 * contentBlock.width; } + // When overflow is 'fit', compute the horizontal scale factor to stretch/compress + // the text glyphs so they exactly fill the target width. + if (overflow === 'fit' && topWidth != null && calculatedWidth > 0) { + contentBlock.fitScaleX = topWidth / calculatedWidth; + } + return contentBlock; } @@ -948,4 +968,3 @@ export function tSpanHasStroke(style: TSpanStyleProps): boolean { const stroke = style.stroke; return stroke != null && stroke !== 'none' && style.lineWidth > 0; } - diff --git a/test/text.html b/test/text.html index 9ca145e6..3e61732d 100644 --- a/test/text.html +++ b/test/text.html @@ -584,6 +584,43 @@ font: '12px Arial' } }); + + + + + createText({ + x: 100, + y: 1700, + style: { + x: 0, + y: 0, + width: 120, + height: 40, + text: 'overflow: fit - long text example', + overflow: 'fit', + backgroundColor: '#ddd', + fill: '#000', + font: '20px Arial' + } + }); + + createText({ + x: 100, + y: 1780, + style: { + x: 0, + y: 0, + width: 0, + height: 40, + text: 'overflow: fit - zero width', + overflow: 'fit', + backgroundColor: '#fdd', + fill: '#000', + font: '20px Arial' + } + }); + + createText({ x: 100, y: 1500, @@ -653,4 +690,4 @@ - \ No newline at end of file +