blob: be606f7084f9ced89b1ebadad9a0f38c79a153ea [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.
*/
/**
* @author Oleg V. Khaschansky
* @version $Revision$
*
*/
package org.apache.harmony.awt.gl.font;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.awt.im.InputMethodHighlight;
import java.awt.font.*;
import java.awt.*;
import java.text.AttributedCharacterIterator;
import java.text.Annotation;
import java.text.AttributedCharacterIterator.Attribute;
import java.util.*;
import org.apache.harmony.awt.gl.font.TextDecorator.Decoration;
import org.apache.harmony.awt.internal.nls.Messages;
import org.apache.harmony.misc.HashCode;
// TODO - bidi not implemented yet
/**
* This class is responsible for breaking the text into the run segments
* with constant font, style, other text attributes and direction.
* It also stores the created text run segments and covers functionality
* related to the operations on the set of segments, like calculating metrics,
* rendering, justification, hit testing, etc.
*/
public class TextRunBreaker implements Cloneable {
AttributedCharacterIterator aci;
FontRenderContext frc;
char[] text;
byte[] levels;
HashMap<Integer, Font> fonts;
HashMap<Integer, Decoration> decorations;
// Related to default font substitution
int forcedFontRunStarts[];
ArrayList<TextRunSegment> runSegments = new ArrayList<TextRunSegment>();
// For fast retrieving of the segment containing
// character with known logical index
int logical2segment[];
int segment2visual[]; // Visual order of segments TODO - implement
int visual2segment[];
int logical2visual[];
int visual2logical[];
SegmentsInfo storedSegments;
private boolean haveAllSegments = false;
int segmentsStart, segmentsEnd;
float justification = 1.0f;
public TextRunBreaker(AttributedCharacterIterator aci, FontRenderContext frc) {
this.aci = aci;
this.frc = frc;
segmentsStart = aci.getBeginIndex();
segmentsEnd = aci.getEndIndex();
int len = segmentsEnd - segmentsStart;
text = new char[len];
aci.setIndex(segmentsEnd);
while (len-- != 0) { // Going in backward direction is faster? Simplier checks here?
text[len] = aci.previous();
}
createStyleRuns();
}
/**
* Visual order of text segments may differ from the logical order.
* This method calculates visual position of the segment from its logical position.
* @param segmentNum - logical position of the segment
* @return visual position of the segment
*/
int getVisualFromSegmentOrder(int segmentNum) {
return (segment2visual == null) ? segmentNum : segment2visual[segmentNum];
}
/**
* Visual order of text segments may differ from the logical order.
* This method calculates logical position of the segment from its visual position.
* @param visual - visual position of the segment
* @return logical position of the segment
*/
int getSegmentFromVisualOrder(int visual) {
return (visual2segment == null) ? visual : visual2segment[visual];
}
/**
* Visual order of the characters may differ from the logical order.
* This method calculates visual position of the character from its logical position.
* @param logical - logical position of the character
* @return visual position
*/
int getVisualFromLogical(int logical) {
return (logical2visual == null) ? logical : logical2visual[logical];
}
/**
* Visual order of the characters may differ from the logical order.
* This method calculates logical position of the character from its visual position.
* @param visual - visual position
* @return logical position
*/
int getLogicalFromVisual(int visual) {
return (visual2logical == null) ? visual : visual2logical[visual];
}
/**
* Calculates the end index of the level run, limited by the given text run.
* @param runStart - run start
* @param runEnd - run end
* @return end index of the level run
*/
int getLevelRunLimit(int runStart, int runEnd) {
if (levels == null) {
return runEnd;
}
int endLevelRun = runStart + 1;
byte level = levels[runStart];
while (endLevelRun <= runEnd && levels[endLevelRun] == level) {
endLevelRun++;
}
return endLevelRun;
}
/**
* Adds InputMethodHighlight to the attributes
* @param attrs - text attributes
* @return patched text attributes
*/
Map<? extends Attribute, ?> unpackAttributes(Map<? extends Attribute, ?> attrs) {
if (attrs.containsKey(TextAttribute.INPUT_METHOD_HIGHLIGHT)) {
Map<TextAttribute, ?> styles = null;
Object val = attrs.get(TextAttribute.INPUT_METHOD_HIGHLIGHT);
if (val instanceof Annotation) {
val = ((Annotation) val).getValue();
}
if (val instanceof InputMethodHighlight) {
InputMethodHighlight ihl = ((InputMethodHighlight) val);
styles = ihl.getStyle();
if (styles == null) {
Toolkit tk = Toolkit.getDefaultToolkit();
styles = tk.mapInputMethodHighlight(ihl);
}
}
if (styles != null) {
HashMap<Attribute, Object> newAttrs = new HashMap<Attribute, Object>();
newAttrs.putAll(attrs);
newAttrs.putAll(styles);
return newAttrs;
}
}
return attrs;
}
/**
* Breaks the text into separate style runs.
*/
void createStyleRuns() {
// TODO - implement fast and simple case
fonts = new HashMap<Integer, Font>();
decorations = new HashMap<Integer, Decoration>();
////
ArrayList<Integer> forcedFontRunStartsList = null;
Map<? extends Attribute, ?> attributes = null;
// Check justification attribute
Object val = aci.getAttribute(TextAttribute.JUSTIFICATION);
if (val != null) {
justification = ((Float) val).floatValue();
}
for (
int index = segmentsStart, nextRunStart = segmentsStart;
index < segmentsEnd;
index = nextRunStart, aci.setIndex(index)
) {
nextRunStart = aci.getRunLimit();
attributes = unpackAttributes(aci.getAttributes());
TextDecorator.Decoration d = TextDecorator.getDecoration(attributes);
decorations.put(new Integer(index), d);
// Find appropriate font or place GraphicAttribute there
// 1. Try to pick up CHAR_REPLACEMENT (compatibility)
Font value = (Font)attributes.get(TextAttribute.CHAR_REPLACEMENT);
if (value == null) {
// 2. Try to Get FONT
value = (Font)attributes.get(TextAttribute.FONT);
if (value == null) {
// 3. Try to create font from FAMILY
if (attributes.get(TextAttribute.FAMILY) != null) {
value = Font.getFont(attributes);
}
if (value == null) {
// 4. No attributes found, using default.
if (forcedFontRunStartsList == null) {
forcedFontRunStartsList = new ArrayList<Integer>();
}
FontFinder.findFonts(
text,
index,
nextRunStart,
forcedFontRunStartsList,
fonts
);
value = fonts.get(new Integer(index));
}
}
}
fonts.put(new Integer(index), value);
}
// We have added some default fonts, so we have some extra runs in text
if (forcedFontRunStartsList != null) {
forcedFontRunStarts = new int[forcedFontRunStartsList.size()];
for (int i=0; i<forcedFontRunStartsList.size(); i++) {
forcedFontRunStarts[i] =
forcedFontRunStartsList.get(i).intValue();
}
}
}
/**
* Starting from the current position looks for the end of the text run with
* constant text attributes.
* @param runStart - start position
* @param maxPos - position where to stop if no run limit found
* @return style run limit
*/
int getStyleRunLimit(int runStart, int maxPos) {
try {
aci.setIndex(runStart);
} catch(IllegalArgumentException e) { // Index out of bounds
if (runStart < segmentsStart) {
aci.first();
} else {
aci.last();
}
}
// If we have some extra runs we need to check for their limits
if (forcedFontRunStarts != null) {
for (int element : forcedFontRunStarts) {
if (element > runStart) {
maxPos = Math.min(element, maxPos);
break;
}
}
}
return Math.min(aci.getRunLimit(), maxPos);
}
/**
* Creates segments for the text run with
* constant decoration, font and bidi level
* @param runStart - run start
* @param runEnd - run end
*/
public void createSegments(int runStart, int runEnd) {
int endStyleRun, endLevelRun;
// TODO - update levels
int pos = runStart, levelPos;
aci.setIndex(pos);
final int firstRunStart = aci.getRunStart();
Object tdd = decorations.get(new Integer(firstRunStart));
Object fontOrGAttr = fonts.get(new Integer(firstRunStart));
logical2segment = new int[runEnd - runStart];
do {
endStyleRun = getStyleRunLimit(pos, runEnd);
// runStart can be non-zero, but all arrays will be indexed from 0
int ajustedPos = pos - runStart;
int ajustedEndStyleRun = endStyleRun - runStart;
levelPos = ajustedPos;
do {
endLevelRun = getLevelRunLimit(levelPos, ajustedEndStyleRun);
if (fontOrGAttr instanceof GraphicAttribute) {
runSegments.add(
new TextRunSegmentImpl.TextRunSegmentGraphic(
(GraphicAttribute)fontOrGAttr,
endLevelRun - levelPos,
levelPos + runStart)
);
Arrays.fill(logical2segment, levelPos, endLevelRun, runSegments.size()-1);
} else {
TextRunSegmentImpl.TextSegmentInfo i =
new TextRunSegmentImpl.TextSegmentInfo(
levels == null ? 0 : levels[ajustedPos],
(Font) fontOrGAttr,
frc,
text,
levelPos + runStart,
endLevelRun + runStart
);
runSegments.add(
new TextRunSegmentImpl.TextRunSegmentCommon(
i,
(TextDecorator.Decoration) tdd
)
);
Arrays.fill(logical2segment, levelPos, endLevelRun, runSegments.size()-1);
}
levelPos = endLevelRun;
} while (levelPos < ajustedEndStyleRun);
// Prepare next iteration
pos = endStyleRun;
tdd = decorations.get(new Integer(pos));
fontOrGAttr = fonts.get(new Integer(pos));
} while (pos < runEnd);
}
/**
* Checks if text run segments are up to date and creates the new segments if not.
*/
public void createAllSegments() {
if ( !haveAllSegments &&
(logical2segment == null ||
logical2segment.length != segmentsEnd - segmentsStart)
) { // Check if we don't have all segments yet
resetSegments();
createSegments(segmentsStart, segmentsEnd);
}
haveAllSegments = true;
}
/**
* Calculates position where line should be broken without
* taking into account word boundaries.
* @param start - start index
* @param maxAdvance - maximum advance, width of the line
* @return position where to break
*/
public int getLineBreakIndex(int start, float maxAdvance) {
int breakIndex;
TextRunSegment s = null;
for (
int segmentIndex = logical2segment[start];
segmentIndex < runSegments.size();
segmentIndex++
) {
s = runSegments.get(segmentIndex);
breakIndex = s.getCharIndexFromAdvance(maxAdvance, start);
if (breakIndex < s.getEnd()) {
return breakIndex;
}
maxAdvance -= s.getAdvanceDelta(start, s.getEnd());
start = s.getEnd();
}
return s.getEnd();
}
/**
* Inserts character into the managed text.
* @param newParagraph - new character iterator
* @param insertPos - insertion position
*/
public void insertChar(AttributedCharacterIterator newParagraph, int insertPos) {
aci = newParagraph;
char insChar = aci.setIndex(insertPos);
Integer key = new Integer(insertPos);
insertPos -= aci.getBeginIndex();
char newText[] = new char[text.length + 1];
System.arraycopy(text, 0, newText, 0, insertPos);
newText[insertPos] = insChar;
System.arraycopy(text, insertPos, newText, insertPos+1, text.length - insertPos);
text = newText;
if (aci.getRunStart() == key.intValue() && aci.getRunLimit() == key.intValue() + 1) {
createStyleRuns(); // We have to create one new run, could be optimized
} else {
shiftStyleRuns(key, 1);
}
resetSegments();
segmentsEnd++;
}
/**
* Deletes character from the managed text.
* @param newParagraph - new character iterator
* @param deletePos - deletion position
*/
public void deleteChar(AttributedCharacterIterator newParagraph, int deletePos) {
aci = newParagraph;
Integer key = new Integer(deletePos);
deletePos -= aci.getBeginIndex();
char newText[] = new char[text.length - 1];
System.arraycopy(text, 0, newText, 0, deletePos);
System.arraycopy(text, deletePos+1, newText, deletePos, newText.length - deletePos);
text = newText;
if (fonts.get(key) != null) {
fonts.remove(key);
}
shiftStyleRuns(key, -1);
resetSegments();
segmentsEnd--;
}
/**
* Shift all runs after specified position, needed to perfom insertion
* or deletion in the managed text
* @param pos - position where to start
* @param shift - shift, could be negative
*/
private void shiftStyleRuns(Integer pos, final int shift) {
ArrayList<Integer> keys = new ArrayList<Integer>();
Integer key, oldkey;
for (Iterator<Integer> it = fonts.keySet().iterator(); it.hasNext(); ) {
oldkey = it.next();
if (oldkey.intValue() > pos.intValue()) {
keys.add(oldkey);
}
}
for (int i=0; i<keys.size(); i++) {
oldkey = keys.get(i);
key = new Integer(shift + oldkey.intValue());
fonts.put(key, fonts.remove(oldkey));
decorations.put(key, decorations.remove(oldkey));
}
}
/**
* Resets state of the class
*/
private void resetSegments() {
runSegments = new ArrayList<TextRunSegment>();
logical2segment = null;
segment2visual = null;
visual2segment = null;
levels = null;
haveAllSegments = false;
}
private class SegmentsInfo {
ArrayList<TextRunSegment> runSegments;
int logical2segment[];
int segment2visual[];
int visual2segment[];
byte levels[];
int segmentsStart;
int segmentsEnd;
}
/**
* Saves the internal state of the class
* @param newSegStart - new start index in the text
* @param newSegEnd - new end index in the text
*/
public void pushSegments(int newSegStart, int newSegEnd) {
storedSegments = new SegmentsInfo();
storedSegments.runSegments = this.runSegments;
storedSegments.logical2segment = this.logical2segment;
storedSegments.segment2visual = this.segment2visual;
storedSegments.visual2segment = this.visual2segment;
storedSegments.levels = this.levels;
storedSegments.segmentsStart = segmentsStart;
storedSegments.segmentsEnd = segmentsEnd;
resetSegments();
segmentsStart = newSegStart;
segmentsEnd = newSegEnd;
}
/**
* Restores the internal state of the class
*/
public void popSegments() {
if (storedSegments == null) {
return;
}
this.runSegments = storedSegments.runSegments;
this.logical2segment = storedSegments.logical2segment;
this.segment2visual = storedSegments.segment2visual;
this.visual2segment = storedSegments.visual2segment;
this.levels = storedSegments.levels;
this.segmentsStart = storedSegments.segmentsStart;
this.segmentsEnd = storedSegments.segmentsEnd;
storedSegments = null;
if (runSegments.size() == 0 && logical2segment == null) {
haveAllSegments = false;
} else {
haveAllSegments = true;
}
}
@Override
public Object clone() {
try {
TextRunBreaker res = (TextRunBreaker) super.clone();
res.storedSegments = null;
ArrayList<TextRunSegment> newSegments = new ArrayList<TextRunSegment>(runSegments.size());
for (int i = 0; i < runSegments.size(); i++) {
TextRunSegment seg = runSegments.get(i);
newSegments.add((TextRunSegment)seg.clone());
}
res.runSegments = newSegments;
return res;
} catch (CloneNotSupportedException e) {
// awt.3E=Clone not supported
throw new UnsupportedOperationException(Messages.getString("awt.3E")); //$NON-NLS-1$
}
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof TextRunBreaker)) {
return false;
}
TextRunBreaker br = (TextRunBreaker) obj;
if (br.getACI().equals(aci) && br.frc.equals(frc)) {
return true;
}
return false;
}
@Override
public int hashCode() {
return HashCode.combine(aci.hashCode(), frc.hashCode());
}
/**
* Renders the managed text
* @param g2d - graphics where to render
* @param xOffset - offset in X direction to the upper left corner
* of the layout from the origin of the graphics
* @param yOffset - offset in Y direction to the upper left corner
* of the layout from the origin of the graphics
*/
public void drawSegments(Graphics2D g2d, float xOffset, float yOffset) {
for (int i=0; i<runSegments.size(); i++) {
runSegments.get(i).draw(g2d, xOffset, yOffset);
}
}
/**
* Creates the black box bounds shape
* @param firstEndpoint - start position
* @param secondEndpoint - end position
* @return black box bounds shape
*/
public Shape getBlackBoxBounds(int firstEndpoint, int secondEndpoint) {
GeneralPath bounds = new GeneralPath();
TextRunSegment segment;
for (int idx = firstEndpoint; idx < secondEndpoint; idx=segment.getEnd()) {
segment = runSegments.get(logical2segment[idx]);
bounds.append(segment.getCharsBlackBoxBounds(idx, secondEndpoint), false);
}
return bounds;
}
/**
* Creates visual bounds shape
* @return visual bounds rectangle
*/
public Rectangle2D getVisualBounds() {
Rectangle2D bounds = null;
for (int i=0; i<runSegments.size(); i++) {
TextRunSegment s = runSegments.get(i);
if (bounds != null) {
Rectangle2D.union(bounds, s.getVisualBounds(), bounds);
} else {
bounds = s.getVisualBounds();
}
}
return bounds;
}
/**
* Creates logical bounds shape
* @return logical bounds rectangle
*/
public Rectangle2D getLogicalBounds() {
Rectangle2D bounds = null;
for (int i=0; i<runSegments.size(); i++) {
TextRunSegment s = runSegments.get(i);
if (bounds != null) {
Rectangle2D.union(bounds, s.getLogicalBounds(), bounds);
} else {
bounds = s.getLogicalBounds();
}
}
return bounds;
}
public int getCharCount() {
return segmentsEnd - segmentsStart;
}
public byte getLevel(int idx) {
if (levels == null) {
return 0;
}
return levels[idx];
}
public int getBaseLevel() {
return 0;
}
public boolean isLTR() {
return true;
}
public char getChar(int index) {
return text[index];
}
public AttributedCharacterIterator getACI() {
return aci;
}
/**
* Creates outline shape for the managed text
* @return outline
*/
public GeneralPath getOutline() {
GeneralPath outline = new GeneralPath();
TextRunSegment segment;
for (int i = 0; i < runSegments.size(); i++) {
segment = runSegments.get(i);
outline.append(segment.getOutline(), false);
}
return outline;
}
/**
* Calculates text hit info from the screen coordinates.
* Current implementation totally ignores Y coordinate.
* If X coordinate is outside of the layout boundaries, this
* method returns leftmost or rightmost hit.
* @param x - x coordinate of the hit
* @param y - y coordinate of the hit
* @return hit info
*/
public TextHitInfo hitTest(float x, float y) {
TextRunSegment segment;
double endOfPrevSeg = -1;
for (int i = 0; i < runSegments.size(); i++) {
segment = runSegments.get(i);
Rectangle2D bounds = segment.getVisualBounds();
if ((bounds.getMinX() <= x && bounds.getMaxX() >= x) || // We are in the segment
(endOfPrevSeg < x && bounds.getMinX() > x)) { // We are somewhere between the segments
return segment.hitTest(x,y);
}
endOfPrevSeg = bounds.getMaxX();
}
return isLTR() ? TextHitInfo.trailing(text.length) : TextHitInfo.leading(0);
}
public float getJustification() {
return justification;
}
/**
* Calculates position of the last non whitespace character
* in the managed text.
* @return position of the last non whitespace character
*/
public int getLastNonWhitespace() {
int lastNonWhitespace = text.length;
while (lastNonWhitespace >= 0) {
lastNonWhitespace--;
if (!Character.isWhitespace(text[lastNonWhitespace])) {
break;
}
}
return lastNonWhitespace;
}
/**
* Performs justification of the managed text by changing segment positions
* and positions of the glyphs inside of the segments.
* @param gap - amount of space which should be compensated by justification
*/
public void justify(float gap) {
// Ignore trailing logical whitespace
int firstIdx = segmentsStart;
int lastIdx = getLastNonWhitespace() + segmentsStart;
JustificationInfo jInfos[] = new JustificationInfo[5];
float gapLeft = gap;
int highestPriority = -1;
// GlyphJustificationInfo.PRIORITY_KASHIDA is 0
// GlyphJustificationInfo.PRIORITY_NONE is 3
for (int priority = 0; priority <= GlyphJustificationInfo.PRIORITY_NONE + 1; priority++) {
JustificationInfo jInfo = new JustificationInfo();
jInfo.lastIdx = lastIdx;
jInfo.firstIdx = firstIdx;
jInfo.grow = gap > 0;
jInfo.gapToFill = gapLeft;
if (priority <= GlyphJustificationInfo.PRIORITY_NONE) {
jInfo.priority = priority;
} else {
jInfo.priority = highestPriority; // Last pass
}
for (int i = 0; i < runSegments.size(); i++) {
TextRunSegment segment = runSegments.get(i);
if (segment.getStart() <= lastIdx) {
segment.updateJustificationInfo(jInfo);
}
}
if (jInfo.priority == highestPriority) {
jInfo.absorb = true;
jInfo.absorbedWeight = jInfo.weight;
}
if (jInfo.weight != 0) {
if (highestPriority < 0) {
highestPriority = priority;
}
jInfos[priority] = jInfo;
} else {
continue;
}
gapLeft -= jInfo.growLimit;
if (((gapLeft > 0) ^ jInfo.grow) || gapLeft == 0) {
gapLeft = 0;
jInfo.gapPerUnit = jInfo.gapToFill/jInfo.weight;
break;
}
jInfo.useLimits = true;
if (jInfo.absorbedWeight > 0) {
jInfo.absorb = true;
jInfo.absorbedGapPerUnit =
(jInfo.gapToFill-jInfo.growLimit)/jInfo.absorbedWeight;
break;
}
}
float currJustificationOffset = 0;
for (int i = 0; i < runSegments.size(); i++) {
TextRunSegment segment =
runSegments.get(getSegmentFromVisualOrder(i));
segment.x += currJustificationOffset;
currJustificationOffset += segment.doJustification(jInfos);
}
justification = -1; // Make further justification impossible
}
/**
* This class represents the information collected before the actual
* justification is started and needed to perform the justification.
* This information is closely related to the information stored in the
* GlyphJustificationInfo for the text represented by glyph vectors.
*/
class JustificationInfo {
boolean grow;
boolean absorb = false;
boolean useLimits = false;
int priority = 0;
float weight = 0;
float absorbedWeight = 0;
float growLimit = 0;
int lastIdx;
int firstIdx;
float gapToFill;
float gapPerUnit = 0; // Precalculated value, gapToFill / weight
float absorbedGapPerUnit = 0; // Precalculated value, gapToFill / weight
}
}