| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039 | 
							- /**
 
-  * RichText is a container that manages complex text label.
 
-  * It will parse text string and create sub displayble elements respectively.
 
-  */
 
- import { TextAlign, TextVerticalAlign, ImageLike, Dictionary, MapToType, FontWeight, FontStyle } from '../core/types';
 
- import { parseRichText, parsePlainText } from './helper/parseText';
 
- import TSpan, { TSpanStyleProps } from './TSpan';
 
- import { retrieve2, each, normalizeCssArray, trim, retrieve3, extend, keys, defaults } from '../core/util';
 
- import { adjustTextX, adjustTextY } from '../contain/text';
 
- import ZRImage from './Image';
 
- import Rect from './shape/Rect';
 
- import BoundingRect from '../core/BoundingRect';
 
- import { MatrixArray } from '../core/matrix';
 
- import Displayable, {
 
-     DisplayableStatePropNames,
 
-     DisplayableProps,
 
-     DEFAULT_COMMON_ANIMATION_PROPS
 
- } from './Displayable';
 
- import { ZRenderType } from '../zrender';
 
- import Animator from '../animation/Animator';
 
- import Transformable from '../core/Transformable';
 
- import { ElementCommonState } from '../Element';
 
- import { GroupLike } from './Group';
 
- import { DEFAULT_FONT, DEFAULT_FONT_SIZE } from '../core/platform';
 
- type TextContentBlock = ReturnType<typeof parseRichText>
 
- type TextLine = TextContentBlock['lines'][0]
 
- type TextToken = TextLine['tokens'][0]
 
- // TODO Default value?
 
- export interface TextStylePropsPart {
 
-     // TODO Text is assigned inside zrender
 
-     text?: string
 
-     fill?: string
 
-     stroke?: string
 
-     strokeNoScale?: boolean
 
-     opacity?: number
 
-     fillOpacity?: number
 
-     strokeOpacity?: number
 
-     /**
 
-      * textStroke may be set as some color as a default
 
-      * value in upper applicaion, where the default value
 
-      * of lineWidth should be 0 to make sure that
 
-      * user can choose to do not use text stroke.
 
-      */
 
-     lineWidth?: number
 
-     lineDash?: false | number[]
 
-     lineDashOffset?: number
 
-     borderDash?: false | number[]
 
-     borderDashOffset?: number
 
-     /**
 
-      * If `fontSize` or `fontFamily` exists, `font` will be reset by
 
-      * `fontSize`, `fontStyle`, `fontWeight`, `fontFamily`.
 
-      * So do not visit it directly in upper application (like echarts),
 
-      * but use `contain/text#makeFont` instead.
 
-      */
 
-     font?: string
 
-     /**
 
-      * The same as font. Use font please.
 
-      * @deprecated
 
-      */
 
-     textFont?: string
 
-     /**
 
-      * It helps merging respectively, rather than parsing an entire font string.
 
-      */
 
-     fontStyle?: FontStyle
 
-     /**
 
-      * It helps merging respectively, rather than parsing an entire font string.
 
-      */
 
-     fontWeight?: FontWeight
 
-     /**
 
-      * It helps merging respectively, rather than parsing an entire font string.
 
-      */
 
-     fontFamily?: string
 
-     /**
 
-      * It helps merging respectively, rather than parsing an entire font string.
 
-      * Should be 12 but not '12px'.
 
-      */
 
-     fontSize?: number | string
 
-     align?: TextAlign
 
-     verticalAlign?: TextVerticalAlign
 
-     /**
 
-      * Line height. Default to be text height of '国'
 
-      */
 
-     lineHeight?: number
 
-     /**
 
-      * Width of text block. Not include padding
 
-      * Used for background, truncate, wrap
 
-      */
 
-     width?: number | string
 
-     /**
 
-      * Height of text block. Not include padding
 
-      * Used for background, truncate
 
-      */
 
-     height?: number
 
-     /**
 
-      * Reserved for special functinality, like 'hr'.
 
-      */
 
-     tag?: string
 
-     textShadowColor?: string
 
-     textShadowBlur?: number
 
-     textShadowOffsetX?: number
 
-     textShadowOffsetY?: number
 
-     // Shadow, background, border of text box.
 
-     backgroundColor?: string | {
 
-         image: ImageLike | string
 
-     }
 
-     /**
 
-      * Can be `2` or `[2, 4]` or `[2, 3, 4, 5]`
 
-      */
 
-     padding?: number | number[]
 
-     /**
 
-      * Margin of label. Used when layouting the label.
 
-      */
 
-     margin?: number
 
-     borderColor?: string
 
-     borderWidth?: number
 
-     borderRadius?: number | number[]
 
-     /**
 
-      * Shadow color for background box.
 
-      */
 
-     shadowColor?: string
 
-     /**
 
-      * Shadow blur for background box.
 
-      */
 
-     shadowBlur?: number
 
-     /**
 
-      * Shadow offset x for background box.
 
-      */
 
-     shadowOffsetX?: number
 
-     /**
 
-      * Shadow offset y for background box.
 
-      */
 
-     shadowOffsetY?: number
 
- }
 
