[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
This commit is contained in:
Peter Johnson
2020-08-25 23:52:09 -07:00
parent 148f43b4a5
commit b80fde4388
17 changed files with 1349 additions and 2 deletions

View File

@@ -26,6 +26,7 @@ includeOtherLibs {
^drake/
^hal/
^imgui
^implot
^mockdata/
^networktables/
^ntcore

View File

@@ -138,6 +138,7 @@ endif()
if (WITH_SIMULATION_MODULES AND NOT USE_EXTERNAL_HAL)
add_subdirectory(imgui)
add_subdirectory(wpigui)
add_subdirectory(simulation)
endif()

View File

@@ -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 "$<BUILD_INTERFACE:${imgui_srcdir}>" "$<BUILD_INTERFACE:${implot_srcdir}>" "$<BUILD_INTERFACE:${imgui_srcdir}/examples>" "$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>" "$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/stb-src>")

View File

@@ -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 ""

View File

@@ -20,6 +20,7 @@ include 'wpiutil'
include 'ntcore'
include 'hal'
include 'cscore'
include 'wpigui'
include 'wpimath'
include 'wpilibc'
include 'wpilibcExamples'

View File

@@ -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"
}
}
}

25
wpigui/.styleguide Normal file
View File

@@ -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
}

44
wpigui/CMakeLists.txt Normal file
View File

@@ -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
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/include>
$<INSTALL_INTERFACE:${include_dest}/wpigui>)
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})

125
wpigui/build.gradle Normal file
View File

@@ -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'
}

71
wpigui/publish.gradle Normal file
View File

@@ -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()
}
}
}
}

View File

@@ -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();
}

View File

