diff --git a/simulation/halsim_gui/src/main/native/cpp/DIOGui.cpp b/simulation/halsim_gui/src/main/native/cpp/DIOGui.cpp index e807fa26d2..93a07f9afb 100644 --- a/simulation/halsim_gui/src/main/native/cpp/DIOGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/DIOGui.cpp @@ -56,7 +56,7 @@ static void DisplayDIO() { } } - ImGui::PushItemWidth(100); + ImGui::PushItemWidth(ImGui::GetFontSize() * 8); for (int i = 0; i < numDIO; ++i) { if (HALSIM_GetDIOInitialized(i)) { hasAny = true; diff --git a/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp b/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp index 0189372609..3cca903b6d 100644 --- a/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp @@ -351,7 +351,7 @@ static void DisplayFMS() { static const char* stations[] = {"Red 1", "Red 2", "Red 3", "Blue 1", "Blue 2", "Blue 3"}; int allianceStationId = HALSIM_GetDriverStationAllianceStationId(); - ImGui::SetNextItemWidth(100); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); if (ImGui::Combo("Alliance Station", &allianceStationId, stations, 6)) HALSIM_SetDriverStationAllianceStationId( static_cast(allianceStationId)); @@ -362,7 +362,7 @@ static void DisplayFMS() { static double startMatchTime = 0.0; double matchTime = HALSIM_GetDriverStationMatchTime(); - ImGui::SetNextItemWidth(100); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); if (ImGui::InputDouble("Match Time", &matchTime, 0, 0, "%.1f", ImGuiInputTextFlags_EnterReturnsTrue)) { HALSIM_SetDriverStationMatchTime(matchTime); @@ -380,7 +380,7 @@ static void DisplayFMS() { // Game Specific Message static HAL_MatchInfo matchInfo; - ImGui::SetNextItemWidth(100); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); if (ImGui::InputText("Game Specific", reinterpret_cast(matchInfo.gameSpecificMessage), sizeof(matchInfo.gameSpecificMessage), @@ -420,7 +420,7 @@ static void DisplaySystemJoysticks() { static void DisplayJoysticks() { // imgui doesn't size columns properly with autoresize, so force it - ImGui::Dummy(ImVec2(14.0 * 9 * HAL_kMaxJoysticks, 0)); + ImGui::Dummy(ImVec2(ImGui::GetFontSize() * 10 * HAL_kMaxJoysticks, 0)); ImGui::Columns(HAL_kMaxJoysticks, "Joysticks", false); for (int i = 0; i < HAL_kMaxJoysticks; ++i) { diff --git a/simulation/halsim_gui/src/main/native/cpp/EncoderGui.cpp b/simulation/halsim_gui/src/main/native/cpp/EncoderGui.cpp index 741747d662..55650fb51d 100644 --- a/simulation/halsim_gui/src/main/native/cpp/EncoderGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/EncoderGui.cpp @@ -54,7 +54,7 @@ static void EncodersWriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, static void DisplayEncoders() { bool hasAny = false; static int numEncoder = HAL_GetNumEncoders(); - ImGui::PushItemWidth(100); + ImGui::PushItemWidth(ImGui::GetFontSize() * 8); for (int i = 0; i < numEncoder; ++i) { if (HALSIM_GetEncoderInitialized(i)) { hasAny = true; diff --git a/simulation/halsim_gui/src/main/native/cpp/ExtraGuiWidgets.cpp b/simulation/halsim_gui/src/main/native/cpp/ExtraGuiWidgets.cpp index 72cfbe633a..81a6855ab0 100644 --- a/simulation/halsim_gui/src/main/native/cpp/ExtraGuiWidgets.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/ExtraGuiWidgets.cpp @@ -12,6 +12,8 @@ namespace halsimgui { void DrawLEDs(int* values, int numValues, int cols, const ImU32* colors, float size, float spacing) { if (numValues == 0) return; + if (size == 0) size = ImGui::GetFontSize() / 2.0; + if (spacing == 0) spacing = ImGui::GetFontSize() / 3.0; ImDrawList* drawList = ImGui::GetWindowDrawList(); const ImVec2 p = ImGui::GetCursorScreenPos(); diff --git a/simulation/halsim_gui/src/main/native/cpp/HALSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/HALSimGui.cpp index e930edbe9c..d43446a550 100644 --- a/simulation/halsim_gui/src/main/native/cpp/HALSimGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/HALSimGui.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,12 @@ struct WindowInfo { static std::atomic_bool gExit{false}; static GLFWwindow* gWindow; +static bool gWindowLoadedWidthHeight = false; +static int gWindowWidth = 1280; +static int gWindowHeight = 720; +static int gWindowMaximized = 0; +static int gWindowXPos = -1; +static int gWindowYPos = -1; static std::vector> gInitializers; static std::vector> gExecutors; static std::vector gWindows; @@ -50,15 +57,39 @@ static wpi::StringMap gWindowMap; // index into gWindows static std::vector gSortedWindows; // index into gWindows static std::vector> gOptionMenus; static std::vector> gMenus; +static int gUserScale = 2; +static int gStyle = 0; +static constexpr int kScaledFontLevels = 9; +static ImFont* gScaledFont[kScaledFontLevels]; static void glfw_error_callback(int error, const char* description) { wpi::errs() << "GLFW Error " << error << ": " << description << '\n'; } +static void glfw_window_size_callback(GLFWwindow*, int width, int height) { + if (!gWindowMaximized) { + gWindowWidth = width; + gWindowHeight = height; + } +} + +static void glfw_window_maximize_callback(GLFWwindow* window, int maximized) { + gWindowMaximized = maximized; +} + +static void glfw_window_pos_callback(GLFWwindow* window, int xpos, int ypos) { + if (!gWindowMaximized) { + gWindowXPos = xpos; + gWindowYPos = ypos; + } +} + // read/write open state to ini file static void* SimWindowsReadOpen(ImGuiContext* ctx, ImGuiSettingsHandler* handler, const char* name) { + if (wpi::StringRef{name} == "GLOBAL") return &gWindow; + int index = gWindowMap.try_emplace(name, gWindows.size()).first->second; if (index == static_cast(gWindows.size())) { gSortedWindows.push_back(index); @@ -71,11 +102,35 @@ static void* SimWindowsReadOpen(ImGuiContext* ctx, static void SimWindowsReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler, void* entry, const char* lineStr) { - auto element = static_cast(entry); wpi::StringRef line{lineStr}; auto [name, value] = line.split('='); name = name.trim(); value = value.trim(); + + if (entry == &gWindow) { + int num; + if (value.getAsInteger(10, num)) return; + if (name == "width") { + gWindowWidth = num; + gWindowLoadedWidthHeight = true; + } else if (name == "height") { + gWindowHeight = num; + gWindowLoadedWidthHeight = true; + } else if (name == "maximized") { + gWindowMaximized = num; + } else if (name == "xpos") { + gWindowXPos = num; + } else if (name == "ypos") { + gWindowYPos = num; + } else if (name == "userScale") { + gUserScale = num; + } else if (name == "style") { + gStyle = num; + } + return; + } + + auto element = static_cast(entry); if (name == "visible") { int num; if (value.getAsInteger(10, num)) return; @@ -89,12 +144,31 @@ static void SimWindowsReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler, static void SimWindowsWriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* out_buf) { + out_buf->appendf( + "[SimWindow][GLOBAL]\nwidth=%d\nheight=%d\nmaximized=%d\n" + "xpos=%d\nypos=%d\nuserScale=%d\nstyle=%d\n\n", + gWindowWidth, gWindowHeight, gWindowMaximized, gWindowXPos, gWindowYPos, + gUserScale, gStyle); for (auto&& window : gWindows) out_buf->appendf("[SimWindow][%s]\nvisible=%d\nenabled=%d\n\n", window.name.c_str(), window.visible ? 1 : 0, window.enabled ? 1 : 0); } +static void UpdateStyle() { + switch (gStyle) { + case 0: + ImGui::StyleColorsClassic(); + break; + case 1: + ImGui::StyleColorsDark(); + break; + case 2: + ImGui::StyleColorsLight(); + break; + } +} + void HALSimGui::Add(std::function initialize) { if (initialize) gInitializers.emplace_back(std::move(initialize)); } @@ -184,6 +258,7 @@ bool HALSimGui::Initialize() { 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 const char* glsl_version = "#version 130"; @@ -193,10 +268,52 @@ bool HALSimGui::Initialize() { // glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // 3.0+ #endif + // Setup Dear ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + (void)io; + + // Hook ini handler to save settings + ImGuiSettingsHandler iniHandler; + iniHandler.TypeName = "SimWindow"; + iniHandler.TypeHash = ImHashStr(iniHandler.TypeName); + iniHandler.ReadOpenFn = SimWindowsReadOpen; + iniHandler.ReadLineFn = SimWindowsReadLine; + iniHandler.WriteAllFn = SimWindowsWriteAll; + ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler); + + for (auto&& initialize : gInitializers) { + if (initialize) initialize(); + } + + // Load INI file + ImGui::LoadIniSettingsFromDisk(io.IniFilename); + + // Set initial window settings + glfwWindowHint(GLFW_MAXIMIZED, gWindowMaximized ? GLFW_TRUE : GLFW_FALSE); + if (!gWindowLoadedWidthHeight) + glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE); + if (gWindowXPos != -1 && gWindowYPos != -1) + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + // Create window with graphics context - gWindow = - glfwCreateWindow(1280, 720, "Robot Simulation GUI", nullptr, nullptr); + gWindow = glfwCreateWindow(gWindowWidth, gWindowHeight, + "Robot Simulation GUI", nullptr, nullptr); if (!gWindow) return false; + + // Update window settings + if (gWindowXPos != -1 && gWindowYPos != -1) { + glfwSetWindowPos(gWindow, gWindowXPos, gWindowYPos); + glfwShowWindow(gWindow); + } + + // Set window callbacks + glfwGetWindowSize(gWindow, &gWindowWidth, &gWindowHeight); + glfwSetWindowSizeCallback(gWindow, glfw_window_size_callback); + glfwSetWindowMaximizeCallback(gWindow, glfw_window_maximize_callback); + glfwSetWindowPosCallback(gWindow, glfw_window_pos_callback); + glfwMakeContextCurrent(gWindow); glfwSwapInterval(1); // Enable vsync @@ -206,15 +323,8 @@ bool HALSimGui::Initialize() { return false; } - // Setup Dear ImGui context - IMGUI_CHECKVERSION(); - ImGui::CreateContext(); - ImGuiIO& io = ImGui::GetIO(); - (void)io; - // Setup Dear ImGui style - // ImGui::StyleColorsDark(); - ImGui::StyleColorsClassic(); + UpdateStyle(); // Setup Platform/Renderer bindings ImGui_ImplGlfw_InitForOpenGL(gWindow, true); @@ -244,17 +354,14 @@ bool HALSimGui::Initialize() { // io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\ArialUni.ttf", 18.0f, // NULL, io.Fonts->GetGlyphRangesJapanese()); IM_ASSERT(font != NULL); - // hook ini handler to save settings - ImGuiSettingsHandler iniHandler; - iniHandler.TypeName = "SimWindow"; - iniHandler.TypeHash = ImHashStr(iniHandler.TypeName); - iniHandler.ReadOpenFn = SimWindowsReadOpen; - iniHandler.ReadLineFn = SimWindowsReadLine; - iniHandler.WriteAllFn = SimWindowsWriteAll; - ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler); - - for (auto&& initialize : gInitializers) { - if (initialize) initialize(); + // this range is based on 13px being the "nominal" 100% size and going from + // ~0.5x (7px) to ~2.0x (25px) + for (int i = 0; i < kScaledFontLevels; ++i) { + float size = 7.0f + i * 3.0f; + ImFontConfig cfg; + std::snprintf(cfg.Name, sizeof(cfg.Name), "ProggyDotted-%d", + static_cast(size)); + gScaledFont[i] = ImGui::AddFontProggyDotted(io, size, &cfg); } return true; @@ -273,6 +380,16 @@ void HALSimGui::Main(void*) { ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); + // Scale based on OS window content scaling + float windowScale = 1.0; + glfwGetWindowContentScale(gWindow, &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 + int fontScale = + std::clamp(gUserScale + static_cast((windowScale - 1.0) * 4), 0, + kScaledFontLevels - 1); + ImGui::GetIO().FontDefault = gScaledFont[fontScale]; + for (auto&& execute : gExecutors) { if (execute) execute(); } @@ -287,6 +404,42 @@ void HALSimGui::Main(void*) { ImGui::EndMenu(); } + if (ImGui::BeginMenu("View")) { + if (ImGui::BeginMenu("Style")) { + bool selected; + selected = gStyle == 0; + if (ImGui::MenuItem("Classic", nullptr, &selected, true)) { + gStyle = 0; + UpdateStyle(); + } + selected = gStyle == 1; + if (ImGui::MenuItem("Dark", nullptr, &selected, true)) { + gStyle = 1; + UpdateStyle(); + } + selected = gStyle == 2; + if (ImGui::MenuItem("Light", nullptr, &selected, true)) { + gStyle = 2; + UpdateStyle(); + } + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Zoom")) { + for (int i = 0; i < kScaledFontLevels && (25 * (i + 2)) <= 200; ++i) { + char label[20]; + std::snprintf(label, sizeof(label), "%d%%", 25 * (i + 2)); + bool selected = gUserScale == i; + bool enabled = (fontScale - gUserScale + i) >= 0 && + (fontScale - gUserScale + i) < kScaledFontLevels; + if (ImGui::MenuItem(label, nullptr, &selected, enabled)) + gUserScale = i; + } + ImGui::EndMenu(); + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Window")) { for (auto&& windowIndex : gSortedWindows) { auto& window = gWindows[windowIndex]; diff --git a/simulation/halsim_gui/src/main/native/cpp/PDPGui.cpp b/simulation/halsim_gui/src/main/native/cpp/PDPGui.cpp index d447e0a7bf..038046e29a 100644 --- a/simulation/halsim_gui/src/main/native/cpp/PDPGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/PDPGui.cpp @@ -21,7 +21,7 @@ static void DisplayPDP() { bool hasAny = false; static int numPDP = HAL_GetNumPDPModules(); static int numChannels = HAL_GetNumPDPChannels(); - ImGui::PushItemWidth(150); + ImGui::PushItemWidth(ImGui::GetFontSize() * 13); for (int i = 0; i < numPDP; ++i) { if (HALSIM_GetPDPInitialized(i)) { hasAny = true; diff --git a/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.cpp b/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.cpp index da40ccb1db..e4d6650e72 100644 --- a/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.cpp @@ -18,7 +18,7 @@ static void DisplayRoboRio() { ImGui::Button("User Button"); HALSIM_SetRoboRioFPGAButton(0, ImGui::IsItemActive()); - ImGui::PushItemWidth(100); + ImGui::PushItemWidth(ImGui::GetFontSize() * 8); if (ImGui::CollapsingHeader("RoboRIO Input")) { { diff --git a/simulation/halsim_gui/src/main/native/include/ExtraGuiWidgets.h b/simulation/halsim_gui/src/main/native/include/ExtraGuiWidgets.h index a545fec0c8..11d83ba2d8 100644 --- a/simulation/halsim_gui/src/main/native/include/ExtraGuiWidgets.h +++ b/simulation/halsim_gui/src/main/native/include/ExtraGuiWidgets.h @@ -23,10 +23,12 @@ namespace halsimgui { * @param numValues size of values array * @param cols number of columns * @param colors colors array - * @param size size of each LED (both horizontal and vertical) - * @param spacing spacing between each LED (both horizontal and vertical) + * @param size size of each LED (both horizontal and vertical); + * if 0, defaults to 1/2 of font size + * @param spacing spacing between each LED (both horizontal and vertical); + * if 0, defaults to 1/3 of font size */ void DrawLEDs(int* values, int numValues, int cols, const ImU32* colors, - float size = 8.0f, float spacing = 6.0f); + float size = 0.0f, float spacing = 0.0f); } // namespace halsimgui