diff --git a/example/build.gradle b/example/build.gradle new file mode 100644 index 0000000..39da07f --- /dev/null +++ b/example/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'application' + id 'checkstyle' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':imgui-app') +} diff --git a/example/src/main/java/Main.java b/example/src/main/java/Main.java new file mode 100644 index 0000000..60b941a --- /dev/null +++ b/example/src/main/java/Main.java @@ -0,0 +1,33 @@ +import imgui.ImGui; +import imgui.ImGuiIO; +import imgui.app.Application; +import imgui.app.Configuration; +import imgui.flag.ImGuiConfigFlags; + +public final class Main extends Application { + @Override + protected void configure(final Configuration config) { + config.setTitle("Example Application"); + } + + @Override + protected void initImGui(final Configuration config) { + super.initImGui(config); + + final ImGuiIO io = ImGui.getIO(); + io.setIniFilename(null); // We don't want to save .ini file + io.addConfigFlags(ImGuiConfigFlags.NavEnableKeyboard); // Enable Keyboard Controls + io.addConfigFlags(ImGuiConfigFlags.DockingEnable); // Enable Docking + io.addConfigFlags(ImGuiConfigFlags.ViewportsEnable); // Enable Multi-Viewport / Platform Windows + io.setConfigViewportsNoTaskBarIcon(true); + } + + @Override + public void process() { + ImGui.text("Hello, World"); + } + + public static void main(final String[] args) { + launch(new Main()); + } +} diff --git a/imgui-app/build.gradle b/imgui-app/build.gradle new file mode 100644 index 0000000..df72155 --- /dev/null +++ b/imgui-app/build.gradle @@ -0,0 +1,66 @@ +plugins { + id 'java-library' + id 'io.freefair.lombok' version '5.3.0' + id 'checkstyle' + id 'com.jfrog.bintray' version '1.8.4' + id 'maven-publish' +} + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + api platform('org.lwjgl:lwjgl-bom:3.2.3') + + api 'org.lwjgl:lwjgl' + api 'org.lwjgl:lwjgl-glfw' + api 'org.lwjgl:lwjgl-opengl' + + ['natives-linux', 'natives-windows', 'natives-macos'].each { + runtime "org.lwjgl:lwjgl::$it" + runtime "org.lwjgl:lwjgl-glfw::$it" + runtime "org.lwjgl:lwjgl-opengl::$it" + } + + api project(':imgui-binding') + api project(':imgui-lwjgl3') +} + +jar { + from('../bin') { + include 'imgui-java64.dll' + include 'libimgui-java64.so' + include 'libimgui-java64.dylib' + into 'io/imgui/java/native-bin/' + } +} + +task sourcesJar(type: Jar, dependsOn: classes) { + archiveClassifier.set('sources') + from sourceSets.main.allSource +} + +publishing { + publications { + MyPublication(MavenPublication) { + from components.java + groupId 'io.imgui.java' + artifactId 'imgui-java-app' + artifact sourcesJar + version property('version') + } + } +} + +bintray { + user = System.getenv('BINTRAY_USER') + key = System.getenv('BINTRAY_API_KEY') + publications = ['MyPublication'] + pkg { + repo = 'io.imgui.java' + name = 'imgui-java-app' + } +} diff --git a/imgui-app/lombok.config b/imgui-app/lombok.config new file mode 100644 index 0000000..6aa51d7 --- /dev/null +++ b/imgui-app/lombok.config @@ -0,0 +1,2 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true diff --git a/imgui-app/src/main/java/imgui/app/Application.java b/imgui-app/src/main/java/imgui/app/Application.java new file mode 100644 index 0000000..ed7a6dd --- /dev/null +++ b/imgui-app/src/main/java/imgui/app/Application.java @@ -0,0 +1,81 @@ +package imgui.app; + +/** + * Application class from which ImGui applications extend. + * Serves as an abstraction layer to hide all low-level details about window creation and rendering routine. + * + *

Life-cycle

+ * The entry point for ImGui applications is the Application class and {@link #launch(Application)} method. + * It initializes application instance and starts the main application loop. + * + *
    + *
  1. {@link #configure(Configuration)} It's called before window creation, so only basic application setups are expected.
  2. + *
  3. {@link #initWindow(Configuration)} Method creates application window.
  4. + *
  5. {@link #initImGui(Configuration)} Method initializes Dear ImGui context. Could be used to do Dear ImGui setup as well.
  6. + *
  7. {@link #process()} Method is meant to be overridden with user application logic.
  8. + *
  9. {@link #disposeImGui()} Destroys Dear ImGui context.
  10. + *
  11. {@link #disposeWindow()} Destroys application window.
  12. + *
+ * + *

As it could be seen, ImGui application differs from the classic one in the way of its life-cycle flow. + * Instead of creating widgets and adding listeners to them we have an application loop where everything is handled right away. + * Read more about Immediate GUI mode to understand that paradigm better. + * + *

Example

+ * The simplest application example could be done in the next way: + * + *
{@code
+ * import imgui.ImGui;
+ * import imgui.app.Application;
+ * import imgui.app.Configuration;
+ *
+ * public class Main extends Application {
+ *     @Override
+ *     protected void configure(final Configuration config) {
+ *         config.setTitle("Example Application");
+ *     }
+ *
+ *     @Override
+ *     public void process() {
+ *         ImGui.text("Hello, World");
+ *     }
+ *
+ *     public static void main(final String[] args) {
+ *         launch(new Main());
+ *     }
+ * }
+ * }
+ * + *

As its said, {@link #process()} method is meant to be overridden. All your application logic should go there. + * + *

Threading

+ * Unlike other Java applications, ImGui is about "one thread for everything". You still can use multi-threading, but be careful. + * For example, large list of computations could be separated between application ticks. {@link #process()} method is called constantly. + * Use that wisely and remember that all GUI should be in the main thread. + */ +public abstract class Application extends Window { + /** + * Method is called before window creation. Could be used to provide basic window information, like title name etc. + * + * @param config configuration object with basic window information + */ + protected void configure(final Configuration config) { + } + + /** + * Entry point of any ImGui application. Use it to start the application loop. + * + * @param app application instance to run + */ + public static void launch(final Application app) { + initialize(app); + app.run(); + app.dispose(); + } + + private static void initialize(final Application app) { + final Configuration config = new Configuration(); + app.configure(config); + app.init(config); + } +} diff --git a/imgui-app/src/main/java/imgui/app/Configuration.java b/imgui-app/src/main/java/imgui/app/Configuration.java new file mode 100644 index 0000000..92f6d55 --- /dev/null +++ b/imgui-app/src/main/java/imgui/app/Configuration.java @@ -0,0 +1,26 @@ +package imgui.app; + +import lombok.Data; + +/** + * Data class to provide basic information about the window. Like, the title name etc. + */ +@Data +public final class Configuration { + /** + * Application title. + */ + private String title = "ImGui Java Application"; + /** + * Application window width. + */ + private int width = 1280; + /** + * Application window height. + */ + private int height = 768; + /** + * When true, application will be maximized by default. + */ + private boolean fullScreen = false; +} diff --git a/imgui-app/src/main/java/imgui/app/Window.java b/imgui-app/src/main/java/imgui/app/Window.java new file mode 100644 index 0000000..c5b36d8 --- /dev/null +++ b/imgui-app/src/main/java/imgui/app/Window.java @@ -0,0 +1,186 @@ +package imgui.app; + +import imgui.ImGui; +import imgui.flag.ImGuiConfigFlags; +import imgui.gl3.ImGuiImplGl3; +import imgui.glfw.ImGuiImplGlfw; +import org.lwjgl.glfw.Callbacks; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.glfw.GLFWErrorCallback; +import org.lwjgl.glfw.GLFWVidMode; +import org.lwjgl.opengl.GL; +import org.lwjgl.opengl.GL32; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; + +import java.awt.Color; +import java.nio.IntBuffer; +import java.util.Objects; + +/** + * Low-level abstraction, which creates application window and starts the main loop. + * It's recommended to use {@link Application}, but this class could be extended directly as well. + * Take a note, that in this case you need to call all application loop methods ({@link #init(Configuration)}, {@link #run()}, {@link #dispose()}) manually. + */ +public abstract class Window { + private final ImGuiImplGlfw imGuiGlfw = new ImGuiImplGlfw(); + private final ImGuiImplGl3 imGuiGl3 = new ImGuiImplGl3(); + + private String glslVersion = null; + + /** + * Pointer to native GLFW window. + */ + protected long handle; + /** + * Background color of the window. + */ + protected Color colorBg = Color.GRAY; + + /** + * Method to initialize application. + * + * @param config configuration object with basic window information + */ + protected final void init(final Configuration config) { + initWindow(config); + initImGui(config); + imGuiGlfw.init(handle, true); + imGuiGl3.init(glslVersion); + } + + /** + * Method to dispose all used application resources and destroy its window. + */ + protected final void dispose() { + imGuiGl3.dispose(); + imGuiGlfw.dispose(); + disposeImGui(); + disposeWindow(); + } + + /** + * Method to create and initialize GLFW window. + * + * @param config configuration object with basic window information + */ + protected void initWindow(final Configuration config) { + GLFWErrorCallback.createPrint(System.err).set(); + + if (!GLFW.glfwInit()) { + throw new IllegalStateException("Unable to initialize GLFW"); + } + + decideGlGlslVersions(); + + GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); + handle = GLFW.glfwCreateWindow(config.getWidth(), config.getHeight(), config.getTitle(), MemoryUtil.NULL, MemoryUtil.NULL); + + if (handle == MemoryUtil.NULL) { + throw new RuntimeException("Failed to create the GLFW window"); + } + + try (MemoryStack stack = MemoryStack.stackPush()) { + final IntBuffer pWidth = stack.mallocInt(1); // int* + final IntBuffer pHeight = stack.mallocInt(1); // int* + + GLFW.glfwGetWindowSize(handle, pWidth, pHeight); + + final GLFWVidMode vidmode = Objects.requireNonNull(GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor())); + + GLFW.glfwSetWindowPos(handle, (vidmode.width() - pWidth.get(0)) / 2, (vidmode.height() - pHeight.get(0)) / 2); + } + + GLFW.glfwMakeContextCurrent(handle); + GLFW.glfwSwapInterval(GLFW.GLFW_TRUE); + GLFW.glfwShowWindow(handle); + + if (config.isFullScreen()) { + GLFW.glfwMaximizeWindow(handle); + } + + GL.createCapabilities(); + } + + private void decideGlGlslVersions() { + final boolean isMac = System.getProperty("os.name").toLowerCase().contains("mac"); + if (isMac) { + glslVersion = "#version 150"; + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3); + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 2); + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE); // 3.2+ only + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE); // Required on Mac + } else { + glslVersion = "#version 130"; + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3); + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 0); + } + } + + /** + * Method to initialize Dear ImGui context. Could be overridden to do custom Dear ImGui setup before application start. + * + * @param config configuration object with basic window information + */ + protected void initImGui(final Configuration config) { + ImGui.createContext(); + } + + /** + * Main application loop. + */ + protected final void run() { + while (!GLFW.glfwWindowShouldClose(handle)) { + startFrame(); + process(); + endFrame(); + } + } + + /** + * Method to be overridden by user to provide main application logic. + */ + public abstract void process(); + + private void startFrame() { + if (colorBg != null) { + GL32.glClearColor(colorBg.getRed() / 255f, colorBg.getGreen() / 255f, colorBg.getBlue() / 255f, colorBg.getAlpha() / 255f); + } + + GL32.glClear(GL32.GL_COLOR_BUFFER_BIT | GL32.GL_DEPTH_BUFFER_BIT); + imGuiGlfw.newFrame(); + ImGui.newFrame(); + } + + private void endFrame() { + ImGui.render(); + imGuiGl3.renderDrawData(ImGui.getDrawData()); + + if (ImGui.getIO().hasConfigFlags(ImGuiConfigFlags.ViewportsEnable)) { + final long backupWindowPtr = GLFW.glfwGetCurrentContext(); + ImGui.updatePlatformWindows(); + ImGui.renderPlatformWindowsDefault(); + GLFW.glfwMakeContextCurrent(backupWindowPtr); + } + + GLFW.glfwSwapBuffers(handle); + GLFW.glfwPollEvents(); + } + + /** + * Method to destroy Dear ImGui context. + */ + protected void disposeImGui() { + ImGui.destroyContext(); + } + + /** + * Method to destroy GLFW window. + */ + protected void disposeWindow() { + Callbacks.glfwFreeCallbacks(handle); + GLFW.glfwDestroyWindow(handle); + GLFW.glfwTerminate(); + Objects.requireNonNull(GLFW.glfwSetErrorCallback(null)).free(); + } +} diff --git a/settings.gradle b/settings.gradle index 3ee5cd0..f07d364 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,4 +2,6 @@ rootProject.name = 'imgui-java' include 'imgui-binding' include 'imgui-lwjgl3' include 'imgui-binding-natives' +include 'imgui-app' +include 'example'