@@ -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 <algorithm>
#include <cstdio>
#include <cstring>
#include <GLFW/glfw3.h>
#include <imgui.h>
#include <imgui_ProggyDotted.h>
#include <imgui_impl_glfw.h>
#include <imgui_internal.h>
#include <implot.h>
#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<SavedSettings*>(gContext);
}
static void IniReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
void* entry, const char* lineStr) {
auto impl = static_cast<SavedSettings*>(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<Style>(gContext->style));
// Load Fonts
// this range is based on 13px being the "nominal" 100% size and going from
// ~0.5x (7px) to ~2.0x (25px)
for (auto&& makeFont : gContext->makeFonts) {
if (makeFont.second) {
auto& font = gContext->fonts.emplace_back();
for (int i = 0; i < Font::kScaledLevels; ++i) {
float size = 7.0f + i * 3.0f;
ImFontConfig cfg;
std::snprintf(cfg.Name, sizeof(cfg.Name), "%s-%d", makeFont.first,
static_cast<int>(size));
font.scaled[i] = makeFont.second(io, size, &cfg);
}
}
}
if (!PlatformInitRenderer()) return false;
return true;
}
void gui::Main() {
// Main loop
while (!glfwWindowShouldClose(gContext->window) && !gContext->exit) {
// Poll and handle events (inputs, window resize, etc.)
glfwPollEvents();
PlatformRenderFrame();
}
// Cleanup
PlatformShutdown();
ImGui_ImplGlfw_Shutdown();
ImPlot::DestroyContext();
ImGui::DestroyContext();
glfwDestroyWindow(gContext->window);
glfwTerminate();
}
void gui::CommonRenderFrame() {
ImGui_ImplGlfw_NewFrame();
// Start the Dear ImGui frame
ImGui::NewFrame();
// Scale based on OS window content scaling
float windowScale = 1.0;
glfwGetWindowContentScale(gContext->window, &windowScale, nullptr);
// map to closest font size: 0 = 0.5x, 1 = 0.75x, 2 = 1.0x, 3 = 1.25x,
// 4 = 1.5x, 5 = 1.75x, 6 = 2x
gContext->fontScale = std::clamp(
gContext->userScale + static_cast<int>((windowScale - 1.0) * 4), 0,
Font::kScaledLevels - 1);
ImGui::GetIO().FontDefault = gContext->fonts[0].scaled[gContext->fontScale];
for (size_t i = 0; i < gContext->earlyExecutors.size(); ++i) {
auto& execute = gContext->earlyExecutors[i];
if (execute) execute();
}
for (size_t i = 0; i < gContext->lateExecutors.size(); ++i) {
auto& execute = gContext->lateExecutors[i];
if (execute) execute();
}
// Rendering
ImGui::Render();
}
void gui::Exit() { gContext->exit = true; }
void gui::AddInit(std::function<void()> initialize) {
if (initialize) gContext->initializers.emplace_back(std::move(initialize));
}
void gui::AddWindowScaler(std::function<void(float scale)> windowScaler) {
if (windowScaler)
gContext->windowScalers.emplace_back(std::move(windowScaler));
}
void gui::AddEarlyExecute(std::function<void()> execute) {
if (execute) gContext->earlyExecutors.emplace_back(std::move(execute));
}
void gui::AddLateExecute(std::function<void()> execute) {
if (execute) gContext->lateExecutors.emplace_back(std::move(execute));
}
GLFWwindow* gui::GetSystemWindow() { return gContext->window; }
int gui::AddFont(
const char* name,
std::function<ImFont*(ImGuiIO& io, float size, const ImFontConfig* cfg)>
makeFont) {
if (makeFont) gContext->makeFonts.emplace_back(name, std::move(makeFont));
return gContext->makeFonts.size() - 1;
}
ImFont* gui::GetFont(int font) {
return gContext->fonts[font].scaled[gContext->fontScale];
}
void gui::SetStyle(Style style) {
gContext->style = static_cast<int>(style);
switch (style) {
case kStyleClassic:
ImGui::StyleColorsClassic();
break;
case kStyleDark:
ImGui::StyleColorsDark();
break;
case kStyleLight:
ImGui::StyleColorsLight();
break;
}
}
void gui::SetClearColor(ImVec4 color) { gContext->clearColor = color; }
void gui::EmitViewMenu() {
if (ImGui::BeginMenu("View")) {
if (ImGui::BeginMenu("Style")) {
bool selected;
selected = gContext->style == kStyleClassic;
if (ImGui::MenuItem("Classic", nullptr, &selected, true))
SetStyle(kStyleClassic);
selected = gContext->style == kStyleDark;
if (ImGui::MenuItem("Dark", nullptr, &selected, true))
SetStyle(kStyleDark);
selected = gContext->style == kStyleLight;
if (ImGui::MenuItem("Light", nullptr, &selected, true))
SetStyle(kStyleLight);
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Zoom")) {
for (int i = 0; i < Font::kScaledLevels && (25 * (i + 2)) <= 200; ++i) {
char label[20];
std::snprintf(label, sizeof(label), "%d%%", 25 * (i + 2));
bool selected = gContext->userScale == i;
bool enabled = (gContext->fontScale - gContext->userScale + i) >= 0 &&
(gContext->fontScale - gContext->userScale + i) <
Font::kScaledLevels;
if (ImGui::MenuItem(label, nullptr, &selected, enabled))
gContext->userScale = i;
}
ImGui::EndMenu();
}
ImGui::EndMenu();
}
}
} // namespace wpi

View File

