add support for positioning cursor by mouse click

Signed-off-by: Florian Thienel <florian@thienel.org>
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/FakeContentBox.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/FakeContentBox.java
index c16eb08..f85fb11 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/FakeContentBox.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/FakeContentBox.java
@@ -93,4 +93,9 @@
 		return area;
 	}
 
+	@Override
+	public int getOffsetForCoordinates(final Graphics graphics, final int x, final int y) {
+		return startOffset;
+	}
+
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/CharSequenceSplitter.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/CharSequenceSplitter.java
index 112c736..3b961af 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/CharSequenceSplitter.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/CharSequenceSplitter.java
@@ -43,8 +43,8 @@
 		return charSequence.charAt(startPosition + position);
 	}
 
-	public int findSplittingPositionBefore(final Graphics graphics, final int y, final int maxWidth, final boolean force) {
-		final int positionAtWidth = findPositionBefore(graphics, y, maxWidth);
+	public int findSplittingPositionBefore(final Graphics graphics, final int x, final int maxWidth, final boolean force) {
+		final int positionAtWidth = findPositionBefore(graphics, x, maxWidth);
 		final int properSplittingPosition = findProperSplittingPositionBefore(positionAtWidth);
 		final int splittingPosition;
 		if (properSplittingPosition == -1 && force) {
@@ -55,22 +55,22 @@
 		return splittingPosition;
 	}
 
-	private int findPositionBefore(final Graphics graphics, final int y, final int maxWidth) {
-		if (y < 0) {
+	public int findPositionBefore(final Graphics graphics, final int x, final int maxWidth) {
+		if (x < 0) {
 			return 0;
 		}
-		if (y >= maxWidth) {
+		if (x >= maxWidth) {
 			return textLength();
 		}
 
 		int begin = 0;
 		int end = textLength();
-		int pivot = guessPositionAt(y, maxWidth);
+		int pivot = guessPositionAt(x, maxWidth);
 		while (begin < end - 1) {
 			final int textWidth = graphics.stringWidth(substring(0, pivot));
-			if (textWidth > y) {
+			if (textWidth > x) {
 				end = pivot;
-			} else if (textWidth < y) {
+			} else if (textWidth < x) {
 				begin = pivot;
 			} else {
 				return pivot;
@@ -80,8 +80,8 @@
 		return pivot;
 	}
 
-	private int guessPositionAt(final int y, final int maxWidth) {
-		final float splittingRatio = (float) y / maxWidth;
+	private int guessPositionAt(final int x, final int maxWidth) {
+		final float splittingRatio = (float) x / maxWidth;
 		return Math.round(splittingRatio * textLength());
 	}
 
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ContentMap.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ContentMap.java
index 324d7fd..f3be2c9 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ContentMap.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/ContentMap.java
@@ -49,4 +49,62 @@
 		});
 	}
 
+	public IContentBox findBoxByCoordinates(final int x, final int y) {
+		return rootBox.accept(new DepthFirstTraversal<IContentBox>() {
+
+			private IContentBox nearBy;
+
+			@Override
+			public IContentBox visit(final NodeReference box) {
+				if (!containsCoordinates(box, x, y)) {
+					return null;
+				}
+
+				final IContentBox componentResult = box.getComponent().accept(this);
+				if (componentResult != null) {
+					return componentResult;
+				}
+				if (nearBy != null && (rightFromX(nearBy, x) && isFirstEnclosedBox(box, nearBy) || leftFromX(nearBy, x) && isLastEnclosedBox(box, nearBy))) {
+					return nearBy;
+				}
+				return box;
+
+			}
+
+			private boolean isLastEnclosedBox(final IContentBox enclosingBox, final IContentBox enclosedBox) {
+				return enclosedBox.getEndOffset() < enclosingBox.getEndOffset() - 1;
+			}
+
+			private boolean isFirstEnclosedBox(final IContentBox enclosingBox, final IContentBox enclosedBox) {
+				return enclosedBox.getStartOffset() > enclosingBox.getStartOffset() + 1;
+			}
+
+			@Override
+			public IContentBox visit(final TextContent box) {
+				if (containsCoordinates(box, x, y)) {
+					return box;
+				}
+				if (containsY(box, y)) {
+					nearBy = box;
+				}
+				return null;
+			}
+
+			private boolean containsCoordinates(final IBox box, final int x, final int y) {
+				return x >= box.getAbsoluteLeft() && x <= box.getAbsoluteLeft() + box.getWidth() && containsY(box, y);
+			}
+
+			private boolean containsY(final IBox box, final int y) {
+				return y >= box.getAbsoluteTop() && y <= box.getAbsoluteTop() + box.getHeight();
+			}
+
+			private boolean rightFromX(final IBox box, final int x) {
+				return x < box.getAbsoluteLeft();
+			}
+
+			private boolean leftFromX(final IBox box, final int x) {
+				return x > box.getAbsoluteLeft() + box.getWidth();
+			}
+		});
+	}
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IContentBox.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IContentBox.java
index 01569aa..8da54d0 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IContentBox.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/IContentBox.java
@@ -22,6 +22,8 @@
 
 	int getEndOffset();
 
-	Rectangle getPositionArea(final Graphics graphics, final int offset);
+	Rectangle getPositionArea(Graphics graphics, int offset);
+
+	int getOffsetForCoordinates(Graphics graphics, int x, int y);
 
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/NodeReference.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/NodeReference.java
index e44a794..b28da2b 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/NodeReference.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/NodeReference.java
@@ -166,6 +166,29 @@
 		return new Rectangle(0, 0, width, height);
 	}
 
+	@Override
+	public int getOffsetForCoordinates(final Graphics graphics, final int x, final int y) {
+		if (isEmpty()) {
+			return getEndOffset();
+		}
+
+		final long dStart = distance(0, 0, x, y);
+		final long dEnd = distance(width, height, x, y);
+		if (dStart < dEnd) {
+			return getStartOffset();
+		} else {
+			return getEndOffset();
+		}
+	}
+
+	private static long distance(final int x1, final int y1, final int x2, final int y2) {
+		return Math.round(Math.sqrt(pow2(x2 - x1) + pow2(y2 - y1)));
+	}
+
+	private static int pow2(final int i) {
+		return i * i;
+	}
+
 	public boolean isEmpty() {
 		return getEndOffset() - getStartOffset() <= 1;
 	}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TextContent.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TextContent.java
index 9cc02f6..11d8215 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TextContent.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TextContent.java
@@ -269,6 +269,27 @@
 	}
 
 	@Override
+	public int getOffsetForCoordinates(final Graphics graphics, final int x, final int y) {
+		if (y < 0) {
+			return getStartOffset();
+		}
+		if (y > height) {
+			return getEndOffset();
+		}
+
+		applyFont(graphics);
+		splitter.setContent(content, startPosition.getOffset(), endPosition.getOffset());
+		final int offset = getStartOffset() + splitter.findPositionBefore(graphics, x, width);
+		final Rectangle area = getPositionArea(graphics, offset);
+		final int halfWidth = area.getWidth() / 2 + 1;
+		if (x < area.getX() + halfWidth) {
+			return offset;
+		} else {
+			return Math.min(offset + 1, getEndOffset());
+		}
+	}
+
+	@Override
 	public String toString() {
 		return "TextContent{ x: " + left + ", y: " + top + ", width: " + width + ", height: " + height + ", startOffset: " + startPosition + ", endOffset: " + endPosition + " }";
 	}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/BoxWidget.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/BoxWidget.java
index 27c8a93..0fee376 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/BoxWidget.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/BoxWidget.java
@@ -30,6 +30,7 @@
 import org.eclipse.swt.widgets.Display;
 import org.eclipse.vex.core.internal.boxes.ContentMap;
 import org.eclipse.vex.core.internal.boxes.Cursor;
+import org.eclipse.vex.core.internal.boxes.IContentBox;
 import org.eclipse.vex.core.internal.boxes.RootBox;
 import org.eclipse.vex.core.internal.core.Graphics;
 
@@ -70,6 +71,7 @@
 			connectScrollVertically();
 		}
 		connectKeyboard();
+		connectMouse();
 
 		rootBox = new RootBox();
 		contentMap = new ContentMap();
@@ -131,6 +133,15 @@
 		});
 	}
 
+	private void connectMouse() {
+		addMouseListener(new MouseAdapter() {
+			@Override
+			public void mouseDown(final MouseEvent e) {
+				BoxWidget.this.mouseDown(e);
+			}
+		});
+	}
+
 	private void widgetDisposed() {
 		rootBox = null;
 		if (bufferImage != null) {
@@ -171,6 +182,22 @@
 		}
 	}
 
+	private void mouseDown(final MouseEvent event) {
+		final int absoluteY = event.y + getVerticalBar().getSelection();
+		final IContentBox clickedBox = contentMap.findBoxByCoordinates(event.x, absoluteY);
+
+		final Image image = new Image(getDisplay(), getSize().x, getSize().y);
+		final GC gc = new GC(image);
+		final Graphics graphics = new SwtGraphics(gc);
+		final int offset = clickedBox.getOffsetForCoordinates(graphics, event.x - clickedBox.getAbsoluteLeft(), absoluteY - clickedBox.getAbsoluteTop());
+		graphics.dispose();
+		gc.dispose();
+		image.dispose();
+
+		cursor.setPosition(offset);
+		invalidate();
+	}
+
 	private void scheduleRenderer(final Runnable renderer) {
 		synchronized (rendererMonitor) {
 			if (currentRenderer != null) {