From b0ef8c582a01d9540546c763427a3f2d99e71fe0 Mon Sep 17 00:00:00 2001
From: StartsMercury <89975834+StartsMercury@users.noreply.github.com>
Date: Sun, 10 Nov 2024 12:19:57 +0800
Subject: [PATCH] Clean-up and Additional Bits

* Add .editorconfig
* Add .gitattributes
* Experimental input adapter, detecting key modifier
* Experimental Math util exposure: intersection calculation for mouse enter and exit
* Input API, allow registering multiple processors on top of normal Gdx.input
* Move single-use utils closer to calling class
* Simplify GameAssetLoaderMixin, moving complexity into helper class
* Use Gradle Kotlin DSL: better IDE support and less weird warnings (that adds "noise")
* Work on UI Component API
  * Initial children components
  * Preparing exact mouse enter and exit detection
---
 .editorconfig                                 |  20 +
 .gitattributes                                |   2 +
 build.gradle                                  |  64 --
 build.gradle.kts                              | 100 +++
 settings.gradle                               |  15 -
 settings.gradle.kts                           |  19 +
 .../flux/api/input/client/InputAdapterEx.java | 172 +++++
 .../input/client/InputProcessorHelper.java    |  60 ++
 .../api/input/client/MouseProcessorEx.java    |   7 +
 .../crmodders/flux/api/math/Intersection.java | 365 ++++++++++
 .../resource/loader/FluxFileHandle.java       |   2 +-
 .../components/client/AbstractUIObject.java   | 378 ----------
 .../api/ui/components/client/Component.java   | 656 ++++++++++++++++++
 .../crmodders/flux/impl/base/Constants.java   |   9 -
 .../dev/crmodders/flux/impl/base/Logging.java |   8 -
 .../dev/crmodders/flux/impl/base/Strings.java |   9 -
 .../client/FluxApiInputClientEntrypoint.java  |  12 +
 .../flux/impl/input/client/FluxInput.java     | 337 +++++++++
 .../impl/input/client/FluxInputProcessor.java | 204 ++++++
 .../impl/resource/loader/AssetFinder.java     |   9 +-
 .../resource/loader/FluxAssetLoading.java     |  95 +++
 .../resource/loader/GameAssetLoaderMixin.java |  80 +--
 src/main/resources/flux-api.mixins.json       |   2 +-
 src/main/resources/quilt.mod.json             |   1 +
 .../flux/api/math/IntersectionTest.java       | 130 ++++
 25 files changed, 2196 insertions(+), 560 deletions(-)
 create mode 100644 .editorconfig
 create mode 100644 .gitattributes
 delete mode 100644 build.gradle
 create mode 100644 build.gradle.kts
 delete mode 100644 settings.gradle
 create mode 100644 settings.gradle.kts
 create mode 100644 src/main/java/dev/crmodders/flux/api/input/client/InputAdapterEx.java
 create mode 100644 src/main/java/dev/crmodders/flux/api/input/client/InputProcessorHelper.java
 create mode 100644 src/main/java/dev/crmodders/flux/api/input/client/MouseProcessorEx.java
 create mode 100644 src/main/java/dev/crmodders/flux/api/math/Intersection.java
 rename src/main/java/dev/crmodders/flux/{impl => api}/resource/loader/FluxFileHandle.java (99%)
 delete mode 100644 src/main/java/dev/crmodders/flux/api/ui/components/client/AbstractUIObject.java
 create mode 100644 src/main/java/dev/crmodders/flux/api/ui/components/client/Component.java
 delete mode 100644 src/main/java/dev/crmodders/flux/impl/base/Constants.java
 delete mode 100644 src/main/java/dev/crmodders/flux/impl/base/Logging.java
 delete mode 100644 src/main/java/dev/crmodders/flux/impl/base/Strings.java
 create mode 100644 src/main/java/dev/crmodders/flux/impl/input/client/FluxApiInputClientEntrypoint.java
 create mode 100644 src/main/java/dev/crmodders/flux/impl/input/client/FluxInput.java
 create mode 100644 src/main/java/dev/crmodders/flux/impl/input/client/FluxInputProcessor.java
 create mode 100644 src/main/java/dev/crmodders/flux/impl/resource/loader/FluxAssetLoading.java
 create mode 100644 src/test/java/dev/crmodders/flux/api/math/IntersectionTest.java

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..e69374a
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,20 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+max_line_length = 100
+tab_width = 4
+ij_continuation_indent_size = 4
+ij_formatter_off_tag = @formatter:off
+ij_formatter_on_tag = @formatter:on
+ij_formatter_tags_enabled = false
+ij_smart_tabs = false
+ij_wrap_on_typing = false
+
+[*.java]
+ij_java_imports_layout = $*,|,*
+ij_java_class_count_to_use_import_on_demand = 999
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..0f09d32
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+* text=auto eol=lf
+*.bat text eol=crlf
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index b8eb06e..0000000
--- a/build.gradle
+++ /dev/null
@@ -1,64 +0,0 @@
-plugins {
-    id "cosmicloom"
-    id "maven-publish"
-}
-
-loom {
-    accessWidenerPath = file("src/main/resources/flux-api.accesswidener")
-}
-
-repositories {
-    flatDir {
-        dirs "lib"
-    }
-}
-
-dependencies {
-    cosmicReach(loom.getCosmicReach(cosmic_reach_version))
-    modImplementation(loom.getCosmicQuilt(cosmic_quilt_version))
-    runtimeOnly(":testmod:")
-}
-
-processResources {
-    // Locations of where to inject the properties
-    def resourceTargets = [
-        "quilt.mod.json"
-    ]
-
-    // Left item is the name in the target, right is the variable name
-    def replaceProperties = [
-        "mod_version" : project.version,
-        "mod_group" : project.group,
-        "mod_name" : project.name,
-        "mod_id" : id,
-        "mod_desc" : flux_desc,
-        "cosmic_reach_version": cosmic_reach_version,
-    ]
-
-    inputs.properties replaceProperties
-    replaceProperties.put "project", project
-    filesMatching(resourceTargets) {
-        expand replaceProperties
-    }
-}
-
-java {
-    withSourcesJar()
-    // withJavadocJar()
-
-    // Sets the Java version
-    sourceCompatibility = JavaVersion.VERSION_17
-    targetCompatibility = JavaVersion.VERSION_17
-}
-
-
-publishing {
-    publications {
-        maven(MavenPublication) {
-            groupId = group
-            artifactId = id
-
-            from components.java
-        }
-    }
-}
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..1eecf70
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,100 @@
+object Constants {
+    const val GROUP = "dev.crmodders"
+    const val MODID = "flux-api"
+    const val VERSION = "0.8.0-alpha.1"
+
+    const val TITLE = "Flux API"
+    const val DESCRIPTION = "Community focused API for Cosmic Reach Quilt"
+
+    const val VERSION_COSMIC_REACH = "0.3.6"
+    const val VERSION_COSMIC_QUILT = "03cc947b041184bc656e170d164ced5bc1477b37"
+}
+
+base {
+    group = Constants.GROUP
+    archivesName = Constants.MODID
+    version = Constants.VERSION
+}
+
+plugins {
+    `java-library`
+    `maven-publish`
+    id("cosmicloom")
+}
+
+java {
+    withSourcesJar()
+//    withJavadocJar()
+
+    // Sets the Java version
+    sourceCompatibility = JavaVersion.VERSION_17
+    targetCompatibility = JavaVersion.VERSION_17
+}
+
+loom {
+    accessWidenerPath = file("src/main/resources/flux-api.accesswidener")
+}
+
+repositories {
+    flatDir {
+        dirs("lib")
+    }
+}
+
+dependencies {
+    cosmicReach(loom.cosmicReachClient("pre-alpha", Constants.VERSION_COSMIC_REACH))
+    modImplementation(loom.cosmicQuilt(Constants.VERSION_COSMIC_QUILT))
+    runtimeOnly(":testmod:")
+
+    compileOnly("com.badlogicgames.gdx:gdx:1.12.1")
+}
+
+tasks.withType<ProcessResources> {
+    // Locations of where to inject the properties
+    val resourceTargets = listOf(
+        "quilt.mod.json"
+    )
+
+    // Left item is the name in the target, right is the variable name
+    val replaceProperties = mapOf(
+        "mod_group" to Constants.GROUP,
+        "mod_id" to Constants.MODID,
+        "mod_version" to Constants.VERSION,
+
+        "mod_name" to Constants.TITLE,
+        "mod_desc" to Constants.DESCRIPTION,
+
+        "cosmic_reach_version" to Constants.VERSION_COSMIC_REACH,
+    )
+
+    inputs.properties(replaceProperties)
+
+    filesMatching(resourceTargets) {
+        expand(replaceProperties)
+    }
+}
+
+testing {
+    suites {
+        val test by getting(JvmTestSuite::class) {
+            useJUnitJupiter()
+
+//            sources {
+//                val main by sourceSets.getting
+//                runtimeClasspath += main.runtimeClasspath
+//                compileClasspath += main.compileClasspath
+//            }
+        }
+    }
+}
+
+publishing {
+    publications {
+        create<MavenPublication>("maven") {
+            groupId = Constants.GROUP
+            artifactId = Constants.MODID
+
+            from(components["java"])
+        }
+    }
+}
diff --git a/settings.gradle b/settings.gradle
deleted file mode 100644
index 1fec258..0000000
--- a/settings.gradle
+++ /dev/null
@@ -1,15 +0,0 @@
-buildscript() {
-    repositories {
-        maven {
-            name "JitPack"
-            url "https://jitpack.io"
-        }
-
-        mavenCentral()
-    }
-    dependencies {
-        classpath "org.codeberg.CRModders:cosmic-loom:PR7-SNAPSHOT"
-    }
-}
-
-rootProject.name = "Flux API"
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..7a9046e
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,19 @@
+buildscript {
+    repositories {
+        maven {
+            name = "JitPack"
+            url = uri("https://jitpack.io")
+        }
+        mavenCentral()
+    }
+
+    dependencies {
+        classpath(
+            group = "org.codeberg.CRModders",
+            name = "cosmic-loom",
+            version = "PR7-SNAPSHOT",
+        )
+    }
+}
+
+rootProject.name = "Flux API"
diff --git a/src/main/java/dev/crmodders/flux/api/input/client/InputAdapterEx.java b/src/main/java/dev/crmodders/flux/api/input/client/InputAdapterEx.java
new file mode 100644
index 0000000..91ab134
--- /dev/null
+++ b/src/main/java/dev/crmodders/flux/api/input/client/InputAdapterEx.java
@@ -0,0 +1,172 @@
+package dev.crmodders.flux.api.input.client;
+
+import com.badlogic.gdx.Input;
+import com.badlogic.gdx.InputProcessor;
+import org.jetbrains.annotations.ApiStatus;
+
+@ApiStatus.Experimental
+public abstract class InputAdapterEx implements InputProcessor {
+    public static class Modifiers {
+        public static final int ALT = 1;
+        public static final int ALT_LEFT = 2;
+        public static final int ALT_RIGHT = 4;
+        public static final int CONTROL = 8;
+        public static final int CONTROL_LEFT = 16;
+        public static final int CONTROL_RIGHT = 32;
+        public static final int SHIFT = 64;
+        public static final int SHIFT_LEFT = 128;
+        public static final int SHIFT_RIGHT = 256;
+
+        public static final int ANY_ALT = ALT | ALT_LEFT | ALT_RIGHT;
+        public static final int ANY_CONTROL = CONTROL | CONTROL_LEFT | CONTROL_RIGHT;
+        public static final int ANY_SHIFT = SHIFT | SHIFT_LEFT | SHIFT_RIGHT;
+
+        public static boolean isAltDown(final int i) {
+            return (i & ANY_ALT) != 0;
+        }
+
+        public static boolean isControlDown(final int i) {
+            return (i & ANY_CONTROL) != 0;
+        }
+
+        public static boolean isShiftDown(final int i) {
+            return (i & ANY_SHIFT) != 0;
+        }
+
+        public static boolean isAltOnly(final int i) {
+            return isAltDown(i) && (i & ~ANY_ALT) == 0;
+        }
+
+        public static boolean isControlOnly(final int i) {
+            return isControlDown(i) && (i & ~ANY_CONTROL) == 0;
+        }
+
+        public static boolean isShiftOnly(final int i) {
+            return isShiftDown(i) && (i & ~ANY_SHIFT) == 0;
+        }
+    }
+
+    private int modifiers;
+
+    @Override
+    public boolean keyDown(final int keycode) {
+        final var modifiers = this.modifiers |= switch (keycode) {
+            case Input.Keys.ALT_LEFT -> Modifiers.ALT | Modifiers.ALT_LEFT;
+            case Input.Keys.ALT_RIGHT -> Modifiers.ALT | Modifiers.ALT_RIGHT;
+            case Input.Keys.SHIFT_LEFT -> Modifiers.SHIFT | Modifiers.SHIFT_LEFT;
+            case Input.Keys.SHIFT_RIGHT -> Modifiers.SHIFT | Modifiers.CONTROL_RIGHT;
+            case Input.Keys.CONTROL_LEFT -> Modifiers.CONTROL | Modifiers.CONTROL_LEFT;
+            case Input.Keys.CONTROL_RIGHT -> Modifiers.CONTROL | Modifiers.CONTROL_RIGHT;
+            default -> 0;
+        };
+        return this.keyDownEx(keycode, modifiers);
+    }
+
+    protected abstract boolean keyDownEx(int keycode, int modifiers);
+
+    @Override
+    public boolean keyUp(final int keycode) {
+        final var modifiers = this.modifiers;
+        final var result = this.keyUpEx(keycode, modifiers);
+
+        final int mask1;
+        final int mask2;
+        switch (keycode) {
+            case Input.Keys.ALT_LEFT -> {
+                mask1 = Modifiers.ALT | Modifiers.ALT_LEFT;
+                mask2 = Modifiers.ALT | Modifiers.ALT_RIGHT;
+            }
+            case Input.Keys.ALT_RIGHT -> {
+                mask1 = Modifiers.ALT | Modifiers.ALT_RIGHT;
+                mask2 = Modifiers.ALT | Modifiers.ALT_LEFT;
+            }
+            case Input.Keys.CONTROL_LEFT -> {
+                mask1 = Modifiers.CONTROL | Modifiers.CONTROL_LEFT;
+                mask2 = Modifiers.CONTROL | Modifiers.CONTROL_RIGHT;
+            }
+            case Input.Keys.CONTROL_RIGHT -> {
+                mask1 = Modifiers.CONTROL | Modifiers.CONTROL_RIGHT;
+                mask2 = Modifiers.CONTROL | Modifiers.CONTROL_LEFT;
+            }
+            case Input.Keys.SHIFT_LEFT -> {
+                mask1 = Modifiers.SHIFT | Modifiers.SHIFT_LEFT;
+                mask2 = Modifiers.SHIFT | Modifiers.SHIFT_RIGHT;
+            }
+            case Input.Keys.SHIFT_RIGHT -> {
+                mask1 = Modifiers.SHIFT | Modifiers.SHIFT_RIGHT;
+                mask2 = Modifiers.SHIFT | Modifiers.SHIFT_LEFT;
+            }
+            default -> {
+                mask1 = 0;
+                mask2 = 0;
+            }
+        }
+        final var mask = (modifiers & mask2) != mask2 ? mask1 : mask1 & ~mask2;
+        this.modifiers = modifiers & ~mask;
+
+        return result;
+    }
+
+    protected abstract boolean keyUpEx(int keycode, int modifiers);
+
+    @Override
+    public boolean touchDown(
+        final int screenX,
+        final int screenY,
+        final int pointer,
+        final int button
+    ) {
+        return this.touchDownEx(screenX, screenY, pointer, button, this.modifiers);
+    }
+
+    protected abstract boolean touchDownEx(int screenX, int screenY, int pointer, int button, int modifiers);
+
+    @Override
+    public boolean touchUp(
+        final int screenX,
+        final int screenY,
+        final int pointer,
+        final int button
+    ) {
+        return this.touchUpEx(screenX, screenY, pointer, button, this.modifiers);
+    }
+
+    protected abstract boolean touchUpEx(int screenX, int screenY, int pointer, int button, int modifiers);
+
+    @Override
+    public boolean touchCancelled(
+        final int screenX,
+        final int screenY,
+        final int pointer,
+        final int button
+    ) {
+        return this.touchCancelledEx(screenX, screenY, pointer, button, this.modifiers);
+    }
+
+    protected abstract boolean touchCancelledEx(int screenX, int screenY, int pointer, int button, int modifiers);
+
+    @Override
+    public boolean touchDragged(
+        final int screenX,
+        final int screenY,
+        final int pointer
+    ) {
+        return this.touchDraggedEx(screenX, screenY, pointer, this.modifiers);
+    }
+
+    protected abstract boolean touchDraggedEx(int screenX, int screenY, int pointer, int modifiers);
+
+    @Override
+    public boolean mouseMoved(final int screenX, final int screenY) {
+        return this.mouseMovedEx(screenX, screenY, this.modifiers);
+    }
+
+    protected abstract boolean mouseMovedEx(int screenX, int screenY, int modifiers);
+
+    @Override
+    public boolean scrolled(final float amountX, final float amountY) {
+        return this.scrolledEx(amountX, amountY, this.modifiers);
+    }
+
+    protected abstract boolean scrolledEx(float amountX, float amountY, int modifiers);
+}
diff --git a/src/main/java/dev/crmodders/flux/api/input/client/InputProcessorHelper.java b/src/main/java/dev/crmodders/flux/api/input/client/InputProcessorHelper.java
new file mode 100644
index 0000000..ab9ca78
--- /dev/null
+++ b/src/main/java/dev/crmodders/flux/api/input/client/InputProcessorHelper.java
@@ -0,0 +1,60 @@
+package dev.crmodders.flux.api.input.client;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.Input;
+import com.badlogic.gdx.InputProcessor;
+import dev.crmodders.flux.impl.input.client.FluxInput;
+
+import java.util.LinkedHashSet;
+
+public final class InputProcessorHelper {
+    /**
+     * Adds an input processor.
+     * <p>
+     * All added input processors are ran in insertion order through an internal
+     * {@link LinkedHashSet}. Addition will not succeed with a {@code null}
+     * processor, {@code this}, or the current main processor (through
+     * {@link Input#setInputProcessor}). The internal set works best with input
+     * processors that did not override the methods {@link Object#equals} and
+     * {@link Object#hashCode()}, wrapper or new-type might be needed otherwise.
+     *
+     * @param processor  The processor to add.
+     * @return {@code boolean} value representing addition success.
+     */
+    public static boolean register(final InputProcessor processor) {
+        return Gdx.input instanceof final FluxInput self && self.addInputProcessor(processor);
+    }
+
+    /**
+     * Checks input processor presence.
+     * <p>
+     * Checks if a given processor is present and added through
+     * {@code register}. This method does not check against the main
+     * processor.
+     *
+     * @param processor  The processor to check.
+     * @return {@code true} if present; {@code false} otherwise.
+     * @see #register
+     */
+    public static boolean isRegistered(final InputProcessor processor) {
+        return Gdx.input instanceof final FluxInput self && self.containsInputProcessor(processor);
+    }
+
+    /**
+     * Removes an input processor.
+     * <p>
+     * Removes a given processor added through {@code register}. This method
+     * cannot remove the main processor, use
+     * {@code Input.setInputProcessor(null)}.
+     *
+     * @param processor  The processor to remove.
+     * @return {@code boolean} value representing removal success.
+     * @see #register
+     * @see Input#setInputProcessor
+     */
+    public static boolean unregister(final InputProcessor processor) {
+        return Gdx.input instanceof final FluxInput self && self.removeInputProcessor(processor);
+    }
+
+    private InputProcessorHelper() {}
+}
diff --git a/src/main/java/dev/crmodders/flux/api/input/client/MouseProcessorEx.java b/src/main/java/dev/crmodders/flux/api/input/client/MouseProcessorEx.java
new file mode 100644
index 0000000..badad60
--- /dev/null
+++ b/src/main/java/dev/crmodders/flux/api/input/client/MouseProcessorEx.java
@@ -0,0 +1,7 @@
+package dev.crmodders.flux.api.input.client;
+
+public interface MouseProcessorEx {
+    boolean mouseEntered(float screenX, float screenY);
+
+    boolean mouseExited(float screenX, float screenY);
+}
diff --git a/src/main/java/dev/crmodders/flux/api/math/Intersection.java b/src/main/java/dev/crmodders/flux/api/math/Intersection.java
new file mode 100644
index 0000000..43d7144
--- /dev/null
+++ b/src/main/java/dev/crmodders/flux/api/math/Intersection.java
@@ -0,0 +1,365 @@
+package dev.crmodders.flux.api.math;
+
+public final class Intersection {
+    public enum Axes {
+        NONE, X, Y, BOTH;
+
+        public static final Axes ABSCISSA = X;
+        public static final Axes ORDINATE = Y;
+
+        public static final Axes DOMAIN = X;
+        public static final Axes RANGE = Y;
+
+        public static final Axes HORIZONTAL = X;
+        public static final Axes VERTICAL = Y;
+
+        @Override
+        public String toString() {
+            return switch (this) {
+                case NONE -> "NONE";
+                case X -> Axis.X.toString();
+                case Y -> Axis.Y.toString();
+                default -> "BOTH";
+            };
+        }
+    }
+
+    public enum Axis {
+        X, Y;
+
+        public static final Axis ABSCISSA = X;
+        public static final Axis ORDINATE = Y;
+
+        public static final Axis DOMAIN = X;
+        public static final Axis RANGE = Y;
+
+        public static final Axis HORIZONTAL = X;
+        public static final Axis VERTICAL = Y;
+
+        @Override
+        public String toString() {
+            return switch (this) {
+                case X -> "X/ASBCISSA/DOMAIN/HORIZONTAL";
+                case Y -> "Y/ORDINATE/RANGE/VERTICAL";
+            };
+        }
+    }
+
+    public enum Convex {
+        NONE, ENTER, EXIT, BOTH;
+
+        public int bufferStart() {
+            return this == EXIT ? 1 : 0;
+        }
+
+        public int bufferSize() {
+            return switch (this) {
+                case NONE -> 0;
+                case ENTER, EXIT -> 1;
+                case BOTH -> 2;
+            };
+        }
+
+        public int componentCount() {
+            return 2 * this.bufferSize();
+        }
+
+        public int componentStart() {
+            return 2 * this.bufferStart();
+        }
+
+        public boolean entered() {
+            return switch (this) {
+                case NONE, EXIT -> false;
+                case ENTER, BOTH -> true;
+            };
+        }
+
+        public boolean exited() {
+            return switch (this) {
+                case NONE, ENTER -> false;
+                case EXIT, BOTH -> true;
+            };
+        }
+    }
+
+    /**
+     * Finds line-AABB intersection.
+     * <p>
+     * This methods collects point intersections between a line segment and an AABB,
+     * storing into a buffer the point components and sorting it with the distance
+     * from the start point; there may be up to two intersections. It is assumed that
+     * moving left or up is negative while right or down is positive.
+     *
+     * <pre>{@code
+     *        float[] buffer = new float[4];
+     *        int count = intersectLineToAabb(x1, y1, x2, y2, l, r, d, u, buffer);
+     *        switch (count) {
+     *            case 2:
+     *                float x1 = buffer[0];
+     *                float y1 = buffer[1];
+     *                float x2 = buffer[2];
+     *                float y2 = buffer[3];
+     *            case 1:
+     *                float x = buffer[0];
+     *                float y = buffer[1];
+     *            case 0:
+     *            default:
+     *                break;
+     *        }
+     * }</pre>
+     *
+     * @param x1  The line's starting point.
+     * @param y1  The line's starting point.
+     * @param x2  The line's ending point.
+     * @param y2  The line's ending point.
+     * @param l  The left side of the AABB.
+     * @param r  The right side of the AABB.
+     * @param d  The top side of the AABB.
+     * @param u  The bottom side of the AABB.
+     * @param buffer  The output buffer.
+     * @return The number of intersections; between zero and two.
+     */
+    public static Convex lineSegmentAndAabbUnchecked(
+        final float x1,
+        final float y1,
+        final float x2,
+        final float y2,
+        final float l,
+        final float r,
+        final float u,
+        final float d,
+        final float[] buffer,
+        final int offset
+    ) {
+        assert l <= r : "Expected left not greater than right";
+        assert u <= d : "Expected up not greater than right";
+        assert buffer != null : "Expected nonnull buffer";
+        assert offset >= 0 : "Expected non-negative buffer offset";
+        assert offset < buffer.length : "Expected offset less than buffer capacity";
+        assert buffer.length >= 4 : "Expected buffer capacity no less than four";
+
+        // Java implementation of: https://www.desmos.com/calculator/vhtj9oiwm7
+
+        final float minX;
+        final float maxX;
+        final float a1;
+        final float a2;
+
+        if (x1 <= x2) {
+            minX = Math.max(x1, a1 = l);
+            maxX = Math.min(x2, a2 = r);
+        } else {
+            minX = Math.max(x2, a2 = l);
+            maxX = Math.min(x1, a1 = r);
+        }
+
+        final float minY;
+        final float maxY;
+        final float b1;
+        final float b2;
+
+        if (y1 <= y2) {
+            minY = Math.max(y1, b1 = u);
+            maxY = Math.min(y2, b2 = d);
+        } else {
+            minY = Math.max(y2, b2 = u);
+            maxY = Math.min(y1, b1 = d);
+        }
+
+        final var enter =
+            lineSegmentAndAxesUnchecked(x1, y1, x2, y2, minX, minY, maxX, maxY, a1, b1, buffer, offset)
+                != Axes.NONE;
+        final var exit =
+            lineSegmentAndAxesUnchecked(x2, y2, x1, y1, minX, minY, maxX, maxY, a2, b2, buffer, offset + 2)
+                != Axes.NONE;
+
+        if (enter && exit) {
+            return Convex.BOTH;
+        } else if (exit) {
+            return Convex.EXIT;
+        } else if (enter) {
+            return Convex.ENTER;
+        } else {
+            return Convex.NONE;
+        }
+    }
+
+    /**
+     * Line segment and axes intersection.
+     * <p>
+     * Collects the point intersection of a line segment and two perpendicular
+     * axes, if any. When the line coincides with the
+     *
+     * @param x1  The line starting x-component.
+     * @param y1  The line starting y-component.
+     * @param x2  The line ending x-component.
+     * @param y2  The line ending y-component.
+     * @param minX  The minimum returned x-component.
+     * @param minY  The minimum returned y-component.
+     * @param maxX  The maximum returned x-component.
+     * @param maxY  The maximum returned y-component.
+     * @param a  The x-position of the vertical line.
+     * @param b  The y-position of the horizontal line.
+     * @param buffer  The output buffer.
+     * @param offset  The output starting offset.
+     * @return The intersection result; amount of intersections.
+     */
+    public static Axes lineSegmentAndAxesUnchecked(
+        final float x1,
+        final float y1,
+        final float x2,
+        final float y2,
+        final float minX,
+        final float minY,
+        final float maxX,
+        final float maxY,
+        final float a,
+        final float b,
+        final float[] buffer,
+        final int offset
+    ) {
+        // This is unchecked after all, only present with `-ea` jvm arg
+        // Avoiding `&&` here might be more helpful when debugging...
+        assert minX <= maxX : "Expected min x no greater than max x";
+        assert minY <= maxY : "Expected min y no greater than max y";
+        assert buffer != null : "Expected nonnull buffer";
+        assert offset >= 0 : "Expected non-negative buffer offset";
+        assert offset < buffer.length : "Expected offset less than buffer capacity";
+        assert buffer.length >= 2 : "Expected buffer capacity no less than two";
+
+        vertical:
+        if (minX <= a && a <= maxX) {
+            final float c;
+
+            if (x1 == a && minX <= y1 && y1 <= maxX) {
+                c = y1;
+            } else if (x1 == x2) {
+                break vertical;
+            } else {
+                c = lineAndVerticalUnchecked(x1, y1, x2, y2, a);
+                if (c < minY || c > maxY) {
+                    break vertical;
+                }
+            }
+
+            buffer[offset] = a;
+            buffer[offset + 1] = c;
+
+            return c == b ? Axes.BOTH : Axes.VERTICAL;
+        }
+
+        horizontal:
+        if (minY <= b && b <= maxY) {
+            final float c;
+
+            if (y1 == b && minY <= y1 && y1 <= maxY) {
+                c = x1;
+            } else if (y1 == y2) {
+                break horizontal;
+            } else {
+                c = lineAndHorizontalUnchecked(x1, y1, x2, y2, b);
+                if (c < minX || c > maxX) {
+                    break horizontal;
+                }
+            }
+
+            buffer[offset] = c;
+            buffer[offset + 1] = b;
+
+            return c == a ? Axes.BOTH : Axes.HORIZONTAL;
+        }
+
+        return Axes.NONE;
+    }
+
+    /**
+     * Line and horizontal intersection.
+     *
+     * @param x1  Line first point x-component.
+     * @param y1  Line first point y-component.
+     * @param x2  Line second point x-component.
+     * @param y2  Line second point y-component.
+     * @param y  The y-position of the horizontal line.
+     * @return  The {@code y}-intercept.
+     * @implSpec <pre>{@code
+     *     Intersection.lineAndAxisUnchecked(x1, x2, y1, y2, y);
+     * }</pre>
+     * @see #lineAndAxisUnchecked
+     */
+    public static float lineAndHorizontalUnchecked(
+        final float x1,
+        final float y1,
+        final float x2,
+        final float y2,
+        final float y
+    ) {
+        return Intersection.lineAndAxisUnchecked(x1, x2, y1, y2, y);
+    }
+
+    /**
+     * Line and vertical intersection.
+     *
+     * @param x1  Line first point x-component.
+     * @param y1  Line first point y-component.
+     * @param x2  Line second point x-component.
+     * @param y2  Line second point y-component.
+     * @param x  The x-position of the vertical line.
+     * @return  The {@code y}-intercept.
+     * @implSpec <pre>{@code
+     *     Intersection.lineAndAxisUnchecked(y1, y2, x1, x2, x);
+     * }</pre>
+     * @see #lineAndAxisUnchecked
+     */
+    public static float lineAndVerticalUnchecked(
+        final float x1,
+        final float y1,
+        final float x2,
+        final float y2,
+        final float x
+    ) {
+        return Intersection.lineAndAxisUnchecked(y1, y2, x1, x2, x);
+    }
+
+    /**
+     * Line and axis intersection.
+     * <p>
+     * This method generalizes the computation for the intersection of a line
+     * and a vertical or horizontal line. Input validation should be done by the
+     * caller with all values are expected to be finite, the output value is
+     * not defined otherwise. Additionally, on {@code g1 == g2}, this will
+     * return {@code Float.NaN}, and usually means that the given line parallels
+     * the axis.
+     * <p>
+     * For a line segment, check the output, for example,
+     * {@code t1 <= t && t <= t2}, given that, {@code t1 <= t2}.
+     *
+     * @param t1  The line target component start.
+     * @param t2  The line target component end.
+     * @param g1  The line given component start.
+     * @param g2  The line given component end.
+     * @param g  The given value of the axis.
+     * @return  The value of {@code t} or the intercept to the axis.
+     * @implSpec <pre>{@code
+     *     (g2 - g1) * (t - t1) == (t2 - t1) * (g - g1)
+     *     // Division Property of Equality
+     *     t - t1  == (t2 - t1) * (g - g1) / (g2 - g1)
+     *     // Subtraction Property of Equality
+     *     t == (t2 - t1) * (g - g1) / (g2 - g1) + t1
+     * }</pre>
+     * @see #lineAndHorizontalUnchecked
+     * @see #lineAndVerticalUnchecked
+     * @see Float#NaN
+     */
+    public static float lineAndAxisUnchecked(
+        final float t1,
+        final float t2,
+        final float g1,
+        final float g2,
+        final float g
+    ) {
+        return (t2 - t1) * (g - g1) / (g2 - g1) + t1;
+    }
+
+    private Intersection() {}
+}
diff --git a/src/main/java/dev/crmodders/flux/impl/resource/loader/FluxFileHandle.java b/src/main/java/dev/crmodders/flux/api/resource/loader/FluxFileHandle.java
similarity index 99%
rename from src/main/java/dev/crmodders/flux/impl/resource/loader/FluxFileHandle.java
rename to src/main/java/dev/crmodders/flux/api/resource/loader/FluxFileHandle.java
index a1ab78e..521ef0d 100644
--- a/src/main/java/dev/crmodders/flux/impl/resource/loader/FluxFileHandle.java
+++ b/src/main/java/dev/crmodders/flux/api/resource/loader/FluxFileHandle.java
@@ -1,4 +1,4 @@
-package dev.crmodders.flux.impl.resource.loader;
+package dev.crmodders.flux.api.resource.loader;
 
 import com.badlogic.gdx.Files.FileType;
 import com.badlogic.gdx.files.FileHandle;