@@ -0,0 +1,203 @@
/*----------------------------------------------------------------------------*/
/* 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 <d3d11.h>
#define GLFW_INCLUDE_NONE
#define GLFW_EXPOSE_NATIVE_WIN32
#include <GLFW/glfw3.h>
#include <GLFW/glfw3native.h>
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_dx11.h>
#include <stb_image.h>
#include "wpigui.h"
#include "wpigui_internal.h"
using namespace wpi::gui;
namespace {
struct PlatformContext {
ID3D11Device* pd3dDevice = nullptr;
ID3D11DeviceContext* pd3dDeviceContext = nullptr;
IDXGISwapChain* pSwapChain = nullptr;
ID3D11RenderTargetView* mainRenderTargetView = nullptr;
};
} // namespace
static PlatformContext* gPlatformContext;
static void CreateRenderTarget() {
ID3D11Texture2D* pBackBuffer;
gPlatformContext->pSwapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer));
gPlatformContext->pd3dDevice->CreateRenderTargetView(
pBackBuffer, nullptr, &gPlatformContext->mainRenderTargetView);
pBackBuffer->Release();
}
static bool CreateDeviceD3D(HWND hWnd) {
// Setup swap chain
DXGI_SWAP_CHAIN_DESC sd;
ZeroMemory(&sd, sizeof(sd));
sd.BufferCount = 2;
sd.BufferDesc.Width = 0;
sd.BufferDesc.Height = 0;
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.OutputWindow = hWnd;
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
sd.Windowed = TRUE;
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
UINT createDeviceFlags = 0;
// createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
D3D_FEATURE_LEVEL featureLevel;
const D3D_FEATURE_LEVEL featureLevelArray[2] = {
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_0,
};
if (D3D11CreateDeviceAndSwapChain(
nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, createDeviceFlags,
featureLevelArray, 2, D3D11_SDK_VERSION, &sd,
&gPlatformContext->pSwapChain, &gPlatformContext->pd3dDevice,
&featureLevel, &gPlatformContext->pd3dDeviceContext) != S_OK)
return false;
CreateRenderTarget();
return true;
}
static void CleanupRenderTarget() {
if (gPlatformContext->mainRenderTargetView) {
gPlatformContext->mainRenderTargetView->Release();
gPlatformContext->mainRenderTargetView = nullptr;
}
}
static void CleanupDeviceD3D() {
CleanupRenderTarget();
if (gPlatformContext->pSwapChain) {
gPlatformContext->pSwapChain->Release();
gPlatformContext->pSwapChain = nullptr;
}
if (gPlatformContext->pd3dDeviceContext) {
gPlatformContext->pd3dDeviceContext->Release();
gPlatformContext->pd3dDeviceContext = nullptr;
}
if (gPlatformContext->pd3dDevice) {
gPlatformContext->pd3dDevice->Release();
gPlatformContext->pd3dDevice = nullptr;
}
}
namespace wpi {
void gui::PlatformCreateContext() { gPlatformContext = new PlatformContext; }
void gui::PlatformDestroyContext() {
CleanupDeviceD3D();
delete gPlatformContext;
gPlatformContext = nullptr;
}
void gui::PlatformGlfwInitHints() {}
void gui::PlatformGlfwWindowHints() {
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
}
bool gui::PlatformInitRenderer() {
// Initialize Direct3D
if (!CreateDeviceD3D(glfwGetWin32Window(gContext->window))) {
CleanupDeviceD3D();
return false;
}
ImGui_ImplGlfw_InitForOpenGL(gContext->window, true);
ImGui_ImplDX11_Init(gPlatformContext->pd3dDevice,
gPlatformContext->pd3dDeviceContext);
return true;
}
void gui::PlatformRenderFrame() {
ImGui_ImplDX11_NewFrame();
CommonRenderFrame();
gPlatformContext->pd3dDeviceContext->OMSetRenderTargets(
1, &gPlatformContext->mainRenderTargetView, nullptr);
gPlatformContext->pd3dDeviceContext->ClearRenderTargetView(
gPlatformContext->mainRenderTargetView,
reinterpret_cast<float*>(&gContext->clearColor));
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
gPlatformContext->pSwapChain->Present(1, 0); // Present with vsync
// gPlatformContext->pSwapChain->Present(0, 0); // Present without vsync
}
void gui::PlatformShutdown() { ImGui_ImplDX11_Shutdown(); }
bool gui::LoadTextureFromFile(const char* filename, ImTextureID* out_texture,
int* out_width, int* out_height) {
// Load from disk into a raw RGBA buffer
int width = 0;
int height = 0;
unsigned char* data = stbi_load(filename, &width, &height, nullptr, 4);
if (!data) return false;
// Create texture
D3D11_TEXTURE2D_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.Width = width;
desc.Height = height;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
desc.CPUAccessFlags = 0;
ID3D11Texture2D* pTexture = nullptr;
D3D11_SUBRESOURCE_DATA subResource;
subResource.pSysMem = data;
subResource.SysMemPitch = desc.Width * 4;
subResource.SysMemSlicePitch = 0;
gPlatformContext->pd3dDevice->CreateTexture2D(&desc, &subResource, &pTexture);
// Create texture view
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
ZeroMemory(&srvDesc, sizeof(srvDesc));
srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = desc.MipLevels;
srvDesc.Texture2D.MostDetailedMip = 0;
ID3D11ShaderResourceView* srv;
gPlatformContext->pd3dDevice->CreateShaderResourceView(pTexture, &srvDesc,
&srv);
pTexture->Release();
*out_texture = srv;
*out_width = width;
*out_height = height;
stbi_image_free(data);
return true;
}
void gui::DeleteTexture(ImTextureID texture) {
if (texture) static_cast<ID3D11ShaderResourceView*>(texture)->Release();
}
} // namespace wpi

View File

@@ -0,0 +1,151 @@
/*----------------------------------------------------------------------------*/
/* 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. */
/*----------------------------------------------------------------------------*/
#pragma once
#include <functional>
#include <imgui.h>
extern "C" struct GLFWwindow;
namespace wpi::gui {
/**
* Creates GUI context. Must be called prior to calling any other functions.
*/
void CreateContext();
/**
* Destroys GUI context.
*/
void DestroyContext();
/**
* Initializes the GUI.
*
* @param title main application window title
* @param width main application window width
* @param height main application window height
*/
bool Initialize(const char* title, int width, int height);
/**
* Runs main GUI loop. On some OS'es this must be called from the main thread.
* Does not return until Exit() is called.
*/
void Main();
/**
* Exits main GUI loop when current loop iteration finishes.
* Safe to call from any thread, including from within main GUI loop.
*/
void Exit();
/**
* Adds initializer to GUI. The passed function is called once, immediately
* after the GUI (both GLFW and Dear ImGui) are initialized in Initialize().
* To have any effect, must be called prior to Initialize().
*
* @param initialize initialization function
*/
void AddInit(std::function<void()> initialize);
/**
* Adds window scaler function. The passed function is called once during
* Initialize() if the window scale is not 1.0. To have any effect, must
* be called prior to Initialize().
*
* @param windowScaler window scaler function
*/
void AddWindowScaler(std::function<void(float scale)> windowScaler);
/**
* Adds per-frame executor to GUI. The passed function is called on each
* Dear ImGui frame prior to any of the late execute functions.
*
* @param execute frame execution function
*/
void AddEarlyExecute(std::function<void()> execute);
/**
* Adds per-frame executor to GUI. The passed function is called on each
* Dear ImGui frame after all of the early execute functions.
*
* @param execute frame execution function
*/
void AddLateExecute(std::function<void()> execute);
/**
* Gets GLFW window handle.
*/
GLFWwindow* GetSystemWindow();
/**
* Adds a font to the GUI. The passed function is called during
* initialization as many times as necessary to create a range of sizes.
*
* @param name font name
* @param makeFont font creation / loader function
* @return Font index for later use with GetFont()
*/
int AddFont(
const char* name,
std::function<ImFont*(ImGuiIO& io, float size, const ImFontConfig* cfg)>
makeFont);
/**
* Gets a font added with AddFont() with the appropriate font size for
* the current scaling of the GUI.
*
* @param font font index returned by AddFont()
* @return Font pointer
*/
ImFont* GetFont(int font);
enum Style { kStyleClassic = 0, kStyleDark, kStyleLight };
/**
* Sets the ImGui style. Using this function makes this setting persistent.
*
* @param style Style
*/
void SetStyle(Style style);
/**
* Sets the clear (background) color.
*
* @param color Color
*/
void SetClearColor(ImVec4 color);
/**
* Emits a View menu (e.g. for a main menu bar) that allows setting of
* style and zoom. Internally starts with ImGui::BeginMenu("View").
*/
void EmitViewMenu();
/**
* Loads a texture from a file.
*
* @param filename filename
* @param out_texture texture (output)
* @param out_width image width (output)
* @param out_height image height (output)
* @return True on success, false on failure.
*/
bool LoadTextureFromFile(const char* filename, ImTextureID* out_texture,
int* out_width, int* out_height);
/**
* Deletes a texture.
*
* @param texture texture
*/
void DeleteTexture(ImTextureID texture);
} // namespace wpi::gui