- export interface TextStyleProps extends TextStylePropsPart {
 
-     text?: string
 
-     x?: number
 
-     y?: number
 
-     /**
 
-      * Only support number in the top block.
 
-      */
 
-     width?: number
 
-     /**
 
-      * Text styles for rich text.
 
-      */
 
-     rich?: Dictionary<TextStylePropsPart>
 
-     /**
 
-      * Strategy when calculated text width exceeds textWidth.
 
-      * break: break by word
 
-      * break: will break inside the word
 
-      * truncate: truncate the text and show ellipsis
 
-      * Do nothing if not set
 
-      */
 
-     overflow?: 'break' | 'breakAll' | 'truncate' | 'none'
 
-     /**
 
-      * Strategy when text lines exceeds textHeight.
 
-      * Do nothing if not set
 
-      */
 
-     lineOverflow?: 'truncate'
 
-     /**
 
-      * Epllipsis used if text is truncated
 
-      */
 
-     ellipsis?: string
 
-     /**
 
-      * Placeholder used if text is truncated to empty
 
-      */
 
-     placeholder?: string
 
-     /**
 
-      * Min characters for truncating
 
-      */
 
-     truncateMinChar?: number
 
- }
 
- export interface TextProps extends DisplayableProps {
 
-     style?: TextStyleProps
 
-     zlevel?: number
 
-     z?: number
 
-     z2?: number
 
-     culling?: boolean
 
-     cursor?: string
 
- }
 
- export type TextState = Pick<TextProps, DisplayableStatePropNames> & ElementCommonState
 
- export type DefaultTextStyle = Pick<TextStyleProps, 'fill' | 'stroke' | 'align' | 'verticalAlign'> & {
 
-     autoStroke?: boolean
 
- };
 
- const DEFAULT_RICH_TEXT_COLOR = {
 
-     fill: '#000'
 
- };
 
- const DEFAULT_STROKE_LINE_WIDTH = 2;
 
- // const DEFAULT_TEXT_STYLE: TextStyleProps = {
 
- //     x: 0,
 
- //     y: 0,
 
- //     fill: '#000',
 
- //     stroke: null,
 
- //     opacity: 0,
 
- //     fillOpacity:
 
- // }
 
- export const DEFAULT_TEXT_ANIMATION_PROPS: MapToType<TextProps, boolean> = {
 
-     style: defaults<MapToType<TextStyleProps, boolean>, MapToType<TextStyleProps, boolean>>({
 
-         fill: true,
 
-         stroke: true,
 
-         fillOpacity: true,
 
-         strokeOpacity: true,
 
-         lineWidth: true,
 
-         fontSize: true,
 
-         lineHeight: true,
 
-         width: true,
 
-         height: true,
 
-         textShadowColor: true,
 
-         textShadowBlur: true,
 
-         textShadowOffsetX: true,
 
-         textShadowOffsetY: true,
 
-         backgroundColor: true,
 
-         padding: true,  // TODO needs normalize padding before animate
 
-         borderColor: true,
 
-         borderWidth: true,
 
-         borderRadius: true  // TODO needs normalize radius before animate
 
-     }, DEFAULT_COMMON_ANIMATION_PROPS.style)
 
-  };
 
- interface ZRText {
 
-     animate(key?: '', loop?: boolean): Animator<this>
 
-     animate(key: 'style', loop?: boolean): Animator<this['style']>
 
-     getState(stateName: string): TextState
 
-     ensureState(stateName: string): TextState
 
-     states: Dictionary<TextState>
 
-     stateProxy: (stateName: string) => TextState
 
- }
 
