make cursor positioning by coordinates more intuitive

When clicking left or right of a line, the cursor should always be
positioned in that line (either at the beginning or end).

When clicking between structural elements, the cursor should be moved to
the beginning or end of the closest structural element.

Signed-off-by: Florian Thienel <florian@thienel.org>
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestCursorPosition.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestCursorPosition.java
index 50e47ff..c329252 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestCursorPosition.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestCursorPosition.java
@@ -80,11 +80,50 @@
 	}
 
 	@Test
-	public void canSetPositionByAbsoluteCoordinates() throws Exception {
+	public void whenClickingIntoText_shouldMoveToPositionInText() throws Exception {
 		cursorPosition.moveToAbsoluteCoordinates(graphics, 18, 11);
 		assertEquals(5, cursorPosition.getOffset());
 	}
 
+	@Test
+	public void whenClickingRightOfLastLine_shouldMoveToEndOfParagraph() throws Exception {
+		cursorPosition.moveToAbsoluteCoordinates(graphics, 86, 400);
+		assertEquals(331, cursorPosition.getOffset());
+	}
+
+	@Test
+	public void whenClickingLeftOfLine_shouldMoveToBeginningOfLine() throws Exception {
+		for (int x = 0; x < 10; x += 1) {
+			cursorPosition.setOffset(0);
+			cursorPosition.moveToAbsoluteCoordinates(graphics, x, 11);
+			assertEquals("x=" + x, 4, cursorPosition.getOffset());
+		}
+	}
+
+	@Test
+	public void whenClickingRightOfLine_shouldMoveToEndOfLine() throws Exception {
+		for (int x = 100; x > 93; x -= 1) {
+			cursorPosition.setOffset(0);
+			cursorPosition.moveToAbsoluteCoordinates(graphics, x, 11);
+			assertEquals("x=" + x, 15, cursorPosition.getOffset());
+		}
+	}
+
+	@Test
+	public void whenClickingInEmptyLine_shouldMoveToEndOfParagraph() throws Exception {
+		cursorPosition.moveToAbsoluteCoordinates(graphics, 10, 407);
+		assertEquals(333, cursorPosition.getOffset());
+	}
+
+	@Test
+	public void whenClickingBelowLastLine_shouldMoveToEndOfParagraph() throws Exception {
+		for (int x = 6; x < 95; x += 1) {
+			cursorPosition.setOffset(0);
+			cursorPosition.moveToAbsoluteCoordinates(graphics, x, 402);
+			assertEquals("x=" + x, 331, cursorPosition.getOffset());
+		}
+	}
+
 	private static RootBox createTestModel() {
 		final IDocument document = createTestDocument();
 		final VisualizationChain visualizationChain = buildVisualizationChain();
@@ -122,4 +161,20 @@
 		final IDocument document = parent.getDocument();
 		document.insertText(parent.getEndOffset(), text);
 	}
+
+	/*
+	 * For visualization of the box structure: 
+	 * 
+	 * private void printBoxStructure() { rootBox.accept(new
+	 * DepthFirstTraversal<Object>() { private String indent = "";
+	 * 
+	 * @Override public Object visit(final NodeReference box) { printBox(box); indent += " "; super.visit(box); indent =
+	 * indent.substring(1); return null; }
+	 * 
+	 * @Override public Object visit(final TextContent box) { printBox(box); return null; }
+	 * 
+	 * private void printBox(final IContentBox box) { System.out.println(indent + "[" + box.getAbsoluteLeft() + ". " +
+	 * box.getAbsoluteTop() + ", " + box.getWidth() + ", " + box.getHeight() + "] [" + box.getStartOffset() + ", " +
+	 * box.getEndOffset() + "]"); } }); }
+	 */
 }
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 38c1b0c..18f4697 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
@@ -72,14 +72,14 @@
 		});
 	}
 