View File

@@ -0,0 +1,73 @@
/*----------------------------------------------------------------------------*/
/* 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. */
/*----------------------------------------------------------------------------*/
#pragma once
#include <atomic>
#include <functional>
#include <string>
#include <utility>
#include <vector>
#include <GLFW/glfw3.h>
#include <imgui.h>
namespace wpi::gui {
struct SavedSettings {
bool loadedWidthHeight = false;
int width;
int height;
int maximized = 0;
int xPos = -1;
int yPos = -1;
int userScale = 2;
int style = 0;
};
struct Font {
static constexpr int kScaledLevels = 9;
ImFont* scaled[kScaledLevels];
};
struct Context : public SavedSettings {
std::atomic_bool exit{false};
std::string title;
int defaultWidth;
int defaultHeight;
GLFWwindow* window = nullptr;
std::vector<std::function<void()>> initializers;
std::vector<std::function<void(float scale)>> windowScalers;
std::vector<std::pair<
const char*,
std::function<ImFont*(ImGuiIO& io, float size, const ImFontConfig* cfg)>>>
makeFonts;
ImVec4 clearColor = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
std::vector<std::function<void()>> earlyExecutors;
std::vector<std::function<void()>> lateExecutors;
int fontScale = 2; // updated by main loop
std::vector<Font> fonts;
};
extern Context* gContext;
void PlatformCreateContext();
void PlatformDestroyContext();
void PlatformGlfwInitHints();
void PlatformGlfwWindowHints();
bool PlatformInitRenderer();
void PlatformRenderFrame();
void PlatformShutdown();
void CommonRenderFrame();
} // namespace wpi::gui

