introduce InlineFrame

Signed-off-by: Florian Thienel <florian@thienel.org>
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBoxVisitor.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBoxVisitor.java
index e3cf0bc..fbc6a2e 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBoxVisitor.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBoxVisitor.java
@@ -56,6 +56,11 @@
 	}
 
 	@Override
+	public void visit(final InlineFrame box) {
+		// ignore
+	}
+
+	@Override
 	public void visit(final StaticText box) {
 		// ignore
 	}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBoxVisitorWithResult.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBoxVisitorWithResult.java
index c6256aa..4c41b99 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBoxVisitorWithResult.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BaseBoxVisitorWithResult.java
@@ -66,6 +66,11 @@
 	}
 
 	@Override
+	public T visit(final InlineFrame box) {
+		return defaultValue;
+	}
+
+	@Override
 	public T visit(final StaticText box) {
 		return defaultValue;
 	}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BoxFactory.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BoxFactory.java
index fb9092b..d425675 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BoxFactory.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BoxFactory.java
@@ -54,6 +54,21 @@
 		return frame;
 	}
 
+	public static InlineFrame frame(final IInlineBox component) {
+		final InlineFrame frame = new InlineFrame();
+		frame.setComponent(component);
+		return frame;
+	}
+
+	public static InlineFrame frame(final IInlineBox component, final Margin margin, final Border border, final Padding padding) {
+		final InlineFrame frame = new InlineFrame();
+		frame.setComponent(component);
+		frame.setMargin(margin);
+		frame.setBorder(border);
+		frame.setPadding(padding);
+		return frame;
+	}
+
 	public static StructuralNodeReference nodeReference(final INode node, final IStructuralBox component) {
 		final StructuralNodeReference structuralNodeReference = new StructuralNodeReference();
 		structuralNodeReference.setNode(node);
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/DepthFirstBoxTraversal.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/DepthFirstBoxTraversal.java
index 8f436b0..ebf176a 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/DepthFirstBoxTraversal.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/DepthFirstBoxTraversal.java
@@ -16,11 +16,7 @@
 public abstract class DepthFirstBoxTraversal<T> extends BaseBoxVisitorWithResult<T> {
 
 	public DepthFirstBoxTraversal() {
-		this(null);
-	}
-
-	public DepthFirstBoxTraversal(final T defaultValue) {
-		super(defaultValue);
+		super(null);
 	}
 
 	@Override
@@ -58,6 +54,11 @@
 		return traverseChildren(box);
 	}
 
+	@Override
+	public T visit(final InlineFrame box) {
+		return box.getComponent().accept(this);
+	}
+
 	protected final <C extends IBox> T traverseChildren(final IParentBox<C> box) {
 		for (final C child : box.getChildren()) {
 			final T childResult = child.accept(this);
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBoxVisitor.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBoxVisitor.java
index fbd3634..b537dc8 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBoxVisitor.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBoxVisitor.java
@@ -31,6 +31,8 @@
 
 	void visit(InlineContainer box);
 
+	void visit(InlineFrame box);
+
 	void visit(StaticText box);
 
 	void visit(TextContent box);
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBoxVisitorWithResult.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBoxVisitorWithResult.java
index e1269e8..f36a958 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBoxVisitorWithResult.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IBoxVisitorWithResult.java
@@ -31,6 +31,8 @@
 
 	T visit(InlineContainer box);
 
+	T visit(InlineFrame box);
+
 	T visit(StaticText box);
 
 	T visit(TextContent box);
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineFrame.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineFrame.java
new file mode 100644
index 0000000..cb8a6b7
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineFrame.java
@@ -0,0 +1,279 @@
+package org.eclipse.vex.core.internal.boxes;
+
+import org.eclipse.vex.core.internal.core.Color;
+import org.eclipse.vex.core.internal.core.ColorResource;
+import org.eclipse.vex.core.internal.core.Graphics;
+import org.eclipse.vex.core.internal.core.Rectangle;
+
+public class InlineFrame extends BaseBox implements IInlineBox, IDecoratorBox<IInlineBox> {
+
+	private IBox parent;
+	private int top;
+	private int left;
+	private int width;
+	private int height;
+
+	private Margin margin = Margin.NULL;
+	private Border border = Border.NULL;
+	private Padding padding = Padding.NULL;
+
+	private IInlineBox component;
+
+	@Override
+	public void setParent(final IBox parent) {
+		this.parent = parent;
+	}
+
+	@Override
+	public IBox getParent() {
+		return parent;
+	}
+
+	@Override
+	public int getAbsoluteTop() {
+		if (parent == null) {
+			return top;
+		}
+		return parent.getAbsoluteTop() + top;
+	}
+
+	@Override
+	public int getAbsoluteLeft() {
+		if (parent == null) {
+			return left;
+		}
+		return parent.getAbsoluteLeft() + left;
+	}
+
+	@Override
+	public int getTop() {
+		return top;
+	}
+
+	@Override
+	public int getLeft() {
+		return left;
+	}
+
+	@Override
+	public void setPosition(final int top, final int left) {
+		this.top = top;
+		this.left = left;
+	}
+
+	@Override
+	public int getWidth() {
+		return width;
+	}
+
+	@Override
+	public int getHeight() {
+		return height;
+	}
+
+	@Override
+	public int getBaseline() {
+		if (component == null) {
+			return 0;
+		}
+		return component.getTop() + component.getBaseline();
+	}
+
+	@Override
+	public Rectangle getBounds() {
+		return new Rectangle(left, top, width, height);
+	}
+
+	@Override
+	public void accept(final IBoxVisitor visitor) {
+		visitor.visit(this);
+	}
+
+	@Override
+	public <T> T accept(final IBoxVisitorWithResult<T> visitor) {
+		return visitor.visit(this);
+	}
+
+	public Margin getMargin() {
+		return margin;
+	}
+
+	public void setMargin(final Margin margin) {
+		this.margin = margin;
+	}
+
+	public Border getBorder() {
+		return border;
+	}
+
+	public void setBorder(final Border border) {
+		this.border = border;
+	}
+
+	public Padding getPadding() {
+		return padding;
+	}
+
+	public void setPadding(final Padding padding) {
+		this.padding = padding;
+	}
+
+	@Override
+	public void setComponent(final IInlineBox component) {
+		this.component = component;
+		component.setParent(this);
+	}
+
+	@Override
+	public IInlineBox getComponent() {
+		return component;
+	}
+
+	@Override
+	public void layout(final Graphics graphics) {
+		layoutComponent(graphics);
+
+		calculateBounds();
+	}
+
+	private void calculateBounds() {
+		if (component == null || component.getWidth() == 0 || component.getHeight() == 0) {
+			height = 0;
+			width = 0;
+		} else {
+			height = topFrame() + component.getHeight() + bottomFrame();
+			width = leftFrame() + component.getWidth() + rightFrame();
+		}
+	}
+
+	private void layoutComponent(final Graphics graphics) {
+		if (component == null) {
+			return;
+		}
+		component.setPosition(topFrame(), leftFrame());
+		component.layout(graphics);
+	}
+
+	@Override
+	public boolean reconcileLayout(final Graphics graphics) {
+		final int oldHeight = height;
+		final int oldWidth = width;
+
+		calculateBounds();
+
+		return oldHeight != height || oldWidth != width;
+	}
+
+	private int rightFrame() {
+		return margin.right + border.right + padding.right;
+	}
+
+	private int leftFrame() {
+		return margin.left + border.left + padding.left;
+	}
+
+	private int bottomFrame() {
+		return margin.bottom + border.bottom + padding.bottom;
+	}
+
+	private int topFrame() {
+		return margin.top + border.top + padding.top;
+	}
+
+	@Override
+	public void paint(final Graphics graphics) {
+		drawBorder(graphics);
+		paintComponent(graphics);
+	}
+
+	private void drawBorder(final Graphics graphics) {
+		final ColorResource colorResource = graphics.getColor(Color.BLACK); // TODO store border color
+		graphics.setColor(colorResource);
+
+		drawBorderLine(graphics, border.top, margin.top, margin.left - border.left / 2, margin.top, width - margin.right + border.right / 2);
+		drawBorderLine(graphics, border.left, margin.top - border.top / 2, margin.left, height - margin.bottom + border.bottom / 2, margin.left);
+		drawBorderLine(graphics, border.bottom, height - margin.bottom, margin.left - border.left / 2, height - margin.bottom, width - margin.right + border.right / 2);
+		drawBorderLine(graphics, border.right, margin.top - border.top / 2, width - margin.right, height - margin.bottom + border.bottom / 2, width - margin.right);
+	}
+
+	private void drawBorderLine(final Graphics graphics, final int lineWidth, final int top, final int left, final int bottom, final int right) {
+		if (lineWidth <= 0) {
+			return;
+		}
+		graphics.setLineWidth(lineWidth);
+		graphics.drawLine(left, top, right, bottom);
+	}
+
+	private void paintComponent(final Graphics graphics) {
+		ChildBoxPainter.paint(component, graphics);
+	}
+
+	@Override
+	public boolean canJoin(final IInlineBox other) {
+		if (!(other instanceof InlineFrame)) {
+			return false;
+		}
+		final InlineFrame otherFrame = (InlineFrame) other;
+
+		if (!margin.equals(otherFrame.margin) || !border.equals(otherFrame.border) || !padding.equals(otherFrame.padding)) {
+			return false;
+		}
+		if (!component.canJoin(otherFrame.component)) {
+			return false;
+		}
+
+		return true;
+	}
+
+	@Override
+	public boolean join(final IInlineBox other) {
+		if (!canJoin(other)) {
+			return false;
+		}
+		final InlineFrame otherFrame = (InlineFrame) other;
+
+		component.join(otherFrame.component);
+
+		calculateBounds();
+
+		return true;
+	}
+
+	@Override
+	public boolean canSplit() {
+		if (component == null) {
+			return false;
+		}
+		return component.canSplit();
+	}
+
+	@Override
+	public IInlineBox splitTail(final Graphics graphics, final int headWidth, final boolean force) {
+		final IInlineBox tailComponent;
+		final int tailHeadWidth = headWidth - leftFrame();
+		if (tailHeadWidth <= 0) {
+			tailComponent = component;
+			component = null;
+		} else {
+			tailComponent = component.splitTail(graphics, tailHeadWidth, force);
+		}
+
+		final InlineFrame tail = new InlineFrame();
+		tail.setComponent(tailComponent);
+		tail.setParent(parent);
+		tail.setMargin(margin);
+		tail.setBorder(border);
+		tail.setPadding(padding);
+		tail.layout(graphics);
+
+		layout(graphics);
+
+		return tail;
+	}
+
+	@Override
+	public String toString() {
+		return "InlineFrame [top=" + top + ", left=" + left + ", width=" + width + ", height=" + height + ", margin=" + margin + ", border=" + border + ", padding=" + padding + "]";
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineNodeReference.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineNodeReference.java
index 920851b..9dfef94 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineNodeReference.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/InlineNodeReference.java
@@ -309,33 +309,40 @@
 
 		final IInlineBox tailComponent = component.splitTail(graphics, headWidth, force);
 
-		final int firstTailOffset = findStartOffset(tailComponent);
-
-		final int splitPosition;
-		if (firstChildOffset == firstTailOffset) {
-			splitPosition = startPosition.getOffset();
-		} else {
-			splitPosition = firstTailOffset;
-		}
-		Assert.isTrue(splitPosition >= getStartOffset(), MessageFormat.format("Splitposition {0} is invalid.", splitPosition));
-
 		final InlineNodeReference tail = new InlineNodeReference();
 		tail.setComponent(tailComponent);
-		tail.setSubrange(node, splitPosition, getEndOffset());
 		tail.setCanContainText(canContainText);
 		tail.setParent(parent);
 		tail.layout(graphics);
 
-		node.getContent().removePosition(endPosition);
-		endPosition = node.getContent().createPosition(splitPosition - 1);
+		adaptContentRanges(firstChildOffset, tail);
 
 		layout(graphics);
 
 		return tail;
 	}
 
+	private void adaptContentRanges(final int oldOffsetOfFirstChild, final InlineNodeReference tail) {
+		if (tail.getComponent().getWidth() == 0) {
+			return;
+		}
+
+		final int offsetOfFirstChildInTail = findStartOffset(tail.getComponent());
+		final int splitPosition;
+		if (oldOffsetOfFirstChild == offsetOfFirstChildInTail) {
+			splitPosition = startPosition.getOffset();
+		} else {
+			splitPosition = offsetOfFirstChildInTail;
+		}
+
+		Assert.isTrue(splitPosition >= getStartOffset(), MessageFormat.format("Split position {0} is invalid.", splitPosition));
+		tail.setSubrange(node, splitPosition, getEndOffset());
+		node.getContent().removePosition(endPosition);
+		endPosition = node.getContent().createPosition(splitPosition - 1);
+	}
+
 	private int findStartOffset(final IBox startBox) {
-		return startBox.accept(new DepthFirstBoxTraversal<Integer>(-1) {
+		final Integer startOffset = startBox.accept(new DepthFirstBoxTraversal<Integer>() {
 			@Override
 			public Integer visit(final InlineNodeReference box) {
 				if (box == startBox) {
@@ -349,6 +356,10 @@
 				return box.getStartOffset();
 			}
 		});
+		if (startOffset == null) {
+			return -1;
+		}
+		return startOffset;
 	}
 
 	@Override
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ParentTraversal.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ParentTraversal.java
index f8d583b..2db2514 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ParentTraversal.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ParentTraversal.java
@@ -66,6 +66,11 @@
 	}
 
 	@Override
+	public T visit(final InlineFrame box) {
+		return box.getParent().accept(this);
+	}
+
+	@Override
 	public T visit(final StaticText box) {
 		return box.getParent().accept(this);
 	}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/ContentTopology.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/ContentTopology.java
index 60491cf..a172d83 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/ContentTopology.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/ContentTopology.java
@@ -39,7 +39,7 @@
 	}
 
 	private static IContentBox findOutmostContentBox(final RootBox rootBox) {
-		return rootBox.accept(new DepthFirstBoxTraversal<IContentBox>(null) {
+		return rootBox.accept(new DepthFirstBoxTraversal<IContentBox>() {
 			@Override
 			public IContentBox visit(final StructuralNodeReference box) {
 				return box;
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveUp.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveUp.java
index fd77da9..bed2ffb 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveUp.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/cursor/MoveUp.java
@@ -233,7 +233,7 @@
 	}
 
 	private static List<TextContent> findVerticallyClosestTextContentChildrenAbove(final IBox parent, final int y) {
-		return parent.accept(new DepthFirstBoxTraversal<List<TextContent>>(Collections.<TextContent> emptyList()) {
+		final List<TextContent> candidates = parent.accept(new DepthFirstBoxTraversal<List<TextContent>>() {
 			private final LinkedList<TextContent> candidates = new LinkedList<TextContent>();
 			private int minVerticalDistance = Integer.MAX_VALUE;
 
@@ -262,6 +262,10 @@
 				return null;
 			}
 		});
+		if (candidates == null) {
+			return Collections.<TextContent> emptyList();
+		}
+		return candidates;
 	}
 
 	private static void removeVerticallyDistantBoxes(final List<? extends IContentBox> boxes, final int y, final int minVerticalDistance) {
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/InlineElementVisualization.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/InlineElementVisualization.java
index d5f86db..42347c5 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/InlineElementVisualization.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/InlineElementVisualization.java
@@ -10,12 +10,16 @@
  *******************************************************************************/
 package org.eclipse.vex.core.internal.visualization;
 
+import static org.eclipse.vex.core.internal.boxes.BoxFactory.frame;
 import static org.eclipse.vex.core.internal.boxes.BoxFactory.inlineContainer;
 import static org.eclipse.vex.core.internal.boxes.BoxFactory.nodeReferenceWithText;
 import static org.eclipse.vex.core.internal.boxes.BoxFactory.staticText;
 
+import org.eclipse.vex.core.internal.boxes.Border;
 import org.eclipse.vex.core.internal.boxes.IInlineBox;
 import org.eclipse.vex.core.internal.boxes.InlineContainer;
+import org.eclipse.vex.core.internal.boxes.Margin;
+import org.eclipse.vex.core.internal.boxes.Padding;
 import org.eclipse.vex.core.internal.core.FontSpec;
 import org.eclipse.vex.core.provisional.dom.IElement;
 
@@ -32,16 +36,16 @@
 			return super.visit(element);
 		}
 
-		return nodeReferenceWithText(element, visualizeInlineElement(element));
+		return nodeReferenceWithText(element, frame(visualizeInlineElement(element), new Margin(4), new Border(2), new Padding(5)));
 	}
 
 	private InlineContainer visualizeInlineElement(final IElement element) {
 		final InlineContainer container = inlineContainer();
 		if (element.hasChildren()) {
-			return visualizeChildrenInline(element.children(), container);
+			visualizeChildrenInline(element.children(), container);
 		} else {
 			container.appendChild(staticText(" ", TIMES_NEW_ROMAN));
-			return container;
 		}
+		return container;
 	}
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/DOMVisualization.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/DOMVisualization.java
index 7df5c52..3a95a40 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/DOMVisualization.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/DOMVisualization.java
@@ -14,13 +14,14 @@
 
 import org.eclipse.core.runtime.Assert;
 import org.eclipse.vex.core.internal.boxes.BaseBoxVisitor;
-import org.eclipse.vex.core.internal.boxes.StructuralFrame;
 import org.eclipse.vex.core.internal.boxes.IBox;
 import org.eclipse.vex.core.internal.boxes.IContentBox;
 import org.eclipse.vex.core.internal.boxes.InlineContainer;
+import org.eclipse.vex.core.internal.boxes.InlineFrame;
 import org.eclipse.vex.core.internal.boxes.InlineNodeReference;
 import org.eclipse.vex.core.internal.boxes.Paragraph;
 import org.eclipse.vex.core.internal.boxes.RootBox;
+import org.eclipse.vex.core.internal.boxes.StructuralFrame;
 import org.eclipse.vex.core.internal.boxes.StructuralNodeReference;
 import org.eclipse.vex.core.internal.boxes.TextContent;
 import org.eclipse.vex.core.internal.boxes.VerticalBlock;
@@ -122,6 +123,11 @@
 			public void visit(final InlineContainer box) {
 				box.replaceChildren(modifiedBoxes, visualizationChain.visualizeInline(node));
 			}
+
+			@Override
+			public void visit(final InlineFrame box) {
+				box.setComponent(visualizationChain.visualizeInline(node));
+			}
 		});
 	}