diff --git a/src/main/java/dev/crmodders/flux/api/ui/components/client/AbstractUIObject.java b/src/main/java/dev/crmodders/flux/api/ui/components/client/AbstractUIObject.java
deleted file mode 100644
index da45098..0000000
--- a/src/main/java/dev/crmodders/flux/api/ui/components/client/AbstractUIObject.java
+++ /dev/null
@@ -1,378 +0,0 @@
-package dev.crmodders.flux.api.ui.components.client;
-
-import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.graphics.Texture;
-import com.badlogic.gdx.graphics.g2d.SpriteBatch;
-import com.badlogic.gdx.graphics.g2d.TextureRegion;
-import com.badlogic.gdx.math.Rectangle;
-import com.badlogic.gdx.math.Vector2;
-import com.badlogic.gdx.utils.viewport.Viewport;
-import finalforeach.cosmicreach.audio.SoundManager;
-import finalforeach.cosmicreach.ui.*;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.Nullable;
-
-import java.util.Objects;
-
-import static finalforeach.cosmicreach.ui.UIElement.*;
-import static finalforeach.cosmicreach.ui.UIElement.uiPanelHoverBoundsTex;
-
-@ApiStatus.Experimental
-public class AbstractUIObject implements UIObject {
-    public static Color defaultTextColor() {
-        final var self = new Color();
-
-        // SAFETY: sets the value to WHITE, without clamping
-        self.a = 1.0f;
-        self.r = 1.0f;
-        self.g = 1.0f;
-        self.b = 1.0f;
-
-        return self;
-    }
-
-    ///////////////////////////////
-    // SIZE AND POSITIONING
-    ///////////////////////////////
-
-    /**
-     * Stores the anchor position and size.
-     */
-    private final Rectangle bounds;
-
-    @Override
-    public void setX(final float x) {
-        this.bounds.x = x;
-    }
-
-    @Override
-    public void setY(final float y) {
-        this.bounds.y = y;
-    }
-
-    @Override
-    public float getWidth() {
-        return this.bounds.width;
-    }
-
-    @Override
-    public float getHeight() {
-        return this.bounds.height;
-    }
-
-    private HorizontalAnchor horizontalAnchor;
-
-    public HorizontalAnchor getHorizontalAnchor() {
-        return this.horizontalAnchor;
-    }
-
-    public void setHorizontalAnchor(final HorizontalAnchor horizontalAnchor) {
-        Objects.requireNonNull(horizontalAnchor);
-        this.horizontalAnchor = horizontalAnchor;
-    }
-
-    public void alignToLeft() {
-        this.horizontalAnchor = HorizontalAnchor.LEFT_ALIGNED;
-    }
-
-    public void centerHorizontally() {
-        this.horizontalAnchor = HorizontalAnchor.CENTERED;
-    }
-
-    public void alignToRight() {
-        this.horizontalAnchor = HorizontalAnchor.RIGHT_ALIGNED;
-    }
-
-    private VerticalAnchor verticalAnchor;
-
-    public VerticalAnchor getVerticalAnchor() {
-        return this.verticalAnchor;
-    }
-
-    public void setVerticalAnchor(final VerticalAnchor verticalAnchor) {
-        Objects.requireNonNull(verticalAnchor);
-        this.verticalAnchor = verticalAnchor;
-    }
-
-    public void alignToTop() {
-        this.verticalAnchor = VerticalAnchor.TOP_ALIGNED;
-    }
-
-    public void centerVertically() {
-        this.verticalAnchor = VerticalAnchor.CENTERED;
-    }
-
-    public void alignToBottom() {
-        this.verticalAnchor = VerticalAnchor.BOTTOM_ALIGNED;
-    }
-
-    /**
-     * The main color, usually used to tint text.
-     */
-    private final Color color;
-
-    /**
-     * Buffer used when dealing with the {@code ScissorStack}.
-     */
-    private final Rectangle scissors;
-
-    private final Vector2 tmpVec;
-
-    private boolean active;
-
-    private Texture buttonTexture;
-
-    private boolean disabled;
-
-    private boolean hovered;
-
-    private @Nullable String text;
-
-    private boolean visible;
-
-    public AbstractUIObject() {
-        System.out.println(Gdx.input.getInputProcessor());
-        this.bounds = new Rectangle();
-        this.horizontalAnchor = HorizontalAnchor.CENTERED;
-        this.color = AbstractUIObject.defaultTextColor();
-        this.scissors = new Rectangle();
-        this.tmpVec = new Vector2();
-        this.verticalAnchor = VerticalAnchor.CENTERED;
-    }
-
-    public void onCreate() {
-    }
-
-    public void onClick() {
-    }
-
-    public void onMouseDown() {
-    }
-
-    public void onMouseUp() {
-    }
-
-    public boolean isHoveredOver(Viewport viewport, float x, float y) {
-        float dx = this.getDisplayX(viewport);
-        float dy = this.getDisplayY(viewport);
-        return x >= dx && y >= dy && x < dx + this.bounds.width && y < dy + this.bounds.height;
-    }
-
-    protected float getDisplayX(final Viewport viewport) {
-        final var x = this.bounds.x;
-
-        return switch (this.horizontalAnchor) {
-            case LEFT_ALIGNED -> x - viewport.getWorldWidth() / 2.0F;
-            case RIGHT_ALIGNED -> x + viewport.getWorldWidth() / 2.0F - this.bounds.width;
-            default -> x - this.bounds.width / 2.0F;
-        };
-    }
-
-    protected float getDisplayY(final Viewport viewport) {
-        final var y = this.bounds.y;
-
-        return switch (this.verticalAnchor) {
-            case TOP_ALIGNED -> y - viewport.getWorldHeight() / 2.0F;
-            case BOTTOM_ALIGNED -> y + viewport.getWorldHeight() / 2.0F - this.bounds.height;
-            default -> y - this.bounds.height / 2.0F;
-        };
-    }
-
-    @Override
-    public void drawBackground(
-            final Viewport viewport,
-            final SpriteBatch batch,
-            final float mouseX,
-            final float mouseY
-    ) {
-        if (this.visible) {
-            this.buttonTexture = uiPanelTex;
-            if (Gdx.input.isButtonJustPressed(0) && Gdx.input.isButtonPressed(0)) {
-                this.active = true;
-            }
-
-            if (this.active && !Gdx.input.isButtonPressed(0)) {
-                this.active = false;
-                this.onMouseUp();
-            }
-
-            if (this.isHoveredOver(viewport, mouseX, mouseY)) {
-                if (!this.hovered) {
-                    SoundManager.INSTANCE.playSound(onHoverSound);
-                    this.hovered = true;
-                }
-
-                if (Gdx.input.isButtonJustPressed(0)) {
-                    this.onMouseDown();
-                }
-
-                if (Gdx.input.isButtonJustPressed(0)) {
-                    this.buttonTexture = uiPanelPressedTex;
-                }
-            } else {
-                this.hovered = false;
-                if (Gdx.input.isButtonJustPressed(0) && !Gdx.input.isButtonPressed(0)) {
-                    this.active = false;
-                }
-            }
-
-            this.drawElementBackground(viewport, batch);
-            if (Gdx.input.isButtonJustPressed(0) && !Gdx.input.isButtonPressed(0)) {
-                this.buttonTexture = uiPanelPressedTex;
-                this.onClick();
-                SoundManager.INSTANCE.playSound(onClickSound);
-            }
-
-        }
-    }
-
-    private void drawElementBackground(
-            final Viewport viewport,
-            final SpriteBatch batch
-    ) {
-        float x = this.getDisplayX(viewport);
-        float y = this.getDisplayY(viewport);
-        if (!this.active && (!this.hovered || currentlyHeldElement != null)) {
-            batch.draw(uiPanelBoundsTex, x, y, 0.0F, 0.0F, this.bounds.width, this.bounds.height, 1.0F, 1.0F, 0.0F, 0, 0, this.buttonTexture.getWidth(), this.buttonTexture.getHeight(), false, true);
-        } else {
-            batch.draw(uiPanelHoverBoundsTex, x, y, 0.0F, 0.0F, this.bounds.width, this.bounds.height, 1.0F, 1.0F, 0.0F, 0, 0, this.buttonTexture.getWidth(), this.buttonTexture.getHeight(), false, true);
-        }
-
-        batch.draw(this.buttonTexture, x + 1.0F, y + 1.0F, 1.0F, 1.0F, this.bounds.width - 2.0F, this.bounds.height - 2.0F, 1.0F, 1.0F, 0.0F, 0, 0, this.buttonTexture.getWidth(), this.buttonTexture.getHeight(), false, true);
-    }
-
-    @Override
-    public void drawText(final Viewport viewport, final SpriteBatch batch) {
-        if (this.visible && this.text != null && !this.text.isEmpty()) {
-            float x = this.getDisplayX(viewport);
-            float y = this.getDisplayY(viewport);
-            FontRenderer.getTextDimensions(viewport, this.text, this.tmpVec);
-            if (this.tmpVec.x > this.bounds.width) {
-                FontRenderer.drawTextbox(batch, viewport, this.text, x, y, this.bounds.width);
-            } else {
-                float maxX = x;
-                float maxY = y;
-
-                for(int i = 0; i < this.text.length(); ++i) {
-                    char c = this.text.charAt(i);
-                    FontTexture f = FontRenderer.getFontTexOfChar(c);
-                    if (f == null) {
-                        c = '?';
-                        f = FontRenderer.getFontTexOfChar(c);
-                    }
-
-                    TextureRegion texReg = f.getTexRegForChar(c);
-                    x -= f.getCharStartPos(c).x % (float)texReg.getRegionWidth();
-                    switch (c) {
-                        case '\n':
-                            y += (float)texReg.getRegionHeight();
-                            x = this.bounds.x;
-                            maxX = Math.max(maxX, x);
-                            maxY = Math.max(maxY, y);
-                            break;
-                        case ' ':
-                            x += f.getCharSize(c).x / 4.0F;
-                            maxX = Math.max(maxX, x);
-                            break;
-                        default:
-                            x += f.getCharSize(c).x + f.getCharStartPos(c).x % (float)texReg.getRegionWidth() + 2.0F;
-                            maxX = Math.max(maxX, x);
-                            maxY = Math.max(maxY, y + (float)texReg.getRegionHeight());
-                    }
-                }
-
-                x = this.getDisplayX(viewport);
-                y = this.getDisplayY(viewport);
-                x += this.bounds.width / 2.0F - (maxX - x) / 2.0F;
-                y += this.bounds.height / 2.0F - (maxY - y) / 2.0F;
-
-                final var oldColor = new Color(batch.getColor());
-                batch.setColor(this.color);
-                FontRenderer.drawText(batch, viewport, this.text, x, y);
-                batch.setColor(oldColor);
-            }
-        }
-    }
-
-    public String getText() {
-        return this.text;
-    }
-
-    public void setText(final String text) {
-        this.text = text;
-    }
-
-    @Override
-    public void updateText() {
-
-    }
-
-    @Override
-    public void show() {
-        this.visible = true;
-    }
-
-    @Override
-    public void hide() {
-        this.visible = false;
-    }
-
-    @Override
-    public void deactivate() {
-
-    }
-
-    @Override
-    public boolean keyDown(int i) {
-        return false;
-    }
-
-    @Override
-    public boolean keyUp(int i) {
-        return false;
-    }
-
-    @Override
-    public boolean keyTyped(char c) {
-        return false;
-    }
-
-    @Override
-    public boolean touchDown(int i, int i1, int i2, int i3) {
-        return false;
-    }
-
-    @Override
-    public boolean touchUp(int i, int i1, int i2, int i3) {
-        return false;
-    }
-
-    @Override
-    public boolean touchCancelled(int i, int i1, int i2, int i3) {
-        return false;
-    }
-
-    @Override
-    public boolean touchDragged(int i, int i1, int i2) {
-        return false;
-    }
-
-    @Override
-    public boolean mouseMoved(int screenX, int screenY) {
-        return false;
-    }
-
-    @Override
-    public boolean scrolled(float v, float v1) {
-        return false;
-    }
-
-    public void setSize(final float width, final float height) {
-        this.bounds.setSize(width, height);
-    }
-
-    public boolean isVisible() {
-        return this.visible;
-    }
-}
diff --git a/src/main/java/dev/crmodders/flux/api/ui/components/client/Component.java b/src/main/java/dev/crmodders/flux/api/ui/components/client/Component.java
new file mode 100644
index 0000000..9ac21bd
--- /dev/null
+++ b/src/main/java/dev/crmodders/flux/api/ui/components/client/Component.java
@@ -0,0 +1,656 @@
+package dev.crmodders.flux.api.ui.components.client;
+
+import static finalforeach.cosmicreach.ui.UIElement.*;
+
+import com.badlogic.gdx.Input;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.g2d.SpriteBatch;
+import com.badlogic.gdx.math.Rectangle;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.viewport.Viewport;
+import dev.crmodders.flux.api.input.client.InputAdapterEx;
+import dev.crmodders.flux.api.input.client.MouseProcessorEx;
+import dev.crmodders.flux.api.math.Intersection;
+import finalforeach.cosmicreach.audio.SoundManager;
+import finalforeach.cosmicreach.ui.FontRenderer;
+import finalforeach.cosmicreach.ui.HorizontalAnchor;
+import finalforeach.cosmicreach.ui.UIObject;
+import finalforeach.cosmicreach.ui.VerticalAnchor;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+@ApiStatus.Experimental
+public class Component implements MouseProcessorEx, UIObject {
+    public static Color defaultTextColor() {
+        final var self = new Color();
+
+        // SAFETY: sets the value to WHITE, without clamping
+        self.a = 1.0f;
+        self.r = 1.0f;
+        self.g = 1.0f;
+        self.b = 1.0f;
+
+        return self;
+    }
+
+    ///////////////////////////////
+    // SIZE AND POSITIONING
+    ///////////////////////////////
+
+    /**
+     * Stores the anchor position and size.
+     */
+    private final Rectangle bounds;
+
+    @Override
+    public void setX(final float x) {
+        this.bounds.x = x;
+    }
+
+    @Override
+    public void setY(final float y) {
+        this.bounds.y = y;
+    }
+
+    @Override
+    public float getWidth() {
+        return this.bounds.width;
+    }
+
+    @Override
+    public float getHeight() {
+        return this.bounds.height;
+    }
+
+    private HorizontalAnchor horizontalAnchor;
+
+    public HorizontalAnchor getHorizontalAnchor() {
+        return this.horizontalAnchor;
+    }
+
+    public void setHorizontalAnchor(final HorizontalAnchor horizontalAnchor) {
+        Objects.requireNonNull(horizontalAnchor);
+        this.horizontalAnchor = horizontalAnchor;
+    }
+
+    public void alignToLeft() {
+        this.horizontalAnchor = HorizontalAnchor.LEFT_ALIGNED;
+    }
+
+    public void centerHorizontally() {
+        this.horizontalAnchor = HorizontalAnchor.CENTERED;
+    }
+
+    public void alignToRight() {
+        this.horizontalAnchor = HorizontalAnchor.RIGHT_ALIGNED;
+    }
+
+    private VerticalAnchor verticalAnchor;
+
+    public VerticalAnchor getVerticalAnchor() {
+        return this.verticalAnchor;
+    }
+
+    public void setVerticalAnchor(final VerticalAnchor verticalAnchor) {
+        Objects.requireNonNull(verticalAnchor);
+        this.verticalAnchor = verticalAnchor;
+    }
+
+    public void alignToTop() {
+        this.verticalAnchor = VerticalAnchor.TOP_ALIGNED;
+    }
+
+    public void centerVertically() {
+        this.verticalAnchor = VerticalAnchor.CENTERED;
+    }
+
+    public void alignToBottom() {
+        this.verticalAnchor = VerticalAnchor.BOTTOM_ALIGNED;
+    }
+
+    /**
+     * The main color, usually used to tint text.
+     */
+    private final Color color;
+
+    /**
+     * Buffer used when dealing with the {@code ScissorStack}.
+     */
+    private final Rectangle scissors;
+
+    private final Vector2 tmpVec;
+
+    private boolean active;
+
+    private final List<Component> children;
+
+    private final List<? extends Component> childrenView;
+
+    private boolean disabled;
+
+    private int focusedChildIndex;
+
+    private boolean hovered;
+
+    private int hoveredChildIndex;
+
+    private final ComponentInputAdapter inputProcessor;
+
+    private final float[] mouseEnterExit;
+
+    private int prevMouseX;
+
+    private int prevMouseY;
+
+    private @Nullable String text;
+
+    private final Viewport viewport;
+
+    private boolean visible;
+
+    public Component(final Viewport viewport) {
+        this.bounds = new Rectangle();
+        this.children = new ArrayList<>();
+        this.childrenView = Collections.unmodifiableList(this.children);
+        this.color = Component.defaultTextColor();
+        this.focusedChildIndex = -1;
+        this.horizontalAnchor = HorizontalAnchor.CENTERED;
+        this.inputProcessor = new ComponentInputAdapter();
+        this.mouseEnterExit = new float[Intersection.Convex.BOTH.bufferSize()];
+        this.prevMouseX = -1;
+        this.prevMouseY = -1;
+        this.scissors = new Rectangle();
+        this.tmpVec = new Vector2();
+        this.verticalAnchor = VerticalAnchor.CENTERED;
+        this.viewport = viewport;
+    }
+
+    public void addChild(final Component component) {
+        this.children.add(component);
+    }
+
+    public void insertChild(final Component component, final int index) {
+        this.children.add(index, component);
+
+        final var curr = this.focusedChildIndex;
+        if (curr >= index) {
+            this.focusNextChildFrom(curr);
+        }
+    }
+
+    public Component removeChildAt(final int index) {
+        final var component = this.children.remove(index);
+
+        final var curr = this.focusedChildIndex;
+        if (curr == index) {
+            this.focusedChildIndex = -1;
+        } else if (curr > index) {
+            this.focusPrevChildFrom(curr);
+        }
+
+        return component;
+    }
+
+    public void clearChildren() {
+        this.focusedChildIndex = -1;
+        this.children.clear();
+    }
+
+    public final List<? extends Component> getChildrenView() {
+        return this.childrenView;
+    }
+
+    protected void focusNextChildFrom(int index) {
+        if (++index >= this.children.size()) {
+            index = -1;
+        }
+        this.focusedChildIndex = index;
+    }
+
+    protected void focusPrevChildFrom(int index) {
+        if (index < 0) {
+            index = this.children.size();
+        }
+        this.focusedChildIndex = index - 1;
+    }
+
+    public boolean isHoveredOver(Viewport viewport, float x, float y) {
+        float dx = this.getDisplayX(viewport);
+        float dy = this.getDisplayY(viewport);
+        return x >= dx && y >= dy && x < dx + this.bounds.width && y < dy + this.bounds.height;
+    }
+
+    protected float getDisplayX(final Viewport viewport) {
+        final var x = this.bounds.x;
+
+        return switch (this.horizontalAnchor) {
+            case LEFT_ALIGNED -> x - viewport.getWorldWidth() / 2.0F;
+            case RIGHT_ALIGNED -> x + viewport.getWorldWidth() / 2.0F - this.bounds.width;
+            default -> x - this.bounds.width / 2.0F;
+        };
+    }
+
+    protected float getDisplayY(final Viewport viewport) {
+        final var y = this.bounds.y;
+
+        return switch (this.verticalAnchor) {
+            case TOP_ALIGNED -> y - viewport.getWorldHeight() / 2.0F;
+            case BOTTOM_ALIGNED -> y + viewport.getWorldHeight() / 2.0F - this.bounds.height;
+            default -> y - this.bounds.height / 2.0F;
+        };
+    }
+
+    protected float getDisplayX2(final Viewport viewport) {
+        final var x = this.bounds.x + this.bounds.width;
+
+        return switch (this.horizontalAnchor) {
+            case LEFT_ALIGNED -> x - viewport.getWorldWidth() / 2.0F;
+            case RIGHT_ALIGNED -> x + viewport.getWorldWidth() / 2.0F - this.bounds.width;
+            default -> x - this.bounds.width / 2.0F;
+        };
+    }
+
+    protected float getDisplayY2(final Viewport viewport) {
+        final var y = this.bounds.y + this.bounds.height;
+
+        return switch (this.verticalAnchor) {
+            case TOP_ALIGNED -> y - viewport.getWorldHeight() / 2.0F;
+            case BOTTOM_ALIGNED -> y + viewport.getWorldHeight() / 2.0F - this.bounds.height;
+            default -> y - this.bounds.height / 2.0F;
+        };
+    }
+
+    @Override
+    public void drawBackground(
+        final Viewport viewport,
+        final SpriteBatch batch,
+        final float mouseX,
+        final float mouseY
+    ) {
+        if (!this.visible) {
+            return;
+        }
+
+        // Cosmic Reach currently only has hover, no focusing, especially tab
+        // focusing, and thus the lack of a dedicated texture.
+        final var boundsTexture =
+            this.active || this.hovered ? uiPanelHoverBoundsTex : uiPanelBoundsTex;
+        final var buttonTexture = this.active ? uiPanelPressedTex : uiPanelTex;
+        final var x = this.getDisplayX(viewport);
+        final var y = this.getDisplayY(viewport);
+
+        batch.draw(boundsTexture, x, y, 0.0F, 0.0F, this.bounds.width, this.bounds.height, 1.0F, 1.0F, 0.0F, 0, 0, buttonTexture.getWidth(), buttonTexture.getHeight(), false, true);
+        batch.draw(buttonTexture, x + 1.0F, y + 1.0F, 1.0F, 1.0F, this.bounds.width - 2.0F, this.bounds.height - 2.0F, 1.0F, 1.0F, 0.0F, 0, 0, buttonTexture.getWidth(), buttonTexture.getHeight(), false, true);
+    }
+
+    @Override
+    public void drawText(final Viewport viewport, final SpriteBatch batch) {
+        final var text = this.text;
+
+        if (!this.visible || text == null || text.isEmpty()) {
+            return;
+        }
+
+        var x = this.getDisplayX(viewport);
+        var y = this.getDisplayY(viewport);
+
+        FontRenderer.getTextDimensions(viewport, text, this.tmpVec);
+
+        if (this.tmpVec.x > this.bounds.width) {
+            FontRenderer.drawTextbox(batch, viewport, text, x, y, this.bounds.width);
+            return;
+        }
+
+        var maxX = x;
+        var maxY = y;
+
+        for (var i = 0; i < text.length(); ++i) {
+            char c = text.charAt(i);
+
+            var f = FontRenderer.getFontTexOfChar(c);
+
+            if (f == null) {
+                c = '?';
+                f = FontRenderer.getFontTexOfChar(c);
+            }
+
+            final var texReg = f.getTexRegForChar(c);
+            x -= f.getCharStartPos(c).x % (float) texReg.getRegionWidth();
+
+            switch (c) {
+                case '\n' -> {
+                    y += (float) texReg.getRegionHeight();
+                    x = this.bounds.x;
+                    maxX = Math.max(maxX, x);
+                    maxY = Math.max(maxY, y);
+                }
+                case ' ' -> {
+                    x += f.getCharSize(c).x / 4.0F;
+                    maxX = Math.max(maxX, x);
+                }
+                default -> {
+                    x += f.getCharSize(c).x + f.getCharStartPos(c).x % (float) texReg.getRegionWidth() + 2.0F;
+                    maxX = Math.max(maxX, x);
+                    maxY = Math.max(maxY, y + (float) texReg.getRegionHeight());
+                }
+            }
+        }
+
+        x = this.getDisplayX(viewport);
+        y = this.getDisplayY(viewport);
+        x += this.bounds.width / 2.0F - (maxX - x) / 2.0F;
+        y += this.bounds.height / 2.0F - (maxY - y) / 2.0F;
+
+        final var oldColor = new Color(batch.getColor());
+        batch.setColor(this.color);
+        FontRenderer.drawText(batch, viewport, text, x, y);
+        batch.setColor(oldColor);
+    }
+
+    public String getText() {
+        return this.text;
+    }
+
+    public void setText(final @Nullable String text) {
+        this.text = text;
+    }
+
+    public @Nullable Component getChildAt(final int index) {
+        return 0 <= index && index < this.children.size() ? this.children.get(index) : null;
+    }
+
+    public int getFocusedChildIndex() {
+        return this.focusedChildIndex;
+    }
+
+    public @Nullable Component getFocusedChild() {
+        return this.getChildAt(this.focusedChildIndex);
+    }
+
+    public int getHoveredChildIndex() {
+        return this.hoveredChildIndex;
+    }
+
+    public @Nullable Component getHoveredChild() {
+        return this.getChildAt(this.hoveredChildIndex);
+    }
+
+    @Override
+    public void updateText() {
+
+    }
+
+    @Override
+    public void show() {
+        this.visible = true;
+    }
+
+    @Override
+    public void hide() {
+        this.visible = false;
+    }
+
+    @Override
+    public void deactivate() {
+
+    }
+
+    @Override
+    public boolean keyDown(int keycode) {
+        return this.inputProcessor.keyDown(keycode);
+    }
+
+    @Override
+    public boolean keyUp(int keycode) {
+        return this.inputProcessor.keyUp(keycode);
+    }
+
+    @Override
+    public boolean keyTyped(char character) {
+        return this.inputProcessor.keyTyped(character);
+    }
+
+    @Override
+    public boolean touchDown(int screenX, int screenY, int pointer, int button) {
+        var index = -1;
+
+        for (var i = 0; i < this.children.size(); i++) {
+            final var child = this.children.get(i);
+
+            if (child != null && child.isHoveredOver(this.viewport, screenX, screenY)) {
+                child.mouseEntered(screenX, screenY);
+                index = i;
+            }
+        }
+
+        final var focused = this.getFocusedChild();
+        if (focused != null) {
+            focused.active = false;
+        }
+
+        final var child = this.children.get(index);
+        if (child != null) {
+            child.active = true;
+            child.hovered = true;
+        }
+
+        this.hoveredChildIndex = index;
+
+        if (this.inputProcessor.touchDown(screenX, screenY, pointer, button)) {
+            SoundManager.INSTANCE.playSound(onClickSound);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean touchUp(int screenX, int screenY, int pointer, int button) {
+        final var child = this.getHoveredChild();
+        if (child != null) {
+            child.mouseExited(screenX, screenY);
+            child.hovered = false;
+        }
+        this.hoveredChildIndex = 0;
+        return this.inputProcessor.touchUp(screenX, screenY, pointer, button);
+    }
+
+    @Override
+    public boolean touchCancelled(int screenX, int screenY, int pointer, int button) {
+        final var child = this.getHoveredChild();
+        if (child != null) {
+            child.mouseExited(screenX, screenY);
+            child.hovered = false;
+        }
+        this.hoveredChildIndex = 0;
+        return this.inputProcessor.touchCancelled(screenX, screenY, pointer, button);
+    }
+
+    @Override
+    public boolean touchDragged(int screenX, int screenY, int pointer) {
+        this.handleMouseMoved(screenX, screenY);
+        return this.inputProcessor.touchDragged(screenX, screenY, pointer);
+    }
+
+    @Override
+    public boolean mouseMoved(int screenX, int screenY) {
+        this.handleMouseMoved(screenX, screenY);
+        return this.inputProcessor.mouseMoved(screenX, screenY);
+    }
+
+    protected void handleMouseMoved(final int screenX, final int screenY) {
+        final var prevMouseX = this.prevMouseX;
+        final var prevMouseY = this.prevMouseY;
+
+        var index = -1;
+
+        for (var i = 0; i < this.children.size(); i++) {
+            final var child = this.children.get(i);
+
+            final var axes = Intersection.lineSegmentAndAabbUnchecked(
+                prevMouseX,
+                prevMouseY,
+                screenX,
+                screenY,
+                this.getDisplayX(this.viewport),
+                this.getDisplayY(this.viewport),
+                this.getDisplayX2(this.viewport),
+                this.getDisplayY2(this.viewport),
+                this.mouseEnterExit,
+                0
+            );
+
+            // Preserves last hovered when a child is entered-exited in one event
+            final var prev = index;
+
+            if (axes.entered()) {
+                child.mouseEntered(this.mouseEnterExit[0], this.mouseEnterExit[1]);
+                index = i;
+            }
+
+            if (axes.exited()) {
+                child.mouseExited(this.mouseEnterExit[2], this.mouseEnterExit[3]);
+                child.hovered = false;
+                index = prev;
+            }
+        }
+
+        final var child = this.children.get(index);
+        if (child != null) {
+            child.hovered = true;
+        }
+
+        this.hoveredChildIndex = index;
+        this.prevMouseX = screenX;
+        this.prevMouseY = screenY;
+    }
+
+    @Override
+    public boolean scrolled(float amountX, float amountY) {
+        return this.inputProcessor.scrolled(amountX, amountY);
+    }
+
+    public void setSize(final float width, final float height) {
+        this.bounds.setSize(width, height);
+    }
+
+    public boolean isVisible() {
+        return this.visible;
+    }
+
+    @Override
+    public boolean mouseEntered(final float screenX, final float screenY) {
+        return true;
+    }
+
+    @Override
+    public boolean mouseExited(final float screenX, final float screenY) {
+        return true;
+    }
+
+    private final class ComponentInputAdapter extends InputAdapterEx {
+        @Override
+        protected boolean keyDownEx(final int keycode, final int modifiers) {
+            final var index = Component.this.focusedChildIndex;
+            final var child = Component.this.getChildAt(index);
+            if (child != null && child.keyDown(keycode)) {
+                return false;
+            }
+            if (keycode != Input.Keys.TAB) {
+                return true;
+            }
+            if (modifiers == 0) {
+                Component.this.focusNextChildFrom(index);
+                return false;
+            }
+            if (Modifiers.isShiftOnly(modifiers)) {
+                Component.this.focusPrevChildFrom(index);
+                return false;
+            }
+            return false;
+        }
+
+        @Override
+        protected boolean keyUpEx(final int keycode, final int modifiers) {
+            final var child = Component.this.getFocusedChild();
+            return child == null || child.keyUp(keycode);
+        }
+
+        @Override
+        protected boolean touchDownEx(
+            final int screenX,
+            final int screenY,
+            final int pointer,
+            final int button,
+            final int modifiers
+        ) {
+            final var child = Component.this.getFocusedChild();
+            return child == null || child.touchDown(screenX, screenY, pointer, button);
+        }
+
+        @Override
+        protected boolean touchUpEx(
+            final int screenX,
+            final int screenY,
+            final int pointer,
+            final int button,
+            final int modifiers
+        ) {
+            final var child = Component.this.getFocusedChild();
+            return child == null || child.touchUp(screenX, screenY, pointer, button);
+        }
+
+        @Override
+        protected boolean touchCancelledEx(
+            final int screenX,
+            final int screenY,
+            final int pointer,
+            final int button,
+            final int modifiers
+        ) {
+            final var child = Component.this.getFocusedChild();
+            return child == null || child.touchCancelled(screenX, screenY, pointer, button);
+        }
+
+        @Override
+        protected boolean touchDraggedEx(
+            final int screenX,
+            final int screenY,
+            final int pointer,
+            final int modifiers
+        ) {
+            final var child = Component.this.getFocusedChild();
+            return child == null || child.touchDragged(screenX, screenY, pointer);
+        }
+
+        @Override
+        protected boolean mouseMovedEx(
+            final int screenX,
+            final int screenY,
+            final int modifiers
+        ) {
+            final var child = Component.this.getFocusedChild();
+            return child == null || child.mouseMoved(screenX, screenY);
+        }
+
+        @Override
+        protected boolean scrolledEx(
+            final float amountX,
+            final float amountY,
+            final int modifiers
+        ) {
+            final var child = Component.this.getHoveredChild();
+            return child == null || child.scrolled(amountX, amountY);
+        }
+
+        @Override
+        public boolean keyTyped(final char character) {
+            final var child = Component.this.getFocusedChild();
+            return child == null || child.keyTyped(character);
+        }
+    }
+}
diff --git a/src/main/java/dev/crmodders/flux/impl/base/Constants.java b/src/main/java/dev/crmodders/flux/impl/base/Constants.java
deleted file mode 100644
index 3357b70..0000000
--- a/src/main/java/dev/crmodders/flux/impl/base/Constants.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package dev.crmodders.flux.impl.base;
-
-public final class Constants {
-    public static final String NAME = "Flux API";
-
-    public static final String NAMESPACE = "flux-api";
-
-    private Constants() {}
-}
diff --git a/src/main/java/dev/crmodders/flux/impl/base/Logging.java b/src/main/java/dev/crmodders/flux/impl/base/Logging.java
deleted file mode 100644
index 95d43ed..0000000
--- a/src/main/java/dev/crmodders/flux/impl/base/Logging.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package dev.crmodders.flux.impl.base;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public final class Logging {
-    public static final Logger LOGGER = LoggerFactory.getLogger(Constants.NAME);
-}
diff --git a/src/main/java/dev/crmodders/flux/impl/base/Strings.java b/src/main/java/dev/crmodders/flux/impl/base/Strings.java
deleted file mode 100644
index 128220c..0000000
--- a/src/main/java/dev/crmodders/flux/impl/base/Strings.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package dev.crmodders.flux.impl.base;
-
-public final class Strings {
-    public static boolean endsWithIgnoreCase(final String self, final String rhs) {
-        return self.regionMatches(true, self.length() - rhs.length(), rhs, 0, rhs.length());
-    }
-
-    private Strings() {}
-}
diff --git a/src/main/java/dev/crmodders/flux/impl/input/client/FluxApiInputClientEntrypoint.java b/src/main/java/dev/crmodders/flux/impl/input/client/FluxApiInputClientEntrypoint.java
new file mode 100644
index 0000000..9b62c76
--- /dev/null
+++ b/src/main/java/dev/crmodders/flux/impl/input/client/FluxApiInputClientEntrypoint.java
@@ -0,0 +1,12 @@
+package dev.crmodders.flux.impl.input.client;
+
+import com.badlogic.gdx.Gdx;
+import dev.crmodders.cosmicquilt.api.entrypoint.client.ClientModInitializer;
+import org.quiltmc.loader.api.ModContainer;
+
+public final class FluxApiInputClientEntrypoint implements ClientModInitializer {
+    @Override
+    public void onInitializeClient(final ModContainer modContainer) {
+        Gdx.input = new FluxInput(Gdx.input);
+    }
+}
diff --git a/src/main/java/dev/crmodders/flux/impl/input/client/FluxInput.java b/src/main/java/dev/crmodders/flux/impl/input/client/FluxInput.java
new file mode 100644
index 0000000..587351a
--- /dev/null
+++ b/src/main/java/dev/crmodders/flux/impl/input/client/FluxInput.java
@@ -0,0 +1,337 @@
+package dev.crmodders.flux.impl.input.client;
+
+import com.badlogic.gdx.Input;
+import com.badlogic.gdx.InputProcessor;
+
+import java.util.Objects;
+
+public final class FluxInput implements Input {
+    private final Input delegate;
+
+    private final FluxInputProcessor inputProcessor;
+
+    public FluxInput(final Input delegate) {
+        Objects.requireNonNull(delegate);
+
+        this.delegate = delegate;
+        this.inputProcessor = new FluxInputProcessor();
+
+        this.inputProcessor.setMainProcessor(this.delegate.getInputProcessor());
+        this.delegate.setInputProcessor(this.inputProcessor);
+    }
+
+    /**
+     * Adds an input processor.
+     *
+     * @param processor  The processor to add.
+     * @return {@code boolean} value representing addition success.
+     * @see FluxInputProcessor#addProcessor
+     */
+    public boolean addInputProcessor(final InputProcessor processor) {
+        return this.inputProcessor.addProcessor(processor);
+    }
+
+    /**
+     * Checks presence of input processor.
+     *
+     * @param processor  The processor to check.
+     * @return {@code true} if present; {@code false} otherwise.
+     * @see FluxInputProcessor#containsProcessor
+     */
+    public boolean containsInputProcessor(final InputProcessor processor) {
+        return this.inputProcessor.containsProcessor(processor);
+    }
+
+    /**
+     * Removes an input processor.
+     *
+     * @param processor  The processor to remove.
+     * @return {@code boolean} value representing removal success.
+     * @see FluxInputProcessor#removeProcessor(InputProcessor)
+     */
+    public boolean removeInputProcessor(final InputProcessor processor) {
+        return this.inputProcessor.removeProcessor(processor);
+    }
+
+    /**
+     * @param processor {@inheritDoc}
+     * @see FluxInputProcessor#setMainProcessor
+     */
+    @Override
+    public void setInputProcessor(final InputProcessor processor) {
+        this.inputProcessor.setMainProcessor(processor);
+    }
+
+    /**
+     * @return {@inheritDoc}
+     * @see FluxInputProcessor#getMainProcessor
+     */
+    @Override
+    public InputProcessor getInputProcessor() {
+        return this.inputProcessor.getMainProcessor();
+    }
+
+    ////////////////////////////////
+    // DELEGATED IMPLEMENTATION
+    ////////////////////////////////
+
+    @Override
+    public float getAccelerometerX() {
+        return delegate.getAccelerometerX();
+    }
+
+    @Override
+    public float getAccelerometerY() {
+        return delegate.getAccelerometerY();
+    }
+
+    @Override
+    public float getAccelerometerZ() {
+        return delegate.getAccelerometerZ();
+    }
+
+    @Override
+    public float getGyroscopeX() {
+        return delegate.getGyroscopeX();
+    }
+
+    @Override
+    public float getGyroscopeY() {
+        return delegate.getGyroscopeY();
+    }
+
+    @Override
+    public float getGyroscopeZ() {
+        return delegate.getGyroscopeZ();
+    }
+
+    @Override
+    public int getMaxPointers() {
+        return delegate.getMaxPointers();
+    }
+
+    @Override
+    public int getX() {
+        return delegate.getX();
+    }
+
+    @Override
+    public int getX(final int pointer) {
+        return delegate.getX(pointer);
+    }
+
+    @Override
+    public int getDeltaX() {
+        return delegate.getDeltaX();
+    }
+
+    @Override
+    public int getDeltaX(final int pointer) {
+        return delegate.getDeltaX(pointer);
+    }
+
+    @Override
+    public int getY() {
+        return delegate.getY();
+    }
+
+    @Override
+    public int getY(final int pointer) {
+        return delegate.getY(pointer);
+    }
+
+    @Override
+    public int getDeltaY() {
+        return delegate.getDeltaY();
+    }
+
+    @Override
+    public int getDeltaY(final int pointer) {
+        return delegate.getDeltaY(pointer);
+    }
+
+    @Override
+    public boolean isTouched() {
+        return delegate.isTouched();
+    }
+
+    @Override
+    public boolean justTouched() {
+        return delegate.justTouched();
+    }
+
+    @Override
+    public boolean isTouched(final int pointer) {
+        return delegate.isTouched(pointer);
+    }
+
+    @Override
+    public float getPressure() {
+        return delegate.getPressure();
+    }
+
+    @Override
+    public float getPressure(final int pointer) {
+        return delegate.getPressure(pointer);
+    }
+
+    @Override
+    public boolean isButtonPressed(final int button) {
+        return delegate.isButtonPressed(button);
+    }
+
+    @Override
+    public boolean isButtonJustPressed(final int button) {
+        return delegate.isButtonJustPressed(button);
+    }
+
+    @Override
+    public boolean isKeyPressed(final int key) {
+        return delegate.isKeyPressed(key);
+    }
+
+    @Override
+    public boolean isKeyJustPressed(final int key) {
+        return delegate.isKeyJustPressed(key);
+    }
+
+    @Override
+    public void getTextInput(
+        final TextInputListener listener,
+        final String title,
+        final String text,
+        final String hint
+    ) {
+        delegate.getTextInput(listener, title, text, hint);
+    }
+
+    @Override
+    public void getTextInput(
+        final TextInputListener listener,
+        final String title,
+        final String text,
+        final String hint,
+        final OnscreenKeyboardType type
+    ) {
+        delegate.getTextInput(listener, title, text, hint, type);
+    }
+
+    @Override
+    public void setOnscreenKeyboardVisible(final boolean visible) {
+        delegate.setOnscreenKeyboardVisible(visible);
+    }
+
+    @Override
+    public void setOnscreenKeyboardVisible(final boolean visible, final OnscreenKeyboardType type) {
+        delegate.setOnscreenKeyboardVisible(visible, type);
+    }
+
+    @Override
+    public void vibrate(final int milliseconds) {
+        delegate.vibrate(milliseconds);
+    }
+
+    @Override
+    public void vibrate(final int milliseconds, final boolean fallback) {
+        delegate.vibrate(milliseconds, fallback);
+    }
+
+    @Override
+    public void vibrate(final int milliseconds, final int amplitude, final boolean fallback) {
+        delegate.vibrate(milliseconds, amplitude, fallback);
+    }
+
+    @Override
+    public void vibrate(final VibrationType vibrationType) {
+        delegate.vibrate(vibrationType);
+    }
+
+    @Override
+    public float getAzimuth() {
+        return delegate.getAzimuth();
+    }
+
+    @Override
+    public float getPitch() {
+        return delegate.getPitch();
+    }
+
+    @Override
+    public float getRoll() {
+        return delegate.getRoll();
+    }
+
+    @Override
+    public void getRotationMatrix(final float[] matrix) {
+        delegate.getRotationMatrix(matrix);
+    }
+
+    @Override
+    public long getCurrentEventTime() {
+        return delegate.getCurrentEventTime();
+    }
+
+    @Deprecated
+    @Override
+    public void setCatchBackKey(final boolean catchBack) {
+        delegate.setCatchBackKey(catchBack);
+    }
+
+    @Deprecated
+    @Override
+    public boolean isCatchBackKey() {
+        return delegate.isCatchBackKey();
+    }
+
+    @Deprecated
+    @Override
+    public void setCatchMenuKey(final boolean catchMenu) {
+        delegate.setCatchMenuKey(catchMenu);
+    }
+
+    @Deprecated
+    @Override
+    public boolean isCatchMenuKey() {
+        return delegate.isCatchMenuKey();
+    }
+
+    @Override
+    public void setCatchKey(final int keycode, final boolean catchKey) {
+        delegate.setCatchKey(keycode, catchKey);
+    }
+
+    @Override
+    public boolean isCatchKey(final int keycode) {
+        return delegate.isCatchKey(keycode);
+    }
+
+    @Override
+    public boolean isPeripheralAvailable(final Peripheral peripheral) {
+        return delegate.isPeripheralAvailable(peripheral);
+    }
+
+    @Override
+    public int getRotation() {
+        return delegate.getRotation();
+    }
+
+    @Override
+    public Orientation getNativeOrientation() {
+        return delegate.getNativeOrientation();
+    }
+
+    @Override
+    public void setCursorCatched(final boolean catched) {
+        delegate.setCursorCatched(catched);
+    }
+
+    @Override
+    public boolean isCursorCatched() {
+        return delegate.isCursorCatched();
+    }
+
+    @Override
+    public void setCursorPosition(final int x, final int y) {
+        delegate.setCursorPosition(x, y);
+    }
+}
diff --git a/src/main/java/dev/crmodders/flux/impl/input/client/FluxInputProcessor.java b/src/main/java/dev/crmodders/flux/impl/input/client/FluxInputProcessor.java
new file mode 100644
index 0000000..fec8ea8
--- /dev/null
+++ b/src/main/java/dev/crmodders/flux/impl/input/client/FluxInputProcessor.java
@@ -0,0 +1,204 @@
+package dev.crmodders.flux.impl.input.client;
+
+import com.badlogic.gdx.InputAdapter;
+import com.badlogic.gdx.InputProcessor;
+
+import java.util.LinkedHashSet;
+
+final class FluxInputProcessor implements InputProcessor {
+    private static final InputProcessor NOOP = new InputAdapter();
+
+    private final LinkedHashSet<InputProcessor> delegates;
+
+    private InputProcessor main;
+
+    public FluxInputProcessor() {
+        this.delegates = new LinkedHashSet<>();
+        this.main = NOOP;
+    }
+
+    /**
+     * Adds an input processor.
+     * <p>
+     * All added input processors are ran in insertion order through an internal
+     * {@link LinkedHashSet}. Addition will not succeed with a {@code null}
+     * processor, {@code this}, or the current main processor. The internal set
+     * works best if the input processors did not override {@link Object#equals}
+     * and {@link Object#hashCode()} and in case that it did, make a wrapper.
+     *
+     * @param processor  The processor to add.
+     * @return {@code boolean} value representing addition success.
+     * @see #getMainProcessor
+     */
+    public boolean addProcessor(final InputProcessor processor) {
+        return processor != null
+            && processor != this
+            && processor != this.main
+            && this.delegates.add(processor);
+    }
+
+    /**
+     * Checks input processor presence.
+     * <p>
+     * Checks if a given processor is present and added through
+     * {@code addProcessor}. This method does not check against the main
+     * processor.
+     *
+     * @param processor  The processor to check.
+     * @return {@code true} if present; {@code false} otherwise.
+     * @see #addProcessor
+     */
+    public boolean containsProcessor(final InputProcessor processor) {
+        return processor != null && this.delegates.contains(processor);
+    }
+
+    /**
+     * Removes an input processor.
+     * <p>
+     * Removes a given processor added through {@code addProcessor}. This method
+     * cannot remove the main processor, use {@code setMainProcessor(null)}.
+     *
+     * @param processor  The processor to remove.
+     * @return {@code boolean} value representing removal success.
+     * @see #addProcessor
+     * @see #setMainProcessor
+     */
+    public boolean removeProcessor(final InputProcessor processor) {
+        return processor != null && this.delegates.remove(processor);
+    }
+
+    /**
+     * The main front-facing input processor.
+     *
+     * @return The main processor.
+     * @see #setMainProcessor
+     */
+    public InputProcessor getMainProcessor() {
+        final var main = this.main;
+        return main == NOOP ? null : main;
+    }
+
+    /**
+     * Sets the main processor.
+     * <p>
+     * The main processor is the front facing processor visible through
+     * {@link FluxInput#getInputProcessor} where this method is the delegate
+     * of {@link FluxInput#setInputProcessor}. Delegated
+     * {@link com.badlogic.gdx.Input}s will return a {@code FluxInputProcessor}.
+     * <p>
+     * If the given processor is already present through {@link #addProcessor},
+     * it's removed, as if through {@link #removeProcessor}, will it then be set
+     * as the new main processor.
+     * <p>
+     * The previous main processor is replaced and is not re-added to the
+     * processor list if it was promoted from {@code addProcessor}. {@code null}
+     * is valid and will behave as a no-op as if a blank extension of
+     * {@link InputAdapter}.
+     *
+     * @param processor  The new main processor.
+     */
+    public void setMainProcessor(final InputProcessor processor) {
+        if (processor == null) {
+            this.main = NOOP;
+        } else {
+            this.delegates.remove(processor);
+            this.main = processor;
+        }
+    }
+
+    @Override
+    public boolean keyDown(final int keycode) {
+        final var result = this.main.keyDown(keycode);
+        for (final var delegate : this.delegates) {
+            delegate.keyDown(keycode);
+        }
+        return result;
+    }
+
+    @Override
+    public boolean keyUp(final int keycode) {
+        final var result = this.main.keyUp(keycode);
+        for (final var delegate : this.delegates) {
+            delegate.keyUp(keycode);
+        }
+        return result;
+    }
+
+    @Override
+    public boolean keyTyped(final char character) {
+        final var result = this.main.keyUp(character);
+        for (final var delegate : this.delegates) {
+            delegate.keyUp(character);
+        }
+        return result;
+    }
+
+    @Override
+    public boolean touchDown(
+        final int screenX,
+        final int screenY,
+        final int pointer,
+        final int button
+    ) {
+        final var result = this.main.touchDown(screenX, screenY, pointer, button);
+        for (final var delegate : this.delegates) {
+            delegate.touchDown(screenX, screenY, pointer, button);
+        }
+        return result;
+    }
+
+    @Override
+    public boolean touchUp(
+        final int screenX,
+        final int screenY,
+        final int pointer,
+        final int button
+    ) {
+        final var result = this.main.touchUp(screenX, screenY, pointer, button);
+        for (final var delegate : this.delegates) {
+            delegate.touchUp(screenX, screenY, pointer, button);
+        }
+        return result;
+    }
+
+    @Override
+    public boolean touchCancelled(
+        final int screenX,
+        final int screenY,
+        final int pointer,
+        final int button
+    ) {
+        final var result = this.main.touchCancelled(screenX, screenY, pointer, button);
+        for (final var delegate : this.delegates) {
+            delegate.touchCancelled(screenX, screenY, pointer, button);
+        }
+        return result;
+    }
+
+    @Override
+    public boolean touchDragged(final int screenX, final int screenY, final int pointer) {
+        final var result = this.main.touchDragged(screenX, screenY, pointer);
+        for (final var delegate : this.delegates) {
+            delegate.touchDragged(screenX, screenY, pointer);
+        }
+        return result;
+    }
+
+    @Override
+    public boolean mouseMoved(final int screenX, final int screenY) {
+        final var result = this.main.mouseMoved(screenX, screenY);
+        for (final var delegate : this.delegates) {
+            delegate.mouseMoved(screenX, screenY);
+        }
+        return result;
+    }
+
+    @Override
+    public boolean scrolled(final float amountX, final float amountY) {
+        final var result = this.main.scrolled(amountX, amountY);
+        for (final var delegate : this.delegates) {
+            delegate.scrolled(amountX, amountY);
+        }
+        return result;
+    }
+}
diff --git a/src/main/java/dev/crmodders/flux/impl/resource/loader/AssetFinder.java b/src/main/java/dev/crmodders/flux/impl/resource/loader/AssetFinder.java
index d1327f9..6ce2e26 100644
--- a/src/main/java/dev/crmodders/flux/impl/resource/loader/AssetFinder.java
+++ b/src/main/java/dev/crmodders/flux/impl/resource/loader/AssetFinder.java
@@ -1,6 +1,5 @@
 package dev.crmodders.flux.impl.resource.loader;
 
-import dev.crmodders.flux.impl.base.Strings;
 import finalforeach.cosmicreach.util.Identifier;
 import org.jetbrains.annotations.Nullable;
 
@@ -12,7 +11,7 @@
 import java.util.Objects;
 import java.util.function.BiConsumer;
 
-import static dev.crmodders.flux.impl.base.Logging.LOGGER;
+import static dev.crmodders.flux.impl.resource.loader.FluxAssetLoading.LOGGER;
 
 /**
  * Helper class to find assets.
@@ -66,7 +65,7 @@ public static void findNamespaced(
 
         try (final var matches = Files.list(namespacedPath.resolve(prefixOnFs))) {
             matches
-                .filter(it -> Strings.endsWithIgnoreCase(it.getFileName().toString(), extension))
+                .filter(it -> endsWithIgnoreCase(it.getFileName().toString(), extension))
                 .filter(Files::isRegularFile)
                 .forEach(it -> {
                     final var name = namespacedPath.relativize(it).toString();
@@ -80,6 +79,10 @@ public static void findNamespaced(
         }
     }
 
+    private static boolean endsWithIgnoreCase(final String self, final String rhs) {
+        return self.regionMatches(true, self.length() - rhs.length(), rhs, 0, rhs.length());
+    }
+
     public AssetFinder {
         Objects.requireNonNull(prefix, "Parameter subAssetComponent is null");
         Objects.requireNonNull(extension, "Parameter extension is null");
diff --git a/src/main/java/dev/crmodders/flux/impl/resource/loader/FluxAssetLoading.java b/src/main/java/dev/crmodders/flux/impl/resource/loader/FluxAssetLoading.java
new file mode 100644
index 0000000..c258701
--- /dev/null
+++ b/src/main/java/dev/crmodders/flux/impl/resource/loader/FluxAssetLoading.java
@@ -0,0 +1,95 @@
+package dev.crmodders.flux.impl.resource.loader;
+
+import com.badlogic.gdx.files.FileHandle;
+import dev.crmodders.flux.api.resource.loader.FluxFileHandle;
+import org.jetbrains.annotations.Nullable;
+import org.quiltmc.loader.api.ModContainer;
+import org.quiltmc.loader.api.QuiltLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.spongepowered.asm.mixin.Unique;
+
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.ProviderNotFoundException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.function.BiConsumer;
+
+public class FluxAssetLoading {
+    static final Logger LOGGER = LoggerFactory.getLogger("Flux Resource Loader");
+
+    public static void loadJarModAssets(
+        final String prefixString,
+        final String extension,
+        final BiConsumer<String, FileHandle> assetConsumer,
+        final boolean includeDirectories,
+        final HashMap<? super String, ? super FileHandle> allAssets
+    ) {
+        final var finder = createAssetFinder(prefixString, extension, assetConsumer, allAssets);
+        if (finder == null) {
+            return;
+        }
+
+        QuiltLoader.getAllMods()
+            .stream()
+            .filter(mod -> mod.getSourceType() != ModContainer.BasicSourceType.BUILTIN)
+            .map(ModContainer::getSourcePaths)
+            .flatMap(List::stream)
+            .flatMap(List::stream)
+            .map(Path::normalize)
+            .forEach(root -> {
+                if (Files.isDirectory(root)) {
+                    finder.scan(root);
+                    return;
+                }
+                try (final var zfs = FileSystems.newFileSystem(root)) {
+                    finder.scan(zfs.getPath("/"));
+                } catch (final ProviderNotFoundException cause) {
+                    LOGGER.warn("No file system provider for {}", root, cause);
+                } catch (final IOException cause) {
+                    LOGGER.warn("Unable to access file system for {}", root, cause);
+                }
+            });
+    }
+
+    @Unique
+    private static @Nullable AssetFinder createAssetFinder(
+        final String prefixNotation,
+        final String extension,
+        final BiConsumer<String, FileHandle> assetConsumer,
+        final HashMap<? super String, ? super FileHandle> allAssets
+    ) {
+        final String namespace;
+        final Path prefix;
+        {
+            final var separator = prefixNotation.indexOf(':');
+            final String prefixString;
+
+            if (separator >= 0) {
+                namespace = prefixNotation.substring(0, separator);
+                prefixString = prefixNotation.substring(separator + 1);
+            } else {
+                namespace = null;
+                prefixString = prefixNotation;
+            }
+
+            try {
+                prefix = Path.of(prefixString);
+            } catch (final InvalidPathException cause) {
+                LOGGER.warn("Invalid prefix path: {}", prefixNotation, cause);
+                return null;
+            }
+        }
+
+        return new AssetFinder(namespace, prefix, extension, (identifier, path) -> {
+            final var handle = new FluxFileHandle(path);
+            final var id = identifier.toString();
+            allAssets.put(id, handle);
+            assetConsumer.accept(id, handle);
+        });
+    }
+}
diff --git a/src/main/java/dev/crmodders/flux/mixin/resource/loader/GameAssetLoaderMixin.java b/src/main/java/dev/crmodders/flux/mixin/resource/loader/GameAssetLoaderMixin.java
index cfb9f13..b51b648 100644
--- a/src/main/java/dev/crmodders/flux/mixin/resource/loader/GameAssetLoaderMixin.java
+++ b/src/main/java/dev/crmodders/flux/mixin/resource/loader/GameAssetLoaderMixin.java
@@ -2,30 +2,20 @@
 
 import com.badlogic.gdx.files.FileHandle;
 import com.llamalad7.mixinextras.sugar.Local;
-import dev.crmodders.flux.impl.resource.loader.AssetFinder;
-import dev.crmodders.flux.impl.resource.loader.FluxFileHandle;
+import dev.crmodders.flux.impl.resource.loader.FluxAssetLoading;
 import finalforeach.cosmicreach.GameAssetLoader;
 import finalforeach.cosmicreach.util.Identifier;
-import org.jetbrains.annotations.Nullable;
-import org.quiltmc.loader.api.ModContainer;
-import org.quiltmc.loader.api.QuiltLoader;
 import org.spongepowered.asm.mixin.Final;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.Shadow;
-import org.spongepowered.asm.mixin.Unique;
 import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 
-import java.io.IOException;
-import java.nio.file.*;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.List;
 import java.util.function.BiConsumer;
 
-import static dev.crmodders.flux.impl.base.Logging.LOGGER;
-
 @Mixin(GameAssetLoader.class)
 public class GameAssetLoaderMixin {
     @Final
@@ -44,66 +34,12 @@ private static void loadJarModAssets(
         final CallbackInfo callback,
         final @Local(ordinal = 0) HashSet<Identifier> allPaths
     ) {
-        final var finder = flux_api$createAssetFinder(prefixString, extension, assetConsumer);
-        if (finder == null) {
-            return;
-        }
-
-        QuiltLoader.getAllMods()
-            .stream()
-            .filter(mod -> mod.getSourceType() != ModContainer.BasicSourceType.BUILTIN)
-            .map(ModContainer::getSourcePaths)
-            .flatMap(List::stream)
-            .flatMap(List::stream)
-            .map(Path::normalize)
-            .forEach(root -> {
-                if (Files.isDirectory(root)) {
-                    finder.scan(root);
-                    return;
-                }
-                try (final var zfs = FileSystems.newFileSystem(root)) {
-                    finder.scan(zfs.getPath("/"));
-                } catch (final ProviderNotFoundException cause) {
-                    LOGGER.warn("No file system provider for {}", root, cause);
-                } catch (final IOException cause) {
-                    LOGGER.warn("Unable to access file system for {}", root, cause);
-                }
-            });
-    }
-
-    @Unique
-    private static @Nullable AssetFinder flux_api$createAssetFinder(
-        final String prefixNotation,
-        final String extension,
-        final BiConsumer<String, FileHandle> assetConsumer
-    ) {
-        final String namespace;
-        final Path prefix;
-        {
-            final var separator = prefixNotation.indexOf(':');
-            final String prefixString;
-
-            if (separator >= 0) {
-                namespace = prefixNotation.substring(0, separator);
-                prefixString = prefixNotation.substring(separator + 1);
-            } else {
-                namespace = null;
-                prefixString = prefixNotation;
-            }
-
-            try {
-                prefix = Path.of(prefixString);
-            } catch (final InvalidPathException cause) {
-                LOGGER.warn("Invalid prefix path: {}", prefixNotation, cause);
-                return null;
-            }
-        }
-
-        return new AssetFinder(namespace, prefix, extension, (identifier, path) -> {
-            final var handle = new FluxFileHandle(path);
-            final var id = identifier.toString();
-            ALL_ASSETS.put(id, handle);
-            assetConsumer.accept(id, handle);
-        });
+        FluxAssetLoading.loadJarModAssets(
+            prefixString,
+            extension,
+            assetConsumer,
+            includeDirectories,
+            ALL_ASSETS
+        );
     }
 }
diff --git a/src/main/resources/flux-api.mixins.json b/src/main/resources/flux-api.mixins.json
index eebe686..863ed9b 100644
--- a/src/main/resources/flux-api.mixins.json
+++ b/src/main/resources/flux-api.mixins.json
@@ -11,4 +11,4 @@
     "injectors": {
         "defaultRequire": 1
   }
-}
\ No newline at end of file
+}
diff --git a/src/main/resources/quilt.mod.json b/src/main/resources/quilt.mod.json
index 0ce686b..3cb5703 100644
--- a/src/main/resources/quilt.mod.json
+++ b/src/main/resources/quilt.mod.json
@@ -29,6 +29,7 @@
         },
 
         "entrypoints": {
+            "client_init": "dev.crmodders.flux.impl.input.client.FluxApiInputClientEntrypoint"
         },
 
         "depends": [
diff --git a/src/test/java/dev/crmodders/flux/api/math/IntersectionTest.java b/src/test/java/dev/crmodders/flux/api/math/IntersectionTest.java
new file mode 100644
index 0000000..437e169
--- /dev/null
+++ b/src/test/java/dev/crmodders/flux/api/math/IntersectionTest.java
@@ -0,0 +1,130 @@
+package dev.crmodders.flux.api.math;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+class IntersectionTest {
+    @Nested
+    class LineSegmentAndAabbUnchecked {
+        @ParameterizedTest
+        @CsvSource({
+            "9.0, 4.5, 4.0, 4.0, 5.0, 10.0, 3.5, 4.0, NONE, 0.0, 0.0, 0.0, 0.0",
+        })
+        void diagonalLineTest(
+            final float x1,
+            final float y1,
+            final float x2,
+            final float y2,
+            final float l,
+            final float r,
+            final float u,
+            final float d,
+            final Intersection.Convex expectedConvex,
+            final float expectedEnterX,
+            final float expectedEnterY,
+            final float expectedExitX,
+            final float expectedExitY
+        ) {
+            final var buffer = new float[4];
+
+            final Intersection.Convex actualConvex;
+
+            Assertions.assertEquals(
+                expectedConvex,
+                actualConvex = Intersection.lineSegmentAndAabbUnchecked(
+                    x1,
+                    y1,
+                    x2,
+                    y2,
+                    l,
+                    r,
+                    u,
+                    d,
+                    buffer,
+                    0
+                ),
+                "Convex Intersections"
+            );
+
+            if (actualConvex.entered()) {
+                final float actualEnterX;
+                final float actualEnterY;
+
+                Assertions.assertEquals(expectedEnterX, actualEnterX = buffer[0], "Entering Intersection Point (x)");
+                Assertions.assertEquals(expectedEnterY, actualEnterY = buffer[1], "Entering Intersection Point (y)");
+            }
+
+            if (actualConvex.exited()) {
+                final float actualExitX;
+                final float actualExitY;
+
+                Assertions.assertEquals(expectedExitX, actualExitX = buffer[2], "Exiting Intersection Point (x)");
+                Assertions.assertEquals(expectedExitY, actualExitY = buffer[3], "Exiting Intersection Point (y)");
+            }
+        }
+    }
+
+    @Nested
+    class LineSegmentAndAxesUnchecked {
+        /**
+         * @see <a href="https://www.desmos.com/calculator/hmba0l06yr">Test Data Visualization on Desmos</a>
+         */
+        @ParameterizedTest
+        @CsvSource({
+            "9.0, 4.5, 4.0,  4.0, 5.0,  4.0, 10.0, 4.5, 10.0, 4.0, NONE, 0.0, 0.0",
+            "1.0, 0.0, 3.0,  1.0, 2.0,  0.0,  3.0, 1.0,  2.0, 1.0,    Y, 2.0, 0.5",
+            "8.0, 3.0, 6.0, -1.0, 6.0, -1.0,  8.0, 2.0,  8.0, 2.0,    X, 7.5, 2.0",
+            "0.0, 8.0, 2.0,  6.0, 1.0,  5.0,  3.0, 7.0,  1.0, 7.0, BOTH, 1.0, 7.0",
+        })
+        void diagonalLineTest(
+            final float x1,
+            final float y1,
+            final float x2,
+            final float y2,
+            final float minX,
+            final float minY,
+            final float maxX,
+            final float maxY,
+            final float a,
+            final float b,
+            final Intersection.Axes expectedAxes,
+            final float expectedX,
+            final float expectedY
+        ) {
+            final var buffer = new float[2];
+
+            final Intersection.Axes actualAxes;
+
+            Assertions.assertEquals(
+                expectedAxes,
+                actualAxes = Intersection.lineSegmentAndAxesUnchecked(
+                    x1,
+                    y1,
+                    x2,
+                    y2,
+                    minX,
+                    minY,
+                    maxX,
+                    maxY,
+                    a,
+                    b,
+                    buffer,
+                    0
+                ),
+                "Intersected Axes"
+            );
+
+            if (actualAxes == Intersection.Axes.NONE) {
+                return;
+            }
+
+            final float actualX;
+            final float actualY;
+
+            Assertions.assertEquals(expectedX, actualX = buffer[0], "Intersection Point (x)");
+            Assertions.assertEquals(expectedY, actualY = buffer[1], "Intersection Point (y)");
+        }
+    }
+}