View File

@@ -0,0 +1,140 @@
/*----------------------------------------------------------------------------*/
/* 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. */
/*----------------------------------------------------------------------------*/
#define GLFW_INCLUDE_NONE
#define GLFW_EXPOSE_NATIVE_COCOA
#include <GLFW/glfw3.h>
#include <GLFW/glfw3native.h>
#import <Metal/Metal.h>
#import <QuartzCore/QuartzCore.h>
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_metal.h>
#include <stb_image.h>
#include "wpigui.h"
#include "wpigui_internal.h"
using namespace wpi::gui;
namespace {
struct PlatformContext {
CAMetalLayer* layer;
MTLRenderPassDescriptor *renderPassDescriptor;
id <MTLCommandQueue> commandQueue;
};
} // namespace
static PlatformContext* gPlatformContext;
namespace wpi {
void gui::PlatformCreateContext() {
gPlatformContext = new PlatformContext;
}
void gui::PlatformDestroyContext() {
delete gPlatformContext;
gPlatformContext = nullptr;
}
void gui::PlatformGlfwInitHints() {}
void gui::PlatformGlfwWindowHints() {
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
}
bool gui::PlatformInitRenderer() {
id <MTLDevice> device = MTLCreateSystemDefaultDevice();
gPlatformContext->commandQueue = [device newCommandQueue];
ImGui_ImplGlfw_InitForOpenGL(gContext->window, true);
ImGui_ImplMetal_Init(device);
NSWindow *nswin = glfwGetCocoaWindow(gContext->window);
gPlatformContext->layer = [CAMetalLayer layer];
gPlatformContext->layer.device = device;
gPlatformContext->layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
nswin.contentView.layer = gPlatformContext->layer;
nswin.contentView.wantsLayer = YES;
gPlatformContext->renderPassDescriptor = [MTLRenderPassDescriptor new];
return true;
}
void gui::PlatformRenderFrame() {
@autoreleasepool
{
int width, height;
glfwGetFramebufferSize(gContext->window, &width, &height);
gPlatformContext->layer.drawableSize = CGSizeMake(width, height);
id<CAMetalDrawable> drawable = [gPlatformContext->layer nextDrawable];
id<MTLCommandBuffer> commandBuffer = [gPlatformContext->commandQueue commandBuffer];
auto renderPassDescriptor = gPlatformContext->renderPassDescriptor;
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(gContext->clearColor.x, gContext->clearColor.y, gContext->clearColor.z, gContext->clearColor.w);
renderPassDescriptor.colorAttachments[0].texture = drawable.texture;
renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
id <MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
[renderEncoder pushDebugGroup:@"WPI GUI"];
// Start the Dear ImGui frame
ImGui_ImplMetal_NewFrame(renderPassDescriptor);
CommonRenderFrame();
ImGui_ImplMetal_RenderDrawData(ImGui::GetDrawData(), commandBuffer, renderEncoder);
[renderEncoder popDebugGroup];
[renderEncoder endEncoding];
[commandBuffer presentDrawable:drawable];
[commandBuffer commit];
}
}
void gui::PlatformShutdown() {
ImGui_ImplMetal_Shutdown();
}
bool gui::LoadTextureFromFile(const char* filename, ImTextureID* out_texture,
int* out_width, int* out_height) {
// Load from file
int width = 0;
int height = 0;
unsigned char* data = stbi_load(filename, &width, &height, nullptr, 4);
if (!data) return false;
MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:width height:height mipmapped:NO];
textureDescriptor.usage = MTLTextureUsageShaderRead;
#if TARGET_OS_OSX
textureDescriptor.storageMode = MTLStorageModeManaged;
#else
textureDescriptor.storageMode = MTLStorageModeShared;
#endif
id <MTLTexture> texture = [gPlatformContext->layer.device newTextureWithDescriptor:textureDescriptor];
[texture replaceRegion:MTLRegionMake2D(0, 0, width, height) mipmapLevel:0 withBytes:data bytesPerRow:width * 4];
*out_texture = (__bridge_retained void *)texture;
*out_width = width;
*out_height = height;
stbi_image_free(data);
return true;
}
void gui::DeleteTexture(ImTextureID texture) {
if (!texture) return;
id <MTLTexture> mtlTexture = (__bridge_transfer id <MTLTexture>)texture;
(void)mtlTexture;
}
} // namespace wpi

