From b80fde4388d66ab46e5c6f6eca8d72a78a2c29d7 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Tue, 25 Aug 2020 23:52:09 -0700 Subject: [PATCH] [wpigui] Add wpigui wrappers for GLFW+imgui These hide the platform specifics behind a common C++ API. Platforms: - Windows: DirectX 11 (with 10 backwards compatibility) - Linux: OpenGL 3 - Mac: Metal --- .styleguide | 1 + CMakeLists.txt | 1 + imgui/CMakeLists.txt | 9 + imgui/CMakeLists.txt.in | 2 +- settings.gradle | 1 + shared/config.gradle | 2 +- wpigui/.styleguide | 25 ++ wpigui/CMakeLists.txt | 44 +++ wpigui/build.gradle | 125 ++++++ wpigui/publish.gradle | 71 ++++ wpigui/src/dev/native/cpp/main.cpp | 14 + wpigui/src/main/native/cpp/wpigui.cpp | 357 ++++++++++++++++++ .../native/directx11/wpigui_directx11.cpp | 203 ++++++++++ wpigui/src/main/native/include/wpigui.h | 151 ++++++++ .../src/main/native/include/wpigui_internal.h | 73 ++++ wpigui/src/main/native/metal/wpigui_metal.mm | 140 +++++++ .../main/native/opengl3/wpigui_opengl3.cpp | 132 +++++++ 17 files changed, 1349 insertions(+), 2 deletions(-) create mode 100644 wpigui/.styleguide create mode 100644 wpigui/CMakeLists.txt create mode 100644 wpigui/build.gradle create mode 100644 wpigui/publish.gradle create mode 100644 wpigui/src/dev/native/cpp/main.cpp create mode 100644 wpigui/src/main/native/cpp/wpigui.cpp create mode 100644 wpigui/src/main/native/directx11/wpigui_directx11.cpp create mode 100644 wpigui/src/main/native/include/wpigui.h create mode 100644 wpigui/src/main/native/include/wpigui_internal.h create mode 100644 wpigui/src/main/native/metal/wpigui_metal.mm create mode 100644 wpigui/src/main/native/opengl3/wpigui_opengl3.cpp diff --git a/.styleguide b/.styleguide index 20c2059833..498109678d 100644 --- a/.styleguide +++ b/.styleguide @@ -26,6 +26,7 @@ includeOtherLibs { ^drake/ ^hal/ ^imgui + ^implot ^mockdata/ ^networktables/ ^ntcore diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d5abafaeb..e3361e087e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -138,6 +138,7 @@ endif() if (WITH_SIMULATION_MODULES AND NOT USE_EXTERNAL_HAL) add_subdirectory(imgui) + add_subdirectory(wpigui) add_subdirectory(simulation) endif() diff --git a/imgui/CMakeLists.txt b/imgui/CMakeLists.txt index 1bfb7ab3bd..aa15d2e15b 100644 --- a/imgui/CMakeLists.txt +++ b/imgui/CMakeLists.txt @@ -54,6 +54,15 @@ file(GLOB imgui_sources ${imgui_srcdir}/*.cpp) set(implot_srcdir ${CMAKE_CURRENT_BINARY_DIR}/implot-src) file(GLOB implot_sources ${implot_srcdir}/*.cpp) add_library(imgui STATIC ${imgui_sources} ${implot_sources} ${imgui_srcdir}/examples/imgui_impl_glfw.cpp ${imgui_srcdir}/examples/imgui_impl_opengl3.cpp ${CMAKE_CURRENT_BINARY_DIR}/imgui_ProggyDotted.cpp ${CMAKE_CURRENT_BINARY_DIR}/stb_image.cpp) +if (MSVC) + target_sources(imgui PRIVATE ${imgui_srcdir}/examples/imgui_impl_directx11.cpp) +else() + if (APPLE) + target_sources(imgui PRIVATE ${imgui_srcdir}/examples/imgui_impl_metal.mm) + else() + #target_sources(imgui PRIVATE ${imgui_srcdir}/examples/imgui_impl_opengl3.cpp) + endif() +endif() target_link_libraries(imgui PUBLIC gl3w glfw) target_include_directories(imgui PUBLIC "$" "$" "$" "$" "$") diff --git a/imgui/CMakeLists.txt.in b/imgui/CMakeLists.txt.in index 076d95a7b6..4fe06e6615 100644 --- a/imgui/CMakeLists.txt.in +++ b/imgui/CMakeLists.txt.in @@ -33,7 +33,7 @@ ExternalProject_Add(imgui ) ExternalProject_Add(implot GIT_REPOSITORY https://github.com/epezent/implot.git - GIT_TAG 4d4cac629b0edcda6f8d99d5f34225a7d0878509 + GIT_TAG db16011e7398e6d9ef062fbd59338ddb689e99c6 SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/implot-src" BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/implot-build" CONFIGURE_COMMAND "" diff --git a/settings.gradle b/settings.gradle index e020d0d9f2..2d59b6efe7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,6 +20,7 @@ include 'wpiutil' include 'ntcore' include 'hal' include 'cscore' +include 'wpigui' include 'wpimath' include 'wpilibc' include 'wpilibcExamples' diff --git a/shared/config.gradle b/shared/config.gradle index 94d0938da2..93002c2af9 100644 --- a/shared/config.gradle +++ b/shared/config.gradle @@ -11,7 +11,7 @@ nativeUtils { niLibVersion = "2020.10.1" opencvVersion = "3.4.7-3" googleTestVersion = "1.9.0-4-437e100-1" - imguiVersion = "1.76-2" + imguiVersion = "1.76-6" } } } diff --git a/wpigui/.styleguide b/wpigui/.styleguide new file mode 100644 index 0000000000..f784b5d157 --- /dev/null +++ b/wpigui/.styleguide @@ -0,0 +1,25 @@ +cppHeaderFileInclude { + \.h$ +} + +cppSrcFileInclude { + \.cpp$ +} + +repoRootNameOverride { + wpigui +} + +includeGuardRoots { + wpigui/src/main/native/cpp/ + wpigui/src/main/native/include/ + wpigui/src/main/native/directx11/ + wpigui/src/main/native/metal/ + wpigui/src/main/native/opengl3/ +} + +includeOtherLibs { + ^imgui + ^implot + ^stb +} diff --git a/wpigui/CMakeLists.txt b/wpigui/CMakeLists.txt new file mode 100644 index 0000000000..a5d1edcfcb --- /dev/null +++ b/wpigui/CMakeLists.txt @@ -0,0 +1,44 @@ +project(wpigui) + +include(CompileWarnings) + +file(GLOB wpigui_src src/main/native/cpp/*.cpp) +file(GLOB wpigui_windows_src src/main/native/directx11/*.cpp) +file(GLOB wpigui_mac_src src/main/native/metal/*.mm) +file(GLOB wpigui_unix_src src/main/native/opengl3/*.cpp) + +add_library(wpigui ${wpigui_src}) +set_target_properties(wpigui PROPERTIES DEBUG_POSTFIX "d") + +set_property(TARGET wpigui PROPERTY FOLDER "libraries") + +target_compile_features(wpigui PUBLIC cxx_std_17) +wpilib_target_warnings(wpigui) +target_link_libraries(wpigui imgui) + +target_include_directories(wpigui PUBLIC + $ + $) + +if (MSVC) + target_sources(wpigui PRIVATE ${wpigui_windows_src}) +else() + if (APPLE) + target_sources(wpigui PRIVATE ${wpigui_mac_src}) + else() + target_sources(wpigui PRIVATE ${wpigui_unix_src}) + endif() +endif() + +install(TARGETS wpigui EXPORT wpigui DESTINATION "${main_lib_dest}") +install(DIRECTORY src/main/native/include/ DESTINATION "${include_dest}/wpigui") + +#if (MSVC OR FLAT_INSTALL_WPILIB) +# set (wpigui_config_dir ${wpilib_dest}) +#else() +# set (wpigui_config_dir share/wpigui) +#endif() + +#configure_file(wpigui-config.cmake.in ${CMAKE_BINARY_DIR}/wpigui-config.cmake ) +#install(FILES ${CMAKE_BINARY_DIR}/wpigui-config.cmake DESTINATION ${wpigui_config_dir}) +#install(EXPORT wpigui DESTINATION ${wpigui_config_dir}) diff --git a/wpigui/build.gradle b/wpigui/build.gradle new file mode 100644 index 0000000000..0628a56805 --- /dev/null +++ b/wpigui/build.gradle @@ -0,0 +1,125 @@ +import org.gradle.internal.os.OperatingSystem + +if (!project.hasProperty('onlylinuxathena') && !project.hasProperty('onlylinuxraspbian') && !project.hasProperty('onlylinuxaarch64bionic')) { + + apply plugin: 'cpp' + if (OperatingSystem.current().isMacOsX()) { + apply plugin: 'objective-cpp' + } + apply plugin: 'visual-studio' + apply plugin: 'edu.wpi.first.NativeUtils' + + ext { + nativeName = 'wpigui' + } + + apply from: "${rootDir}/shared/config.gradle" + + nativeUtils.exportsConfigs { + wpigui { + x86ExcludeSymbols = ['_CT??_R0?AV_System_error', '_CT??_R0?AVexception', '_CT??_R0?AVfailure', + '_CT??_R0?AVruntime_error', '_CT??_R0?AVsystem_error', '_CTA5?AVfailure', + '_TI5?AVfailure', '_CT??_R0?AVout_of_range', '_CTA3?AVout_of_range', + '_TI3?AVout_of_range', '_CT??_R0?AVbad_cast'] + x64ExcludeSymbols = ['_CT??_R0?AV_System_error', '_CT??_R0?AVexception', '_CT??_R0?AVfailure', + '_CT??_R0?AVruntime_error', '_CT??_R0?AVsystem_error', '_CTA5?AVfailure', + '_TI5?AVfailure', '_CT??_R0?AVout_of_range', '_CTA3?AVout_of_range', + '_TI3?AVout_of_range', '_CT??_R0?AVbad_cast'] + } + } + + model { + components { + "${nativeName}"(NativeLibrarySpec) { + sources { + cpp { + source { + srcDirs "src/main/native/cpp" + include '*.cpp' + } + exportedHeaders { + srcDirs 'src/main/native/include' + } + } + } + binaries.all { + nativeUtils.useRequiredLibrary(it, 'imgui_static') + if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio || it.targetPlatform.name == nativeUtils.wpi.platforms.raspbian || it.targetPlatform.name == nativeUtils.wpi.platforms.aarch64bionic) { + it.buildable = false + return + } + if (it.targetPlatform.operatingSystem.isWindows()) { + it.sources { + wpiguiWindowsCpp(CppSourceSet) { + source { + srcDirs 'src/main/native/directx11' + include '*.cpp' + } + } + } + } else if (it.targetPlatform.operatingSystem.isMacOsX()) { + it.sources { + wpiguiMacObjectiveCpp(ObjectiveCppSourceSet) { + source { + srcDirs 'src/main/native/metal' + include '*.mm' + } + } + } + } else { + it.sources { + wpiguiUnixCpp(CppSourceSet) { + source { + srcDirs 'src/main/native/opengl3' + include '*.cpp' + } + } + } + } + it.sources.each { + it.exportedHeaders { + srcDirs 'src/main/native/include' + } + } + } + } + // By default, a development executable will be generated. This is to help the case of + // testing specific functionality of the library. + "${nativeName}Dev"(NativeExecutableSpec) { + targetBuildTypes 'debug' + sources { + cpp { + source { + srcDirs 'src/dev/native/cpp' + include '**/*.cpp' + lib library: 'wpigui' + } + exportedHeaders { + srcDirs 'src/dev/native/include' + } + } + } + binaries.all { + nativeUtils.useRequiredLibrary(it, 'imgui_static') + } + } + } + binaries { + all { + if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio || it.targetPlatform.name == nativeUtils.wpi.platforms.raspbian || it.targetPlatform.name == nativeUtils.wpi.platforms.aarch64bionic) { + it.buildable = false + return + } + if (it.targetPlatform.operatingSystem.isWindows()) { + it.linker.args << 'Gdi32.lib' << 'Shell32.lib' << 'd3d11.lib' << 'd3dcompiler.lib' + } else if (it.targetPlatform.operatingSystem.isMacOsX()) { + it.linker.args << '-framework' << 'Metal' << '-framework' << 'MetalKit' << '-framework' << 'Cocoa' << '-framework' << 'IOKit' << '-framework' << 'CoreFoundation' << '-framework' << 'CoreVideo' << '-framework' << 'QuartzCore' + } else { + it.linker.args << '-lX11' + } + } + } + } + + apply from: 'publish.gradle' +} diff --git a/wpigui/publish.gradle b/wpigui/publish.gradle new file mode 100644 index 0000000000..3114118e04 --- /dev/null +++ b/wpigui/publish.gradle @@ -0,0 +1,71 @@ +apply plugin: 'maven-publish' + +def baseArtifactId = 'wpigui-cpp' +def artifactGroupId = 'edu.wpi.first.wpigui' +def zipBaseName = '_GROUP_edu_wpi_first_wpigui_ID_wpigui-cpp_CLS' + +def outputsFolder = file("$project.buildDir/outputs") + +task cppSourcesZip(type: Zip) { + destinationDirectory = outputsFolder + archiveBaseName = zipBaseName + classifier = "sources" + + from(licenseFile) { + into '/' + } + + from('src/main/native/cpp') { + into '/' + } + from('src/main/native/directx11') { + into '/' + } + from('src/main/native/metal') { + into '/' + } + from('src/main/native/opengl3') { + into '/' + } +} + +task cppHeadersZip(type: Zip) { + destinationDirectory = outputsFolder + archiveBaseName = zipBaseName + classifier = "headers" + + from(licenseFile) { + into '/' + } + + from('src/main/native/include') { + into '/' + } +} + +build.dependsOn cppHeadersZip +build.dependsOn cppSourcesZip + +addTaskToCopyAllOutputs(cppHeadersZip) +addTaskToCopyAllOutputs(cppSourcesZip) + +model { + publishing { + def wpiguiTaskList = createComponentZipTasks($.components, ['wpigui'], zipBaseName, Zip, project, includeStandardZipFormat) + + publications { + cpp(MavenPublication) { + wpiguiTaskList.each { + artifact it + } + + artifact cppHeadersZip + artifact cppSourcesZip + + artifactId = baseArtifactId + groupId artifactGroupId + version wpilibVersioning.version.get() + } + } + } +} diff --git a/wpigui/src/dev/native/cpp/main.cpp b/wpigui/src/dev/native/cpp/main.cpp new file mode 100644 index 0000000000..abd58a1675 --- /dev/null +++ b/wpigui/src/dev/native/cpp/main.cpp @@ -0,0 +1,14 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "wpigui.h" + +int main() { + wpi::gui::CreateContext(); + wpi::gui::Initialize("Hello World", 1024, 768); + wpi::gui::Main(); +} diff --git a/wpigui/src/main/native/cpp/wpigui.cpp b/wpigui/src/main/native/cpp/wpigui.cpp new file mode 100644 index 0000000000..eac88ac592 --- /dev/null +++ b/wpigui/src/main/native/cpp/wpigui.cpp @@ -0,0 +1,357 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "wpigui.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "wpigui_internal.h" + +using namespace wpi::gui; + +namespace wpi { + +Context* gui::gContext; + +static void ErrorCallback(int error, const char* description) { + std::fprintf(stderr, "GLFW Error %d: %s\n", error, description); +} + +static void WindowSizeCallback(GLFWwindow* window, int width, int height) { + if (!gContext->maximized) { + gContext->width = width; + gContext->height = height; + } +} + +static void WindowMaximizeCallback(GLFWwindow* window, int maximized) { + gContext->maximized = maximized; +} + +static void WindowPosCallback(GLFWwindow* window, int xpos, int ypos) { + if (!gContext->maximized) { + gContext->xPos = xpos; + gContext->yPos = ypos; + } +} + +static void* IniReadOpen(ImGuiContext* ctx, ImGuiSettingsHandler* handler, + const char* name) { + if (std::strcmp(name, "GLOBAL") != 0) return nullptr; + return static_cast(gContext); +} + +static void IniReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler, + void* entry, const char* lineStr) { + auto impl = static_cast(entry); + const char* value = std::strchr(lineStr, '='); + if (!value) return; + ++value; + int num = std::atoi(value); + if (std::strncmp(lineStr, "width=", 6) == 0) { + impl->width = num; + impl->loadedWidthHeight = true; + } else if (std::strncmp(lineStr, "height=", 7) == 0) { + impl->height = num; + impl->loadedWidthHeight = true; + } else if (std::strncmp(lineStr, "maximized=", 10) == 0) { + impl->maximized = num; + } else if (std::strncmp(lineStr, "xpos=", 5) == 0) { + impl->xPos = num; + } else if (std::strncmp(lineStr, "ypos=", 5) == 0) { + impl->yPos = num; + } else if (std::strncmp(lineStr, "userScale=", 10) == 0) { + impl->userScale = num; + } else if (std::strncmp(lineStr, "style=", 6) == 0) { + impl->style = num; + } +} + +static void IniWriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, + ImGuiTextBuffer* out_buf) { + if (!gContext) return; + out_buf->appendf( + "[MainWindow][GLOBAL]\nwidth=%d\nheight=%d\nmaximized=%d\n" + "xpos=%d\nypos=%d\nuserScale=%d\nstyle=%d\n\n", + gContext->width, gContext->height, gContext->maximized, gContext->xPos, + gContext->yPos, gContext->userScale, gContext->style); +} + +void gui::CreateContext() { + gContext = new Context; + AddFont("ProggyDotted", [](ImGuiIO& io, float size, const ImFontConfig* cfg) { + return ImGui::AddFontProggyDotted(io, size, cfg); + }); + PlatformCreateContext(); +} + +void gui::DestroyContext() { + PlatformDestroyContext(); + delete gContext; + gContext = nullptr; +} + +bool gui::Initialize(const char* title, int width, int height) { + gContext->title = title; + gContext->width = width; + gContext->height = height; + gContext->defaultWidth = width; + gContext->defaultHeight = height; + + // Setup window + glfwSetErrorCallback(ErrorCallback); + glfwInitHint(GLFW_JOYSTICK_HAT_BUTTONS, GLFW_FALSE); + PlatformGlfwInitHints(); + if (!glfwInit()) return false; + + PlatformGlfwWindowHints(); + + // Setup Dear ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImPlot::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + + // Hook ini handler to save settings + ImGuiSettingsHandler iniHandler; + iniHandler.TypeName = "MainWindow"; + iniHandler.TypeHash = ImHashStr(iniHandler.TypeName); + iniHandler.ReadOpenFn = IniReadOpen; + iniHandler.ReadLineFn = IniReadLine; + iniHandler.WriteAllFn = IniWriteAll; + ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler); + + for (auto&& initialize : gContext->initializers) { + if (initialize) initialize(); + } + + // Load INI file + ImGui::LoadIniSettingsFromDisk(io.IniFilename); + + // Set initial window settings + glfwWindowHint(GLFW_MAXIMIZED, gContext->maximized ? GLFW_TRUE : GLFW_FALSE); + + if (gContext->width == 0 || gContext->height == 0) { + gContext->width = gContext->defaultWidth; + gContext->height = gContext->defaultHeight; + gContext->loadedWidthHeight = false; + } + + float windowScale = 1.0; + if (!gContext->loadedWidthHeight) { + glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE); + // get the primary monitor work area to see if we have a reasonable initial + // window size; if not, maximize, and default scaling to smaller + if (GLFWmonitor* primary = glfwGetPrimaryMonitor()) { + int monWidth, monHeight; + glfwGetMonitorWorkarea(primary, nullptr, nullptr, &monWidth, &monHeight); + if (monWidth < gContext->width || monHeight < gContext->height) { + glfwWindowHint(GLFW_MAXIMIZED, GLFW_TRUE); + windowScale = (std::min)(monWidth * 1.0 / gContext->width, + monHeight * 1.0 / gContext->height); + } + } + } + if (gContext->xPos != -1 && gContext->yPos != -1) + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + + // Create window with graphics context + gContext->window = + glfwCreateWindow(gContext->width, gContext->height, + gContext->title.c_str(), nullptr, nullptr); + if (!gContext->window) return false; + + if (!gContext->loadedWidthHeight) { + if (windowScale == 1.0) + glfwGetWindowContentScale(gContext->window, &windowScale, nullptr); + // force user scale if window scale is smaller + if (windowScale <= 0.5) + gContext->userScale = 0; + else if (windowScale <= 0.75) + gContext->userScale = 1; + if (windowScale != 1.0) { + for (auto&& func : gContext->windowScalers) func(windowScale); + } + } + + // Update window settings + if (gContext->xPos != -1 && gContext->yPos != -1) { + glfwSetWindowPos(gContext->window, gContext->xPos, gContext->yPos); + glfwShowWindow(gContext->window); + } + + // Set window callbacks + glfwGetWindowSize(gContext->window, &gContext->width, &gContext->height); + glfwSetWindowSizeCallback(gContext->window, WindowSizeCallback); + glfwSetWindowMaximizeCallback(gContext->window, WindowMaximizeCallback); + glfwSetWindowPosCallback(gContext->window, WindowPosCallback); + + // Setup Dear ImGui style + SetStyle(static_cast