blob: 31544f92f2cdf94beb45b24746750440be8ce86c [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// 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 flashx.textLayout.compose
{
import flash.geom.Matrix;
import flash.geom.Point;
import flash.geom.Rectangle;
import flashx.textLayout.container.ContainerController;
import flashx.textLayout.container.IFloatController;
import flashx.textLayout.container.IWrapManager;
import flashx.textLayout.debug.assert;
import flashx.textLayout.formats.BlockProgression;
import flashx.textLayout.tlf_internal;
use namespace tlf_internal;
/** The area inside a text container is sub-divided into smaller areas available for
* text layout. These smaller areas within the container are called Parcels. The
* ParcelList manages the parcel associated with a TextFlow during composition.
*
* A container will always have at least one parcel, which corresponds to the container's
* bounding box. If the container has more than one column, each column appears as
* a parcel in the parcel list. If the container has wrap applied, the area around the
* wrap that is available for layout is divided into rectangular-shaped parcels.
* Lastly, parcels may be created during composition for use by flow elements that require
* layout within a specific geometry that may not be the same as the columns: for instance,
* a straddle head, a table, or a sidehead.
*/
internal class LayoutParcelList extends ParcelList
{
/** This is a list of the wraps, or parcels that are NOT available for text layout. */
private var wraps:Array; /* of Rectangle */
/** Inlines can extend across columns, so we have a separate parcel list
that is independent of columns (just has container bboxes) used for
creating new parcels that can extend across columns. */
// private var inlineWrap:LayoutParcelList;
/** minimum allowable width of a line */
private static const MIN_WIDTH:Number = 40;
// a single parcellist that is checked out and checked in
static private var _sharedLayoutParcelList:LayoutParcelList;
/** @private */
static tlf_internal function getLayoutParcelList():LayoutParcelList
{
var rslt:LayoutParcelList = _sharedLayoutParcelList ? _sharedLayoutParcelList : new LayoutParcelList();
_sharedLayoutParcelList = null;
return rslt;
}
/** @private */
static tlf_internal function releaseLayoutParcelList(list:IParcelList):void
{
if (_sharedLayoutParcelList == null)
{
_sharedLayoutParcelList = list as LayoutParcelList;
if (_sharedLayoutParcelList)
_sharedLayoutParcelList.releaseAnyReferences();
}
}
/** Constructor */
public function LayoutParcelList()
{
super();
}
/** prevent any leaks. @private */
tlf_internal override function releaseAnyReferences():void
{
super.releaseAnyReferences();
wraps = null;
}
override protected function addOneControllerToParcelList(controllerToInitialize:ContainerController):void
{
super.addOneControllerToParcelList(controllerToInitialize);
if (controllerToInitialize is IFloatController)
{
if (IFloatController(controllerToInitialize).wraps)
this.wraps = IFloatController(controllerToInitialize).wraps.wraps;
for each (var wrapExclude:Rectangle in wraps)
addWrap(wrapExclude, controllerToInitialize, false /* wrap around the sides */);
}
}
private function isNextAdjacent():Boolean
{
// trace("LPL: isNextAdjacent");
CONFIG::debug { assert(_currentParcelIndex >= 0 && _currentParcelIndex < numParcels, "invalid _currentParcelIndex in ParcelList"); }
if (_currentParcelIndex + 1 >= numParcels)
return false;
var nextParcel:Parcel = getParcelAtIndex(_currentParcelIndex + 1);
if(_blockProgression != BlockProgression.RL)
{
if (_currentParcel.bottom != nextParcel.top || _currentParcel.left > nextParcel.right)
return false;
}
else
{
if(_currentParcel.left != nextParcel.right || _currentParcel.top > nextParcel.bottom)
return false;
}
return true;
}
private function getBBox(controller:ContainerController):Rectangle
{
// trace("LPL: getBBox");
var bbox:Rectangle = null;
for (var idx:int = 0; idx < numParcels; idx++)
{
var p:Parcel = getParcelAtIndex(idx);
if (p.controller == controller)
{
if (!bbox)
bbox = p.clone();
else
bbox = bbox.union(p);
}
}
return bbox;
}
private function intersects(r:Rectangle, rectArray:Array):Boolean
// true if r intersects any of the rectangles in rectArray
{
// trace("LPL: getBBox");
if (rectArray)
{
for each (var i:Rectangle in rectArray)
{
if (i.intersects(r))
return true;
}
}
return false;
}
/** @private */
override public function createParcel(parcel:Rectangle, blockProgression:String, verticalJump:Boolean):Boolean
// If we can get the requested parcel to fit, create it in the parcels list
{
// trace("LPL: createParcel");
// trace("createParcel start", parcel.toString());
// traceOut();
var fitRect:Rectangle = new Rectangle();
do {
getLineSlug(fitRect,parcel.height);
if (!fitRect || atEnd()) // Can't find contiguous vertical space for the parcel
return false;
else
{
if (blockProgression == BlockProgression.TB)
parcel.offset(0, fitRect.top - parcel.top); // parcel location may have been adjusted by getLineSlug
else
parcel.offset(fitRect.left - parcel.left, 0); // parcel location may have been adjusted by getLineSlug
if (fitRect.width >= parcel.width)
break; // found enough vertical & horizontal space
// Found vertical space, but not enough horizontal.
// It could be that parcel would fit across contiguous columns. Check for a wrap.
// Check that it fits within the bounding box for the container,
// and doesn't intersect any wraps
var bbox:Rectangle = getBBox(_currentParcel.controller);
if (bbox.containsRect(parcel) && !intersects(parcel, wraps))
break;
next(); // keep looking in other parcels
if (_currentParcelIndex >= numParcels)
return false; // hit the end: space not found
}
} while (fitRect.width < parcel.width);
// Create a new parcel, and insert it in the correct location in the parcels array
var targetController:ContainerController = _currentParcel.controller;
addWrap(parcel, targetController, verticalJump /* jumpOver */);
for (var l:int = 0; l < numParcels; l++)
{
var testParcel:Parcel = getParcelAtIndex(l);
if (testParcel.top > parcel.top &&
!((testParcel.left > parcel.right) || (testParcel.right < parcel.left)))
{
if (testParcel.controller == targetController)
break;
}
}
var splitParcel:Parcel = getParcelAtIndex(l);
var splitCoverage:int = splitParcel.columnCoverage;
// clear top of column splitParcel is not that anymore
splitParcel.columnCoverage = splitCoverage & ~Parcel.TOP_OF_COLUMN;
// we're inserting before so its bot_of_column - we aren't
insertParcel(l,new Parcel(parcel.x, parcel.y, parcel.width, parcel.height, targetController, splitParcel.column, splitCoverage&~Parcel.BOT_OF_COLUMN));
// Set the current parcel to be the one we just added
_currentParcelIndex = l;
_currentParcel = getParcelAtIndex(l);
// trace("createParcel done");
// traceOut();
return true;
}
/** @private */
override public function getLineSlug(slugRect:Rectangle,height:Number, minWidth:Number = 0):Boolean
{
return (_currentParcelIndex < numParcels) && getWidthInternal(slugRect, height, minWidth);
}
/**Helper function for getLineSlug, used internally. Similar to getWidth, but
* although it allows a line to extend across parcels, if necessary, the line
* will always start in the current parce. If a line does not fit starting
* from the current parcel, this function (unlike getWidth) will return
* 0 instead of changing the parcel.
* @param height amount of contiguous vertical space that must be available
* @param minWidth amount of contiguous horizontal space that must be available
* @return amount of contiguous horizontal space actually available
*/
private function getWidthInternal(slugRect:Rectangle, height:Number, minWidth:Number):Boolean
{
// trace("LPL: getWidthInternal");
CONFIG::debug { assert(_currentParcelIndex >= 0 && _currentParcelIndex < numParcels, "invalid position in ParcelList"); }
// See if it fits in the current parcel
var tileWidth:Number = getComposeWidth(_currentParcel);
if (tileWidth <= minWidth)
return false;
var requiredHeight:Number = currentParcel.fitAny ? 1 : height;
if (currentParcel.composeToPosition || _totalDepth + requiredHeight <= getComposeHeight(_currentParcel))
{
if (this._blockProgression != BlockProgression.RL)
{
slugRect.x = left;
slugRect.y = _currentParcel.top + _totalDepth;
slugRect.width = tileWidth;
slugRect.height = height;
}
else
{
slugRect.x = left;
slugRect.y = _currentParcel.top;
slugRect.width = _currentParcel.width-_totalDepth;
slugRect.height = tileWidth;
}
return true;
}
// This tile won't fit in the parcel. Look at the next parcel(s) that are adejacent,
// as many as are required fit the tile. The width of the tile will be the minimum
// of this list. If the width of the tile is less than the minimum specified by the
// caller, or if there aren't enough adejacent parcels to fit the tile,
// return 0 to indicate the tile doesn't fit in the parcel. The client code can try
// again on the next parcel.
var offset:Number = getComposeHeight(_currentParcel) - _totalDepth;
var heightRemaining:Number = height - offset;
var leftEdge:Number = _currentParcel.left;
var rightEdge:Number = _currentParcel.right;
var topEdge:Number = _currentParcel.top;
if (_blockProgression == BlockProgression.RL)
rightEdge -= _totalDepth;
else
topEdge += _totalDepth;
do
{
// Next parcel isn't adejacent. We can't fit the tile.
if (!isNextAdjacent())
return false;
next();
tileWidth = Math.min(tileWidth, getComposeWidth(_currentParcel));
if (tileWidth <= minWidth)
return false; // try again on next tile
var newDepth:Number;
if (_blockProgression == BlockProgression.RL)
{
topEdge = Math.max(topEdge, _currentParcel.top);
newDepth = -(height - heightRemaining);
heightRemaining -= Math.min(heightRemaining, _currentParcel.width);
}
else
{
leftEdge = Math.max(leftEdge, _currentParcel.left);
newDepth = -(height - heightRemaining);
heightRemaining -= Math.min(heightRemaining, _currentParcel.height);
}
} while (heightRemaining > 0)
_totalDepth = newDepth;
_hasContent = true;
slugRect.x = leftEdge;
slugRect.y = topEdge;
if (_blockProgression == BlockProgression.RL)
{
slugRect.x = rightEdge - height;
slugRect.width = height;
slugRect.height = tileWidth;
}
else
{
slugRect.width = tileWidth;
slugRect.height = height;
}
return true;
}
private function getSlugParcel(parcel:Rectangle, blockProgression:String):Rectangle
{
var fitRect:Rectangle = new Rectangle();
do {
getLineSlug(fitRect,parcel.height);
if (!fitRect || atEnd()) // Can't find contiguous vertical space for the parcel
return null;
else
{
if (blockProgression == BlockProgression.TB)
parcel.offset(0, fitRect.top - parcel.top); // parcel location may have been adjusted by getLineSlug
else
parcel.offset(fitRect.right - parcel.right, 0); // parcel location may have been adjusted by getLineSlug
if (fitRect.width >= parcel.width)
break; // found enough vertical & horizontal space
// Found vertical space, but not enough horizontal.
// It could be that parcel would fit across contiguous columns. Check for a wrap.
// Check that it fits within the bounding box for the container,
// and doesn't intersect any wraps
var bbox:Rectangle = getBBox(_currentParcel.controller);
if (bbox.containsRect(parcel) && !intersects(parcel, wraps))
break;
next(); // keep looking in other parcels
if (_currentParcelIndex >= numParcels)
return null; // hit the end: space not found
}
} while (fitRect.width < parcel.width);
return parcel;
}
public const NONE:String = "none"; // don't wrap -- allow overlap
public const LEFT:String = "left"; // allow text to the left
public const RIGHT:String = "right"; // allow text to the right
// public const BIGGEST:String = "biggest"; // wrap either left or right, whichever is a bigger space - not yet supported
// public const BOTH:String = "both"; // "allow text on left and right - BUT jump-overs not supported
/** @private */
override public function createParcelExperimental(parcel:Rectangle, wrapType:String) : Boolean
{
// trace("LPL: createParcel");
// trace("createParcel start", parcel.toString());
// traceOut();
parcel = getSlugParcel(parcel, _blockProgression);
if (!parcel)
return false; // doesn't fit
// trace("before");
// traceOut();
// trace("parcel", parcel.toString());
var p:Parcel = getParcelAtIndex(0);
var rotationPoint:Point = new Point(p.left, p.top);
if (_blockProgression == BlockProgression.RL)
{
// rotate parcels to TB orientation
for (var i:int = 0; i < numParcels; i++)
{
p = getParcelAtIndex(i);
p.replaceBounds(transformRect(p, rotationPoint, true));
}
parcel = transformRect(parcel, rotationPoint, true);
}
// Create a new parcel, and insert it in the correct location in the parcels array
var targetController:ContainerController = _currentParcel.controller;
addWrapExperimental(parcel, targetController, wrapType);
_currentParcelIndex = numParcels;
_currentParcel = null;
for (var l:int = 0; l < numParcels; l++)
{
p = getParcelAtIndex(l);
if (p.equals(parcel))
{
_currentParcelIndex = l;
_currentParcel = p;
break;
}
}
CONFIG::debug { assert (_currentParcelIndex != numParcels, "New parcel not current!"); }
if (_blockProgression == BlockProgression.RL)
{
// rotate back
parcel = transformRect(parcel, rotationPoint, false);
for (i = 0; i < numParcels; i++)
{
p = getParcelAtIndex(i);
p.replaceBounds(transformRect(p, rotationPoint, false));
}
}
// trace("createParcel done");
// traceOut();
return true;
}
private function transformRect(r:Rectangle, rotationPoint:Point, up:Boolean):Rectangle
{
var dx:Number = r.x - rotationPoint.x;
var dy:Number = (up ? r.y : r.bottom) - rotationPoint.y;
if (up)
return new Rectangle((rotationPoint.x + dy), (rotationPoint.y - dx) - r.width, r.height, r.width);
return new Rectangle((rotationPoint.x - dy), (rotationPoint.y + dx), r.height, r.width);
}
private function addWrapExperimental(wrap:Rectangle, controller:ContainerController, wrapType:String):void
{
// trace("addWrap", wrap.toString());
// traceOut();
// Subtracts the wrap from the space available for laying out text.
var insertWrap:Boolean = true;
var newparcels:Array = new Array(); /* of Parcel */
for (var idx:int = 0; idx < numParcels; idx++)
{
var p:Parcel = getParcelAtIndex(idx);
if (p.controller == controller && wrap.intersects(p))
{
// If the wrap intersects the area, return the unintersected
// part of the area as an array of parcels that replaces
// the area. If the area is entirely contained within the wrap,
// this will remove the area.
//
// If the wrap appears inside the area, pick the larger side
// to be in the area, and let the smaller side drop out as if
// it were part of the wrap. Note that this means we do not
// support jump-overs.
unintersectExperimental(newparcels, p, wrap, wrapType, insertWrap);
insertWrap = false;
}
else
newparcels.push(p);
}
// Keep track of the number of wraps per text frame. It is needed to for vertical alignment.
if (controller.container is IFloatController)
IFloatController(controller.container).numWraps++;
parcels = newparcels;
CONFIG::debug { validate(); }
// trace("after");
// traceOut();
}
private function unintersectExperimental(result:Array, parcel:Parcel, wrap:Rectangle, wrapType:String, insertWrap:Boolean):void
{
var area:Rectangle = parcel;
// trace("LPL: unintersect");
var columnCoverage:int = parcel.columnCoverage;
// Return the unintersected part of the area that wrap does not
// intersect as an array of parcels
var r:Rectangle = area.intersection(wrap);
// split into zero, one, two or three chunks
var addTopParcel:Boolean = r.top > area.top;
var addMidParcel:Boolean = ((r.left - area.left > Math.max(area.right - r.right, MIN_WIDTH)) || (area.right > r.right && area.right - r.right > MIN_WIDTH));
var addBotParcel:Boolean = area.bottom > r.bottom;
var newCoverage:int; // scratch for computing coverage
var parcelRect:Rectangle;
// Space above the intersection
if (addTopParcel)
{
newCoverage = columnCoverage;
if (addBotParcel || addMidParcel)
newCoverage &= ~Parcel.BOT_OF_COLUMN;
columnCoverage &= ~Parcel.TOP_OF_COLUMN;
result.push(new Parcel(area.left, area.top, area.width, r.top - area.top, parcel.controller, parcel.column, newCoverage));
}
newCoverage = columnCoverage;
if (addBotParcel)
newCoverage &= ~Parcel.BOT_OF_COLUMN;
columnCoverage &= ~Parcel.TOP_OF_COLUMN;
// Push our parcel
if (insertWrap)
result.push(new Parcel(wrap.x, wrap.y, wrap.width, wrap.height, parcel.controller, parcel.column, newCoverage));
// Allocate space beside the parcel, depending on wrapType
if (wrapType == LEFT)
{
if (r.left - area.left > MIN_WIDTH)
result.push(new Parcel(area.left, r.top, r.left - area.left, r.height, parcel.controller, parcel.column, newCoverage));
}
if (wrapType == RIGHT)
{
if (area.right - r.right > MIN_WIDTH)
result.push(new Parcel(r.right, r.top, area.right - r.right, r.height, parcel.controller, parcel.column, newCoverage));
}
// Space along the bottom
if (addBotParcel)
result.push(new Parcel(area.left, r.bottom, area.width, area.bottom - r.bottom, parcel.controller, parcel.column, columnCoverage));
}
private function addWrap(wrap:Rectangle, controller:ContainerController, verticalJump:Boolean):void
{
// trace("LPL: addWrap");
// trace("addWrap", wrap.toString());
// trace("before");
// traceOut();
// Subtracts the wrap from the space available for laying out text.
var newparcels:Array = new Array(); /* of Parcel */
for (var idx:int = 0; idx < numParcels; idx++)
{
var p:Parcel = getParcelAtIndex(idx);
if (p.controller == controller && wrap.intersects(p))
{
// If the wrap intersects the area, return the unintersected
// part of the area as an array of parcels that replaces
// the area. If the area is entirely contained within the wrap,
// this will remove the area.
//
// If the wrap appears inside the area, pick the larger side
// to be in the area, and let the smaller side drop out as if
// it were part of the wrap. Note that this means we do not
// support jump-overs.
unintersect(newparcels, p, wrap, verticalJump);
// verticalJump = false; // Yuck! Turning off multiple insertions
}
else
newparcels.push(p);
}
// Keep track of the number of wraps per text frame. It is needed to for vertical alignment.
if (controller.container is IFloatController)
IFloatController(controller.container).numWraps++;
parcels = newparcels;
CONFIG::debug { validate(); }
// trace("after");
// traceOut();
}
private function unintersect(result:Array, parcel:Parcel, wrap:Rectangle, verticalJump:Boolean):void
{
// trace("LPL: unintersect");
var area:Rectangle = parcel;
var columnCoverage:int = parcel.columnCoverage;
// Return the unintersected part of the area that wrap does not
// intersect as an array of parcels
var r:Rectangle = area.intersection(wrap);
// split into zero, one, two or three chunks
var addTopParcel:Boolean = r.top > area.top;
var addMidParcel:Boolean = !verticalJump && ((r.left - area.left > Math.max(area.right - r.right, MIN_WIDTH)) || (area.right > r.right && area.right - r.right > MIN_WIDTH));
var addBotParcel:Boolean = area.bottom > r.bottom;
var newCoverage:int; // scratch for computing coverage
// Space above the intersection
if (addTopParcel)
{
newCoverage = columnCoverage;
if (addBotParcel || addMidParcel)
newCoverage &= ~Parcel.BOT_OF_COLUMN;
columnCoverage &= ~Parcel.TOP_OF_COLUMN;
result.push(new Parcel(area.left, area.top, area.width, r.top - area.top, parcel.controller, parcel.column, newCoverage));
}
// Space along the left side of the intersection, if the width is great than on the right
if (addMidParcel)
{
newCoverage = columnCoverage;
if (addBotParcel)
newCoverage &= ~Parcel.BOT_OF_COLUMN;
columnCoverage &= ~Parcel.TOP_OF_COLUMN;
if (r.left - area.left > Math.max(area.right - r.right, MIN_WIDTH))
result.push(new Parcel(area.left, r.top, r.left - area.left, r.height, parcel.controller, parcel.column, newCoverage));
// Space along the right side if that's bigger
else // if (area.right > r.right && area.right - r.right > MIN_WIDTH)
result.push(new Parcel(r.right, r.top, area.right - r.right, r.height, parcel.controller, parcel.column, newCoverage));
}
// Space along the bottom
if (addBotParcel)
result.push(new Parcel(area.left, r.bottom, area.width, area.bottom - r.bottom, parcel.controller, parcel.column, columnCoverage));
}
/** @private */
CONFIG::debug private function validate():void
{
// Check that there are no empty parcels, and that no parcels intersect any other parcel.
for (var idx:int; idx < numParcels; idx++)
{
var i:Parcel = getParcelAtIndex(idx);
var parcelRect:Rectangle = i;
assert(parcelRect.height != 0 && parcelRect.width != 0, "empty parcel");
// assert(parcelRect.width > MIN_WIDTH, "parcel too small"); we can get a small parcel if we explicitly ask for it (e.g., for a small float)
for (var nidx:int; nidx < numParcels; nidx++)
{
var n:Parcel = getParcelAtIndex(nidx);
assert(i == n || i.controller != n.controller || !parcelRect.intersects(n), "parcels intersect");
}
}
}
CONFIG::debug {
private function traceOut():void
{
trace("parcels\n");
for (var idx:int; idx < numParcels; idx++)
{
var p:Parcel = getParcelAtIndex(idx);
trace(idx.toString(), ":", p, "controller", p.controller);
}
// traceRects("wraps\n", wraps);
}
}
CONFIG::debug {
private function traceRects(description:String, rects:Array):void
{
trace(description);
var count:int = 0;
for each (var i:Rectangle in rects)
{
trace(count.toString(), i);
count++;
}
}
}
CONFIG::debug {
private function traceParcels(description:String, parcels:Array /* of Parcel */):void
{
trace(description);
var count:int = 0;
for each (var i:Parcel in parcels)
{
trace(count.toString(), ":", i, "controller", i.controller);
count++;
}
}
}
} //end class
} //end package