1//////////////////////////////////////////////////////////////////////////////// 2// 3// ADOBE SYSTEMS INCORPORATED 4// Copyright 2008 Adobe Systems Incorporated 5// All Rights Reserved. 6// 7// NOTICE: Adobe permits you to use, modify, and distribute this file 8// in accordance with the terms of the license agreement accompanying it. 9// 10//////////////////////////////////////////////////////////////////////////////// 11 12package spark.components 13{ 14 15import flash.display.DisplayObject; 16import flash.display.DisplayObjectContainer; 17import flash.display.Graphics; 18import flash.display.Shape; 19import flash.geom.Rectangle; 20import flash.text.TextFormat; 21import flash.text.engine.EastAsianJustifier; 22import flash.text.engine.ElementFormat; 23import flash.text.engine.FontDescription; 24import flash.text.engine.FontLookup; 25import flash.text.engine.FontMetrics; 26import flash.text.engine.Kerning; 27import flash.text.engine.LineJustification; 28import flash.text.engine.SpaceJustifier; 29import flash.text.engine.TextBaseline; 30import flash.text.engine.TextBlock; 31import flash.text.engine.TextElement; 32import flash.text.engine.TextLine; 33import flash.text.engine.TypographicCase; 34 35import flashx.textLayout.compose.ISWFContext; 36import flashx.textLayout.compose.TextLineRecycler; 37import flashx.textLayout.formats.BaselineShift; 38import flashx.textLayout.formats.TLFTypographicCase; 39 40import mx.core.IEmbeddedFontRegistry; 41import mx.core.IFlexModuleFactory; 42import mx.core.IUIComponent; 43import mx.core.Singleton; 44import mx.core.mx_internal; 45 46import spark.components.supportClasses.TextBase; 47import spark.utils.TextUtil; 48 49use namespace mx_internal; 50 51//-------------------------------------- 52// Styles 53//-------------------------------------- 54 55include "../styles/metadata/BasicInheritingTextStyles.as" 56include "../styles/metadata/BasicNonInheritingTextStyles.as" 57 58//-------------------------------------- 59// Other metadata 60//-------------------------------------- 61 62[DefaultProperty("text")] 63 64[IconFile("Label.png")] 65 66/** 67 * Label is a low-level UIComponent that can render 68 * one or more lines of uniformly-formatted text. 69 * The text to be displayed is determined by the 70 * <code>text</code> property inherited from TextBase. 71 * The formatting of the text is specified by the element's CSS styles, 72 * such as <code>fontFamily</code> and <code>fontSize</code>. 73 * 74 * <p>Label uses of the 75 * Flash Text Engine (FTE) in Flash Player to provide high-quality 76 * international typography. 77 * Because Label is fast and lightweight, it is especially suitable 78 * for use cases that involve rendering many small pieces of non-interactive 79 * text, such as item renderers and labels in Button skins.</p> 80 * 81 * <p>The Spark architecture provides three text "primitives" -- 82 * Label, RichText, and RichEditableText -- 83 * as part of its pay-only-for-what-you-need philosophy. 84 * Label is the fastest and most lightweight, 85 * but is limited in its capabilities: no complex formatting, 86 * no scrolling, no selection, no editing, and no hyperlinks. 87 * RichText and RichEditableText are built on the Text Layout 88 * Framework (TLF) library, rather than on FTE. 89 * RichText adds the ability to render rich HTML-like text 90 * with complex formatting, but is still completely non-interactive. 91 * RichEditableText is the slowest and heaviest, 92 * but can do it all: it supports scrolling with virtualized TextLines, 93 * selection, editing, hyperlinks, and images loaded from URLs. 94 * You should use the fastest one that meets your needs.</p> 95 * 96 * <p>The Spark Label control is similar to the MX Label control, mx.controls.Label. 97 * The most important differences are: 98 * <ul> 99 * <li>Spark Label uses FTE, the player's new text engine, 100 * while MX Label uses the TextField class.</li> 101 * <li>Spark Label offers better typography, and better support 102 * for international languages, than MX Label.</li> 103 * <li>Spark Label can display multiple lines, which MX Label cannot.</li> 104 * <li>MX Label can display a limited subset of HTML, 105 * while Spark Label can only display text with uniform formatting.</li> 106 * <li>MX Label can be made selectable, while Spark Label cannot.</li> 107 * </ul></p> 108 * 109 * <p>In Spark Label, three character sequences are recognized 110 * as explicit line breaks: CR (<code>"\r"</code>), LF (<code>"\n"</code>), 111 * and CR+LF (<code>"\r\n"</code>).</p> 112 * 113 * <p>If you don't specify any kind of width for a Label, 114 * then the longest line, as determined by these explicit line breaks, 115 * determines the width of the Label.</p> 116 * 117 * <p>If you do specify some kind of width, then the specified text is 118 * word-wrapped at the right edge of the component's bounds, because the 119 * default value of the <code>lineBreak</code> style is <code>"toFit"</code>. 120 * If the text extends below the bottom of the component, 121 * it is clipped.</p> 122 * 123 * <p>To disable this automatic wrapping, set the <code>lineBreak</code> 124 * style to <code>"explicit"</code>. Then lines are broken only where 125 * the <code>text</code> contains an explicit line break, 126 * and the ends of lines extending past the right edge is clipped.</p> 127 * 128 * <p>If you have more text than you have room to display it, 129 * Label can truncate the text for you. 130 * Truncating text means replacing excess text 131 * with a truncation indicator such as "...". 132 * See the inherited properties <code>maxDisplayedLines</code> 133 * and <code>isTruncated</code>.</p> 134 * 135 * <p>You can control the line spacing with the <code>lineHeight</code> style. 136 * You can horizontally and vertically align the text within the element's 137 * bounds using the <code>textAlign</code>, <code>textAlignLast</code>, 138 * and <code>verticalAlign</code> styles. 139 * You can inset the text from the element's edges using the 140 * <code>paddingLeft</code>, <code>paddingTop</code>, 141 * <code>paddingRight</code>, and <code>paddingBottom</code> styles.</p> 142 * 143 * <p>By default a Label has no background, 144 * but you can draw one using the <code>backgroundColor</code> 145 * and <code>backgroundAlpha</code> styles. 146 * Borders are not supported. 147 * If you need a border, or a more complicated background, use a separate 148 * graphic element, such as a Rect, behind the Label.</p> 149 * 150 * <p>Label supports displaying left-to-right (LTR) text such as French, 151 * right-to-left (RTL) text such as Arabic, and bidirectional text 152 * such as a French phrase inside of an Arabic paragraph. 153 * If the predominant text direction is right-to-left, 154 * set the <code>direction</code> style to <code>"rtl"</code>. 155 * The <code>textAlign</code> style defaults to <code>"start"</code>, 156 * which makes the text left-aligned when <code>direction</code> 157 * is <code>"ltr"</code> and right-aligned when <code>direction</code> 158 * is <code>"rtl"</code>. 159 * To get the opposite alignment, 160 * set <code>textAlign</code> to <code>"end"</code>.</p> 161 * 162 * <p>Label uses the TextBlock class in the Flash Text Engine 163 * to create one or more TextLine objects to statically display 164 * its text String in the format determined by its CSS styles. 165 * For performance, its TextLines do not contain information 166 * about individual glyphs; for more info, see 167 * flash.text.engine.TextLineValidity.STATIC.</p> 168 * 169 * <p>The Label control has the following default characteristics:</p> 170 * <table class="innertable"> 171 * <tr><th>Characteristic</th><th>Description</th></tr> 172 * <tr><td>Default size</td><td>0 pixels wide by 12 pixels high if it contains no text, 173 * and large enough ti display the text if it does</td></tr> 174 * <tr><td>Minimum size</td><td>0 pixels</td></tr> 175 * <tr><td>Maximum size</td><td>10000 pixels wide and 10000 pixels high</td></tr> 176 * </table> 177 * 178 * @mxml <p>The <code><s:Label></code> tag inherits all of the tag 179 * attributes of its superclass and adds the following tag attributes:</p> 180 * 181 * <pre> 182 * <s:Label 183 * <strong>Properties</strong> 184 * fontContext="" 185 * 186 * <strong>Styles</strong> 187 * alignmentBaseline="baseline" 188 * baselineShift="0" 189 * cffHinting="0.0" 190 * color="0x000000" 191 * digitCase="default" 192 * digitWidth="default" 193 * direction="ltr" 194 * dominantBaseline="auto" 195 * fontFamily="Arial" 196 * fontLookup="embeddedCFF" 197 * fontSize="12" 198 * fontStyle="normal" 199 * fontWeight="normal" 200 * justificationRule="auto" 201 * justificationStyle="auto" 202 * kerning="false" 203 * ligatureLevel="common" 204 * lineBreak="toFit" 205 * lineHeight="120%" 206 * lineThrough="false" 207 * locale="en" 208 * paddingBottom="0" 209 * paddingLeft="0" 210 * paddingRight="0" 211 * paddingTop="0" 212 * renderingMode="cff" 213 * textAlign="start" 214 * textAlignLast="start" 215 * textAlpha="1" 216 * textDecoration="start" 217 * textJustify="interWord" 218 * trackingLeft="0" 219 * trackingRight="00" 220 * typographicCase="default" 221 * verticalAlign="top" 222 * /> 223 * </pre> 224 * 225 * @see spark.components.RichEditableText 226 * @see spark.components.RichText 227 * @see flash.text.engine.TextLineValidity#STATIC 228 * 229 * @includeExample examples/LabelExample.mxml 230 * 231 * @langversion 3.0 232 * @playerversion Flash 10 233 * @playerversion AIR 1.5 234 * @productversion Flex 4 235 */ 236public class Label extends TextBase 237{ 238 include "../core/Version.as"; 239 240 //-------------------------------------------------------------------------- 241 // 242 // Class initialization 243 // 244 //-------------------------------------------------------------------------- 245 246 /** 247 * @private 248 */ 249 private static function initClass():void 250 { 251 staticTextBlock = new TextBlock(); 252 253 staticTextElement = new TextElement(); 254 255 staticSpaceJustifier = new SpaceJustifier(); 256 257 staticEastAsianJustifier = new EastAsianJustifier(); 258 259 if ("recreateTextLine" in staticTextBlock) 260 recreateTextLine = staticTextBlock["recreateTextLine"]; 261 } 262 263 initClass(); 264 265 //-------------------------------------------------------------------------- 266 // 267 // Class variables 268 // 269 //-------------------------------------------------------------------------- 270 271 // We can re-use single instances of a few FTE classes over and over, 272 // since they just serve as a factory for the TextLines that we care about. 273 274 /** 275 * @private 276 */ 277 private static var staticTextBlock:TextBlock; 278 279 /** 280 * @private 281 */ 282 private static var staticTextElement:TextElement; 283 284 /** 285 * @private 286 */ 287 private static var staticSpaceJustifier:SpaceJustifier; 288 289 /** 290 * @private 291 */ 292 private static var staticEastAsianJustifier:EastAsianJustifier; 293 294 /** 295 * @private 296 * A reference to the recreateTextLine() method in staticTextBlock, 297 * if it exists. This method was added in player 10.1. 298 * It allows better performance by making it possible to reuse 299 * existing TextLines instead of having to create new ones. 300 */ 301 private static var recreateTextLine:Function; 302 303 //-------------------------------------------------------------------------- 304 // 305 // Class properties 306 // 307 //-------------------------------------------------------------------------- 308 309 //---------------------------------- 310 // embeddedFontRegistry 311 //---------------------------------- 312 313 /** 314 * @private 315 * Storage for the _embeddedFontRegistry property. 316 * Note: This gets initialized on first access, 317 * not when this class is initialized, in order to ensure 318 * that the Singleton registry has already been initialized. 319 */ 320 private static var _embeddedFontRegistry:IEmbeddedFontRegistry; 321 322 /** 323 * @private 324 * A reference to the embedded font registry. 325 * Single registry in the system. 326 * Used to look up the moduleFactory of a font. 327 */ 328 private static function get embeddedFontRegistry():IEmbeddedFontRegistry 329 { 330 if (!_embeddedFontRegistry) 331 { 332 _embeddedFontRegistry = IEmbeddedFontRegistry( 333 Singleton.getInstance("mx.core::IEmbeddedFontRegistry")); 334 } 335 336 return _embeddedFontRegistry; 337 } 338 339 //-------------------------------------------------------------------------- 340 // 341 // Class methods 342 // 343 //-------------------------------------------------------------------------- 344 345 /** 346 * @private 347 */ 348 private static function getNumberOrPercentOf(value:Object, 349 n:Number):Number 350 { 351 // If 'value' is a Number like 10.5, return it. 352 if (value is Number) 353 return Number(value); 354 355 // If 'value' is a percentage String like "10.5%", 356 // return that percentage of 'n'. 357 if (value is String) 358 { 359 var len:int = String(value).length; 360 if (len >= 1 && value.charAt(len - 1) == "%") 361 { 362 var percent:Number = Number(value.substring(0, len - 1)); 363 return percent / 100 * n; 364 } 365 } 366 367 // Otherwise, return NaN. 368 return NaN; 369 } 370 371 //-------------------------------------------------------------------------- 372 // 373 // Constructor 374 // 375 //-------------------------------------------------------------------------- 376 377 /** 378 * Constructor. 379 * 380 * @langversion 3.0 381 * @playerversion Flash 10 382 * @playerversion AIR 1.5 383 * @productversion Flex 4 384 */ 385 public function Label() 386 { 387 super(); 388 } 389 390 //-------------------------------------------------------------------------- 391 // 392 // Variables 393 // 394 //-------------------------------------------------------------------------- 395 396 /** 397 * @private 398 * Holds the last recorded value of the module factory 399 * used to create the font. 400 */ 401 private var embeddedFontContext:IFlexModuleFactory; 402 403 /** 404 * @private 405 * When we render the text using FTE, this object represents the formatting 406 * for our text element(s). Every time format related styles change, this 407 * object is released because it is invalid. It is regenerated just in time 408 * to render the text. 409 */ 410 private var elementFormat:ElementFormat; 411 412 //-------------------------------------------------------------------------- 413 // 414 // Overidden Methods: ISimpleStyleClient 415 // 416 //-------------------------------------------------------------------------- 417 418 /** 419 * @private 420 */ 421 override public function stylesInitialized():void 422 { 423 super.stylesInitialized(); 424 elementFormat = null; 425 } 426 427 /** 428 * @private 429 */ 430 override public function styleChanged(styleProp:String):void 431 { 432 super.styleChanged(styleProp); 433 elementFormat = null; 434 } 435 436 //-------------------------------------------------------------------------- 437 // 438 // Overridden methods: TextBase 439 // 440 //-------------------------------------------------------------------------- 441 442 /** 443 * @private 444 * This helper method is used by measure() and updateDisplayList(). 445 * It composes TextLines to render the 'text' String, 446 * using the staticTextBlock as a factory, 447 * and using the 'width' and 'height' parameters to define the size 448 * of the composition rectangle, with NaN meaning "no limit". 449 * It stops composing when the composition rectangle has been filled. 450 * Returns true if all lines were composed, otherwise false. 451 */ 452 override mx_internal function composeTextLines(width:Number = NaN, 453 height:Number = NaN):Boolean 454 { 455 super.composeTextLines(width, height); 456 457 if (!elementFormat) 458 elementFormat = createElementFormat(); 459 460 // Set the composition bounds to be used by createTextLines(). 461 // If the width or height is NaN, it will be computed by this method 462 // by the time it returns. 463 // The bounds are then used by the addTextLines() method 464 // to determine the isOverset flag. 465 // The composition bounds are also reported by the measure() method. 466 bounds.x = 0; 467 bounds.y = 0; 468 bounds.width = width; 469 bounds.height = height; 470 471 // Remove the TextLines from the container and then release them for 472 // reuse, if supported by the player. 473 removeTextLines(); 474 releaseTextLines(); 475 476 // Create the TextLines. 477 var allLinesComposed:Boolean = createTextLines(elementFormat); 478 479 // Need truncation if all the following are true 480 // - there is text (even if there is no text there is may be padding 481 // which may not fit and the text would be reported as truncated) 482 // - truncation options exist (0=no trunc, -1=fill up bounds then trunc, 483 // n=n lines then trunc) 484 // - compose width is specified 485 // - content doesn't fit 486 var lb:String = getStyle("lineBreak"); 487 if (text != null && text.length > 0 && 488 maxDisplayedLines && 489 !doesComposedTextFit(height, width, allLinesComposed, maxDisplayedLines, lb)) 490 { 491 truncateText(width, height, lb); 492 } 493 494 // Detach the TextLines from the TextBlock that created them. 495 releaseLinesFromTextBlock(); 496 497 // Add the new text lines to the container. 498 addTextLines(); 499 500 // Figure out if a scroll rect is needed. 501 isOverset = isTextOverset(width, height); 502 503 // Just recomposed so reset. 504 invalidateCompose = false; 505 506 return allLinesComposed; 507 } 508 509 //-------------------------------------------------------------------------- 510 // 511 // Methods 512 // 513 //-------------------------------------------------------------------------- 514 515 /** 516 * @private 517 * Creates an ElementFormat (and its FontDescription) 518 * based on the Label's CSS styles. 519 * These must be recreated each time because FTE 520 * does not allow them to be reused. 521 * As a side effect, this method also sets embeddedFontContext 522 * so that we know which SWF should be used to create TextLines. 523 * (TextLines using an embedded font must be created in the SWF 524 * where the font is.) 525 */ 526 private function createElementFormat():ElementFormat 527 { 528 // When you databind to a text formatting style on a Label, 529 // as in <Label fontFamily="{fontCombo.selectedItem}"/> 530 // the databinding can cause the style to be set to null. 531 // Setting null values for properties in an FTE FontDescription 532 // or ElementFormat throw an error, so the following code does 533 // null-checking on the problematic properties. 534 535 var s:String; 536 537 // If the CSS styles for this component specify an embedded font, 538 // embeddedFontContext will be set to the module factory that 539 // should create TextLines (since they must be created in the 540 // SWF where the embedded font is.) 541 // Otherwise, this will be null. 542 embeddedFontContext = getEmbeddedFontContext(); 543 544 // Fill out a FontDescription based on the CSS styles. 545 546 var fontDescription:FontDescription = new FontDescription(); 547 548 s = getStyle("cffHinting"); 549 if (s != null) 550 fontDescription.cffHinting = s; 551 552 s = getStyle("fontLookup"); 553 if (s != null) 554 { 555 // FTE understands only "device" and "embeddedCFF" 556 // for fontLookup. But Flex allows this style to be 557 // set to "auto", in which case we automatically 558 // determine it based on whether the CSS styles 559 // specify an embedded font. 560 if (s == "auto") 561 { 562 s = embeddedFontContext ? 563 FontLookup.EMBEDDED_CFF : 564 FontLookup.DEVICE; 565 } 566 else if (s == FontLookup.EMBEDDED_CFF && !embeddedFontContext) 567 { 568 // If the embedded font isn't found, fall back to device font, 569 // before falling back to the player's default font. 570 s = FontLookup.DEVICE; 571 } 572 fontDescription.fontLookup = s; 573 } 574 575 s = getStyle("fontFamily"); 576 if (s != null) 577 fontDescription.fontName = s; 578 579 s = getStyle("fontStyle"); 580 if (s != null) 581 fontDescription.fontPosture = s; 582 583 s = getStyle("fontWeight"); 584 if (s != null) 585 fontDescription.fontWeight = s; 586 587 s = getStyle("renderingMode"); 588 if (s != null) 589 fontDescription.renderingMode = s; 590 591 // Fill our an ElementFormat based on the CSS styles. 592 593 var elementFormat:ElementFormat = new ElementFormat(); 594 595 // Out of order so it can be used by baselineShift. 596 elementFormat.fontSize = getStyle("fontSize"); 597 598 s = getStyle("alignmentBaseline"); 599 if (s != null) 600 elementFormat.alignmentBaseline = s; 601 602 elementFormat.alpha = getStyle("textAlpha"); 603 604 setBaselineShift(elementFormat); 605 606 // Note: Label doesn't support a breakOpportunity style, 607 // so we leave elementFormat.breakOpportunity with its 608 // default value of "auto". 609 610 elementFormat.color = getStyle("color"); 611 612 s = getStyle("digitCase"); 613 if (s != null) 614 elementFormat.digitCase = s; 615 616 s = getStyle("digitWidth"); 617 if (s != null) 618 elementFormat.digitWidth = s; 619 620 s = getStyle("dominantBaseline"); 621 if (s != null) 622 { 623 // TLF adds the concept of a locale-based "auto" setting for 624 // dominantBaseline, so we support that in Label as well 625 // so that "auto" can be used in the global selector. 626 // TLF's rule is that "auto" means "ideographicCenter" 627 // for Japanese and Chinese locales and "roman" for other locales. 628 // (See TLF's LocaleUtil, which we avoid linking in here.) 629 if (s == "auto") 630 { 631 s = TextBaseline.ROMAN; 632 var locale:String = getStyle("locale"); 633 if (locale != null) 634 { 635 var lowercaseLocale:String = locale.toLowerCase(); 636 if (lowercaseLocale.indexOf("ja") == 0 || 637 lowercaseLocale.indexOf("zh") == 0) 638 { 639 s = TextBaseline.IDEOGRAPHIC_CENTER; 640 } 641 } 642 } 643 elementFormat.dominantBaseline = s; 644 } 645 646 elementFormat.fontDescription = fontDescription; 647 648 setKerning(elementFormat); 649 650 s = getStyle("ligatureLevel"); 651 if (s != null) 652 elementFormat.ligatureLevel = s; 653 654 s = getStyle("locale"); 655 if (s != null) 656 elementFormat.locale = s; 657 658 setTracking(elementFormat); 659 660 setTypographicCase(elementFormat); 661 662 return elementFormat; 663 } 664 665 /** 666 * @private 667 */ 668 private function setBaselineShift(elementFormat:ElementFormat):void 669 { 670 var baselineShift:* = getStyle("baselineShift"); 671 var fontSize:Number = elementFormat.fontSize; 672 673 if (baselineShift == BaselineShift.SUPERSCRIPT || 674 baselineShift == BaselineShift.SUBSCRIPT) 675 { 676 var fontMetrics:FontMetrics; 677 if (embeddedFontContext) 678 fontMetrics = embeddedFontContext.callInContext(elementFormat.getFontMetrics, elementFormat, null); 679 else 680 fontMetrics = elementFormat.getFontMetrics(); 681 if (baselineShift == BaselineShift.SUPERSCRIPT) 682 { 683 elementFormat.baselineShift = 684 fontMetrics.superscriptOffset * fontSize; 685 elementFormat.fontSize = fontMetrics.superscriptScale * fontSize; 686 } 687 else // it's subscript 688 { 689 elementFormat.baselineShift = 690 fontMetrics.subscriptOffset * fontSize; 691 elementFormat.fontSize = fontMetrics.subscriptScale * fontSize; 692 } 693 } 694 else 695 { 696 // TLF will throw a range error if percentage not between 697 // -1000% and 1000%. Label does not. 698 baselineShift = 699 getNumberOrPercentOf(baselineShift, fontSize); 700 if (!isNaN(baselineShift)) 701 elementFormat.baselineShift = -baselineShift; 702 // Note: The negative sign is because, as in TLF, 703 // we want a positive number to shift the baseline up, 704 // whereas FTE does it the opposite way. 705 // In FTE, a positive baselineShift increases 706 // the y coordinate of the baseline, which is 707 // mathematically appropriate, but unintuitive. 708 } 709 } 710 711 /** 712 * @private 713 */ 714 private function setKerning(elementFormat:ElementFormat):void 715 { 716 var kerning:Object = getStyle("kerning"); 717 718 // In Halo components based on TextField, 719 // kerning is supposed to be true or false. 720 // The default in TextField and Flex 3 is false 721 // because kerning doesn't work for device fonts 722 // and is slow for embedded fonts. 723 // In Spark components based on TLF and FTE, 724 // kerning is "auto", "on", or, "off". 725 // The default in TLF and FTE is "auto" 726 // (which means kern non-Asian characters) 727 // because kerning works even on device fonts 728 // and has miminal performance impact. 729 // Since a CSS selector or parent container 730 // can affect both Halo and Spark components, 731 // we need to map true to "on" and false to "off" 732 // here and in Label. 733 // For Halo components, UITextField and UIFTETextField 734 // do the opposite mapping 735 // of "auto" and "on" to true and "off" to false. 736 // We also support a value of "default" 737 // (which we set in the global selector) 738 // to mean "auto" for Spark and false for Halo 739 // to get the recommended behavior in both sets of components. 740 if (kerning === "default") 741 kerning = Kerning.AUTO; 742 else if (kerning === true) 743 kerning = Kerning.ON; 744 else if (kerning === false) 745 kerning = Kerning.OFF; 746 747 if (kerning != null) 748 elementFormat.kerning = String(kerning); 749 } 750 751 /** 752 * @private 753 */ 754 private function setTracking(elementFormat:ElementFormat):void 755 { 756 var trackingLeft:Object = getStyle("trackingLeft"); 757 var trackingRight:Object = getStyle("trackingRight"); 758 759 var value:Number; 760 var fontSize:Number = elementFormat.fontSize; 761 762 value = getNumberOrPercentOf(trackingLeft, fontSize); 763 if (!isNaN(value)) 764 elementFormat.trackingLeft = value; 765 766 value = getNumberOrPercentOf(trackingRight, fontSize); 767 if (!isNaN(value)) 768 elementFormat.trackingRight = value; 769 } 770 771 /** 772 * @private 773 */ 774 private function setTypographicCase(elementFormat:ElementFormat):void 775 { 776 var s:String = getStyle("typographicCase"); 777 if (s != null) 778 { 779 switch (s) 780 { 781 case TLFTypographicCase.LOWERCASE_TO_SMALL_CAPS: 782 { 783 elementFormat.typographicCase = 784 TypographicCase.CAPS_AND_SMALL_CAPS; 785 break; 786 } 787 case TLFTypographicCase.CAPS_TO_SMALL_CAPS: 788 { 789 elementFormat.typographicCase = TypographicCase.SMALL_CAPS; 790 break; 791 } 792 default: 793 { 794 // Others map directly so handle it in the default case. 795 elementFormat.typographicCase = s; 796 break; 797 } 798 } 799 } 800 } 801 802 803 /** 804 * @private 805 * Stuffs the specified text and formatting info into a TextBlock 806 * and uses it to create as many TextLines as fit into the bounds. 807 * Returns true if all the text was composed into textLines. 808 */ 809 private function createTextLines(elementFormat:ElementFormat):Boolean 810 { 811 // Get CSS styles that affect a TextBlock and its justifier. 812 var direction:String = getStyle("direction"); 813 var justificationRule:String = getStyle("justificationRule"); 814 var justificationStyle:String = getStyle("justificationStyle"); 815 var textAlign:String = getStyle("textAlign"); 816 var textAlignLast:String = getStyle("textAlignLast"); 817 var textJustify:String = getStyle("textJustify"); 818 819 // TLF adds the concept of a locale-based "auto" setting for 820 // justificationRule and justificationStyle, so we support 821 // that in Label as well so that "auto" can be used 822 // in the global selector. 823 // TLF's rule is that "auto" for justificationRule means "eastAsian" 824 // for Japanese and Chinese locales and "space" for other locales, 825 // and that "auto" for justificationStyle (which only affects 826 // the EastAsianJustifier) always means "pushInKinsoku". 827 // (See TLF's LocaleUtil, which we avoid linking in here.) 828 if (justificationRule == "auto") 829 { 830 justificationRule = "space"; 831 var locale:String = getStyle("locale"); 832 if (locale != null) 833 { 834 var lowercaseLocale:String = locale.toLowerCase(); 835 if (lowercaseLocale.indexOf("ja") == 0 || 836 lowercaseLocale.indexOf("zh") == 0) 837 { 838 justificationRule = "eastAsian"; 839 } 840 } 841 } 842 if (justificationStyle == "auto") 843 justificationStyle = "pushInKinsoku"; 844 845 // Set the TextBlock's content. 846 // Note: If there is no text, we do what TLF does and compose 847 // a paragraph terminator character, so that a TextLine 848 // gets created and we can measure it. 849 // It will have a width of 0 but a height equal 850 // to the font's ascent plus descent. 851 staticTextElement.text = text != null && text.length > 0 ? text : "\u2029"; 852 staticTextElement.elementFormat = elementFormat; 853 staticTextBlock.content = staticTextElement; 854 855 // And its bidiLevel. 856 staticTextBlock.bidiLevel = direction == "ltr" ? 0 : 1; 857 858 // And its justifier. 859 var lineJustification:String; 860 if (textAlign == "justify") 861 { 862 lineJustification = textAlignLast == "justify" ? 863 LineJustification.ALL_INCLUDING_LAST : 864 LineJustification.ALL_BUT_LAST; 865 } 866 else 867 { 868 lineJustification = LineJustification.UNJUSTIFIED; 869 } 870 if (justificationRule == "space") 871 { 872 staticSpaceJustifier.lineJustification = lineJustification; 873 staticSpaceJustifier.letterSpacing = textJustify == "distribute"; 874 staticTextBlock.textJustifier = staticSpaceJustifier; 875 } 876 else 877 { 878 staticEastAsianJustifier.lineJustification = lineJustification; 879 staticEastAsianJustifier.justificationStyle = justificationStyle; 880 881 staticTextBlock.textJustifier = staticEastAsianJustifier; 882 } 883 884 // Then create TextLines using this TextBlock. 885 return createTextLinesFromTextBlock(staticTextBlock, textLines, bounds); 886 } 887 888 /** 889 * @private 890 * Compose into textLines. bounds on input is size of composition 891 * area and on output is the size of the composed content. 892 * The caller must call releaseLinesFromTextBlock() to release the 893 * textLines from the TextBlock. This must be done after truncation 894 * so that the composed lines can be broken into atoms to figure out 895 * where the truncation indicator should be placed. 896 * 897 * Returns true if all the text was composed into textLines. 898 */ 899 private function createTextLinesFromTextBlock(textBlock:TextBlock, 900 textLines:Vector.<DisplayObject>, 901 bounds:Rectangle):Boolean 902 { 903 // Start with 0 text lines. 904 releaseTextLines(textLines); 905 906 // Get CSS styles for formats that we have to apply ourselves. 907 var direction:String = getStyle("direction"); 908 var lineBreak:String = getStyle("lineBreak"); 909 var lineHeight:Object = getStyle("lineHeight"); 910 var lineThrough:Boolean = getStyle("lineThrough"); 911 var paddingBottom:Number = getStyle("paddingBottom"); 912 var paddingLeft:Number = getStyle("paddingLeft"); 913 var paddingRight:Number = getStyle("paddingRight"); 914 var paddingTop:Number = getStyle("paddingTop"); 915 var textAlign:String = getStyle("textAlign"); 916 var textAlignLast:String = getStyle("textAlignLast"); 917 var textDecoration:String = getStyle("textDecoration"); 918 var verticalAlign:String = getStyle("verticalAlign"); 919 920 var innerWidth:Number = bounds.width - paddingLeft - paddingRight; 921 var innerHeight:Number = bounds.height - paddingTop - paddingBottom; 922 923 var measureWidth:Boolean = isNaN(innerWidth); 924 if (measureWidth) 925 innerWidth = maxWidth; 926 927 var maxLineWidth:Number = lineBreak == "explicit" ? 928 TextLine.MAX_LINE_WIDTH : 929 innerWidth; 930 931 if (innerWidth < 0 || innerHeight < 0 || !textBlock) 932 { 933 bounds.width = 0; 934 bounds.height = 0; 935 return false; 936 } 937 938 var fontSize:Number = staticTextElement.elementFormat.fontSize; 939 var actualLineHeight:Number; 940 if (lineHeight is Number) 941 { 942 actualLineHeight = Number(lineHeight); 943 } 944 else if (lineHeight is String) 945 { 946 var len:int = lineHeight.length; 947 var percent:Number = 948 Number(String(lineHeight).substring(0, len - 1)); 949 actualLineHeight = percent / 100 * fontSize; 950 } 951 if (isNaN(actualLineHeight)) 952 actualLineHeight = 1.2 * fontSize; 953 954 var maxTextWidth:Number = 0; 955 var totalTextHeight:Number = 0; 956 var n:int = 0; 957 var nextTextLine:TextLine; 958 var nextY:Number = 0; 959 var textLine:TextLine; 960 961 var swfContext:ISWFContext = ISWFContext(embeddedFontContext); 962 963 // For truncation, need to know if all lines have been composed. 964 var createdAllLines:Boolean = false; 965 // sometimes we need to create an extra line in order to compute 966 // truncation 967 var extraLine:Boolean; 968 969 // Generate TextLines, stopping when we run out of text 970 // or reach the bottom of the requested bounds. 971 // In this loop the lines are positioned within the rectangle 972 // (0, 0, innerWidth, innerHeight), with top-left alignment. 973 while (true) 974 { 975 var recycleLine:TextLine = TextLineRecycler.getLineForReuse(); 976 if (recycleLine) 977 { 978 if (swfContext) 979 { 980 nextTextLine = swfContext.callInContext( 981 textBlock["recreateTextLine"], textBlock, 982 [ recycleLine, textLine, maxLineWidth ]); 983 } 984 else 985 { 986 nextTextLine = recreateTextLine( 987 recycleLine, textLine, maxLineWidth); 988 } 989 } 990 else 991 { 992 if (swfContext) 993 { 994 nextTextLine = swfContext.callInContext( 995 textBlock.createTextLine, textBlock, 996 [ textLine, maxLineWidth ]); 997 } 998 else 999 { 1000 nextTextLine = textBlock.createTextLine( 1001 textLine, maxLineWidth); 1002 } 1003 } 1004 1005 if (!nextTextLine) 1006 { 1007 createdAllLines = !extraLine; 1008 break; 1009 } 1010 1011 // Determine the natural baseline position for this line. 1012 // Note: The y coordinate of a TextLine is the location 1013 // of its baseline, not of its top. 1014 nextY += (n == 0 ? nextTextLine.ascent : actualLineHeight); 1015 1016 // If verticalAlign is top and the next line is completely outside 1017 // the rectangle, we're done. If verticalAlign is middle or bottom 1018 // then we need to compose all the lines so the alignment is done 1019 // correctly. 1020 if (verticalAlign == "top" && 1021 nextY - nextTextLine.ascent > innerHeight) 1022 { 1023 // make an extra line so we can compute truncation 1024 if (!extraLine) 1025 extraLine = true; 1026 else 1027 break; 1028 } 1029 1030 // We'll keep this line. Put it into the textLines array. 1031 textLine = nextTextLine; 1032 textLines[n++] = textLine; 1033 1034 // Assign its location based on left/top alignment. 1035 // Its x position is 0 by default. 1036 textLine.y = nextY; 1037 1038 // Keep track of the maximum textWidth 1039 // and the accumulated textHeight of the TextLines. 1040 maxTextWidth = Math.max(maxTextWidth, textLine.textWidth); 1041 totalTextHeight += textLine.textHeight; 1042 1043 if (lineThrough || textDecoration == "underline") 1044 { 1045 // FTE doesn't render strikethroughs or underlines, 1046 // but it can tell us where to draw them. 1047 // You can't draw in a TextLine but it can have children, 1048 // so we create a child Shape to draw them in. 1049 1050 var elementFormat:ElementFormat = 1051 TextElement(textBlock.content).elementFormat; 1052 var fontMetrics:FontMetrics; 1053 if (embeddedFontContext) 1054 fontMetrics = embeddedFontContext.callInContext(elementFormat.getFontMetrics, elementFormat, null); 1055 else 1056 fontMetrics = elementFormat.getFontMetrics(); 1057 1058 var shape:Shape = new Shape(); 1059 var g:Graphics = shape.graphics; 1060 if (lineThrough) 1061 { 1062 g.lineStyle(fontMetrics.strikethroughThickness, 1063 elementFormat.color, elementFormat.alpha); 1064 g.moveTo(0, fontMetrics.strikethroughOffset); 1065 g.lineTo(textLine.textWidth, fontMetrics.strikethroughOffset); 1066 } 1067 if (textDecoration == "underline") 1068 { 1069 g.lineStyle(fontMetrics.underlineThickness, 1070 elementFormat.color, elementFormat.alpha); 1071 g.moveTo(0, fontMetrics.underlineOffset); 1072 g.lineTo(textLine.textWidth, fontMetrics.underlineOffset); 1073 } 1074 1075 textLine.addChild(shape); 1076 } 1077 } 1078 1079 // At this point, n is the number of lines that fit 1080 // and textLine is the last line that fit. 1081 1082 if (n == 0) 1083 { 1084 bounds.width = paddingLeft + paddingRight; 1085 bounds.height = paddingTop + paddingBottom; 1086 return false; 1087 } 1088 1089 // If not measuring the width, innerWidth remains the same since 1090 // alignment is done over the innerWidth not over the width of the 1091 // text that was just composed. 1092 if (measureWidth) 1093 innerWidth = maxTextWidth; 1094 1095 if (isNaN(bounds.height)) 1096 innerHeight = textLine.y + textLine.descent; 1097 1098 // Ensure we snap for consistent results. 1099 innerWidth = Math.ceil(innerWidth); 1100 innerHeight = Math.ceil(innerHeight); 1101 1102 var leftAligned:Boolean = 1103 textAlign == "start" && direction == "ltr" || 1104 textAlign == "end" && direction == "rtl" || 1105 textAlign == "left" || 1106 textAlign == "justify"; 1107 var centerAligned:Boolean = textAlign == "center"; 1108 var rightAligned:Boolean = 1109 textAlign == "start" && direction == "rtl" || 1110 textAlign == "end" && direction == "ltr" || 1111 textAlign == "right"; 1112 1113 // Calculate loop constants for horizontal alignment. 1114 var leftOffset:Number = bounds.left + paddingLeft; 1115 var centerOffset:Number = leftOffset + innerWidth / 2; 1116 var rightOffset:Number = leftOffset + innerWidth; 1117 1118 // Calculate loop constants for vertical alignment. 1119 var topOffset:Number = bounds.top + paddingTop; 1120 var bottomOffset:Number = innerHeight - (textLine.y + textLine.descent); 1121 var middleOffset:Number = bottomOffset / 2; 1122 bottomOffset += topOffset; 1123 middleOffset += topOffset; 1124 var leading:Number = (innerHeight - totalTextHeight) / (n - 1); 1125 1126 var previousTextLine:TextLine; 1127 var y:Number = 0; 1128 1129 var lastLineIsSpecial:Boolean = 1130 textAlign == "justify" && createdAllLines; 1131 1132 var minX:Number = innerWidth; 1133 var minY:Number = innerHeight; 1134 var maxX:Number = 0; 1135 1136 var clipping:Boolean = (n) ? (textLines[n - 1].y + TextLine(textLines[n - 1]).descent > innerHeight) : false; 1137 1138 // Reposition each line if necessary. 1139 // based on the horizontal and vertical alignment. 1140 for (var i:int = 0; i < n; i++) 1141 { 1142 textLine = TextLine(textLines[i]); 1143 1144 // If textAlign is "justify" and there is more than one line, 1145 // the last one (if we created it) gets horizontal aligned 1146 // according to textAlignLast. 1147 if (lastLineIsSpecial && i == n - 1) 1148 { 1149 leftAligned = 1150 textAlignLast == "start" && direction == "ltr" || 1151 textAlignLast == "end" && direction == "rtl" || 1152 textAlignLast == "left" || 1153 textAlignLast == "justify"; 1154 centerAligned = textAlignLast == "center"; 1155 rightAligned = 1156 textAlignLast == "start" && direction == "rtl" || 1157 textAlignLast == "end" && direction == "ltr" || 1158 textAlignLast == "right"; 1159 } 1160 1161 if (leftAligned) 1162 textLine.x = leftOffset; 1163 else if (centerAligned) 1164 textLine.x = centerOffset - textLine.textWidth / 2; 1165 else if (rightAligned) 1166 textLine.x = rightOffset - textLine.textWidth; 1167 1168 if (verticalAlign == "top" || !createdAllLines || clipping) 1169 { 1170 textLine.y += topOffset; 1171 } 1172 else if (verticalAlign == "middle") 1173 { 1174 textLine.y += middleOffset; 1175 } 1176 else if (verticalAlign == "bottom") 1177 { 1178 textLine.y += bottomOffset; 1179 } 1180 else if (verticalAlign == "justify") 1181 { 1182 // Determine the natural baseline position for this line. 1183 // Note: The y coordinate of a TextLine is the location 1184 // of its baseline, not of its top. 1185 y += i == 0 ? 1186 topOffset + textLine.ascent : 1187 previousTextLine.descent + leading + textLine.ascent; 1188 1189 textLine.y = y; 1190 previousTextLine = textLine; 1191 } 1192 1193 // Upper left corner of bounding box may not be 0,0 after 1194 // styles are applied or rounding error from minY calculation. 1195 // y is one decimal place and ascent isn't rounded so minY can be 1196 // slightly less than zero. 1197 minX = Math.min(minX, textLine.x); 1198 minY = Math.min(minY, textLine.y - textLine.ascent); 1199 maxX = Math.max(maxX, textLine.x + textLine.textWidth); 1200 } 1201 1202 bounds.x = minX - paddingLeft; 1203 bounds.y = minY - paddingTop; 1204 bounds.right = maxX + paddingRight; 1205 bounds.bottom = textLine.y + textLine.descent + paddingBottom; 1206 1207 return createdAllLines; 1208 } 1209 1210 /** 1211 * @private 1212 * Create textLine using paragraph terminator "\u2029" so it creates one 1213 * text line that we can use to get the baseline. The height is 1214 * important if the text is vertically aligned. 1215 */ 1216 override mx_internal function createEmptyTextLine(height:Number=NaN):void 1217 { 1218 staticTextElement.text = "\u2029"; 1219 1220 bounds.width = NaN; 1221 bounds.height = height; 1222 1223 createTextLinesFromTextBlock(staticTextBlock, textLines, bounds); 1224 1225 releaseLinesFromTextBlock(); 1226 } 1227 1228 /** 1229 * @private 1230 * Determines if the composed text fits in the given height and 1231 * line count limit. 1232 */ 1233 private function doesComposedTextFit(height:Number, width:Number, 1234 createdAllLines:Boolean, 1235 lineCountLimit:int, lineBreak:String):Boolean 1236 { 1237 // Not all text composed because it didn't fit within bounds. 1238 if (!createdAllLines) 1239 return false; 1240 1241 // More text lines than allowed lines. 1242 if (lineCountLimit != -1 && textLines.length > lineCountLimit) 1243 return false; 1244 1245 if (lineBreak == "explicit") 1246 { 1247 // if explicit, if the right edge of any lines go outside the 1248 // desired width 1249 if (bounds.right > width) 1250 return false; 1251 } 1252 1253 // No lines or one line or no height restriction. We don't truncate away 1254 // the one and only line just because height is too small. Clipping 1255 // will take care of it later 1256 if (textLines.length <= 1 || isNaN(height)) 1257 return true; 1258 1259 // Does the bottom of the last line fall within the bounds? 1260 var lastLine:TextLine = TextLine(textLines[textLines.length - 1]); 1261 var lastLineExtent:Number = lastLine.y + lastLine.descent; 1262 1263 return lastLineExtent <= height; 1264 } 1265 1266 /** 1267 * @private 1268 * width and height are the ones used to do the compose, not the measured 1269 * results resulting from the compose. 1270 * 1271 * Adapted from justification code in TLF's 1272 * TextLineFactory.textLinesFromString(). 1273 */ 1274 private function truncateText(width:Number, height:Number, lineBreak:String):void 1275 { 1276 var lineCountLimit:int = maxDisplayedLines; 1277 var somethingFit:Boolean = false; 1278 var truncLineIndex:int = 0; 1279 1280 if (lineBreak == "explicit") 1281 { 1282 truncateExplicitLineBreakText(width, height); 1283 return; 1284 } 1285 1286 // Compute the truncation line. 1287 truncLineIndex = computeLastAllowedLineIndex(height, lineCountLimit); 1288 1289 // Add extra line in case we wordwrapped some characters 1290 // onto extra lines. If we truncate in the middle of the last word 1291 // it may then fit on the line above. 1292 var extraLine:Boolean; 1293 if (truncLineIndex + 1 < textLines.length) 1294 { 1295 truncLineIndex++; 1296 extraLine = true; 1297 } 1298 1299 if (truncLineIndex >= 0) 1300 { 1301 // Estimate the initial truncation position using the following 1302 // steps. 1303 1304 // 1. Measure the space that the truncation indicator will take 1305 // by composing the truncation resource using the same bounds 1306 // and formats. The measured indicator lines could be cached but 1307 // as well as being dependent on the indicator string, they are 1308 // dependent on the given width. 1309 staticTextElement.text = truncationIndicatorResource; 1310 var indicatorLines:Vector.<DisplayObject> = 1311 new Vector.<DisplayObject>(); 1312 var indicatorBounds:Rectangle = new Rectangle(0, 0, width, NaN); 1313 1314 var indicatorFits:Boolean = createTextLinesFromTextBlock(staticTextBlock, 1315 indicatorLines, 1316 indicatorBounds); 1317 1318 releaseLinesFromTextBlock(); 1319 1320 // 2. Move target line for truncation higher by as many lines 1321 // as the number of full lines taken by the truncation 1322 // indicator. Indicator should also be able to fit. 1323 truncLineIndex -= (indicatorLines.length - 1); 1324 if (truncLineIndex >= 0 && indicatorFits) 1325 { 1326 // 3. Calculate allowed width (width left over from the 1327 // last line of the truncation indicator). 1328 var measuredTextLine:TextLine = 1329 TextLine(indicatorLines[indicatorLines.length - 1]); 1330 var allowedWidth:Number = 1331 measuredTextLine.specifiedWidth - 1332 measuredTextLine.unjustifiedTextWidth; 1333 1334 measuredTextLine = null; 1335 releaseTextLines(indicatorLines); 1336 1337 // 4. Get the initial truncation position on the target 1338 // line given this allowed width. 1339 var truncateAtCharPosition:int = getTruncationPosition( 1340 TextLine(textLines[truncLineIndex]), allowedWidth, extraLine); 1341 1342 // The following loop executes repeatedly composing text until 1343 // it fits. In each iteration, an atoms's worth of characters 1344 // of original content is dropped 1345 do 1346 { 1347 // Replace all content starting at the inital truncation 1348 // position with the truncation indicator. 1349 var truncText:String = text.slice(0, truncateAtCharPosition) + 1350 truncationIndicatorResource; 1351 1352 // (Re)-initialize bounds for next compose. 1353 bounds.x = 0; 1354 bounds.y = 0; 1355 bounds.width = width; 1356 bounds.height = height; 1357 1358 staticTextElement.text = truncText; 1359 1360 var createdAllLines:Boolean = createTextLinesFromTextBlock( 1361 staticTextBlock, textLines, bounds); 1362 1363 if (doesComposedTextFit(height, width, 1364 createdAllLines, 1365 lineCountLimit, lineBreak)) 1366 1367 { 1368 somethingFit = true; 1369 break; 1370 } 1371 1372 // No original content left to make room for 1373 // truncation indicator. 1374 if (truncateAtCharPosition == 0) 1375 break; 1376 1377 // Try again by truncating at the beginning of the 1378 // preceding atom. 1379 var oldCharPosition:int = truncateAtCharPosition; 1380 truncateAtCharPosition = getNextTruncationPosition( 1381 truncLineIndex, truncateAtCharPosition); 1382 // check to see if we've run out of chars 1383 if (oldCharPosition == truncateAtCharPosition) 1384 break; 1385 } 1386 while (true); 1387 } 1388 } 1389 1390 // If nothing fit, return no lines and bounds that just contains 1391 // padding. 1392 if (!somethingFit) 1393 { 1394 releaseTextLines(); 1395 1396 var paddingBottom:Number = getStyle("paddingBottom"); 1397 var paddingLeft:Number = getStyle("paddingLeft"); 1398 var paddingRight:Number = getStyle("paddingRight"); 1399 var paddingTop:Number = getStyle("paddingTop"); 1400 1401 bounds.x = 0; 1402 bounds.y = 0; 1403 bounds.width = paddingLeft + paddingRight; 1404 bounds.height = paddingTop + paddingBottom; 1405 } 1406 1407 // The text was truncated. 1408 setIsTruncated(true); 1409 } 1410 1411 /** 1412 * @private 1413 * width and height are the ones used to do the compose, not the measured 1414 * results resulting from the compose. 1415 */ 1416 private function truncateExplicitLineBreakText(width:Number, height:Number):void 1417 { 1418 // 1. Measure the space that the truncation indicator will take 1419 // by composing the truncation resource using the same bounds 1420 // and formats. The measured indicator lines could be cached but 1421 // as well as being dependent on the indicator string, they are 1422 // dependent on the given width. 1423 staticTextElement.text = truncationIndicatorResource; 1424 var indicatorLines:Vector.<DisplayObject> = 1425 new Vector.<DisplayObject>(); 1426 var indicatorBounds:Rectangle = new Rectangle(0, 0, width, NaN); 1427 1428 createTextLinesFromTextBlock(staticTextBlock, 1429 indicatorLines, 1430 indicatorBounds); 1431 1432 releaseLinesFromTextBlock(); 1433 1434 // check each line to see if it needs truncation 1435 var n:int = textLines.length; 1436 for (var i:int = 0; i < n; i++) 1437 { 1438 var line:TextLine = textLines[i] as TextLine; 1439 // if the line is wider than bounds or off the left side 1440 // TODO (aharui): What if text runs off left side because of 1441 // alignment or direction? 1442 if ((line.x + line.width) > width) 1443 { 1444 // clip this line 1445 var lineLength:int = line.rawTextLength; 1446 // start chopping from the end until it fits 1447 while (--lineLength > 0) 1448 { 1449 var lineStr:String = text.substr(line.textBlockBeginIndex, lineLength); 1450 lineStr += truncationIndicatorResource; 1451 staticTextElement.text = lineStr; 1452 var clippedLines:Vector.<DisplayObject> = 1453 new Vector.<DisplayObject>(); 1454 1455 createTextLinesFromTextBlock(staticTextBlock, 1456 clippedLines, 1457 indicatorBounds); 1458 1459 releaseLinesFromTextBlock(); 1460 if (clippedLines.length == 1 && 1461 (clippedLines[0].x + clippedLines[0].width) <= width) 1462 { 1463 // replace with the clipped line 1464 clippedLines[0].x = line.x; 1465 clippedLines[0].y = line.y; 1466 textLines[i] = clippedLines[0]; 1467 break; 1468 } 1469 1470 } 1471 } 1472 } 1473 } 1474 1475 /** 1476 * @private 1477 * Calculates the last line that fits in the given height and line count 1478 * limit. 1479 */ 1480 private function computeLastAllowedLineIndex(height:Number, 1481 lineCountLimit:int):int 1482 { 1483 var truncationLineIndex:int = textLines.length - 1; 1484 // return -1 if no textLines (usually because zero size) 1485 if (truncationLineIndex < 0) 1486 return truncationLineIndex; 1487 1488 if (!isNaN(height)) 1489 { 1490 // Search in reverse order since truncation near the end is the 1491 // more common use case. 1492 do 1493 { 1494 var textLine:TextLine = TextLine(textLines[truncationLineIndex]); 1495 if (textLine.y + textLine.descent <= height) 1496 break; 1497 1498 truncationLineIndex--; 1499 } 1500 while (truncationLineIndex >= 0); 1501 } 1502 1503 // if line count limit is smaller, use that 1504 if (lineCountLimit != -1 && lineCountLimit <= truncationLineIndex) 1505 truncationLineIndex = lineCountLimit - 1; 1506 1507 return truncationLineIndex; 1508 } 1509 1510 /** 1511 * @private 1512 * Gets the initial truncation position on a line. 1513 * 1514 * If there is an extra line, start at the first word boundary since 1515 * truncating characters in this word may make it fit on the line above. 1516 * 1517 * If there is not an extra line, start at the allowed width. 1518 * 1519 * - Must be at an atom boundary. 1520 * - Must scan the line for atoms in logical order, not physical position 1521 * order. 1522 * For example, given bi-di text ABאבCD 1523 * atoms must be scanned in this order: 1524 * A, B, א 1525 * ג, C, D 1526 */ 1527 private function getTruncationPosition(line:TextLine, 1528 allowedWidth:Number, 1529 extraLine:Boolean):int 1530 { 1531 var consumedWidth:Number = 0; 1532 var charPosition:int = line.textBlockBeginIndex; 1533 1534 while (charPosition < line.textBlockBeginIndex + line.rawTextLength) 1535 { 1536 var atomIndex:int = line.getAtomIndexAtCharIndex(charPosition); 1537 if (extraLine) 1538 { 1539 // Skip the initial word boundary. 1540 if (charPosition != line.textBlockBeginIndex && 1541 line.getAtomWordBoundaryOnLeft(atomIndex)) 1542 { 1543 break; 1544 } 1545 } 1546 else 1547 { 1548 var atomBounds:Rectangle = line.getAtomBounds(atomIndex); 1549 consumedWidth += atomBounds.width; 1550 if (consumedWidth > allowedWidth) 1551 break; 1552 } 1553 1554 charPosition = line.getAtomTextBlockEndIndex(atomIndex); 1555 } 1556 1557 return charPosition; 1558 } 1559 1560 /** 1561 * @private 1562 * Gets the next truncation position by shedding an atom's worth of 1563 * characters. 1564 */ 1565 private function getNextTruncationPosition(truncationLineIndex:int, 1566 truncateAtCharPosition:int):int 1567 { 1568 // 1. Get the position of the last character of the preceding atom 1569 // truncateAtCharPosition-1, because truncateAtCharPosition is an 1570 // atom boundary. 1571 truncateAtCharPosition--; 1572 1573 // 2. Find the new target line (i.e., the line that has the new 1574 // truncation position). If the last truncation position was at the 1575 // beginning of the target line, the new position may have moved to a 1576 // previous line. It is also possible for this position to be found 1577 // in the next line because the truncation indicator may have combined 1578 // with original content to form a word that may not have afforded a 1579 // suitable break opportunity. In any case, the new truncation 1580 // position lies in the vicinity of the previous target line, so a 1581 // linear search suffices. 1582 var line:TextLine = TextLine(textLines[truncationLineIndex]); 1583 do 1584 { 1585 if (truncateAtCharPosition >= line.textBlockBeginIndex && 1586 truncateAtCharPosition < line.textBlockBeginIndex + line.rawTextLength) 1587 { 1588 break; 1589 } 1590 1591 if (truncateAtCharPosition < line.textBlockBeginIndex) 1592 { 1593 truncationLineIndex--; 1594 // if we run out of chars, just return the same 1595 // position to warn the caller to stop 1596 if (truncationLineIndex < 0) 1597 return truncateAtCharPosition; 1598 } 1599 else 1600 { 1601 truncationLineIndex++; 1602 // if we run out of chars, just return the same 1603 // position to warn the caller to stop 1604 if (truncationLineIndex >= textLines.length) 1605 return truncateAtCharPosition; 1606 } 1607 1608 line = TextLine(textLines[truncationLineIndex]); 1609 } 1610 while (true); 1611 1612 // 3. Get the line atom index at this position 1613 var atomIndex:int = 1614 line.getAtomIndexAtCharIndex(truncateAtCharPosition); 1615 1616 // 4. Get the char index for this atom index 1617 var nextTruncationPosition:int = 1618 line.getAtomTextBlockBeginIndex(atomIndex); 1619 1620 return nextTruncationPosition; 1621 } 1622 1623 /** 1624 * @private 1625 * Cleans up and sets the validity of the lines associated 1626 * with the TextBlock to TextLineValidity.INVALID. 1627 */ 1628 private function releaseLinesFromTextBlock():void 1629 { 1630 var firstLine:TextLine = staticTextBlock.firstLine; 1631 var lastLine:TextLine = staticTextBlock.lastLine; 1632 1633 if (firstLine) 1634 staticTextBlock.releaseLines(firstLine, lastLine); 1635 } 1636} 1637 1638} 1639