////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package spark.components
{
import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;
import flash.display.Graphics;
import flash.display.Shape;
import flash.geom.Rectangle;
import flash.text.TextFormat;
import flash.text.engine.EastAsianJustifier;
import flash.text.engine.ElementFormat;
import flash.text.engine.FontDescription;
import flash.text.engine.FontLookup;
import flash.text.engine.FontMetrics;
import flash.text.engine.Kerning;
import flash.text.engine.LineJustification;
import flash.text.engine.SpaceJustifier;
import flash.text.engine.TextBaseline;
import flash.text.engine.TextBlock;
import flash.text.engine.TextElement;
import flash.text.engine.TextLine;
import flash.text.engine.TypographicCase;
import flashx.textLayout.compose.ISWFContext;
import flashx.textLayout.compose.TextLineRecycler;
import flashx.textLayout.formats.BaselineShift;
import flashx.textLayout.formats.TLFTypographicCase;
import mx.core.IEmbeddedFontRegistry;
import mx.core.IFlexModuleFactory;
import mx.core.IUIComponent;
import mx.core.Singleton;
import mx.core.mx_internal;
import spark.components.supportClasses.TextBase;
import spark.utils.TextUtil;
use namespace mx_internal;
//--------------------------------------
// Styles
//--------------------------------------
include "../styles/metadata/BasicInheritingTextStyles.as"
include "../styles/metadata/BasicNonInheritingTextStyles.as"
//--------------------------------------
// Other metadata
//--------------------------------------
[DefaultProperty("text")]
[IconFile("Label.png")]
/**
* Label is a low-level UIComponent that can render
* one or more lines of uniformly-formatted text.
* The text to be displayed is determined by the
* text
property inherited from TextBase.
* The formatting of the text is specified by the element's CSS styles,
* such as fontFamily
and fontSize
.
*
*
Label uses of the * Flash Text Engine (FTE) in Flash Player to provide high-quality * international typography. * Because Label is fast and lightweight, it is especially suitable * for use cases that involve rendering many small pieces of non-interactive * text, such as item renderers and labels in Button skins.
* *The Spark architecture provides three text "primitives" -- * Label, RichText, and RichEditableText -- * as part of its pay-only-for-what-you-need philosophy. * Label is the fastest and most lightweight, * but is limited in its capabilities: no complex formatting, * no scrolling, no selection, no editing, and no hyperlinks. * RichText and RichEditableText are built on the Text Layout * Framework (TLF) library, rather than on FTE. * RichText adds the ability to render rich HTML-like text * with complex formatting, but is still completely non-interactive. * RichEditableText is the slowest and heaviest, * but can do it all: it supports scrolling with virtualized TextLines, * selection, editing, hyperlinks, and images loaded from URLs. * You should use the fastest one that meets your needs.
* *The Spark Label control is similar to the MX Label control, mx.controls.Label. * The most important differences are: *
In Spark Label, three character sequences are recognized
* as explicit line breaks: CR ("\r"
), LF ("\n"
),
* and CR+LF ("\r\n"
).
If you don't specify any kind of width for a Label, * then the longest line, as determined by these explicit line breaks, * determines the width of the Label.
* *If you do specify some kind of width, then the specified text is
* word-wrapped at the right edge of the component's bounds, because the
* default value of the lineBreak
style is "toFit"
.
* If the text extends below the bottom of the component,
* it is clipped.
To disable this automatic wrapping, set the lineBreak
* style to "explicit"
. Then lines are broken only where
* the text
contains an explicit line break,
* and the ends of lines extending past the right edge is clipped.
If you have more text than you have room to display it,
* Label can truncate the text for you.
* Truncating text means replacing excess text
* with a truncation indicator such as "...".
* See the inherited properties maxDisplayedLines
* and isTruncated
.
You can control the line spacing with the lineHeight
style.
* You can horizontally and vertically align the text within the element's
* bounds using the textAlign
, textAlignLast
,
* and verticalAlign
styles.
* You can inset the text from the element's edges using the
* paddingLeft
, paddingTop
,
* paddingRight
, and paddingBottom
styles.
By default a Label has no background,
* but you can draw one using the backgroundColor
* and backgroundAlpha
styles.
* Borders are not supported.
* If you need a border, or a more complicated background, use a separate
* graphic element, such as a Rect, behind the Label.
Label supports displaying left-to-right (LTR) text such as French,
* right-to-left (RTL) text such as Arabic, and bidirectional text
* such as a French phrase inside of an Arabic paragraph.
* If the predominant text direction is right-to-left,
* set the direction
style to "rtl"
.
* The textAlign
style defaults to "start"
,
* which makes the text left-aligned when direction
* is "ltr"
and right-aligned when direction
* is "rtl"
.
* To get the opposite alignment,
* set textAlign
to "end"
.
Label uses the TextBlock class in the Flash Text Engine * to create one or more TextLine objects to statically display * its text String in the format determined by its CSS styles. * For performance, its TextLines do not contain information * about individual glyphs; for more info, see * flash.text.engine.TextLineValidity.STATIC.
* *The Label control has the following default characteristics:
*Characteristic | Description |
---|---|
Default size | 0 pixels wide by 12 pixels high if it contains no text, * and large enough ti display the text if it does |
Minimum size | 0 pixels |
Maximum size | 10000 pixels wide and 10000 pixels high |
The <s:Label>
tag inherits all of the tag
* attributes of its superclass and adds the following tag attributes:
* <s:Label * Properties * fontContext="" * * Styles * alignmentBaseline="baseline" * baselineShift="0" * cffHinting="0.0" * color="0x000000" * digitCase="default" * digitWidth="default" * direction="ltr" * dominantBaseline="auto" * fontFamily="Arial" * fontLookup="embeddedCFF" * fontSize="12" * fontStyle="normal" * fontWeight="normal" * justificationRule="auto" * justificationStyle="auto" * kerning="false" * ligatureLevel="common" * lineBreak="toFit" * lineHeight="120%" * lineThrough="false" * locale="en" * paddingBottom="0" * paddingLeft="0" * paddingRight="0" * paddingTop="0" * renderingMode="cff" * textAlign="start" * textAlignLast="start" * textAlpha="1" * textDecoration="start" * textJustify="interWord" * trackingLeft="0" * trackingRight="00" * typographicCase="default" * verticalAlign="top" * /> ** * @see spark.components.RichEditableText * @see spark.components.RichText * @see flash.text.engine.TextLineValidity#STATIC * * @includeExample examples/LabelExample.mxml * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public class Label extends TextBase { include "../core/Version.as"; //-------------------------------------------------------------------------- // // Class initialization // //-------------------------------------------------------------------------- /** * @private */ private static function initClass():void { staticTextBlock = new TextBlock(); staticTextElement = new TextElement(); staticSpaceJustifier = new SpaceJustifier(); staticEastAsianJustifier = new EastAsianJustifier(); if ("recreateTextLine" in staticTextBlock) recreateTextLine = staticTextBlock["recreateTextLine"]; } initClass(); //-------------------------------------------------------------------------- // // Class variables // //-------------------------------------------------------------------------- // We can re-use single instances of a few FTE classes over and over, // since they just serve as a factory for the TextLines that we care about. /** * @private */ private static var staticTextBlock:TextBlock; /** * @private */ private static var staticTextElement:TextElement; /** * @private */ private static var staticSpaceJustifier:SpaceJustifier; /** * @private */ private static var staticEastAsianJustifier:EastAsianJustifier; /** * @private * A reference to the recreateTextLine() method in staticTextBlock, * if it exists. This method was added in player 10.1. * It allows better performance by making it possible to reuse * existing TextLines instead of having to create new ones. */ private static var recreateTextLine:Function; //-------------------------------------------------------------------------- // // Class properties // //-------------------------------------------------------------------------- //---------------------------------- // embeddedFontRegistry //---------------------------------- /** * @private * Storage for the _embeddedFontRegistry property. * Note: This gets initialized on first access, * not when this class is initialized, in order to ensure * that the Singleton registry has already been initialized. */ private static var _embeddedFontRegistry:IEmbeddedFontRegistry; /** * @private * A reference to the embedded font registry. * Single registry in the system. * Used to look up the moduleFactory of a font. */ private static function get embeddedFontRegistry():IEmbeddedFontRegistry { if (!_embeddedFontRegistry) { _embeddedFontRegistry = IEmbeddedFontRegistry( Singleton.getInstance("mx.core::IEmbeddedFontRegistry")); } return _embeddedFontRegistry; } //-------------------------------------------------------------------------- // // Class methods // //-------------------------------------------------------------------------- /** * @private */ private static function getNumberOrPercentOf(value:Object, n:Number):Number { // If 'value' is a Number like 10.5, return it. if (value is Number) return Number(value); // If 'value' is a percentage String like "10.5%", // return that percentage of 'n'. if (value is String) { var len:int = String(value).length; if (len >= 1 && value.charAt(len - 1) == "%") { var percent:Number = Number(value.substring(0, len - 1)); return percent / 100 * n; } } // Otherwise, return NaN. return NaN; } //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Constructor. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function Label() { super(); } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- /** * @private * Holds the last recorded value of the module factory * used to create the font. */ private var embeddedFontContext:IFlexModuleFactory; /** * @private * When we render the text using FTE, this object represents the formatting * for our text element(s). Every time format related styles change, this * object is released because it is invalid. It is regenerated just in time * to render the text. */ private var elementFormat:ElementFormat; //-------------------------------------------------------------------------- // // Overidden Methods: ISimpleStyleClient // //-------------------------------------------------------------------------- /** * @private */ override public function stylesInitialized():void { super.stylesInitialized(); elementFormat = null; } /** * @private */ override public function styleChanged(styleProp:String):void { super.styleChanged(styleProp); elementFormat = null; } //-------------------------------------------------------------------------- // // Overridden methods: TextBase // //-------------------------------------------------------------------------- /** * @private * This helper method is used by measure() and updateDisplayList(). * It composes TextLines to render the 'text' String, * using the staticTextBlock as a factory, * and using the 'width' and 'height' parameters to define the size * of the composition rectangle, with NaN meaning "no limit". * It stops composing when the composition rectangle has been filled. * Returns true if all lines were composed, otherwise false. */ override mx_internal function composeTextLines(width:Number = NaN, height:Number = NaN):Boolean { super.composeTextLines(width, height); if (!elementFormat) elementFormat = createElementFormat(); // Set the composition bounds to be used by createTextLines(). // If the width or height is NaN, it will be computed by this method // by the time it returns. // The bounds are then used by the addTextLines() method // to determine the isOverset flag. // The composition bounds are also reported by the measure() method. bounds.x = 0; bounds.y = 0; bounds.width = width; bounds.height = height; // Remove the TextLines from the container and then release them for // reuse, if supported by the player. removeTextLines(); releaseTextLines(); // Create the TextLines. var allLinesComposed:Boolean = createTextLines(elementFormat); // Need truncation if all the following are true // - there is text (even if there is no text there is may be padding // which may not fit and the text would be reported as truncated) // - truncation options exist (0=no trunc, -1=fill up bounds then trunc, // n=n lines then trunc) // - compose width is specified // - content doesn't fit var lb:String = getStyle("lineBreak"); if (text != null && text.length > 0 && maxDisplayedLines && !doesComposedTextFit(height, width, allLinesComposed, maxDisplayedLines, lb)) { truncateText(width, height, lb); } // Detach the TextLines from the TextBlock that created them. releaseLinesFromTextBlock(); // Add the new text lines to the container. addTextLines(); // Figure out if a scroll rect is needed. isOverset = isTextOverset(width, height); // Just recomposed so reset. invalidateCompose = false; return allLinesComposed; } //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * @private * Creates an ElementFormat (and its FontDescription) * based on the Label's CSS styles. * These must be recreated each time because FTE * does not allow them to be reused. * As a side effect, this method also sets embeddedFontContext * so that we know which SWF should be used to create TextLines. * (TextLines using an embedded font must be created in the SWF * where the font is.) */ private function createElementFormat():ElementFormat { // When you databind to a text formatting style on a Label, // as in // the databinding can cause the style to be set to null. // Setting null values for properties in an FTE FontDescription // or ElementFormat throw an error, so the following code does // null-checking on the problematic properties. var s:String; // If the CSS styles for this component specify an embedded font, // embeddedFontContext will be set to the module factory that // should create TextLines (since they must be created in the // SWF where the embedded font is.) // Otherwise, this will be null. embeddedFontContext = getEmbeddedFontContext(); // Fill out a FontDescription based on the CSS styles. var fontDescription:FontDescription = new FontDescription(); s = getStyle("cffHinting"); if (s != null) fontDescription.cffHinting = s; s = getStyle("fontLookup"); if (s != null) { // FTE understands only "device" and "embeddedCFF" // for fontLookup. But Flex allows this style to be // set to "auto", in which case we automatically // determine it based on whether the CSS styles // specify an embedded font. if (s == "auto") { s = embeddedFontContext ? FontLookup.EMBEDDED_CFF : FontLookup.DEVICE; } else if (s == FontLookup.EMBEDDED_CFF && !embeddedFontContext) { // If the embedded font isn't found, fall back to device font, // before falling back to the player's default font. s = FontLookup.DEVICE; } fontDescription.fontLookup = s; } s = getStyle("fontFamily"); if (s != null) fontDescription.fontName = s; s = getStyle("fontStyle"); if (s != null) fontDescription.fontPosture = s; s = getStyle("fontWeight"); if (s != null) fontDescription.fontWeight = s; s = getStyle("renderingMode"); if (s != null) fontDescription.renderingMode = s; // Fill our an ElementFormat based on the CSS styles. var elementFormat:ElementFormat = new ElementFormat(); // Out of order so it can be used by baselineShift. elementFormat.fontSize = getStyle("fontSize"); s = getStyle("alignmentBaseline"); if (s != null) elementFormat.alignmentBaseline = s; elementFormat.alpha = getStyle("textAlpha"); setBaselineShift(elementFormat); // Note: Label doesn't support a breakOpportunity style, // so we leave elementFormat.breakOpportunity with its // default value of "auto". elementFormat.color = getStyle("color"); s = getStyle("digitCase"); if (s != null) elementFormat.digitCase = s; s = getStyle("digitWidth"); if (s != null) elementFormat.digitWidth = s; s = getStyle("dominantBaseline"); if (s != null) { // TLF adds the concept of a locale-based "auto" setting for // dominantBaseline, so we support that in Label as well // so that "auto" can be used in the global selector. // TLF's rule is that "auto" means "ideographicCenter" // for Japanese and Chinese locales and "roman" for other locales. // (See TLF's LocaleUtil, which we avoid linking in here.) if (s == "auto") { s = TextBaseline.ROMAN; var locale:String = getStyle("locale"); if (locale != null) { var lowercaseLocale:String = locale.toLowerCase(); if (lowercaseLocale.indexOf("ja") == 0 || lowercaseLocale.indexOf("zh") == 0) { s = TextBaseline.IDEOGRAPHIC_CENTER; } } } elementFormat.dominantBaseline = s; } elementFormat.fontDescription = fontDescription; setKerning(elementFormat); s = getStyle("ligatureLevel"); if (s != null) elementFormat.ligatureLevel = s; s = getStyle("locale"); if (s != null) elementFormat.locale = s; setTracking(elementFormat); setTypographicCase(elementFormat); return elementFormat; } /** * @private */ private function setBaselineShift(elementFormat:ElementFormat):void { var baselineShift:* = getStyle("baselineShift"); var fontSize:Number = elementFormat.fontSize; if (baselineShift == BaselineShift.SUPERSCRIPT || baselineShift == BaselineShift.SUBSCRIPT) { var fontMetrics:FontMetrics; if (embeddedFontContext) fontMetrics = embeddedFontContext.callInContext(elementFormat.getFontMetrics, elementFormat, null); else fontMetrics = elementFormat.getFontMetrics(); if (baselineShift == BaselineShift.SUPERSCRIPT) { elementFormat.baselineShift = fontMetrics.superscriptOffset * fontSize; elementFormat.fontSize = fontMetrics.superscriptScale * fontSize; } else // it's subscript { elementFormat.baselineShift = fontMetrics.subscriptOffset * fontSize; elementFormat.fontSize = fontMetrics.subscriptScale * fontSize; } } else { // TLF will throw a range error if percentage not between // -1000% and 1000%. Label does not. baselineShift = getNumberOrPercentOf(baselineShift, fontSize); if (!isNaN(baselineShift)) elementFormat.baselineShift = -baselineShift; // Note: The negative sign is because, as in TLF, // we want a positive number to shift the baseline up, // whereas FTE does it the opposite way. // In FTE, a positive baselineShift increases // the y coordinate of the baseline, which is // mathematically appropriate, but unintuitive. } } /** * @private */ private function setKerning(elementFormat:ElementFormat):void { var kerning:Object = getStyle("kerning"); // In Halo components based on TextField, // kerning is supposed to be true or false. // The default in TextField and Flex 3 is false // because kerning doesn't work for device fonts // and is slow for embedded fonts. // In Spark components based on TLF and FTE, // kerning is "auto", "on", or, "off". // The default in TLF and FTE is "auto" // (which means kern non-Asian characters) // because kerning works even on device fonts // and has miminal performance impact. // Since a CSS selector or parent container // can affect both Halo and Spark components, // we need to map true to "on" and false to "off" // here and in Label. // For Halo components, UITextField and UIFTETextField // do the opposite mapping // of "auto" and "on" to true and "off" to false. // We also support a value of "default" // (which we set in the global selector) // to mean "auto" for Spark and false for Halo // to get the recommended behavior in both sets of components. if (kerning === "default") kerning = Kerning.AUTO; else if (kerning === true) kerning = Kerning.ON; else if (kerning === false) kerning = Kerning.OFF; if (kerning != null) elementFormat.kerning = String(kerning); } /** * @private */ private function setTracking(elementFormat:ElementFormat):void { var trackingLeft:Object = getStyle("trackingLeft"); var trackingRight:Object = getStyle("trackingRight"); var value:Number; var fontSize:Number = elementFormat.fontSize; value = getNumberOrPercentOf(trackingLeft, fontSize); if (!isNaN(value)) elementFormat.trackingLeft = value; value = getNumberOrPercentOf(trackingRight, fontSize); if (!isNaN(value)) elementFormat.trackingRight = value; } /** * @private */ private function setTypographicCase(elementFormat:ElementFormat):void { var s:String = getStyle("typographicCase"); if (s != null) { switch (s) { case TLFTypographicCase.LOWERCASE_TO_SMALL_CAPS: { elementFormat.typographicCase = TypographicCase.CAPS_AND_SMALL_CAPS; break; } case TLFTypographicCase.CAPS_TO_SMALL_CAPS: { elementFormat.typographicCase = TypographicCase.SMALL_CAPS; break; } default: { // Others map directly so handle it in the default case. elementFormat.typographicCase = s; break; } } } } /** * @private * Stuffs the specified text and formatting info into a TextBlock * and uses it to create as many TextLines as fit into the bounds. * Returns true if all the text was composed into textLines. */ private function createTextLines(elementFormat:ElementFormat):Boolean { // Get CSS styles that affect a TextBlock and its justifier. var direction:String = getStyle("direction"); var justificationRule:String = getStyle("justificationRule"); var justificationStyle:String = getStyle("justificationStyle"); var textAlign:String = getStyle("textAlign"); var textAlignLast:String = getStyle("textAlignLast"); var textJustify:String = getStyle("textJustify"); // TLF adds the concept of a locale-based "auto" setting for // justificationRule and justificationStyle, so we support // that in Label as well so that "auto" can be used // in the global selector. // TLF's rule is that "auto" for justificationRule means "eastAsian" // for Japanese and Chinese locales and "space" for other locales, // and that "auto" for justificationStyle (which only affects // the EastAsianJustifier) always means "pushInKinsoku". // (See TLF's LocaleUtil, which we avoid linking in here.) if (justificationRule == "auto") { justificationRule = "space"; var locale:String = getStyle("locale"); if (locale != null) { var lowercaseLocale:String = locale.toLowerCase(); if (lowercaseLocale.indexOf("ja") == 0 || lowercaseLocale.indexOf("zh") == 0) { justificationRule = "eastAsian"; } } } if (justificationStyle == "auto") justificationStyle = "pushInKinsoku"; // Set the TextBlock's content. // Note: If there is no text, we do what TLF does and compose // a paragraph terminator character, so that a TextLine // gets created and we can measure it. // It will have a width of 0 but a height equal // to the font's ascent plus descent. staticTextElement.text = text != null && text.length > 0 ? text : "\u2029"; staticTextElement.elementFormat = elementFormat; staticTextBlock.content = staticTextElement; // And its bidiLevel. staticTextBlock.bidiLevel = direction == "ltr" ? 0 : 1; // And its justifier. var lineJustification:String; if (textAlign == "justify") { lineJustification = textAlignLast == "justify" ? LineJustification.ALL_INCLUDING_LAST : LineJustification.ALL_BUT_LAST; } else { lineJustification = LineJustification.UNJUSTIFIED; } if (justificationRule == "space") { staticSpaceJustifier.lineJustification = lineJustification; staticSpaceJustifier.letterSpacing = textJustify == "distribute"; staticTextBlock.textJustifier = staticSpaceJustifier; } else { staticEastAsianJustifier.lineJustification = lineJustification; staticEastAsianJustifier.justificationStyle = justificationStyle; staticTextBlock.textJustifier = staticEastAsianJustifier; } // Then create TextLines using this TextBlock. return createTextLinesFromTextBlock(staticTextBlock, textLines, bounds); } /** * @private * Compose into textLines. bounds on input is size of composition * area and on output is the size of the composed content. * The caller must call releaseLinesFromTextBlock() to release the * textLines from the TextBlock. This must be done after truncation * so that the composed lines can be broken into atoms to figure out * where the truncation indicator should be placed. * * Returns true if all the text was composed into textLines. */ private function createTextLinesFromTextBlock(textBlock:TextBlock, textLines:Vector.