- class ZRText extends Displayable<TextProps> implements GroupLike {
 
-     type = 'text'
 
-     style: TextStyleProps
 
-     /**
 
-      * How to handling label overlap
 
-      *
 
-      * hidden:
 
-      */
 
-     overlap: 'hidden' | 'show' | 'blur'
 
-     /**
 
-      * Will use this to calculate transform matrix
 
-      * instead of Element itseelf if it's give.
 
-      * Not exposed to developers
 
-      */
 
-     innerTransformable: Transformable
 
-     private _children: (ZRImage | Rect | TSpan)[] = []
 
-     private _childCursor: 0
 
-     private _defaultStyle: DefaultTextStyle = DEFAULT_RICH_TEXT_COLOR
 
-     constructor(opts?: TextProps) {
 
-         super();
 
-         this.attr(opts);
 
-     }
 
-     childrenRef() {
 
-         return this._children;
 
-     }
 
-     update() {
 
-         super.update();
 
-         // Update children
 
-         if (this.styleChanged()) {
 
-             this._updateSubTexts();
 
-         }
 
-         for (let i = 0; i < this._children.length; i++) {
 
-             const child = this._children[i];
 
-             // Set common properties.
 
-             child.zlevel = this.zlevel;
 
-             child.z = this.z;
 
-             child.z2 = this.z2;
 
-             child.culling = this.culling;
 
-             child.cursor = this.cursor;
 
-             child.invisible = this.invisible;
 
-         }
 
-     }
 
-      updateTransform() {
 
-         const innerTransformable = this.innerTransformable;
 
-         if (innerTransformable) {
 
-             innerTransformable.updateTransform();
 
-             if (innerTransformable.transform) {
 
-                 this.transform = innerTransformable.transform;
 
-             }
 
-         }
 
-         else {
 
-             super.updateTransform();
 
-         }
 
-     }
 
-     getLocalTransform(m?: MatrixArray): MatrixArray {
 
-         const innerTransformable = this.innerTransformable;
 
-         return innerTransformable
 
-             ? innerTransformable.getLocalTransform(m)
 
-             : super.getLocalTransform(m);
 
-     }
 
-     // TODO override setLocalTransform?
 
-     getComputedTransform() {
 
-         if (this.__hostTarget) {
 
-             // Update host target transform
 
-             this.__hostTarget.getComputedTransform();
 
-             // Update text position.
 
-             this.__hostTarget.updateInnerText(true);
 
-         }
 
-         return super.getComputedTransform();
 
-     }
 
-     private _updateSubTexts() {
 
-         // Reset child visit cursor
 
-         this._childCursor = 0;
 
-         normalizeTextStyle(this.style);
 
-         this.style.rich
 
-             ? this._updateRichTexts()
 
-             : this._updatePlainTexts();
 
-         this._children.length = this._childCursor;
 
-         this.styleUpdated();
 
-     }
 
-     addSelfToZr(zr: ZRenderType) {
 
-         super.addSelfToZr(zr);
 
-         for (let i = 0; i < this._children.length; i++) {
 
-             // Also need mount __zr for case like hover detection.
 
-             // The case: hover on a label (position: 'top') causes host el
 
-             // scaled and label Y position lifts a bit so that out of the
 
-             // pointer, then mouse move should be able to trigger "mouseout".
 
-             this._children[i].__zr = zr;
 
-         }
 
-     }
 
-     removeSelfFromZr(zr: ZRenderType) {
 
-         super.removeSelfFromZr(zr);
 
-         for (let i = 0; i < this._children.length; i++) {
 
-             this._children[i].__zr = null;
 
-         }
 
-     }
 
-     getBoundingRect(): BoundingRect {
 
-         if (this.styleChanged()) {
 
-             this._updateSubTexts();
 
-         }
 
-         if (!this._rect) {
 
-             // TODO: Optimize when using width and overflow: wrap/truncate
 
-             const tmpRect = new BoundingRect(0, 0, 0, 0);
 
-             const children = this._children;
 
-             const tmpMat: MatrixArray = [];
 
-             let rect = null;
 
-             for (let i = 0; i < children.length; i++) {
 
-                 const child = children[i];
 
-                 const childRect = child.getBoundingRect();
 
-                 const transform = child.getLocalTransform(tmpMat);
 
-                 if (transform) {
 
-                     tmpRect.copy(childRect);
 
-                     tmpRect.applyTransform(transform);
 
-                     rect = rect || tmpRect.clone();
 
-                     rect.union(tmpRect);
 
-                 }
 
-                 else {
 
-                     rect = rect || childRect.clone();
 
-                     rect.union(childRect);
 
-                 }
 
-             }
 
-             this._rect = rect || tmpRect;
 
-         }
 
-         return this._rect;
 
-     }
 
-     // Can be set in Element. To calculate text fill automatically when textContent is inside element
 
-     setDefaultTextStyle(defaultTextStyle: DefaultTextStyle) {
 
-         // Use builtin if defaultTextStyle is not given.
 
-         this._defaultStyle = defaultTextStyle || DEFAULT_RICH_TEXT_COLOR;
 
-     }
 
-     setTextContent(textContent: never) {
 
-         if (process.env.NODE_ENV !== 'production') {
 
-             throw new Error('Can\'t attach text on another text');
 
-         }
 
-     }
 
-     // getDefaultStyleValue<T extends keyof TextStyleProps>(key: T): TextStyleProps[T] {
 
-     //     // Default value is on the prototype.
 
-     //     return this.style.prototype[key];
 
-     // }
 
-     protected _mergeStyle(targetStyle: TextStyleProps, sourceStyle: TextStyleProps) {
 
-         if (!sourceStyle) {
 
-             return targetStyle;
 
-         }
 
-         // DO deep merge on rich configurations.
 
-         const sourceRich = sourceStyle.rich;
 
-         const targetRich = targetStyle.rich || (sourceRich && {});  // Create a new one if source have rich but target don't
 
-         extend(targetStyle, sourceStyle);
 
-         if (sourceRich && targetRich) {
 
-             // merge rich and assign rich again.
 
-             this._mergeRich(targetRich, sourceRich);
 
-             targetStyle.rich = targetRich;
 
-         }
 
-         else if (targetRich) {
 
-             // If source rich not exists. DON'T override the target rich
 
-             targetStyle.rich = targetRich;
 
-         }
 
-         return targetStyle;
 
-     }
 
-     private _mergeRich(targetRich: TextStyleProps['rich'], sourceRich: TextStyleProps['rich']) {
 
-         const richNames = keys(sourceRich);
 
-         // Merge by rich names.
 
-         for (let i = 0; i < richNames.length; i++) {
 
-             const richName = richNames[i];
 
-             targetRich[richName] = targetRich[richName] || {};
 
-             extend(targetRich[richName], sourceRich[richName]);
 
-         }
 
-     }
 
-     getAnimationStyleProps() {
 
-         return DEFAULT_TEXT_ANIMATION_PROPS;
 
-     }
 
-     private _getOrCreateChild(Ctor: {new(): TSpan}): TSpan
 
-     private _getOrCreateChild(Ctor: {new(): ZRImage}): ZRImage
 
-     private _getOrCreateChild(Ctor: {new(): Rect}): Rect
 
-     private _getOrCreateChild(Ctor: {new(): TSpan | Rect | ZRImage}): TSpan | Rect | ZRImage {
 
-         let child = this._children[this._childCursor];
 
-         if (!child || !(child instanceof Ctor)) {
 
-             child = new Ctor();
 
-         }
 
-         this._children[this._childCursor++] = child;
 
-         child.__zr = this.__zr;
 
-         // TODO to users parent can only be group.
 
-         child.parent = this as any;
 
-         return child;
 
-     }
 
-     private _updatePlainTexts() {
 
-         const style = this.style;
 
-         const textFont = style.font || DEFAULT_FONT;
 
-         const textPadding = style.padding as number[];
 
-         const text = getStyleText(style);
 
-         const contentBlock = parsePlainText(text, style);
 
-         const needDrawBg = needDrawBackground(style);
 
-         const bgColorDrawn = !!(style.backgroundColor);
 
-         const outerHeight = contentBlock.outerHeight;
 
-         const outerWidth = contentBlock.outerWidth;
 
-         const contentWidth = contentBlock.contentWidth;
 
-         const textLines = contentBlock.lines;
 
-         const lineHeight = contentBlock.lineHeight;
 
-         const defaultStyle = this._defaultStyle;
 
-         const baseX = style.x || 0;
 
-         const baseY = style.y || 0;
 
-         const textAlign = style.align || defaultStyle.align || 'left';
 
-         const verticalAlign = style.verticalAlign || defaultStyle.verticalAlign || 'top';
 
-         let textX = baseX;
 
-         let textY = adjustTextY(baseY, contentBlock.contentHeight, verticalAlign);
 
-         if (needDrawBg || textPadding) {
 
-             // Consider performance, do not call getTextWidth util necessary.
 
-             const boxX = adjustTextX(baseX, outerWidth, textAlign);
 
-             const boxY = adjustTextY(baseY, outerHeight, verticalAlign);
 
-             needDrawBg && this._renderBackground(style, style, boxX, boxY, outerWidth, outerHeight);
 
-         }
 
-         // `textBaseline` is set as 'middle'.
 
-         textY += lineHeight / 2;
 
-         if (textPadding) {
 
-             textX = getTextXForPadding(baseX, textAlign, textPadding);
 
-             if (verticalAlign === 'top') {
 
-                 textY += textPadding[0];
 
-             }
 
-             else if (verticalAlign === 'bottom') {
 
-                 textY -= textPadding[2];
 
-             }
 
-         }
 
-         let defaultLineWidth = 0;
 
-         let useDefaultFill = false;
 
-         const textFill = getFill(
 
-             'fill' in style
 
-                 ? style.fill
 
-                 : (useDefaultFill = true, defaultStyle.fill)
 
-         );
 
-         const textStroke = getStroke(
 
-             'stroke' in style
 
-                 ? style.stroke
 
-                 : (!bgColorDrawn
 
-                     // If we use "auto lineWidth" widely, it probably bring about some bad case.
 
-                     // So the current strategy is:
 
-                     // If `style.fill` is specified (i.e., `useDefaultFill` is `false`)
 
-                     // (A) And if `textConfig.insideStroke/outsideStroke` is not specified as a color
 
-                     //   (i.e., `defaultStyle.autoStroke` is `true`), we do not actually display
 
-                     //   the auto stroke because we can not make sure wether the stoke is approperiate to
 
-                     //   the given `fill`.
 
-                     // (B) But if `textConfig.insideStroke/outsideStroke` is specified as a color,
 
-                     // we give the auto lineWidth to display the given stoke color.
 
-                     && (!defaultStyle.autoStroke || useDefaultFill)
 
-                 )
 
-                 ? (defaultLineWidth = DEFAULT_STROKE_LINE_WIDTH, 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.
 
-             const subElStyle: TSpanStyleProps = el.createStyle();
 
-             el.useStyle(subElStyle);
 
-             subElStyle.text = textLines[i];
 
-             subElStyle.x = textX;
 
-             subElStyle.y = textY;
 
-             // Always set textAlign and textBase line, because it is difficute to calculate
 
-             // textAlign from prevEl, and we dont sure whether textAlign will be reset if
 
-             // font set happened.
 
-             if (textAlign) {
 
-                 subElStyle.textAlign = textAlign;
 
-             }
 
-             // Force baseline to be "middle". Otherwise, if using "top", the
 
-             // text will offset downward a little bit in font "Microsoft YaHei".
 
-             subElStyle.textBaseline = 'middle';
 
-             subElStyle.opacity = style.opacity;
 
-             // Fill after stroke so the outline will not cover the main part.
 
-             subElStyle.strokeFirst = true;
 
-             if (hasShadow) {
 
-                 subElStyle.shadowBlur = style.textShadowBlur || 0;
 
-                 subElStyle.shadowColor = style.textShadowColor || 'transparent';
 
-                 subElStyle.shadowOffsetX = style.textShadowOffsetX || 0;
 
-                 subElStyle.shadowOffsetY = style.textShadowOffsetY || 0;
 
-             }
 
-             // Always override default fill and stroke value.
 
-             subElStyle.stroke = textStroke as string;
 
-             subElStyle.fill = textFill as string;
 
-             if (textStroke) {
 
-                 subElStyle.lineWidth = style.lineWidth || defaultLineWidth;
 
-                 subElStyle.lineDash = style.lineDash;
 
-                 subElStyle.lineDashOffset = style.lineDashOffset || 0;
 
-             }
 
-             subElStyle.font = textFont;
 
-             setSeparateFont(subElStyle, style);
 
-             textY += lineHeight;
 
-             if (fixedBoundingRect) {
 
-                 el.setBoundingRect(new BoundingRect(
 
-                     adjustTextX(subElStyle.x, style.width, 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
 
-                 ));
 
-             }
 
-         }
 
-     }
 
-     private _updateRichTexts() {
 
-         const style = this.style;
 
-         // TODO Only parse when text changed?
 
-         const text = getStyleText(style);
 
-         const contentBlock = parseRichText(text, style);
 
-         const contentWidth = contentBlock.width;
 
-         const outerWidth = contentBlock.outerWidth;
 
-         const outerHeight = contentBlock.outerHeight;
 
-         const textPadding = style.padding as number[];
 
-         const baseX = style.x || 0;
 
-         const baseY = style.y || 0;
 
-         const defaultStyle = this._defaultStyle;
 
-         const textAlign = style.align || defaultStyle.align;
 
-         const verticalAlign = style.verticalAlign || defaultStyle.verticalAlign;
 
-         const boxX = adjustTextX(baseX, outerWidth, textAlign);
 
-         const boxY = adjustTextY(baseY, outerHeight, verticalAlign);
 
-         let xLeft = boxX;
 
-         let lineTop = boxY;
 
-         if (textPadding) {
 
-             xLeft += textPadding[3];
 
-             lineTop += textPadding[0];
 
-         }
 
-         let xRight = xLeft + contentWidth;
 
-         if (needDrawBackground(style)) {
 
-             this._renderBackground(style, style, boxX, boxY, outerWidth, outerHeight);
 
-         }
 
-         const bgColorDrawn = !!(style.backgroundColor);
 
-         for (let i = 0; i < contentBlock.lines.length; i++) {
 
-             const line = contentBlock.lines[i];
 
-             const tokens = line.tokens;
 
-             const tokenCount = tokens.length;
 
-             const lineHeight = line.lineHeight;
 
-             let remainedWidth = line.width;
 
-             let leftIndex = 0;
 
-             let lineXLeft = xLeft;
 
-             let lineXRight = xRight;
 
-             let rightIndex = tokenCount - 1;
 
-             let token;
 
-             while (
 
-                 leftIndex < tokenCount
 
-                 && (token = tokens[leftIndex], !token.align || token.align === 'left')
 
-             ) {
 
-                 this._placeToken(token, style, lineHeight, lineTop, lineXLeft, 'left', bgColorDrawn);
 
-                 remainedWidth -= token.width;
 
-                 lineXLeft += token.width;
 
-                 leftIndex++;
 
-             }
 
-             while (
 
-                 rightIndex >= 0
 
-                 && (token = tokens[rightIndex], token.align === 'right')
 
-             ) {
 
-                 this._placeToken(token, style, lineHeight, lineTop, lineXRight, 'right', bgColorDrawn);
 
-                 remainedWidth -= token.width;
 
-                 lineXRight -= token.width;
 
-                 rightIndex--;
 
-             }
 
-             // The other tokens are placed as textAlign 'center' if there is enough space.
 
-             lineXLeft += (contentWidth - (lineXLeft - xLeft) - (xRight - lineXRight) - remainedWidth) / 2;
 
-             while (leftIndex <= rightIndex) {
 
-                 token = tokens[leftIndex];
 
-                 // 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;
 
-                 leftIndex++;
 
-             }
 
-             lineTop += lineHeight;
 
-         }
 
-     }
 
-     private _placeToken(
 
-         token: TextToken,
 
-         style: TextStyleProps,
 
-         lineHeight: number,
 
-         lineTop: number,
 
-         x: number,
 
-         textAlign: string,
 
-         parentBgColorDrawn: boolean
 
-     ) {
 
-         const tokenStyle = style.rich[token.styleName] || {};
 
-         tokenStyle.text = token.text;
 
-         // 'ctx.textBaseline' is always set as 'middle', for sake of
 
-         // the bias of "Microsoft YaHei".
 
-         const verticalAlign = token.verticalAlign;
 
-         let y = lineTop + lineHeight / 2;
 
-         if (verticalAlign === 'top') {
 
-             y = lineTop + token.height / 2;
 
-         }
 
-         else if (verticalAlign === 'bottom') {
 
-             y = lineTop + lineHeight - token.height / 2;
 
-         }
 
-         const needDrawBg = !token.isLineHolder && needDrawBackground(tokenStyle);
 
-         needDrawBg && this._renderBackground(
 
-             tokenStyle,
 
-             style,
 
-             textAlign === 'right'
 
-                 ? x - token.width
 
-                 : textAlign === 'center'
 
-                 ? x - token.width / 2
 
-                 : x,
 
-             y - token.height / 2,
 
-             token.width,
 
-             token.height
 
-         );
 
-         const bgColorDrawn = !!tokenStyle.backgroundColor;
 
-         const textPadding = token.textPadding;
 
-         if (textPadding) {
 
-             x = getTextXForPadding(x, textAlign, textPadding);
 
-             y -= token.height / 2 - textPadding[0] - token.innerHeight / 2;
 
-         }
 
-         const el = this._getOrCreateChild(TSpan);
 
-         const subElStyle: TSpanStyleProps = el.createStyle();
 
-         // Always create new style.
 
-         el.useStyle(subElStyle);
 
-         const defaultStyle = this._defaultStyle;
 
-         let useDefaultFill = false;
 
-         let defaultLineWidth = 0;
 
-         const textFill = getFill(
 
-             'fill' in tokenStyle ? tokenStyle.fill
 
-                 : 'fill' in style ? style.fill
 
-                 : (useDefaultFill = true, defaultStyle.fill)
 
-         );
 
-         const textStroke = getStroke(
 
-             'stroke' in tokenStyle ? tokenStyle.stroke
 
-                 : 'stroke' in style ? style.stroke
 
-                 : (
 
-                     !bgColorDrawn
 
-                     && !parentBgColorDrawn
 
-                     // See the strategy explained above.
 
-                     && (!defaultStyle.autoStroke || useDefaultFill)
 
-                 ) ? (defaultLineWidth = DEFAULT_STROKE_LINE_WIDTH, defaultStyle.stroke)
 
-                 : null
 
-         );
 
-         const hasShadow = tokenStyle.textShadowBlur > 0
 
-             || style.textShadowBlur > 0;
 
-         subElStyle.text = token.text;
 
-         subElStyle.x = x;
 
-         subElStyle.y = y;
 
-         if (hasShadow) {
 
-             subElStyle.shadowBlur = tokenStyle.textShadowBlur || style.textShadowBlur || 0;
 
-             subElStyle.shadowColor = tokenStyle.textShadowColor || style.textShadowColor || 'transparent';
 
-             subElStyle.shadowOffsetX = tokenStyle.textShadowOffsetX || style.textShadowOffsetX || 0;
 
-             subElStyle.shadowOffsetY = tokenStyle.textShadowOffsetY || style.textShadowOffsetY || 0;
 
-         }
 
-         subElStyle.textAlign = textAlign as CanvasTextAlign;
 
-         // Force baseline to be "middle". Otherwise, if using "top", the
 
-         // text will offset downward a little bit in font "Microsoft YaHei".
 
-         subElStyle.textBaseline = 'middle';
 
-         subElStyle.font = token.font || DEFAULT_FONT;
 
-         subElStyle.opacity = retrieve3(tokenStyle.opacity, style.opacity, 1);
 
-         // TODO inherit each item from top style in token style?
 
-         setSeparateFont(subElStyle, tokenStyle);
 
-         if (textStroke) {
 
-             subElStyle.lineWidth = retrieve3(tokenStyle.lineWidth, style.lineWidth, defaultLineWidth);
 
-             subElStyle.lineDash = retrieve2(tokenStyle.lineDash, style.lineDash);
 
-             subElStyle.lineDashOffset = style.lineDashOffset || 0;
 
-             subElStyle.stroke = textStroke;
 
-         }
 
-         if (textFill) {
 
-             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
 
-         ));
 
-     }
 
-     private _renderBackground(
 
-         style: TextStylePropsPart,
 
-         topStyle: TextStylePropsPart,
 
-         x: number,
 
-         y: number,
 
-         width: number,
 
-         height: number
 
-     ) {
 
-         const textBackgroundColor = style.backgroundColor;
 
-         const textBorderWidth = style.borderWidth;
 
-         const textBorderColor = style.borderColor;
 
-         const isImageBg = textBackgroundColor && (textBackgroundColor as {image: ImageLike}).image;
 
-         const isPlainOrGradientBg = textBackgroundColor && !isImageBg;
 
-         const textBorderRadius = style.borderRadius;
 
-         const self = this;
 
-         let rectEl: Rect;
 
-         let imgEl: ZRImage;
 
-         if (isPlainOrGradientBg || style.lineHeight || (textBorderWidth && textBorderColor)) {
 
-             // Background is color
 
-             rectEl = this._getOrCreateChild(Rect);
 
-             rectEl.useStyle(rectEl.createStyle());    // Create an empty style.
 
-             rectEl.style.fill = null;
 
-             const rectShape = rectEl.shape;
 
-             rectShape.x = x;
 
-             rectShape.y = y;
 
-             rectShape.width = width;
 
-             rectShape.height = height;
 
-             rectShape.r = textBorderRadius;
 
-             rectEl.dirtyShape();
 
-         }
 
-         if (isPlainOrGradientBg) {
 
-             const rectStyle = rectEl.style;
 
-             rectStyle.fill = textBackgroundColor as string || null;
 
-             rectStyle.fillOpacity = retrieve2(style.fillOpacity, 1);
 
-         }
 
-         else if (isImageBg) {
 
-             imgEl = this._getOrCreateChild(ZRImage);
 
-             imgEl.onload = function () {
 
-                 // Refresh and relayout after image loaded.
 
-                 self.dirtyStyle();
 
-             };
 
-             const imgStyle = imgEl.style;
 
-             imgStyle.image = (textBackgroundColor as {image: ImageLike}).image;
 
-             imgStyle.x = x;
 
-             imgStyle.y = y;
 
-             imgStyle.width = width;
 
-             imgStyle.height = height;
 
-         }
 
-         if (textBorderWidth && textBorderColor) {
 
-             const rectStyle = rectEl.style;
 
-             rectStyle.lineWidth = textBorderWidth;
 
-             rectStyle.stroke = textBorderColor;
 
-             rectStyle.strokeOpacity = retrieve2(style.strokeOpacity, 1);
 
-             rectStyle.lineDash = style.borderDash;
 
-             rectStyle.lineDashOffset = style.borderDashOffset || 0;
 
-             rectEl.strokeContainThreshold = 0;
 
-             // Making shadow looks better.
 
-             if (rectEl.hasFill() && rectEl.hasStroke()) {
 
-                 rectStyle.strokeFirst = true;
 
-                 rectStyle.lineWidth *= 2;
 
-             }
 
-         }
 
-         const commonStyle = (rectEl || imgEl).style;
 
-         commonStyle.shadowBlur = style.shadowBlur || 0;
 
-         commonStyle.shadowColor = style.shadowColor || 'transparent';
 
-         commonStyle.shadowOffsetX = style.shadowOffsetX || 0;
 
-         commonStyle.shadowOffsetY = style.shadowOffsetY || 0;
 
-         commonStyle.opacity = retrieve3(style.opacity, topStyle.opacity, 1);
 
-     }
 
-     static makeFont(style: TextStylePropsPart): string {
 
-         // FIXME in node-canvas fontWeight is before fontStyle
 
-         // Use `fontSize` `fontFamily` to check whether font properties are defined.
 
-         let font = '';
 
-         if (hasSeparateFont(style)) {
 
-             font = [
 
-                 style.fontStyle,
 
-                 style.fontWeight,
 
-                 parseFontSize(style.fontSize),
 
-                 // If font properties are defined, `fontFamily` should not be ignored.
 
-                 style.fontFamily || 'sans-serif'
 
-             ].join(' ');
 
-         }
 
-         return font && trim(font) || style.textFont || style.font;
 
-     }
 
- }
 
- const VALID_TEXT_ALIGN = {left: true, right: 1, center: 1};
 
- const VALID_TEXT_VERTICAL_ALIGN = {top: 1, bottom: 1, middle: 1};
 
- const FONT_PARTS = ['fontStyle', 'fontWeight', 'fontSize', 'fontFamily'] as const;
 
- export function parseFontSize(fontSize: number | string) {
 
-     if (
 
-         typeof fontSize === 'string'
 
-         && (
 
-             fontSize.indexOf('px') !== -1
 
-             || fontSize.indexOf('rem') !== -1
 
-             || fontSize.indexOf('em') !== -1
 
-         )
 
-     ) {
 
-         return fontSize;
 
-     }
 
-     else if (!isNaN(+fontSize)) {
 
-         return fontSize + 'px';
 
-     }
 
-     else {
 
-         return DEFAULT_FONT_SIZE + 'px';
 
-     }
 
- }
 
- function setSeparateFont(
 
-     targetStyle: TSpanStyleProps,
 
-     sourceStyle: TextStylePropsPart
 
- ) {
 
-     for (let i = 0; i < FONT_PARTS.length; i++) {
 
-         const fontProp = FONT_PARTS[i];
 
-         const val = sourceStyle[fontProp];
 
-         if (val != null) {
 
-             (targetStyle as any)[fontProp] = val;
 
-         }
 
-     }
 
- }
 
- export function hasSeparateFont(style: Pick<TextStylePropsPart, 'fontSize' | 'fontFamily' | 'fontWeight'>) {
 
-     return style.fontSize != null || style.fontFamily || style.fontWeight;
 
- }
 
- export function normalizeTextStyle(style: TextStyleProps): TextStyleProps {
 
-     normalizeStyle(style);
 
-     // TODO inherit each item from top style in token style?
 
-     each(style.rich, normalizeStyle);
 
-     return style;
 
- }
 
- function normalizeStyle(style: TextStylePropsPart) {
 
-     if (style) {
 
-         style.font = ZRText.makeFont(style);
 
-         let textAlign = style.align;
 
-         // 'middle' is invalid, convert it to 'center'
 
-         (textAlign as string) === 'middle' && (textAlign = 'center');
 
-         style.align = (
 
-             textAlign == null || VALID_TEXT_ALIGN[textAlign]
 
-         ) ? textAlign : 'left';
 
-         // Compatible with textBaseline.
 
-         let verticalAlign = style.verticalAlign;
 
-         (verticalAlign as string) === 'center' && (verticalAlign = 'middle');
 
-         style.verticalAlign = (
 
-             verticalAlign == null || VALID_TEXT_VERTICAL_ALIGN[verticalAlign]
 
-         ) ? verticalAlign : 'top';
 
-         // TODO Should not change the orignal value.
 
-         const textPadding = style.padding;
 
-         if (textPadding) {
 
-             style.padding = normalizeCssArray(style.padding);
 
-         }
 
-     }
 
- }
 
- /**
 
-  * @param stroke If specified, do not check style.textStroke.
 
-  * @param lineWidth If specified, do not check style.textStroke.
 
-  */
 
- function getStroke(
 
-     stroke?: TextStylePropsPart['stroke'],
 
-     lineWidth?: number
 
- ) {
 
-     return (stroke == null || lineWidth <= 0 || stroke === 'transparent' || stroke === 'none')
 
-         ? null
 
-         : ((stroke as any).image || (stroke as any).colorStops)
 
-         ? '#000'
 
-         : stroke;
 
- }
 
- function getFill(
 
-     fill?: TextStylePropsPart['fill']
 
- ) {
 
-     return (fill == null || fill === 'none')
 
-         ? null
 
-         // TODO pattern and gradient?
 
-         : ((fill as any).image || (fill as any).colorStops)
 
-         ? '#000'
 
-         : fill;
 
- }
 
- function getTextXForPadding(x: number, textAlign: string, textPadding: number[]): number {
 
-     return textAlign === 'right'
 
-         ? (x - textPadding[1])
 
-         : textAlign === 'center'
 
-         ? (x + textPadding[3] / 2 - textPadding[1] / 2)
 
-         : (x + textPadding[3]);
 
- }
 
- function getStyleText(style: TextStylePropsPart): string {
 
-     // Compat: set number to text is supported.
 
-     // set null/undefined to text is supported.
 
-     let text = style.text;
 
-     text != null && (text += '');
 
-     return text;
 
- }
 
- /**
 
-  * If needs draw background
 
-  * @param style Style of element
 
-  */
 
- function needDrawBackground(style: TextStylePropsPart): boolean {
 
-     return !!(
 
-         style.backgroundColor
 
-         || style.lineHeight
 
-         || (style.borderWidth && style.borderColor)
 
-     );
 
- }
 
- export default ZRText;
 
 
  |