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>&lt;s:Label&gt;</code> tag inherits all of the tag
179 *  attributes of its superclass and adds the following tag attributes:</p>
180 *
181 *  <pre>
182 *  &lt;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 *  /&gt;
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