-	public IContentBox findBoxByCoordinates(final int x, final int y) {
+	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)) {
+				if (!box.containsCoordinates(x, y)) {
 					return null;
 				}
 
@@ -87,7 +87,7 @@
 				if (componentResult != null) {
 					return componentResult;
 				}
-				if (nearBy != null && (rightFromX(nearBy, x) && isFirstEnclosedBox(box, nearBy) || leftFromX(nearBy, x) && isLastEnclosedBox(box, nearBy))) {
+				if (nearBy != null && (nearBy.isRightFrom(x) && isFirstEnclosedBox(box, nearBy) || nearBy.isLeftFrom(x) && isLastEnclosedBox(box, nearBy))) {
 					return nearBy;
 				}
 				return box;
@@ -104,30 +104,135 @@
 
 			@Override
 			public IContentBox visit(final TextContent box) {
-				if (containsCoordinates(box, x, y)) {
+				if (box.containsCoordinates(x, y)) {
 					return box;
 				}
-				if (containsY(box, y)) {
+				if (box.containsY(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);
+		});
+	}
+
+	public IContentBox findClosestBoxOnLineByCoordinates(final int x, final int y) {
+		final Neighbourhood neighbours = new Neighbourhood();
+		final IContentBox deepestContainer = rootBox.accept(new DepthFirstTraversal<IContentBox>() {
+			@Override
+			public IContentBox visit(final NodeReference box) {
+				if (box.isAbove(y)) {
+					neighbours.setAbove(box, box.getAbsoluteTop() + box.getHeight() - y, false);
+					return super.visit(box);
+				} else if (box.isBelow(y)) {
+					neighbours.setBelow(box, y - box.getAbsoluteTop(), false);
+					return super.visit(box);
+				} else if (box.isRightFrom(x)) {
+					neighbours.setRight(box, box.getAbsoluteLeft() - x, false);
+					return super.visit(box);
+				} else if (box.isLeftFrom(x)) {
+					neighbours.setLeft(box, x - box.getAbsoluteLeft() - box.getWidth(), false);
+					return super.visit(box);
+				} else {
+					final IContentBox deeperContainer = super.visit(box);
+					if (deeperContainer != null) {
+						return deeperContainer;
+					}
+					return box;
+				}
 			}
 
-			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();
+			@Override
+			public IContentBox visit(final TextContent box) {
+				if (box.isAbove(y)) {
+					neighbours.setAbove(box, y - box.getAbsoluteTop() - box.getHeight(), true);
+					return null;
+				} else if (box.isBelow(y)) {
+					neighbours.setBelow(box, box.getAbsoluteTop() - y, true);
+					return null;
+				} else if (box.isRightFrom(x)) {
+					neighbours.setRight(box, box.getAbsoluteLeft() - x, true);
+					return null;
+				} else if (box.isLeftFrom(x)) {
+					neighbours.setLeft(box, x - box.getAbsoluteLeft() - box.getWidth(), true);
+					return null;
+				} else {
+					return box;
+				}
 			}
 		});
+		if (deepestContainer == null) {
+			return outmostContentBox;
+		}
+		return deepestContainer.accept(new BaseBoxVisitorWithResult<IContentBox>() {
+			@Override
+			public IContentBox visit(final NodeReference box) {
+				final IContentBox closestOnLine = neighbours.getClosestOnLine().box;
+				if (closestOnLine != null) {
+					return closestOnLine;
+				}
+				return box;
+			}
+
+			@Override
+			public IContentBox visit(final TextContent box) {
+				return box;
+			}
+		});
+	}
+
+	private static class Neighbour {
+		public static final Neighbour NULL = new Neighbour(null, Integer.MAX_VALUE);
+
+		public final IContentBox box;
+		public final int distance;
+
+		public Neighbour(final IContentBox box, final int distance) {
+			this.box = box;
+			this.distance = distance;
+		}
+	}
+
+	private static class Neighbourhood {
+		private Neighbour above = Neighbour.NULL;
+		private Neighbour below = Neighbour.NULL;
+		private Neighbour left = Neighbour.NULL;
+		private Neighbour right = Neighbour.NULL;
+
+		public void setAbove(final IContentBox box, final int distance, final boolean overwrite) {
+			if (above == null || overwrite && above.distance >= distance) {
+				above = new Neighbour(box, distance);
+			}
+		}
+
+		public void setBelow(final IContentBox box, final int distance, final boolean overwrite) {
+			if (below == null || overwrite && below.distance >= distance) {
+				below = new Neighbour(box, distance);
+			}
+		}
+
+		public void setLeft(final IContentBox box, final int distance, final boolean overwrite) {
+			if (left == null || overwrite && left.distance >= distance) {
+				left = new Neighbour(box, distance);
+			}
+		}
+
+		public void setRight(final IContentBox box, final int distance, final boolean overwrite) {
+			if (right == null || overwrite && right.distance >= distance) {
+				right = new Neighbour(box, distance);
+			}
+		}
+
+		public Neighbour getClosestOnLine() {
+			return closest(left, right);
+		}
+
+		private Neighbour closest(final Neighbour neighbour1, final Neighbour neighbour2) {
+			if (neighbour1.distance < neighbour2.distance) {
+				return neighbour1;
+			} else {
+				return neighbour2;
+			}
+		}
 	}
 }
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/CursorPosition.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/CursorPosition.java
index f6ff5e3..a656e81 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/CursorPosition.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/CursorPosition.java
@@ -41,7 +41,38 @@
 	}
 
 	public void moveToAbsoluteCoordinates(final Graphics graphics, final int x, final int y) {
-		final IContentBox box = contentMap.findBoxByCoordinates(x, y);
-		offset = box.getOffsetForCoordinates(graphics, x - box.getAbsoluteLeft(), y - box.getAbsoluteTop());
+		final IContentBox box = contentMap.findClosestBoxOnLineByCoordinates(x, y);
+		if (box.containsCoordinates(x, y)) {
+			offset = box.getOffsetForCoordinates(graphics, x - box.getAbsoluteLeft(), y - box.getAbsoluteTop());
+		} else if (box.isLeftFrom(x)) {
+			if (isLastEnclosedBox(box)) {
+				offset = box.getEndOffset() + 1;
+			} else {
+				offset = box.getEndOffset();
+			}
+		} else if (box.isRightFrom(x)) {
+			offset = box.getStartOffset();
+		}
 	}
+
+	private static boolean isLastEnclosedBox(final IContentBox enclosedBox) {
+		final IContentBox parent = findParentContentBox(enclosedBox);
+		if (parent == null) {
+			return true;
+		}
+		return enclosedBox.getEndOffset() == parent.getEndOffset() - 1;
+	}
+
+	private static IContentBox findParentContentBox(final IContentBox child) {
+		return child.accept(new ParentTraversal<IContentBox>() {
+			@Override
+			public IContentBox visit(final NodeReference box) {
+				if (child == box) {
+					return super.visit(box);
+				}
+				return box;
+			}
+		});
+	}
+
 }
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 b48ae77..a8fbd0f 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
@@ -172,23 +172,14 @@
 			return getEndOffset();
 		}
 
-		final long dStart = distance(0, 0, x, y);
-		final long dEnd = distance(width, height, x, y);
-		if (dStart < dEnd) {
+		final int half = height / 2;
+		if (y < half) {
 			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;
 	}