View File

@@ -0,0 +1,132 @@
/*----------------------------------------------------------------------------*/
/* 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 <cstdio>
#include <GL/gl3w.h>
#include <GLFW/glfw3.h>
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>
#include <stb_image.h>
#include "wpigui.h"
#include "wpigui_internal.h"
using namespace wpi::gui;
namespace wpi {
void gui::PlatformCreateContext() {}
void gui::PlatformDestroyContext() {}
void gui::PlatformGlfwInitHints() {}
void gui::PlatformGlfwWindowHints() {
// Decide GL versions
#if __APPLE__
// GL 3.2 + GLSL 150
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac
glfwWindowHint(GLFW_COCOA_GRAPHICS_SWITCHING, GLFW_TRUE);
#else
// GL 3.0 + GLSL 130
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
// glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+
// glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // 3.0+
#endif
// enable 4xMSAA
glfwWindowHint(GLFW_SAMPLES, 4);
}
bool gui::PlatformInitRenderer() {
glfwMakeContextCurrent(gContext->window);
glfwSwapInterval(1); // Enable vsync
// Initialize OpenGL loader
if (gl3wInit() != 0) {
std::fprintf(stderr, "Failed to initialize OpenGL loader!\n");
return false;
}
// Turn on multisampling
glEnable(GL_MULTISAMPLE);
// Setup Platform/Renderer bindings
ImGui_ImplGlfw_InitForOpenGL(gContext->window, true);
// Decide GLSL versions
#if __APPLE__
const char* glsl_version = "#version 150";
#else
const char* glsl_version = "#version 130";
#endif
ImGui_ImplOpenGL3_Init(glsl_version);
return true;
}
void gui::PlatformRenderFrame() {
ImGui_ImplOpenGL3_NewFrame();
CommonRenderFrame();
int display_w, display_h;
glfwGetFramebufferSize(gContext->window, &display_w, &display_h);
glViewport(0, 0, display_w, display_h);
glClearColor(gContext->clearColor.x, gContext->clearColor.y,
gContext->clearColor.z, gContext->clearColor.w);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(gContext->window);
}
void gui::PlatformShutdown() { ImGui_ImplOpenGL3_Shutdown(); }
bool gui::LoadTextureFromFile(const char* filename, ImTextureID* out_texture,
int* out_width, int* out_height) {
// Load from file
int width = 0;
int height = 0;
unsigned char* data = stbi_load(filename, &width, &height, nullptr, 4);
if (!data) return false;
// Create a OpenGL texture identifier
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// Setup filtering parameters for display
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// Upload pixels into texture
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA,
GL_UNSIGNED_BYTE, data);
stbi_image_free(data);
*out_texture = reinterpret_cast<ImTextureID>(static_cast<uintptr_t>(texture));
if (out_width) *out_width = width;
if (out_height) *out_height = height;
return true;
}
void gui::DeleteTexture(ImTextureID texture) {
GLuint glTexture = static_cast<GLuint>(reinterpret_cast<uintptr_t>(texture));
if (glTexture != 0) glDeleteTextures(1, &glTexture);
}
} // namespace wpi