extract calculation of cursor position in own class

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
new file mode 100644
index 0000000..4cea87f
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestCursorPosition.java
@@ -0,0 +1,110 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Florian Thienel 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:
+ * 		Florian Thienel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.boxes;
+
+import static org.junit.Assert.assertEquals;
+
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.vex.core.internal.dom.Document;
+import org.eclipse.vex.core.internal.visualization.DocumentRootVisualization;
+import org.eclipse.vex.core.internal.visualization.ParagraphVisualization;
+import org.eclipse.vex.core.internal.visualization.StructureElementVisualization;
+import org.eclipse.vex.core.internal.visualization.TextVisualization;
+import org.eclipse.vex.core.internal.visualization.VisualizationChain;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+import org.eclipse.vex.core.provisional.dom.IElement;
+import org.eclipse.vex.core.provisional.dom.IParent;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Florian Thienel
+ */
+public class TestCursorPosition {
+
+	private RootBox rootBox;
+	private ContentMap contentMap;
+	private CursorPosition cursorPosition;
+
+	@Before
+	public void setUp() throws Exception {
+		rootBox = createTestModel();
+		contentMap = new ContentMap();
+		contentMap.setRootBox(rootBox);
+		cursorPosition = new CursorPosition(contentMap);
+	}
+
+	@Test
+	public void canMoveCursorOneCharacterLeft() throws Exception {
+		cursorPosition.setOffset(5);
+		cursorPosition.left();
+		assertEquals(4, cursorPosition.getOffset());
+	}
+
+	@Test
+	public void whenAtFirstPosition_cannotMoveCursorOneCharacterLeft() throws Exception {
+		cursorPosition.setOffset(0);
+		cursorPosition.left();
+		assertEquals(0, cursorPosition.getOffset());
+	}
+
+	@Test
+	public void canMoveCursorOneCharacterRight() throws Exception {
+		cursorPosition.setOffset(5);
+		cursorPosition.right();
+		assertEquals(6, cursorPosition.getOffset());
+	}
+
+	@Test
+	public void whenAtLastPosition_cannotMoveCursorOneCharacterRight() throws Exception {
+		cursorPosition.setOffset(37);
+		cursorPosition.right();
+		assertEquals(37, cursorPosition.getOffset());
+	}
+
+	private static RootBox createTestModel() {
+		final IDocument document = createTestDocument();
+		final VisualizationChain visualizationChain = buildVisualizationChain();
+		return visualizationChain.visualizeRoot(document);
+	}
+
+	private static VisualizationChain buildVisualizationChain() {
+		final VisualizationChain visualizationChain = new VisualizationChain();
+		visualizationChain.addForRoot(new DocumentRootVisualization());
+		visualizationChain.addForStructure(new ParagraphVisualization());
+		visualizationChain.addForStructure(new StructureElementVisualization());
+		visualizationChain.addForInline(new TextVisualization());
+		return visualizationChain;
+	}
+
+	private static IDocument createTestDocument() {
+		final Document document = new Document(new QualifiedName(null, "doc"));
+		insertSection(document.getRootElement());
+		insertSection(document.getRootElement());
+		return document;
+	}
+
+	private static void insertSection(final IParent parent) {
+		final IElement section = insertElement(parent, "section");
+		insertText(insertElement(section, "para"), "LOREM IPSUM");
+		insertElement(section, "para");
+	}
+
+	private static IElement insertElement(final IParent parent, final String localName) {
+		final IDocument document = parent.getDocument();
+		return document.insertElement(parent.getEndOffset(), new QualifiedName(null, localName));
+	}
+
+	private static void insertText(final IParent parent, final String text) {
+		final IDocument document = parent.getDocument();
+		document.insertText(parent.getEndOffset(), text);
+	}
+}
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 f3be2c9..38c1b0c 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
@@ -16,9 +16,32 @@
 public class ContentMap {
 
 	private RootBox rootBox;
+	private IContentBox outmostContentBox;
 
 	public void setRootBox(final RootBox rootBox) {
 		this.rootBox = rootBox;
+		outmostContentBox = findOutmostContentBox();
+	}
+
+	private IContentBox findOutmostContentBox() {
+		return rootBox.accept(new DepthFirstTraversal<IContentBox>(null) {
+			@Override
+			public IContentBox visit(final NodeReference box) {
+				return box;
+			}
+
+			@Override
+			public IContentBox visit(final TextContent box) {
+				return box;
+			}
+		});
+	}
+
+	public int getLastPosition() {
+		if (outmostContentBox == null) {
+			return 0;
+		}
+		return outmostContentBox.getEndOffset();
 	}
 
 	public IContentBox findBoxForPosition(final int offset) {
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
new file mode 100644
index 0000000..03839fb
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/CursorPosition.java
@@ -0,0 +1,40 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Florian Thienel 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:
+ * 		Florian Thienel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.boxes;
+
+/**
+ * @author Florian Thienel
+ */
+public class CursorPosition {
+
+	private final ContentMap contentMap;
+	private int offset;
+
+	public CursorPosition(final ContentMap contentMap) {
+		this.contentMap = contentMap;
+	}
+
+	public void setOffset(final int offset) {
+		this.offset = offset;
+	}
+
+	public int getOffset() {
+		return offset;
+	}
+
+	public void left() {
+		offset = Math.max(0, offset - 1);
+	}
+
+	public void right() {
+		offset = Math.min(offset + 1, contentMap.getLastPosition());
+	}
+}
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 934e023..548b577 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.CursorPosition;
 import org.eclipse.vex.core.internal.boxes.IContentBox;
 import org.eclipse.vex.core.internal.boxes.RootBox;
 import org.eclipse.vex.core.internal.core.Graphics;
@@ -45,6 +46,7 @@
 
 	private final ContentMap contentMap;
 	private final Cursor cursor;
+	private final CursorPosition cursorPosition;
 
 	/*
 	 * Use double buffering with a dedicated render thread to render the box model: This prevents flickering and keeps
@@ -77,6 +79,7 @@
 		contentMap = new ContentMap();
 		contentMap.setRootBox(rootBox);
 		cursor = new Cursor(contentMap);
+		cursorPosition = new CursorPosition(contentMap);
 	}
 
 	public void setContent(final RootBox rootBox) {
@@ -172,8 +175,7 @@
 			cursorRight();
 			break;
 		case SWT.HOME:
-			setCursorPosition(0);
-			invalidate();
+			cursorHome();
 			break;
 		default:
 			break;
@@ -189,32 +191,41 @@
 		invalidate();
 	}
 
-	private void cursorLeft() {
-		setCursorPosition(Math.max(0, cursor.getPosition() - 1));
-		invalidate();
-	}
-
-	private void cursorRight() {
-		setCursorPosition(cursor.getPosition() + 1);
-		invalidate();
-	}
-
 	private void setCursorPositionInBoxByAbsoluteCoordinates(final IContentBox box, final int x, final int y) {
 		runWithGraphics(new IRunnableWithGraphics<Object>() {
 			@Override
 			public Integer run(final Graphics graphics) {
 				final int offset = box.getOffsetForCoordinates(graphics, x - box.getAbsoluteLeft(), y - box.getAbsoluteTop());
+				cursorPosition.setOffset(offset);
 				cursor.setPosition(graphics, offset);
 				return null;
 			}
 		});
 	}
 
-	private void setCursorPosition(final int offset) {
+	private void cursorLeft() {
+		cursorPosition.left();
+		transferCursorPositionToCursor();
+		invalidate();
+	}
+
+	private void cursorRight() {
+		cursorPosition.right();
+		transferCursorPositionToCursor();
+		invalidate();
+	}
+
+	private void cursorHome() {
+		cursorPosition.setOffset(0);
+		transferCursorPositionToCursor();
+		invalidate();
+	}
+
+	private void transferCursorPositionToCursor() {
 		runWithGraphics(new IRunnableWithGraphics<Object>() {
 			@Override
 			public Integer run(final Graphics graphics) {
-				cursor.setPosition(graphics, offset);
+				cursor.setPosition(graphics, cursorPosition.getOffset());
 				return null;
 			}
 		});