diff --git a/src/contain/text.ts b/src/contain/text.ts index 6abb2a87..cdafcd6d 100644 --- a/src/contain/text.ts +++ b/src/contain/text.ts @@ -106,7 +106,7 @@ export function measureWidth(fontMeasureInfo: FontMeasureInfo, text: string): nu /** - * + * @deprecated See `getBoundingRect`. * Get bounding rect for inner usage(TSpan) * Which not include text newline. */ @@ -128,6 +128,9 @@ export function innerGetBoundingRect( } /** + * @deprecated Use `(new Text(...)).getBoundingRect()` or `(new TSpan(...)).getBoundingRect()` instead. + * This method behaves differently from `Text#getBoundingRect()` - e.g., it does not support the overflow + * strategy, and only has single line height even if multiple lines. * * Get bounding rect for outer usage. Compatitable with old implementation * Which includes text newline. diff --git a/src/graphic/TSpan.ts b/src/graphic/TSpan.ts index 4b501290..40496b8c 100644 --- a/src/graphic/TSpan.ts +++ b/src/graphic/TSpan.ts @@ -1,10 +1,10 @@ import Displayable, { DisplayableProps, DisplayableStatePropNames } from './Displayable'; -import { getBoundingRect } from '../contain/text'; import BoundingRect from '../core/BoundingRect'; import { PathStyleProps, DEFAULT_PATH_STYLE } from './Path'; import { createObject, defaults } from '../core/util'; -import { FontStyle, FontWeight, TextAlign, TextVerticalAlign } from '../core/types'; +import { FontStyle, FontWeight } from '../core/types'; import { DEFAULT_FONT } from '../core/platform'; +import { tSpanCreateBoundingRect, tSpanHasStroke } from './helper/parseText'; export interface TSpanStyleProps extends PathStyleProps { @@ -53,9 +53,7 @@ class TSpan extends Displayable { style: TSpanStyleProps hasStroke() { - const style = this.style; - const stroke = style.stroke; - return stroke != null && stroke !== 'none' && style.lineWidth > 0; + return tSpanHasStroke(this.style); } hasFill() { @@ -81,31 +79,8 @@ class TSpan extends Displayable { } getBoundingRect(): BoundingRect { - const style = this.style; - if (!this._rect) { - let text = style.text; - text != null ? (text += '') : (text = ''); - - const rect = getBoundingRect( - text, - style.font, - style.textAlign as TextAlign, - style.textBaseline as TextVerticalAlign - ); - - rect.x += style.x || 0; - rect.y += style.y || 0; - - if (this.hasStroke()) { - const w = style.lineWidth; - rect.x -= w / 2; - rect.y -= w / 2; - rect.width += w; - rect.height += w; - } - - this._rect = rect; + this._rect = tSpanCreateBoundingRect(this.style); } return this._rect; diff --git a/src/graphic/Text.ts b/src/graphic/Text.ts index c9512789..6d0e8a59 100644 --- a/src/graphic/Text.ts +++ b/src/graphic/Text.ts @@ -6,7 +6,8 @@ import { TextAlign, TextVerticalAlign, ImageLike, Dictionary, MapToType, FontWeight, FontStyle, NullUndefined } from '../core/types'; import { - parseRichText, parsePlainText, CalcInnerTextOverflowAreaOut, calcInnerTextOverflowArea + parseRichText, parsePlainText, CalcInnerTextOverflowAreaOut, calcInnerTextOverflowArea, + tSpanCreateBoundingRect2, } from './helper/parseText'; import TSpan, { TSpanStyleProps } from './TSpan'; import { retrieve2, each, normalizeCssArray, trim, retrieve3, extend, keys, defaults } from '../core/util'; @@ -332,7 +333,7 @@ class ZRText extends Displayable implements GroupLike { } } - updateTransform() { + updateTransform() { const innerTransformable = this.innerTransformable; if (innerTransformable) { innerTransformable.updateTransform(); @@ -528,7 +529,6 @@ class ZRText extends Displayable implements GroupLike { const outerHeight = contentBlock.outerHeight; const outerWidth = contentBlock.outerWidth; - const contentWidth = contentBlock.contentWidth; const textLines = contentBlock.lines; const lineHeight = contentBlock.lineHeight; @@ -544,6 +544,13 @@ class ZRText extends Displayable implements GroupLike { const boxY = adjustTextY(baseY, outerHeight, verticalAlign); needDrawBg && this._renderBackground(style, style, boxX, boxY, outerWidth, outerHeight); } + // PENDING: + // Should text bounding rect contains style.padding, style.width, style.height when NO background + // and border displayed? It depends on how to define "boundingRect". HTML `getBoundingClientRect` + // contains padding in that case. But currently ZRText does not. + // If implement that, an extra invisible Rect may need to be added as the placeholder for the bounding + // rect computation, considering animation of padding. But will it degrade performance for the most + // used plain texts cases? // `textBaseline` is set as 'middle'. textY += lineHeight / 2; @@ -559,6 +566,7 @@ class ZRText extends Displayable implements GroupLike { } let defaultLineWidth = 0; + let usingDefaultStroke = false; let useDefaultFill = false; const textFill = getFill( 'fill' in style @@ -580,16 +588,12 @@ class ZRText extends Displayable implements GroupLike { // we give the auto lineWidth to display the given stoke color. && (!defaultStyle.autoStroke || useDefaultFill) ) - ? (defaultLineWidth = DEFAULT_STROKE_LINE_WIDTH, defaultStyle.stroke) + ? (defaultLineWidth = DEFAULT_STROKE_LINE_WIDTH, usingDefaultStroke = true, defaultStyle.stroke) : null ); const hasShadow = style.textShadowBlur > 0; - const fixedBoundingRect = style.width != null - && (style.overflow === 'truncate' || style.overflow === 'break' || style.overflow === 'breakAll'); - const calculatedLineHeight = contentBlock.calculatedLineHeight; - for (let i = 0; i < textLines.length; i++) { const el = this._getOrCreateChild(TSpan); // Always create new style. @@ -633,19 +637,27 @@ class ZRText extends Displayable implements GroupLike { textY += lineHeight; - if (fixedBoundingRect) { - el.setBoundingRect(new BoundingRect( - adjustTextX(subElStyle.x, contentWidth, subElStyle.textAlign as TextAlign), - adjustTextY(subElStyle.y, calculatedLineHeight, subElStyle.textBaseline as TextVerticalAlign), - /** - * Text boundary should be the real text width. - * Otherwise, there will be extra space in the - * bounding rect calculated. - */ - contentWidth, - calculatedLineHeight - )); - } + // Always set tspan bounding rect to guarantee the consistency if users lays out based + // on these bounding rects. + el.setBoundingRect(tSpanCreateBoundingRect2( + subElStyle, + contentBlock.contentWidth, + contentBlock.calculatedLineHeight, + // Should text bounding rect includes text stroke width? + // Pros: + // - Intuitively, and by convention, bounding rect of `Path` always includes stroke width. + // Cons: + // - It's unpredictable for users whether "auto stroke" is applied. If stroke width is included + // and multiple texts are laid out based on its bounding rect, the position of texts may vary + // and is unpredictable - especially in limited space (e.g., see echarts pie label cases). + // - "auto stroke" attempts to use the same color as the background to make the border to be + // invisible in most cases, thus it might be more reasonable to be excluded from bounding rect. + // Conclusion: + // - If users specifies style.stroke, it will be included into the bounding rect as normal. + // Otherwise, keep the stroke width as `0` in this case to guarantee consistency of bounding + // rect based layout, regardless of whether "auto stroke" is applied. + usingDefaultStroke ? 0 : null + )); } } @@ -801,6 +813,7 @@ class ZRText extends Displayable implements GroupLike { const defaultStyle = this._defaultStyle; let useDefaultFill = false; let defaultLineWidth = 0; + let usingDefaultStroke = false; const textFill = getFill( 'fill' in tokenStyle ? tokenStyle.fill : 'fill' in style ? style.fill @@ -812,9 +825,9 @@ class ZRText extends Displayable implements GroupLike { : ( !bgColorDrawn && !parentBgColorDrawn - // See the strategy explained above. + // See the strategy explained `_updatePlainTexts`. && (!defaultStyle.autoStroke || useDefaultFill) - ) ? (defaultLineWidth = DEFAULT_STROKE_LINE_WIDTH, defaultStyle.stroke) + ) ? (defaultLineWidth = DEFAULT_STROKE_LINE_WIDTH, usingDefaultStroke = true, defaultStyle.stroke) : null ); @@ -852,14 +865,13 @@ class ZRText extends Displayable implements GroupLike { subElStyle.fill = textFill; } - const textWidth = token.contentWidth; - const textHeight = token.contentHeight; // NOTE: Should not call dirtyStyle after setBoundingRect. Or it will be cleared. - el.setBoundingRect(new BoundingRect( - adjustTextX(subElStyle.x, textWidth, subElStyle.textAlign as TextAlign), - adjustTextY(subElStyle.y, textHeight, subElStyle.textBaseline as TextVerticalAlign), - textWidth, - textHeight + el.setBoundingRect(tSpanCreateBoundingRect2( + subElStyle, + token.contentWidth, + token.contentHeight, + // See the strategy explained `_updatePlainTexts`. + usingDefaultStroke ? 0 : null )); } diff --git a/src/graphic/helper/parseText.ts b/src/graphic/helper/parseText.ts index 22a28ba2..69c63a69 100644 --- a/src/graphic/helper/parseText.ts +++ b/src/graphic/helper/parseText.ts @@ -6,7 +6,8 @@ import { reduce, } from '../../core/util'; import { TextAlign, TextVerticalAlign, ImageLike, Dictionary, NullUndefined } from '../../core/types'; -import { DefaultTextStyle, TextStyleProps } from '../Text'; +import type { DefaultTextStyle, TextStyleProps } from '../Text'; +import type { TSpanStyleProps } from '../TSpan'; import { adjustTextX, adjustTextY, @@ -210,12 +211,12 @@ export interface PlainTextContentBlock { } export function parsePlainText( - text: string, + rawText: unknown, style: Omit, // Exclude props in DefaultTextStyle defaultOuterWidth: number | NullUndefined, defaultOuterHeight: number | NullUndefined ): PlainTextContentBlock { - text != null && (text += ''); + const text = formatText(rawText); // textPadding has been normalized const overflow = style.overflow; @@ -382,7 +383,7 @@ type WrapInfo = { * If styleName is undefined, it is plain text. */ export function parseRichText( - text: string, + rawText: unknown, style: Omit, // Exclude props in DefaultTextStyle defaultOuterWidth: number | NullUndefined, defaultOuterHeight: number | NullUndefined, @@ -390,7 +391,7 @@ export function parseRichText( ): RichTextContentBlock { const contentBlock = new RichTextContentBlock(); - text != null && (text += ''); + const text = formatText(rawText); if (!text) { return contentBlock; } @@ -890,3 +891,61 @@ export type CalcInnerTextOverflowAreaOut = { outerHeight: number | NullUndefined }; +// For backward compatibility, and possibly loose type. +function formatText(text: unknown): string { + return text != null ? (text += '') : (text = ''); +} + +export function tSpanCreateBoundingRect( + style: Pick, +): BoundingRect { + // Should follow the same way as `parsePlainText` to guarantee the consistency of the result. + const text = formatText(style.text); + const font = style.font; + const contentWidth = measureWidth(ensureFontMeasureInfo(font), text); + const contentHeight = getLineHeight(font); + + return tSpanCreateBoundingRect2( + style, + contentWidth, + contentHeight, + null + ); +} + +export function tSpanCreateBoundingRect2( + style: Pick, + contentWidth: number, + contentHeight: number, + forceLineWidth: number | NullUndefined, +): BoundingRect { + const rect = new BoundingRect( + adjustTextX(style.x || 0, contentWidth, style.textAlign as TextAlign), + adjustTextY(style.y || 0, contentHeight, style.textBaseline as TextVerticalAlign), + /** + * Text boundary should be the real text width. + * Otherwise, there will be extra space in the + * bounding rect calculated. + */ + contentWidth, + contentHeight + ); + + const lineWidth = forceLineWidth != null + ? forceLineWidth + : (tSpanHasStroke(style) ? style.lineWidth : 0); + if (lineWidth > 0) { + rect.x -= lineWidth / 2; + rect.y -= lineWidth / 2; + rect.width += lineWidth; + rect.height += lineWidth; + } + + return rect; +} + +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 d2de00fd..9ca145e6 100644 --- a/test/text.html +++ b/test/text.html @@ -8,15 +8,14 @@ -
+
+
-
+