mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-21 01:01:43 +00:00
[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:
357
wpigui/src/main/native/cpp/wpigui.cpp
Normal file
357
wpigui/src/main/native/cpp/wpigui.cpp
Normal 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
|
||||
Reference in New Issue
Block a user