summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar bigfoot547 <[email protected]>2025-08-25 22:28:24 -0500
committerLibravatar bigfoot547 <[email protected]>2025-08-25 22:28:24 -0500
commit954e102e836e19ea425253f1990fff8d76dd98b4 (patch)
tree2dc68f8a09efc5b11c88ee4154b6487db0baadbb
initial commit
-rw-r--r--.gitignore40
-rw-r--r--build.gradle.kts19
-rw-r--r--core/build.gradle.kts28
-rw-r--r--core/src/main/java/dev/figboot/launcher/core/Launcher.java24
-rw-r--r--core/src/main/java/dev/figboot/launcher/core/LauncherInterfaceAccess.java7
-rw-r--r--core/src/main/java/dev/figboot/launcher/core/config/LauncherConfiguration.java8
-rw-r--r--core/src/main/java/dev/figboot/launcher/core/config/SimpleLauncherConfiguration.java24
-rw-r--r--core/src/main/java/dev/figboot/launcher/core/system/CurrentSystemInfo.java147
-rw-r--r--core/src/main/java/dev/figboot/launcher/core/system/SystemInfo.java38
-rw-r--r--core/src/main/java/dev/figboot/launcher/core/task/Task.java5
-rw-r--r--core/src/main/java/dev/figboot/launcher/core/version/CompleteVersion.java4
-rw-r--r--core/src/main/java/dev/figboot/launcher/core/version/VersionInfo.java11
-rw-r--r--core/src/main/java/dev/figboot/launcher/core/version/VersionManifest.java5
-rw-r--r--core/src/main/java/dev/figboot/launcher/core/version/VersionStorage.java10
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 60756 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xgradlew234
-rw-r--r--gradlew.bat89
-rw-r--r--gui/build.gradle.kts30
-rw-r--r--gui/src/main/java/dev/figboot/launcher/gui/GUIMain.java106
-rw-r--r--gui/src/main/java/dev/figboot/launcher/gui/InstanceListPanel.java212
-rw-r--r--gui/src/main/java/dev/figboot/launcher/gui/WrapLayout.java204
-rw-r--r--gui/src/main/resources/icons/play.svg137
-rw-r--r--gui/src/main/resources/icons/play_24.pngbin0 -> 1027 bytes
-rw-r--r--guiconcept.pngbin0 -> 230916 bytes
-rw-r--r--settings.gradle.kts3
26 files changed, 1391 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5e6cdcc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,40 @@
+.gradle
+build/
+work/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### IntelliJ IDEA ###
+.idea/
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..b0da77f
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,19 @@
+plugins {
+ id("java")
+}
+
+group = "dev.figboot.launcher"
+version = "1.0-SNAPSHOT"
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ testImplementation(platform("org.junit:junit-bom:5.10.0"))
+ testImplementation("org.junit.jupiter:junit-jupiter")
+}
+
+tasks.test {
+ useJUnitPlatform()
+} \ No newline at end of file
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
new file mode 100644
index 0000000..b65c6b9
--- /dev/null
+++ b/core/build.gradle.kts
@@ -0,0 +1,28 @@
+plugins {
+ `java-library`
+ id("io.freefair.lombok") version "8.14.2"
+}
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(8)
+ }
+}
+
+group = "dev.figboot.launcher"
+version = "1.0-SNAPSHOT"
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation("org.slf4j:slf4j-api:1.7.25")
+
+ testImplementation(platform("org.junit:junit-bom:5.10.0"))
+ testImplementation("org.junit.jupiter:junit-jupiter")
+}
+
+tasks.test {
+ useJUnitPlatform()
+}
diff --git a/core/src/main/java/dev/figboot/launcher/core/Launcher.java b/core/src/main/java/dev/figboot/launcher/core/Launcher.java
new file mode 100644
index 0000000..a95fed4
--- /dev/null
+++ b/core/src/main/java/dev/figboot/launcher/core/Launcher.java
@@ -0,0 +1,24 @@
+package dev.figboot.launcher.core;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+public class Launcher {
+ private static final Logger LOGGER = LoggerFactory.getLogger(Launcher.class);
+
+ protected final LauncherInterfaceAccess iface;
+ protected final ScheduledExecutorService service;
+
+ public Launcher(LauncherInterfaceAccess iface) {
+ this.iface = iface;
+ service = new ScheduledThreadPoolExecutor(4);
+ }
+
+ public void loadVersions() throws IOException {
+
+ }
+}
diff --git a/core/src/main/java/dev/figboot/launcher/core/LauncherInterfaceAccess.java b/core/src/main/java/dev/figboot/launcher/core/LauncherInterfaceAccess.java
new file mode 100644
index 0000000..b1292ae
--- /dev/null
+++ b/core/src/main/java/dev/figboot/launcher/core/LauncherInterfaceAccess.java
@@ -0,0 +1,7 @@
+package dev.figboot.launcher.core;
+
+import dev.figboot.launcher.core.task.Task;
+
+public interface LauncherInterfaceAccess {
+ Task createTask(String description);
+}
diff --git a/core/src/main/java/dev/figboot/launcher/core/config/LauncherConfiguration.java b/core/src/main/java/dev/figboot/launcher/core/config/LauncherConfiguration.java
new file mode 100644
index 0000000..99bcca4
--- /dev/null
+++ b/core/src/main/java/dev/figboot/launcher/core/config/LauncherConfiguration.java
@@ -0,0 +1,8 @@
+package dev.figboot.launcher.core.config;
+
+import java.nio.file.Path;
+
+public interface LauncherConfiguration {
+ Path getConfigPath();
+ Path getInstancesPath();
+}
diff --git a/core/src/main/java/dev/figboot/launcher/core/config/SimpleLauncherConfiguration.java b/core/src/main/java/dev/figboot/launcher/core/config/SimpleLauncherConfiguration.java
new file mode 100644
index 0000000..15ab1be
--- /dev/null
+++ b/core/src/main/java/dev/figboot/launcher/core/config/SimpleLauncherConfiguration.java
@@ -0,0 +1,24 @@
+package dev.figboot.launcher.core.config;
+
+import dev.figboot.launcher.core.system.SystemInfo;
+
+import java.nio.file.Path;
+
+public class SimpleLauncherConfiguration implements LauncherConfiguration {
+ //private final Path configPath, instancePath;
+
+ public SimpleLauncherConfiguration() {
+
+ //configPath = FileSystems.getDefault().
+ }
+
+ @Override
+ public Path getConfigPath() {
+ return null;
+ }
+
+ @Override
+ public Path getInstancesPath() {
+ return null;
+ }
+}
diff --git a/core/src/main/java/dev/figboot/launcher/core/system/CurrentSystemInfo.java b/core/src/main/java/dev/figboot/launcher/core/system/CurrentSystemInfo.java
new file mode 100644
index 0000000..0c67506
--- /dev/null
+++ b/core/src/main/java/dev/figboot/launcher/core/system/CurrentSystemInfo.java
@@ -0,0 +1,147 @@
+package dev.figboot.launcher.core.system;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+class CurrentSystemInfo implements SystemInfo {
+ private static final Logger LOGGER = LoggerFactory.getLogger(CurrentSystemInfo.class);
+
+ private static CurrentSystemInfo info = null;
+
+ static synchronized CurrentSystemInfo get() {
+ if (info != null) {
+ return info;
+ }
+
+ return (info = gather());
+ }
+
+ private static String logSystemProperty(String key, String value, boolean custom) {
+ LOGGER.info("System property: \"{}\" = {}{}", key, value == null ? "(unset)" : value, custom ? " (*)" : "");
+ return value;
+ }
+
+ private static String getSystemProperty(String key) {
+ String customVal = System.getProperty("dev.figboot.launcher.core.SystemInfo." + key);
+ if (customVal != null)
+ return logSystemProperty(key, customVal, true);
+ return logSystemProperty(key, System.getProperty(key), false);
+ }
+
+ private static boolean isBitMismatchCondition(OperatingSystem os) {
+ if (System.getProperty("dev.figboot.launcher.core.SystemInfo.acknowledgeBitMismatch") != null) return false;
+ if (os != OperatingSystem.WINDOWS) return false; // seems like only windows users typically have this issue
+
+ String realArch = System.getenv("PROCESSOR_ARCHITEW6432");
+ return realArch != null && realArch.equalsIgnoreCase("amd64");
+ }
+
+ private static String getRuntimePlatform(OperatingSystem os, String arch) {
+ for (RuntimePlatformRule rule : JRE_PLAT_RULES) {
+ if (rule.matches(os, arch)) {
+ LOGGER.info("Java runtime platform detected as '{}'.", rule.platform);
+ return rule.platform;
+ }
+ }
+
+ LOGGER.info("Java runtime platform not detected. Please file a bug report if you believe your platform should be detected.");
+ return "gamecore";
+ }
+
+ private static CurrentSystemInfo gather() {
+ String osName = getSystemProperty("os.name");
+ String osArch = getSystemProperty("os.arch");
+ String osVersion = getSystemProperty("os.version");
+
+ String fileSep = getSystemProperty("file.separator");
+ String pathSep = getSystemProperty("path.separator");
+
+ OperatingSystem detectedOS = OperatingSystem.UNKNOWN;
+ int archBits;
+
+ for (Map.Entry<OperatingSystem, Pattern> ent : OS_PATTERNS.entrySet()) {
+ if (ent.getValue().matcher(osName).find()) { // NOTE: not necessarily a complete match
+ LOGGER.info("Detected operating system: {}", ent.getKey());
+ detectedOS = ent.getKey();
+ }
+ }
+
+ if (detectedOS == OperatingSystem.UNKNOWN) {
+ LOGGER.warn("Unknown operating system \"{}\"! The launcher may not function correctly.", osName);
+ LOGGER.warn("Try specifying \"windows\" or \"macos\" or \"linux\" in the \"dev.figboot.launcher.core.SystemInfo.os.name\" system property, " +
+ "and consider filing a bug report with your OS details if you believe your OS should be detected automatically.");
+ }
+
+ if (osArch.contains("64")) { // seems to work well enough.
+ archBits = 64;
+ } else {
+ archBits = 32;
+
+ if (isBitMismatchCondition(detectedOS)) {
+ /* NOTE: it's not safe to just pretend we're on 64-bit windows, since WOW64 mode changes various things
+ * about the application's environment. I don't intend to do more than just send a warning in the log. */
+ LOGGER.warn("It appears that you're using a 32-bit version of java on 64-bit Windows. You may have issues " +
+ "launching the game when using Mojang-provided java runtimes. To suppress this warning, define the " +
+ "\"dev.figboot.launcher.core.SystemInfo.acknowledgeBitMismatch\" system property.");
+ }
+ }
+
+ return new CurrentSystemInfo(detectedOS, getRuntimePlatform(detectedOS, osArch), archBits, osArch, osVersion, fileSep, pathSep);
+ }
+
+ @Getter private final OperatingSystem operatingSystem;
+ @Getter private final String runtimePlatform;
+ @Getter private final int archBits;
+
+ @Getter private final String archName;
+ @Getter private final String osVersion;
+
+ @Getter private final String fileSeparator;
+ @Getter private final String pathSeparator;
+
+ private static final Pattern ARCH_X8664 = Pattern.compile("^((x(86_)?|amd|ia)64)$", Pattern.CASE_INSENSITIVE);
+ private static final Pattern ARCH_X86 = Pattern.compile("^(x86(_?32)?|i[3-6]86|ia32|x32)$", Pattern.CASE_INSENSITIVE);
+ private static final Pattern ARCH_ARM64 = Pattern.compile("^aarch64$", Pattern.CASE_INSENSITIVE);
+ private static final Pattern ARCH_PPC = Pattern.compile("^ppc$", Pattern.CASE_INSENSITIVE);
+
+ private static final RuntimePlatformRule[] JRE_PLAT_RULES = {
+ new RuntimePlatformRule(OperatingSystem.WINDOWS, ARCH_X8664, "windows-x64"),
+ new RuntimePlatformRule(OperatingSystem.WINDOWS, ARCH_X86, "windows-x86"),
+ new RuntimePlatformRule(OperatingSystem.WINDOWS, ARCH_ARM64, "windows-arm64"),
+ new RuntimePlatformRule(OperatingSystem.MACOS, ARCH_PPC, "mac-os"),
+ new RuntimePlatformRule(OperatingSystem.MACOS, ARCH_ARM64, "mac-os-arm64"),
+ new RuntimePlatformRule(OperatingSystem.LINUX, ARCH_X8664, "linux"),
+ new RuntimePlatformRule(OperatingSystem.LINUX, ARCH_X86, "linux-i386")
+ };
+
+ private static final Map<OperatingSystem, Pattern> OS_PATTERNS;
+
+ static {
+ OS_PATTERNS = new EnumMap<>(OperatingSystem.class);
+
+ OS_PATTERNS.put(OperatingSystem.WINDOWS, Pattern.compile("^windows", Pattern.CASE_INSENSITIVE));
+ OS_PATTERNS.put(OperatingSystem.MACOS, Pattern.compile("^(darwin$|mac)", Pattern.CASE_INSENSITIVE));
+
+ // are BSDs and other UNIXs "close enough" to being Linux?
+ OS_PATTERNS.put(OperatingSystem.LINUX, Pattern.compile("^linux", Pattern.CASE_INSENSITIVE));
+ }
+
+ @RequiredArgsConstructor
+ private static class RuntimePlatformRule {
+ private final OperatingSystem os;
+ private final Pattern archPattern;
+ private final String platform;
+
+ boolean matches(OperatingSystem os, String arch) {
+ return this.os == os && archPattern.matcher(arch).find();
+ }
+ }
+}
diff --git a/core/src/main/java/dev/figboot/launcher/core/system/SystemInfo.java b/core/src/main/java/dev/figboot/launcher/core/system/SystemInfo.java
new file mode 100644
index 0000000..2ed8237
--- /dev/null
+++ b/core/src/main/java/dev/figboot/launcher/core/system/SystemInfo.java
@@ -0,0 +1,38 @@
+package dev.figboot.launcher.core.system;
+
+import lombok.RequiredArgsConstructor;
+
+public interface SystemInfo {
+ static SystemInfo current() {
+ return CurrentSystemInfo.get();
+ }
+
+ OperatingSystem getOperatingSystem();
+ String getRuntimePlatform();
+ int getArchBits();
+
+ String getArchName();
+ String getOsVersion();
+
+ String getFileSeparator();
+ String getPathSeparator();
+
+ @RequiredArgsConstructor
+ enum OperatingSystem {
+ WINDOWS,
+ MACOS,
+ LINUX,
+ UNKNOWN;
+
+ public OperatingSystem getByVersionName(String name) {
+ switch (name) {
+ case "windows": return WINDOWS;
+ case "osx": return MACOS;
+ case "linux": return LINUX;
+ }
+
+ // don't return UNKNOWN here, it could mask a launcher bug
+ throw new IllegalArgumentException("unknown OS family name: '" + name + "'");
+ }
+ }
+}
diff --git a/core/src/main/java/dev/figboot/launcher/core/task/Task.java b/core/src/main/java/dev/figboot/launcher/core/task/Task.java
new file mode 100644
index 0000000..9fb603a
--- /dev/null
+++ b/core/src/main/java/dev/figboot/launcher/core/task/Task.java
@@ -0,0 +1,5 @@
+package dev.figboot.launcher.core.task;
+
+public interface Task {
+ void complete();
+}
diff --git a/core/src/main/java/dev/figboot/launcher/core/version/CompleteVersion.java b/core/src/main/java/dev/figboot/launcher/core/version/CompleteVersion.java
new file mode 100644
index 0000000..b69b857
--- /dev/null
+++ b/core/src/main/java/dev/figboot/launcher/core/version/CompleteVersion.java
@@ -0,0 +1,4 @@
+package dev.figboot.launcher.core.version;
+
+public interface CompleteVersion extends VersionInfo {
+}
diff --git a/core/src/main/java/dev/figboot/launcher/core/version/VersionInfo.java b/core/src/main/java/dev/figboot/launcher/core/version/VersionInfo.java
new file mode 100644
index 0000000..4ffb588
--- /dev/null
+++ b/core/src/main/java/dev/figboot/launcher/core/version/VersionInfo.java
@@ -0,0 +1,11 @@
+package dev.figboot.launcher.core.version;
+
+import java.util.Date;
+
+public interface VersionInfo {
+ String getId();
+ Object getType();
+ Date getTime();
+ Date getUpdateTime();
+ int getComplianceLevel();
+}
diff --git a/core/src/main/java/dev/figboot/launcher/core/version/VersionManifest.java b/core/src/main/java/dev/figboot/launcher/core/version/VersionManifest.java
new file mode 100644
index 0000000..2bc4ac6
--- /dev/null
+++ b/core/src/main/java/dev/figboot/launcher/core/version/VersionManifest.java
@@ -0,0 +1,5 @@
+package dev.figboot.launcher.core.version;
+
+public interface VersionManifest extends VersionStorage<VersionInfo> {
+
+}
diff --git a/core/src/main/java/dev/figboot/launcher/core/version/VersionStorage.java b/core/src/main/java/dev/figboot/launcher/core/version/VersionStorage.java
new file mode 100644
index 0000000..7381d5a
--- /dev/null
+++ b/core/src/main/java/dev/figboot/launcher/core/version/VersionStorage.java
@@ -0,0 +1,10 @@
+package dev.figboot.launcher.core.version;
+
+public interface VersionStorage<V extends VersionInfo> {
+ /**
+ * Gets an (already loaded) version from this storage's cache.
+ * @param id the version id (string)
+ * @return the version info or null
+ */
+ V getVersion(String id);
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..249e583
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..51340db
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sun Aug 17 14:44:52 CDT 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..1b6c787
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,234 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/gui/build.gradle.kts b/gui/build.gradle.kts
new file mode 100644
index 0000000..16b7bf3
--- /dev/null
+++ b/gui/build.gradle.kts
@@ -0,0 +1,30 @@
+plugins {
+ application
+}
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(8)
+ }
+}
+
+group = "dev.figboot.launcher"
+version = "1.0-SNAPSHOT"
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ testImplementation(platform("org.junit:junit-bom:5.10.0"))
+ testImplementation("org.junit.jupiter:junit-jupiter")
+}
+
+tasks.test {
+ useJUnitPlatform()
+}
+
+application {
+ mainClass = "dev.figboot.launcher.gui.GUIMain"
+ applicationDefaultJvmArgs = listOf("-Dswing.aatext=true", "-Dawt.useSystemAAFontSettings=on")
+}
diff --git a/gui/src/main/java/dev/figboot/launcher/gui/GUIMain.java b/gui/src/main/java/dev/figboot/launcher/gui/GUIMain.java
new file mode 100644
index 0000000..dc5b2ed
--- /dev/null
+++ b/gui/src/main/java/dev/figboot/launcher/gui/GUIMain.java
@@ -0,0 +1,106 @@
+package dev.figboot.launcher.gui;
+
+import javax.swing.*;
+import javax.swing.border.CompoundBorder;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.MatteBorder;
+import java.awt.*;
+
+public class GUIMain {
+ public static void main(String[] args) {
+ JFrame mainFrame = new JFrame("amogus");
+ mainFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+ mainFrame.setMinimumSize(new Dimension(480, 360));
+
+ JPanel mainPanel = new JPanel();
+ mainPanel.setLayout(new BorderLayout());
+ mainFrame.setContentPane(mainPanel);
+
+ JPanel statusPanel = new JPanel();
+ statusPanel.setBorder(new CompoundBorder(new MatteBorder(1, 0, 0, 0, Color.GRAY), new EmptyBorder(0, 4, 0, 4)));
+ //statusPanel.setBorder(new LineBorder(Color.BLACK, 1));
+
+ JProgressBar statusProgress = new JProgressBar(0, 100);
+ statusProgress.setValue(33);
+ statusPanel.add(statusProgress);
+
+ JLabel statusLabel = new JLabel("Loading online versions (+2 tasks)");
+ statusLabel.setOpaque(false);
+ statusPanel.add(statusLabel);
+
+ mainPanel.add(statusPanel, BorderLayout.PAGE_END);
+
+ JPanel launchPanel = new JPanel();
+ launchPanel.setLayout(new BorderLayout());
+ //launchPanel.setBorder(new LineBorder(Color.GREEN, 1));
+ mainPanel.add(launchPanel, BorderLayout.CENTER);
+
+ JPanel navPanel = new JPanel();
+ navPanel.setLayout(new BorderLayout());
+ //navPanel.setBorder(new LineBorder(Color.CYAN, 1));
+ launchPanel.add(navPanel, BorderLayout.LINE_END);
+
+ JPanel navItemPanel = new JPanel();
+ navItemPanel.setLayout(new GridLayout(0, 1));
+ navPanel.add(navItemPanel, BorderLayout.PAGE_START);
+
+ Font font = new Font(null, Font.BOLD, 16);
+ ButtonGroup navItems = new ButtonGroup();
+ for (String name : new String[]{"News", "Instances", "Accounts", "Explore", "Add-ons", "Settings"}) {
+ JToggleButton btnNavItem = new JToggleButton(name);
+ navItems.add(btnNavItem);
+ btnNavItem.setSelected(name.equals("Instances"));
+ btnNavItem.setFont(font);
+ btnNavItem.setPreferredSize(new Dimension(175, 40));
+ navItemPanel.add(btnNavItem);
+ }
+
+ JPanel playPanel = new JPanel();
+ playPanel.setLayout(new GridLayout(1, 0));
+ //playPanel.setBorder(new LineBorder(Color.RED, 1));
+ playPanel.setBorder(new EmptyBorder(2, 0, 2, 0));
+ launchPanel.add(playPanel, BorderLayout.PAGE_END);
+
+ JPanel accountSelectPanelOuter = new JPanel();
+ accountSelectPanelOuter.setLayout(new BoxLayout(accountSelectPanelOuter, BoxLayout.PAGE_AXIS));
+ accountSelectPanelOuter.setBorder(new EmptyBorder(4, 4, 4, 4));
+ JPanel accountSelectPanel = new JPanel();
+ accountSelectPanel.setLayout(new BorderLayout());
+ accountSelectPanel.setPreferredSize(new Dimension(200, 30));
+ accountSelectPanel.setMaximumSize(new Dimension(200, 30));
+
+ JComboBox<String> accountSelector = new JComboBox<>(new String[]{"figboot", "bootfig", "figroot", "FacePalmOS", "EXR0"});
+ accountSelectPanel.add(accountSelector, BorderLayout.CENTER);
+
+ accountSelectPanel.add(new JButton("+"), BorderLayout.LINE_END);
+
+ accountSelectPanelOuter.add(Box.createGlue());
+ accountSelectPanelOuter.add(accountSelectPanel);
+ accountSelectPanelOuter.add(Box.createGlue());
+ playPanel.add(accountSelectPanelOuter);
+
+ JButton playButton = new JButton("<html><center><p>Play<br><span style=\"font-weight: normal; font-size: 0.8em\">Latest release</span></p></center></html>");
+ playButton.setFont(font);
+ playButton.setPreferredSize(new Dimension(0, 60));
+ playPanel.add(playButton);
+
+ playPanel.add(new JPanel());
+
+ JPanel actionPanel = new JPanel();
+ CardLayout cardLayout = new CardLayout();
+ actionPanel.setLayout(cardLayout);
+ cardLayout.show(actionPanel, "Instances");
+
+ InstanceListPanel instances = new InstanceListPanel();
+ instances.setOpaque(false);
+
+ JScrollPane instanceListScroll = new JScrollPane(instances, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
+ //instanceListScroll.setBorder(new LineBorder(Color.ORANGE, 1));
+ instanceListScroll.getViewport().setBackground(Color.LIGHT_GRAY);
+ launchPanel.add(instanceListScroll, BorderLayout.CENTER);
+
+ mainFrame.setSize(1067, 600);
+ mainFrame.invalidate();
+ mainFrame.setVisible(true);
+ }
+}
diff --git a/gui/src/main/java/dev/figboot/launcher/gui/InstanceListPanel.java b/gui/src/main/java/dev/figboot/launcher/gui/InstanceListPanel.java
new file mode 100644
index 0000000..8ed8c5c
--- /dev/null
+++ b/gui/src/main/java/dev/figboot/launcher/gui/InstanceListPanel.java
@@ -0,0 +1,212 @@
+package dev.figboot.launcher.gui;
+
+import javax.swing.*;
+import javax.swing.border.CompoundBorder;
+import javax.swing.border.LineBorder;
+import java.awt.*;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+
+public class InstanceListPanel extends JPanel implements Scrollable {
+ private Font titleFont, versionFont;
+
+ public InstanceListPanel() {
+ setLayout(new WrapLayout(FlowLayout.LEADING, 5, 5));
+
+ updateFont();
+
+ add(new Instance("Latest release", "Release 1.21.8", true));
+ add(new Instance("Latest snapshot", "Snapshot 25w19a", false));
+ add(new Instance("SevTech Ages", "Release forge_1.7.10-1059.10.aaaaaaa", false));
+ add(new Instance("Hypixel Modpack", "Release forge_1.8.9-1750.19.4", false));
+ add(new Instance("gangsta shi", "Release fabric-1.16.5+0.19.0", false));
+ }
+
+ private void updateFont() {
+ titleFont = new Font(null, Font.BOLD, getFont().getSize() + 2);
+ versionFont = new Font(null, Font.PLAIN, getFont().getSize());
+ }
+
+ private void handleSelect(Instance selected) {
+ for (int i = 0, max = getComponentCount(); i < max; ++i) {
+ Component comp = getComponent(i);
+ if (comp instanceof Instance) {
+ ((Instance)comp).setSelected(comp == selected);
+ }
+ }
+ }
+
+ private static final Color SELECTED_COLOR = new Color(0x00B0FF);
+ private static final Color ARMED_COLOR = SELECTED_COLOR.darker();
+ private static final Color ROLLOVER_COLOR = new Color(0x909090);
+
+ private class Instance extends JPanel implements MouseListener {
+ private boolean selected;
+ private boolean rollover = false;
+ private boolean armed = false;
+
+ Instance(String text, String version, boolean selected) {
+ setBorder(new CompoundBorder(new LineBorder(Color.BLACK, 1), new InstBorder()));
+
+ setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
+
+ JLabel iconLabel = new JLabel(new Icon() {
+ @Override
+ public void paintIcon(Component c, Graphics _g, int x, int y) {
+ Graphics2D g = (Graphics2D)_g;
+
+ Object oldAAHint = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
+ Color oldColor = g.getColor();
+ Stroke oldStroke = g.getStroke();
+
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g.setColor(Color.RED);
+ g.setStroke(new BasicStroke(5, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+ g.drawLine(128 / 4, 128 / 4, 3 * 128 / 4, 3 * 128 / 4);
+ g.drawLine(128 / 4, 3 * 128 / 4, 3 * 128 / 4, 128 / 4);
+
+ g.setColor(Color.BLACK);
+ g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1));
+ g.drawRect(0, 0, 127, 127);
+
+ g.setStroke(oldStroke);
+ g.setColor(oldColor);
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldAAHint);
+ }
+
+ @Override
+ public int getIconWidth() {
+ return 128;
+ }
+
+ @Override
+ public int getIconHeight() {
+ return 128;
+ }
+ });
+ iconLabel.setAlignmentX(CENTER_ALIGNMENT);
+
+ JLabel titleLabel = new JLabel(text);
+ titleLabel.setFont(titleFont);
+ titleLabel.setAlignmentX(CENTER_ALIGNMENT);
+
+ JLabel versionLabel = new JLabel(version);
+ versionLabel.setFont(versionFont);
+ versionLabel.setAlignmentX(CENTER_ALIGNMENT);
+
+ add(titleLabel);
+ add(iconLabel);
+ add(Box.createGlue());
+ add(versionLabel);
+ add(Box.createGlue());
+
+ JPanel actionsPanel = new JPanel();
+ actionsPanel.setAlignmentX(CENTER_ALIGNMENT);
+ actionsPanel.setLayout(new BoxLayout(actionsPanel, BoxLayout.LINE_AXIS));
+ actionsPanel.add(new JButton("Play"));
+ actionsPanel.add(new JButton("Edit"));
+
+ add(actionsPanel);
+ add(Box.createVerticalStrut(2));
+
+ this.selected = selected;
+
+ Dimension prefSz = getPreferredSize();
+ setPreferredSize(new Dimension(215, prefSz.height));
+
+ addMouseListener(this);
+ }
+
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (SwingUtilities.isLeftMouseButton(e)) {
+ InstanceListPanel.this.handleSelect(this);
+ }
+ }
+
+ @Override
+ public void mousePressed(MouseEvent e) {
+ if (SwingUtilities.isLeftMouseButton(e)) {
+ armed = true;
+ repaint();
+ }
+ }
+
+ @Override
+ public void mouseReleased(MouseEvent e) {
+ if (SwingUtilities.isLeftMouseButton(e)) {
+ armed = false;
+ repaint();
+ }
+ }
+
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ rollover = true;
+ repaint();
+ }
+
+ @Override
+ public void mouseExited(MouseEvent e) {
+ rollover = false;
+ repaint();
+ }
+
+ public void setSelected(boolean sel) {
+ if (this.selected == sel) return;
+
+ this.selected = sel;
+ repaint();
+ }
+
+ class InstBorder extends LineBorder {
+ InstBorder() {
+ super(null, 3, false);
+ }
+
+ @Override
+ public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
+ if (armed) {
+ lineColor = ARMED_COLOR;
+ super.paintBorder(c, g, x, y, width, height);
+ } else if (selected) {
+ lineColor = SELECTED_COLOR;
+ super.paintBorder(c, g, x, y, width, height);
+ } else if (rollover) {
+ lineColor = ROLLOVER_COLOR;
+ super.paintBorder(c, g, x, y, width, height);
+ }
+ }
+
+ @Override
+ public boolean isBorderOpaque() {
+ return selected || rollover || armed;
+ }
+ }
+ }
+
+ @Override
+ public Dimension getPreferredScrollableViewportSize() {
+ return getPreferredSize();
+ }
+
+ @Override
+ public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
+ return 20; // TODO
+ }
+
+ @Override
+ public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
+ return 40; // TODO
+ }
+
+ @Override
+ public boolean getScrollableTracksViewportWidth() {
+ return true;
+ }
+
+ @Override
+ public boolean getScrollableTracksViewportHeight() {
+ return false;
+ }
+}
diff --git a/gui/src/main/java/dev/figboot/launcher/gui/WrapLayout.java b/gui/src/main/java/dev/figboot/launcher/gui/WrapLayout.java
new file mode 100644
index 0000000..f0b3095
--- /dev/null
+++ b/gui/src/main/java/dev/figboot/launcher/gui/WrapLayout.java
@@ -0,0 +1,204 @@
+/*
+MIT License
+
+Copyright (c) 2023 Rob Camick
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+
+package dev.figboot.launcher.gui;
+
+import java.awt.*;
+import javax.swing.JScrollPane;
+import javax.swing.SwingUtilities;
+
+/**
+ * FlowLayout subclass that fully supports wrapping of components.
+ */
+public class WrapLayout extends FlowLayout {
+ private Dimension preferredLayoutSize;
+
+ /**
+ * Constructs a new <code>WrapLayout</code> with a left
+ * alignment and a default 5-unit horizontal and vertical gap.
+ */
+ public WrapLayout() {
+ super();
+ }
+
+ /**
+ * Constructs a new <code>FlowLayout</code> with the specified
+ * alignment and a default 5-unit horizontal and vertical gap.
+ * The value of the alignment argument must be one of
+ * <code>WrapLayout</code>, <code>WrapLayout</code>,
+ * or <code>WrapLayout</code>.
+ *
+ * @param align the alignment value
+ */
+ public WrapLayout(int align) {
+ super(align);
+ }
+
+ /**
+ * Creates a new flow layout manager with the indicated alignment
+ * and the indicated horizontal and vertical gaps.
+ * <p>
+ * The value of the alignment argument must be one of
+ * <code>WrapLayout</code>, <code>WrapLayout</code>,
+ * or <code>WrapLayout</code>.
+ *
+ * @param align the alignment value
+ * @param hgap the horizontal gap between components
+ * @param vgap the vertical gap between components
+ */
+ public WrapLayout(int align, int hgap, int vgap) {
+ super(align, hgap, vgap);
+ }
+
+ /**
+ * Returns the preferred dimensions for this layout given the
+ * <i>visible</i> components in the specified target container.
+ *
+ * @param target the component which needs to be laid out
+ * @return the preferred dimensions to lay out the
+ * subcomponents of the specified container
+ */
+ @Override
+ public Dimension preferredLayoutSize(Container target) {
+ return layoutSize(target, true);
+ }
+
+ /**
+ * Returns the minimum dimensions needed to layout the <i>visible</i>
+ * components contained in the specified target container.
+ *
+ * @param target the component which needs to be laid out
+ * @return the minimum dimensions to lay out the
+ * subcomponents of the specified container
+ */
+ @Override
+ public Dimension minimumLayoutSize(Container target) {
+ Dimension minimum = layoutSize(target, false);
+ minimum.width -= (getHgap() + 1);
+ return minimum;
+ }
+
+ /**
+ * Returns the minimum or preferred dimension needed to layout the target
+ * container.
+ *
+ * @param target target to get layout size for
+ * @param preferred should preferred size be calculated
+ * @return the dimension to layout the target container
+ */
+ private Dimension layoutSize(Container target, boolean preferred) {
+ synchronized (target.getTreeLock()) {
+ // Each row must fit with the width allocated to the containter.
+ // When the container width = 0, the preferred width of the container
+ // has not yet been calculated so lets ask for the maximum.
+
+ int targetWidth = target.getSize().width;
+ Container container = target;
+
+ while (container.getSize().width == 0 && container.getParent() != null) {
+ container = container.getParent();
+ }
+
+ targetWidth = container.getSize().width;
+
+ if (targetWidth == 0)
+ targetWidth = Integer.MAX_VALUE;
+
+ int hgap = getHgap();
+ int vgap = getVgap();
+ Insets insets = target.getInsets();
+ int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2);
+ int maxWidth = targetWidth - horizontalInsetsAndGap;
+
+ // Fit components into the allowed width
+
+ Dimension dim = new Dimension(0, 0);
+ int rowWidth = 0;
+ int rowHeight = 0;
+
+ int nmembers = target.getComponentCount();
+
+ for (int i = 0; i < nmembers; i++) {
+ Component m = target.getComponent(i);
+
+ if (m.isVisible()) {
+ Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize();
+
+ // Can't add the component to current row. Start a new row.
+
+ if (rowWidth + d.width > maxWidth) {
+ addRow(dim, rowWidth, rowHeight);
+ rowWidth = 0;
+ rowHeight = 0;
+ }
+
+ // Add a horizontal gap for all components after the first
+
+ if (rowWidth != 0) {
+ rowWidth += hgap;
+ }
+
+ rowWidth += d.width;
+ rowHeight = Math.max(rowHeight, d.height);
+ }
+ }
+
+ addRow(dim, rowWidth, rowHeight);
+
+ dim.width += horizontalInsetsAndGap;
+ dim.height += insets.top + insets.bottom + vgap * 2;
+
+ // When using a scroll pane or the DecoratedLookAndFeel we need to
+ // make sure the preferred size is less than the size of the
+ // target containter so shrinking the container size works
+ // correctly. Removing the horizontal gap is an easy way to do this.
+
+ Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target);
+
+ if (scrollPane != null && target.isValid()) {
+ dim.width -= (hgap + 1);
+ }
+
+ return dim;
+ }
+ }
+
+ /*
+ * A new row has been completed. Use the dimensions of this row
+ * to update the preferred size for the container.
+ *
+ * @param dim update the width and height when appropriate
+ * @param rowWidth the width of the row to add
+ * @param rowHeight the height of the row to add
+ */
+ private void addRow(Dimension dim, int rowWidth, int rowHeight) {
+ dim.width = Math.max(dim.width, rowWidth);
+
+ if (dim.height > 0) {
+ dim.height += getVgap();
+ }
+
+ dim.height += rowHeight;
+ }
+}
diff --git a/gui/src/main/resources/icons/play.svg b/gui/src/main/resources/icons/play.svg
new file mode 100644
index 0000000..799ea9c
--- /dev/null
+++ b/gui/src/main/resources/icons/play.svg
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="48"
+ height="48"
+ viewBox="0 0 48 48"
+ version="1.1"
+ id="svg1"
+ inkscape:export-filename="play_24.png"
+ inkscape:export-xdpi="48"
+ inkscape:export-ydpi="48"
+ inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
+ sodipodi:docname="drawing.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="px"
+ inkscape:zoom="7.2773073"
+ inkscape:cx="-6.3210193"
+ inkscape:cy="40.743092"
+ inkscape:window-width="1916"
+ inkscape:window-height="1026"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs1">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ id="perspective1" />
+ <filter
+ inkscape:label="Button"
+ inkscape:menu="Bevels"
+ inkscape:menu-tooltip="Soft bevel, slightly depressed middle"
+ style="color-interpolation-filters:sRGB;"
+ id="filter21"
+ x="-0.4555"
+ y="-0.4555"
+ width="1.911"
+ height="1.911">
+ <feGaussianBlur
+ stdDeviation="2.3"
+ in="SourceAlpha"
+ result="result0"
+ id="feGaussianBlur18" />
+ <feMorphology
+ in="SourceAlpha"
+ radius="6.5"
+ result="result1"
+ id="feMorphology18" />
+ <feGaussianBlur
+ stdDeviation="7"
+ in="result1"
+ id="feGaussianBlur19" />
+ <feColorMatrix
+ values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.3 0"
+ result="result91"
+ id="feColorMatrix19" />
+ <feComposite
+ in="result0"
+ operator="out"
+ result="result2"
+ in2="result91"
+ id="feComposite19" />
+ <feDiffuseLighting
+ surfaceScale="10"
+ id="feDiffuseLighting19">
+ <feDistantLight
+ azimuth="225"
+ elevation="45"
+ id="feDistantLight19" />
+ </feDiffuseLighting>
+ <feBlend
+ in2="SourceGraphic"
+ mode="multiply"
+ id="feBlend19" />
+ <feComposite
+ in2="SourceAlpha"
+ operator="in"
+ result="result3"
+ id="feComposite20" />
+ <feGaussianBlur
+ stdDeviation="1"
+ in="result3"
+ result="result4"
+ id="feGaussianBlur20" />
+ <feComposite
+ in2="result3"
+ operator="atop"
+ id="feComposite21" />
+ </filter>
+ </defs>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <circle
+ style="fill:#00ff00;filter:url(#filter21)"
+ id="path1"
+ r="20"
+ cy="24"
+ cx="24" />
+ <path
+ sodipodi:type="star"
+ style="fill:#ffffff;stroke-width:1;stroke-dasharray:none;fill-opacity:1"
+ id="path7"
+ inkscape:flatsided="true"
+ sodipodi:sides="3"
+ sodipodi:cx="19.031765"
+ sodipodi:cy="16.146082"
+ sodipodi:r1="9.0851498"
+ sodipodi:r2="4.5425749"
+ sodipodi:arg1="2.0943951"
+ sodipodi:arg2="3.1415927"
+ inkscape:rounded="0"
+ inkscape:randomized="0"
+ d="m 14.48919,24.014052 0,-15.7359406 13.627725,7.8679706 z"
+ inkscape:transform-center-x="-2.2712875"
+ transform="translate(2.6969476,7.8539181)" />
+ </g>
+</svg>
diff --git a/gui/src/main/resources/icons/play_24.png b/gui/src/main/resources/icons/play_24.png
new file mode 100644
index 0000000..11bd88e
--- /dev/null
+++ b/gui/src/main/resources/icons/play_24.png
Binary files differ
diff --git a/guiconcept.png b/guiconcept.png
new file mode 100644
index 0000000..b0284cc
--- /dev/null
+++ b/guiconcept.png
Binary files differ
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..f4624c2
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,3 @@
+rootProject.name = "l4s2"
+include("core")
+include("gui") \ No newline at end of file