/*******************************************************************************
* Copyright (c) 2004, 2008 John Krasnay and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* John Krasnay - initial API and implementation
*******************************************************************************/
package net.sf.vex.layout;
import java.util.ArrayList;
import java.util.List;
import net.sf.vex.core.Drawable;
import net.sf.vex.core.FontMetrics;
import net.sf.vex.core.FontResource;
import net.sf.vex.core.Graphics;
import net.sf.vex.core.Rectangle;
import net.sf.vex.css.Styles;
import net.sf.vex.dom.Element;
import net.sf.vex.dom.Node;
import net.sf.vex.dom.Text;
/**
* An inline box that represents an inline element. This box is responsible
* for creating and laying out its child boxes.
*/
public class InlineElementBox extends CompositeInlineBox {
private Element element;
private InlineBox[] children;
private InlineBox firstContentChild = null;
private InlineBox lastContentChild = null;
private int baseline;
private int halfLeading;
/**
* Class constructor, called by the createInlineBoxes static factory method.
* @param context LayoutContext to use.
* @param element Element that generated this box
* @param startOffset Start offset of the range being rendered, which may be arbitrarily
* before or inside the element.
* @param endOffset End offset of the range being rendered, which may be arbitrarily
* after or inside the element.
*/
private InlineElementBox(LayoutContext context, Element element, int startOffset, int endOffset) {
this.element = element;
List childList = new ArrayList();
Styles styles = context.getStyleSheet().getStyles(element);
if (startOffset <= element.getStartOffset()) {
// space for the left margin/border/padding
int space = styles.getMarginLeft().get(0)
+ styles.getBorderLeftWidth()
+ styles.getPaddingLeft().get(0);
if (space > 0) {
childList.add(new SpaceBox(space, 1));
}
// :before content
Element beforeElement = context.getStyleSheet().getBeforeElement(element);
if (beforeElement != null) {
childList.addAll(LayoutUtils.createGeneratedInlines(context, beforeElement));
}
// left marker
childList.add(createLeftMarker(element, styles));
}
InlineBoxes inlines = createInlineBoxes(context, element, startOffset, endOffset);
childList.addAll(inlines.boxes);
this.firstContentChild = inlines.firstContentBox;
this.lastContentChild = inlines.lastContentBox;
if (endOffset > element.getEndOffset()) {
childList.add(new PlaceholderBox(context, element, element.getEndOffset() - element.getStartOffset()));
// trailing marker
childList.add(createRightMarker(element, styles));
// :after content
Element afterElement = context.getStyleSheet().getAfterElement(element);
if (afterElement != null) {
childList.addAll(LayoutUtils.createGeneratedInlines(context, afterElement));
}
// space for the right margin/border/padding
int space = styles.getMarginRight().get(0)
+ styles.getBorderRightWidth()
+ styles.getPaddingRight().get(0);
if (space > 0) {
childList.add(new SpaceBox(space, 1));
}
}
this.children = (InlineBox[]) childList.toArray(new InlineBox[childList.size()]);
this.layout(context);
}
/**
* Class constructor. This constructor is called by the split method.
* @param context LayoutContext used for the layout.
* @param element Element to which this box applies.
* @param children Child boxes.
*/
private InlineElementBox(LayoutContext context, Element element, InlineBox[] children) {
this.element = element;
this.children = children;
this.layout(context);
for (int i = 0; i < children.length; i++) {
InlineBox child = children[i];
if (child.hasContent()) {
if (this.firstContentChild == null) {
this.firstContentChild = child;
}
this.lastContentChild = child;
}
}
}
/**
* @see net.sf.vex.layout.InlineBox#getBaseline()
*/
public int getBaseline() {
return this.baseline;
}
/**
* @see net.sf.vex.layout.Box#getChildren()
*/
public Box[] getChildren() {
return this.children;
}
/**
* Returns the element associated with this box.
*/
public Element getElement() {
return this.element;
}
/**
* @see net.sf.vex.layout.Box#getEndOffset()
*/
public int getEndOffset() {
if (this.lastContentChild == null) {
return this.getElement().getEndOffset();
} else {
return this.lastContentChild.getEndOffset();
}
}
/**
* @see net.sf.vex.layout.Box#getStartOffset()
*/
public int getStartOffset() {
if (this.firstContentChild == null) {
return this.getElement().getStartOffset();
} else {
return this.firstContentChild.getStartOffset();
}
}
/**
* Override to paint background and borders.
* @see net.sf.vex.layout.AbstractBox#paint(net.sf.vex.layout.LayoutContext, int, int)
*/
public void paint(LayoutContext context, int x, int y) {
this.drawBox(context, x, y, 0, true); // TODO CSS violation
super.paint(context, x, y);
}
public Pair split(LayoutContext context, InlineBox[] lefts, InlineBox[] rights) {
InlineElementBox left = null;
InlineElementBox right = null;
if (lefts.length > 0 || rights.length == 0) {
left = new InlineElementBox(context, this.getElement(), lefts);
}
if (rights.length > 0) {
right = new InlineElementBox(context, this.getElement(), rights);
}
return new Pair(left, right);
}
public String toString() {
StringBuffer sb = new StringBuffer();
if (this.getStartOffset() == this.getElement().getStartOffset() + 1) {
sb.append("<");
sb.append(this.getElement().getName());
sb.append(">");
}
Box[] children = this.getChildren();
for (int i = 0; i < children.length; i++) {
sb.append(children[i]);
}
if (this.getEndOffset() == this.getElement().getEndOffset()) {
sb.append("</");
sb.append(this.getElement().getName());
sb.append(">");
}
return sb.toString();
}
/**
* Holds the results of the createInlineBoxes method.
*/
static class InlineBoxes {
/** List of generated boxes */
public List boxes = new ArrayList();
/** First generated box that has content */
public InlineBox firstContentBox;
/** Last generated box that has content */
public InlineBox lastContentBox;
}
/**
* Creates a list of inline boxes given a range of offsets. This method is
* used when creating both ParagraphBoxes and InlineElementBoxes.
* @param context LayoutContext to be used.
* @param containingElement Element containing both offsets
* @param startOffset The start of the range to convert to inline boxes.
* @param endOffset The end of the range to convert to inline boxes.
* @return
*/
static InlineBoxes createInlineBoxes(LayoutContext context, Element containingElement, int startOffset, int endOffset) {
InlineBoxes result = new InlineBoxes();
Node[] nodes = containingElement.getChildNodes();
for (int i = 0; i < nodes.length; i++) {
Node node = nodes[i];
InlineBox child;
if (node.getStartOffset() >= endOffset) {
break;
} else if (node instanceof Text) {
// This check is different for Text and Element, so we have to
// do it here and below, too.
if (node.getEndOffset() <= startOffset) {
continue;
}
int start = Math.max(startOffset, node.getStartOffset());
int end = Math.min(endOffset, node.getEndOffset());
child = new DocumentTextBox(context, containingElement, start, end);
} else {
if (node.getEndOffset() < startOffset) {
continue;
}
Element childElement = (Element) node;
InlineBox placeholder = new PlaceholderBox(context, containingElement, childElement.getStartOffset() - containingElement.getStartOffset());
result.boxes.add(placeholder);
if (result.firstContentBox == null) {
result.firstContentBox = placeholder;
}
child = new InlineElementBox(context, childElement, startOffset, endOffset);
}
if (result.firstContentBox == null) {
result.firstContentBox = child;
}
result.lastContentBox = child;
result.boxes.add(child);
}
return result;
}
//========================================================== PRIVATE
private static InlineBox createLeftMarker(Element element, Styles styles) {
final int size = Math.round(0.5f * styles.getFontSize());
final int lift = Math.round(0.1f * styles.getFontSize());
Drawable drawable = new Drawable() {
public void draw(Graphics g, int x, int y) {
g.setLineStyle(Graphics.LINE_SOLID);
g.setLineWidth(1);
y -= lift;
g.drawLine(x, y - size, x, y);
g.drawLine(x, y, x + size - 1, y - size/2);
g.drawLine(x + size - 1, y - size/2, x, y - size);
}
public Rectangle getBounds() {
return new Rectangle(0, -size, size, size);
}
};
return new DrawableBox(drawable, element, DrawableBox.START_MARKER);
}
private static InlineBox createRightMarker(Element element, Styles styles) {
final int size = Math.round(0.5f * styles.getFontSize());
final int lift = Math.round(0.1f * styles.getFontSize());
Drawable drawable = new Drawable() {
public void draw(Graphics g, int x, int y) {
g.setLineStyle(Graphics.LINE_SOLID);
g.setLineWidth(1);
y -= lift;
g.drawLine(x + size - 1, y - size, x + size - 1, y);
g.drawLine(x + size - 1, y, x, y - size/2);
g.drawLine(x, y - size/2, x + size - 1, y - size);
}
public Rectangle getBounds() {
return new Rectangle(0, -size, size, size);
}
};
return new DrawableBox(drawable, element, DrawableBox.END_MARKER);
}
private void layout(LayoutContext context) {
Graphics g = context.getGraphics();
Styles styles = context.getStyleSheet().getStyles(element);
FontResource font = g.createFont(styles.getFont());
FontResource oldFont = g.setFont(font);
FontMetrics fm = g.getFontMetrics();
this.setHeight(styles.getLineHeight());
this.halfLeading = (styles.getLineHeight() - fm.getAscent() - fm.getDescent()) / 2;
this.baseline = this.halfLeading + fm.getAscent();
g.setFont(oldFont);
font.dispose();
int x = 0;
for (int i = 0; i < this.children.length; i++) {
InlineBox child = this.children[i];
// TODO: honour the child's vertical-align property
child.setX(x);
child.setY(this.baseline - child.getBaseline());
x += child.getWidth();
}
this.setWidth(x);
}
}