diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..acb7754
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,39 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+.idea
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..7c01dbe
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,151 @@
+
+
+ 4.0.0
+
+ com.andrewlalis
+ javafx-scene-router
+ 0.0.1-SNAPSHOT
+ JavaFX Scene Router
+ A library that provides a router implementation for JavaFX, for browser-like navigation between pages.
+ https://github.com/andrewlalis/javafx-scene-router
+
+
+ 21
+ 21
+ UTF-8
+ 21.0.1
+
+
+
+
+ org.openjfx
+ javafx-controls
+ ${javafx.version}
+
+
+ org.openjfx
+ javafx-fxml
+ ${javafx.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.3.1
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.1.0
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+ net.ju-n.maven.plugins
+ checksum-maven-plugin
+ 1.4
+
+
+ checksum-maven-plugin-files
+ verify
+
+ files
+
+
+
+
+
+
+ ${project.build.directory}
+
+ *.pom
+ *.jar
+
+
+
+
+ SHA-1
+ MD5
+ SHA-256
+ SHA-512
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.6.13
+ true
+
+ ossrh
+ https://s01.oss.sonatype.org
+ false
+
+
+
+
+
+
+ https://github.com/andrewlalis/javafx-scene-router
+ scm:git:git://github.com/andrewlalis/javafx-scene-router.git
+ scm:git:ssh://github.com/andrewlalis/javafx-scene-router.git
+
+
+
+
+ MIT License
+ https://www.opensource.org/licenses/mit-license.php
+
+
+
+
+
+ Andrew Lalis
+ andrewlalisofficial@gmail.com
+
+
+
+
+
+ ossrh
+ https://s01.oss.sonatype.org/content/repositories/snapshots
+
+
+ ossrh
+ https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/
+
+
+
\ No newline at end of file
diff --git a/src/main/java/com/andrewlalis/javafx_scene_router/BreadCrumb.java b/src/main/java/com/andrewlalis/javafx_scene_router/BreadCrumb.java
new file mode 100644
index 0000000..1dc7b26
--- /dev/null
+++ b/src/main/java/com/andrewlalis/javafx_scene_router/BreadCrumb.java
@@ -0,0 +1,10 @@
+package com.andrewlalis.javafx_scene_router;
+
+/**
+ * A breadcrumb entry that represents one item in a route history.
+ * @param label The display label.
+ * @param route The route.
+ * @param context The context object for this route.
+ * @param current Whether the history this was generated from is at this route right now.
+ */
+public record BreadCrumb(String label, String route, Object context, boolean current) {}
diff --git a/src/main/java/com/andrewlalis/javafx_scene_router/RouteHistory.java b/src/main/java/com/andrewlalis/javafx_scene_router/RouteHistory.java
new file mode 100644
index 0000000..63bb612
--- /dev/null
+++ b/src/main/java/com/andrewlalis/javafx_scene_router/RouteHistory.java
@@ -0,0 +1,127 @@
+package com.andrewlalis.javafx_scene_router;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * A component that tracks navigation history through a series of routes, and
+ * provides facilities for navigating forward and backward through the history,
+ * as well as pushing new routes into the history.
+ *
+ * This history is designed to work much like a typical web browser, where
+ * a linear history is maintained, such that you can move backward and
+ * forward along the history, but once you navigate to a new page, any
+ * forward-history is cleared.
+ *
+ */
+public class RouteHistory {
+ private final List items = new ArrayList<>();
+ private int currentItemIndex = -1;
+
+ /**
+ * Constructs a new history instance.
+ */
+ public RouteHistory() {}
+
+ /**
+ * Pushes a new route after the current place in the history, and clears
+ * all forward-routes beyond that.
+ * @param route The route to push.
+ * @param context The context object associated with the route.
+ */
+ public void push(String route, Object context) {
+ int nextIndex = currentItemIndex + 1;
+ items.subList(nextIndex, items.size()).clear();
+ items.add(nextIndex, new RouteHistoryItem(route, context));
+ currentItemIndex = nextIndex;
+ }
+
+ /**
+ * Gets the current context object, or null if none is set.
+ * @return The context object associated with the current route.
+ * @param The type to implicitly cast to. Note that this may result in
+ * an unchecked exception if you attempt to coerce to an invalid
+ * type.
+ */
+ @SuppressWarnings("unchecked")
+ public T getCurrentContext() {
+ if (currentItemIndex >= 0 && currentItemIndex < items.size()) {
+ return (T) items.get(currentItemIndex).context();
+ }
+ return null;
+ }
+
+ /**
+ * Checks if it's possible to navigate back in the history.
+ * @return True if it is possible to go back.
+ */
+ public boolean canGoBack() {
+ return currentItemIndex > 0;
+ }
+
+ /**
+ * Attempts to go back in the history.
+ * @return If successful, the previous history item that we went back to;
+ * empty otherwise.
+ */
+ public Optional back() {
+ if (canGoBack()) {
+ RouteHistoryItem prev = items.get(currentItemIndex - 1);
+ currentItemIndex--;
+ return Optional.of(prev);
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Checks if it's possible to navigate forward in the history.
+ * @return True if it is possible to go forward.
+ */
+ public boolean canGoForward() {
+ return currentItemIndex + 1 < items.size();
+ }
+
+ /**
+ * Attempts to go forward in the history.
+ * @return If successful, the next history item that we went forward to;
+ * empty otherwise.
+ */
+ public Optional forward() {
+ if (canGoForward()) {
+ RouteHistoryItem next = items.get(currentItemIndex + 1);
+ currentItemIndex++;
+ return Optional.of(next);
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Clears the history completely.
+ */
+ public void clear() {
+ items.clear();
+ currentItemIndex = -1;
+ }
+
+ /**
+ * Gets a list of "breadcrumbs", or a representation of the current history
+ * and indication of where we are in that history.
+ * @return The list of breadcrumbs.
+ */
+ public List getBreadCrumbs() {
+ if (items.isEmpty()) return Collections.emptyList();
+ List breadCrumbs = new ArrayList<>(items.size());
+ for (int i = 0; i < items.size(); i++) {
+ var item = items.get(i);
+ breadCrumbs.add(new BreadCrumb(
+ item.route(),
+ item.route(),
+ item.context(),
+ i == currentItemIndex
+ ));
+ }
+ return breadCrumbs;
+ }
+}
diff --git a/src/main/java/com/andrewlalis/javafx_scene_router/RouteHistoryItem.java b/src/main/java/com/andrewlalis/javafx_scene_router/RouteHistoryItem.java
new file mode 100644
index 0000000..6a3ce9b
--- /dev/null
+++ b/src/main/java/com/andrewlalis/javafx_scene_router/RouteHistoryItem.java
@@ -0,0 +1,8 @@
+package com.andrewlalis.javafx_scene_router;
+
+/**
+ * An entry that stores information about a point in a user's route history.
+ * @param route The route.
+ * @param context The context object associated with the route.
+ */
+record RouteHistoryItem(String route, Object context) {}
diff --git a/src/main/java/com/andrewlalis/javafx_scene_router/SceneRouter.java b/src/main/java/com/andrewlalis/javafx_scene_router/SceneRouter.java
new file mode 100644
index 0000000..5cb30c7
--- /dev/null
+++ b/src/main/java/com/andrewlalis/javafx_scene_router/SceneRouter.java
@@ -0,0 +1,163 @@
+package com.andrewlalis.javafx_scene_router;
+
+import javafx.application.Platform;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Parent;
+import javafx.scene.layout.Pane;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * A router that shows different content in a pane depending on which route is
+ * selected. Each router must be initialized with a JavaFX pane, or a consumer
+ * function that's called to set the content each time a new route is selected.
+ *
+ * The router has a mapping of "routes" (think, Strings) to JavaFX Parent
+ * nodes. When a route is selected, the router will lookup the mapped node,
+ * and put that node into the pre-defined pane or consumer function.
+ *
+ *
+ * The router maintains a {@link RouteHistory} so that it's possible to
+ * navigate backward and forward, much like a web browser would.
+ *
+ */
+public class SceneRouter {
+ private final Consumer setter;
+ private final Map routeMap = new HashMap<>();
+ private final RouteHistory history = new RouteHistory();
+
+ /**
+ * Constructs the router to show route content in the given pane.
+ * @param pane The pane to show route content in.
+ */
+ public SceneRouter(Pane pane) {
+ this(p -> pane.getChildren().setAll(p));
+ }
+
+ /**
+ * Constructs the router to supply route content to the given consumer, so
+ * that it may place the content somewhere. For example, you might like to
+ * use this if you'd like to have a router place content in the center of a
+ * {@link javafx.scene.layout.BorderPane}, like so:
+ * var router = new SceneRouter(myBorderPane::setCenter);
+ * @param setter The consumer that is supplied route content to show.
+ */
+ public SceneRouter(Consumer setter) {
+ this.setter = setter;
+ }
+
+ /**
+ * Maps the given route to a node, so that when the route is selected, the
+ * given node is shown.
+ * @param route The route.
+ * @param node The node to show.
+ * @return This router.
+ */
+ public SceneRouter map(String route, Parent node) {
+ routeMap.put(route, node);
+ return this;
+ }
+
+ /**
+ * Maps the given route to a node that is loaded from a given FXML resource.
+ * @param route The route.
+ * @param fxml The FXML classpath resource to load from.
+ * @param controllerCustomizer A function that takes controller instance
+ * from the loaded FXML and customizes it. This
+ * may be null.
+ * @return This router.
+ */
+ public SceneRouter map(String route, String fxml, Consumer> controllerCustomizer) {
+ return map(route, loadNode(fxml, controllerCustomizer));
+ }
+
+ /**
+ * Maps the given route to a node that is loaded from a given FXML resource.
+ * @param route The route.
+ * @param fxml The FXML classpath resource to load from.
+ * @return This router.
+ */
+ public SceneRouter map(String route, String fxml) {
+ return map(route, fxml, null);
+ }
+
+ /**
+ * Navigates to a given route, with a given context object.
+ * @param route The route to navigate to.
+ * @param context The context that should be available at that route.
+ */
+ public void navigate(String route, Object context) {
+ Platform.runLater(() -> {
+ history.push(route, context);
+ setter.accept(getMappedNode(route));
+ });
+ }
+
+ /**
+ * Navigates to a given route, without any context.
+ * @param route The route to navigate to.
+ */
+ public void navigate(String route) {
+ navigate(route, null);
+ }
+
+ /**
+ * Attempts to navigate back.
+ */
+ public void navigateBack() {
+ Platform.runLater(() -> history.back()
+ .ifPresent(prev -> setter.accept(getMappedNode(prev.route())))
+ );
+ }
+
+ /**
+ * Attempts to navigate forward.
+ */
+ public void navigateForward() {
+ Platform.runLater(() -> history.forward()
+ .ifPresent(next -> setter.accept(getMappedNode(next.route())))
+ );
+ }
+
+ /**
+ * Gets the context object for the current route.
+ * @return The context object, or null.
+ * @param The type of the object.
+ */
+ public T getContext() {
+ return history.getCurrentContext();
+ }
+
+ /**
+ * Gets the internal history representation of this router.
+ * @return The history used by this router.
+ */
+ public RouteHistory getHistory() {
+ return history;
+ }
+
+ private Parent getMappedNode(String route) {
+ Parent node = routeMap.get(route);
+ if (node == null) throw new IllegalArgumentException("Route " + route + " is not mapped to any node.");
+ return node;
+ }
+
+ private Parent loadNode(String fxml, Consumer controllerCustomizer) {
+ FXMLLoader loader = new FXMLLoader(SceneRouter.class.getResource(fxml));
+ try {
+ Parent p = loader.load();
+ if (controllerCustomizer != null) {
+ T controller = loader.getController();
+ if (controller == null) throw new IllegalStateException("No controller found when loading " + fxml);
+ controllerCustomizer.accept(controller);
+ }
+ return p;
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
new file mode 100644
index 0000000..acf59e9
--- /dev/null
+++ b/src/main/java/module-info.java
@@ -0,0 +1,10 @@
+/**
+ * The JavaFX-Scene-Router module. Require this to use the library.
+ */
+module com.andrewlalis.javafx_scene_router {
+ requires javafx.base;
+ requires javafx.controls;
+ requires javafx.fxml;
+
+ exports com.andrewlalis.javafx_scene_router;
+}