/* =========================================================================== Copyright (C) 2022-2023 Gian 'myT' Schellenbaum This file is part of Challenge Quake 3 (CNQ3). Challenge Quake 3 is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Challenge Quake 3 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Challenge Quake 3. If not, see . =========================================================================== */ // Dear ImGui client integration and utility functions #include "client.h" #include "cl_imgui.h" #include "../imgui/font_proggy_clean.h" #include "../imgui/font_sweet16_mono.h" static int keyMap[256]; bool BeginTable(const char* name, int count) { ImGui::Text(name); const int flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp; return ImGui::BeginTable(name, count, flags); } void TableHeader(int count, ...) { va_list args; va_start(args, count); for(int i = 0; i < count; ++i) { const char* header = va_arg(args, const char*); ImGui::TableSetupColumn(header); } va_end(args); ImGui::TableHeadersRow(); } void TableRow(int count, ...) { ImGui::TableNextRow(); va_list args; va_start(args, count); for(int i = 0; i < count; ++i) { const char* item = va_arg(args, const char*); ImGui::TableSetColumnIndex(i); ImGui::Text(item); } va_end(args); } void TableRow2(const char* item0, bool item1) { TableRow(2, item0, item1 ? "YES" : "NO"); } void TableRow2(const char* item0, int item1) { TableRow(2, item0, va("%d", item1)); } void TableRow2(const char* item0, float item1, const char* format) { TableRow(2, item0, va(format, item1)); } bool IsShortcutPressed(ImGuiKey key) { return ImGui::IsKeyDown(ImGuiMod_Ctrl) && ImGui::IsKeyPressed(key, false); } void ToggleBooleanWithShortcut(bool& value, ImGuiKey key) { if(IsShortcutPressed(key)) { value = !value; } } struct MainMenuItem { GUI_MainMenu::Id menu; const char* name; const char* shortcut; bool* selected; bool enabled; }; struct MainMenu { MainMenuItem items[64]; int itemCount; int itemCountPerMenu[GUI_MainMenu::Count]; // effectively a histogram }; static MainMenu mm; #define M(Enum, Desc) Desc, static const char* mainMenuNames[GUI_MainMenu::Count + 1] = { MAIN_MENU_LIST(M) "" }; #undef M void GUI_AddMainMenuItem(GUI_MainMenu::Id menu, const char* name, const char* shortcut, bool* selected, bool enabled) { if(mm.itemCount >= ARRAY_LEN(mm.items) || (unsigned int)menu >= GUI_MainMenu::Count) { Q_assert(!"GUI_AddMainMenuItem: can't add menu entry"); return; } MainMenuItem& item = mm.items[mm.itemCount++]; item.menu = menu; item.name = name; item.shortcut = shortcut; item.selected = selected; item.enabled = enabled; mm.itemCountPerMenu[menu]++; } void GUI_DrawMainMenu() { if(ImGui::BeginMainMenuBar()) { for(int m = 0; m < GUI_MainMenu::Count; ++m) { if(mm.itemCountPerMenu[m] <= 0) { continue; } if(ImGui::BeginMenu(mainMenuNames[m])) { for(int i = 0; i < mm.itemCount; ++i) { const MainMenuItem& item = mm.items[i]; if(item.menu == m) { ImGui::MenuItem(item.name, item.shortcut, item.selected, item.enabled); } } ImGui::EndMenu(); } } ImGui::EndMainMenuBar(); } mm.itemCount = 0; memset(mm.itemCountPerMenu, 0, sizeof(mm.itemCountPerMenu)); } // applies a modified version of Jan Bielak's Deep Dark theme static void ImGUI_ApplyTheme() { ImVec4* colors = ImGui::GetStyle().Colors; colors[ImGuiCol_Text] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); colors[ImGuiCol_WindowBg] = ImVec4(0.10f, 0.10f, 0.10f, 1.00f); colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); colors[ImGuiCol_PopupBg] = ImVec4(0.19f, 0.19f, 0.19f, 0.92f); colors[ImGuiCol_Border] = ImVec4(0.19f, 0.19f, 0.19f, 0.29f); colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.24f); colors[ImGuiCol_FrameBg] = ImVec4(0.05f, 0.05f, 0.05f, 0.54f); colors[ImGuiCol_FrameBgHovered] = ImVec4(0.19f, 0.19f, 0.19f, 0.54f); colors[ImGuiCol_FrameBgActive] = ImVec4(0.20f, 0.22f, 0.23f, 1.00f); colors[ImGuiCol_TitleBg] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f); colors[ImGuiCol_TitleBgActive] = ImVec4(0.06f, 0.06f, 0.06f, 1.00f); colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f); colors[ImGuiCol_MenuBarBg] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); colors[ImGuiCol_ScrollbarBg] = ImVec4(0.05f, 0.05f, 0.05f, 0.54f); colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.34f, 0.34f, 0.34f, 0.54f); colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.40f, 0.40f, 0.40f, 0.54f); colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.56f, 0.56f, 0.56f, 0.54f); colors[ImGuiCol_CheckMark] = ImVec4(0.33f, 0.67f, 0.86f, 1.00f); colors[ImGuiCol_SliderGrab] = ImVec4(0.34f, 0.34f, 0.34f, 0.54f); colors[ImGuiCol_SliderGrabActive] = ImVec4(0.56f, 0.56f, 0.56f, 0.54f); colors[ImGuiCol_Button] = ImVec4(0.05f, 0.05f, 0.05f, 0.54f); colors[ImGuiCol_ButtonHovered] = ImVec4(0.19f, 0.19f, 0.19f, 0.54f); colors[ImGuiCol_ButtonActive] = ImVec4(0.20f, 0.22f, 0.23f, 1.00f); colors[ImGuiCol_Header] = ImVec4(0.00f, 0.00f, 0.00f, 0.52f); colors[ImGuiCol_HeaderHovered] = ImVec4(0.00f, 0.00f, 0.00f, 0.36f); colors[ImGuiCol_HeaderActive] = ImVec4(0.20f, 0.22f, 0.23f, 0.33f); colors[ImGuiCol_Separator] = ImVec4(0.28f, 0.28f, 0.28f, 0.29f); colors[ImGuiCol_SeparatorHovered] = ImVec4(0.44f, 0.44f, 0.44f, 0.29f); colors[ImGuiCol_SeparatorActive] = ImVec4(0.40f, 0.44f, 0.47f, 1.00f); colors[ImGuiCol_ResizeGrip] = ImVec4(0.28f, 0.28f, 0.28f, 0.29f); colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.44f, 0.44f, 0.44f, 0.29f); colors[ImGuiCol_ResizeGripActive] = ImVec4(0.40f, 0.44f, 0.47f, 1.00f); colors[ImGuiCol_Tab] = ImVec4(0.00f, 0.00f, 0.00f, 0.52f); colors[ImGuiCol_TabHovered] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); colors[ImGuiCol_TabActive] = ImVec4(0.20f, 0.20f, 0.20f, 0.36f); colors[ImGuiCol_TabUnfocused] = ImVec4(0.00f, 0.00f, 0.00f, 0.52f); colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); colors[ImGuiCol_PlotLines] = ImVec4(1.00f, 0.00f, 0.00f, 1.00f); colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.00f, 0.00f, 1.00f); colors[ImGuiCol_PlotHistogram] = ImVec4(1.00f, 0.00f, 0.00f, 1.00f); colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.00f, 0.00f, 1.00f); colors[ImGuiCol_TableHeaderBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.52f); colors[ImGuiCol_TableBorderStrong] = ImVec4(0.00f, 0.00f, 0.00f, 0.52f); colors[ImGuiCol_TableBorderLight] = ImVec4(0.28f, 0.28f, 0.28f, 0.29f); colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.06f); colors[ImGuiCol_TextSelectedBg] = ImVec4(0.20f, 0.22f, 0.23f, 1.00f); colors[ImGuiCol_DragDropTarget] = ImVec4(0.33f, 0.67f, 0.86f, 1.00f); colors[ImGuiCol_NavHighlight] = ImVec4(1.00f, 0.00f, 0.00f, 1.00f); colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 0.00f, 0.00f, 0.70f); colors[ImGuiCol_NavWindowingDimBg] = ImVec4(1.00f, 0.00f, 0.00f, 0.20f); colors[ImGuiCol_ModalWindowDimBg] = ImVec4(1.00f, 0.00f, 0.00f, 0.35f); const ImVec4 hover(0.26f, 0.59f, 0.98f, 0.4f); const ImVec4 active(0.2f, 0.41f, 0.68f, 0.5f); colors[ImGuiCol_HeaderHovered] = hover; colors[ImGuiCol_HeaderActive] = active; colors[ImGuiCol_TabHovered] = hover; colors[ImGuiCol_TabActive] = active; colors[ImGuiCol_NavHighlight] = hover; ImGuiStyle& style = ImGui::GetStyle(); style.WindowPadding = ImVec2(8.00f, 8.00f); style.FramePadding = ImVec2(5.00f, 2.00f); style.CellPadding = ImVec2(6.00f, 6.00f); style.ItemSpacing = ImVec2(6.00f, 6.00f); style.ItemInnerSpacing = ImVec2(6.00f, 6.00f); style.TouchExtraPadding = ImVec2(0.00f, 0.00f); style.IndentSpacing = 25; style.ScrollbarSize = 15; style.GrabMinSize = 10; style.WindowBorderSize = 1; style.ChildBorderSize = 1; style.PopupBorderSize = 1; style.FrameBorderSize = 1; style.TabBorderSize = 1; style.WindowRounding = 7; style.ChildRounding = 4; style.FrameRounding = 3; style.PopupRounding = 4; style.ScrollbarRounding = 9; style.GrabRounding = 3; style.LogSliderDeadzone = 4; style.TabRounding = 4; } static void ToggleGui_f() { const bool guiActive = Cvar_VariableIntegerValue("r_debugUI") != 0; const char* const newValue = guiActive ? "0" : "1"; Cvar_Set("r_debugUI", newValue); Cvar_Set("r_debugInput", newValue); } static void ToggleGuiInput_f() { Cvar_Set("r_debugInput", Cvar_VariableIntegerValue("r_debugInput") ? "0" : "1"); } static const cmdTableItem_t imgui_cmds[] = { { "togglegui", &ToggleGui_f, NULL, "toggles the CNQ3 GUI" }, { "toggleguiinput", &ToggleGuiInput_f, NULL, "toggles CNQ3 GUI input capture" } }; static const char* GetClipboardText(void*) { return Sys_GetClipboardData(); } static void SetClipboardText(void*, const char* text) { Sys_SetClipboardData(text); } static const ImWchar codepointRanges[] = { 32, 126, 0 }; static void AddProggyCleanFont() { ImFontConfig config; config.FontDataOwnedByAtlas = false; config.OversampleH = 1; config.OversampleV = 1; config.PixelSnapH = true; config.SizePixels = 13.0f; Q_strncpyz(config.Name, "Proggy Clean (13px)", sizeof(config.Name)); ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF( ProggyClean_compressed_data, ProggyClean_compressed_size, config.SizePixels, &config, codepointRanges); } static void AddSweet16MonoFont() { ImFontConfig config; config.FontDataOwnedByAtlas = false; config.OversampleH = 1; config.OversampleV = 1; config.PixelSnapH = true; config.SizePixels = 16.0f; Q_strncpyz(config.Name, "Sweet16 Mono (16px)", sizeof(config.Name)); ImGui::GetIO().Fonts->AddFontFromMemoryCompressedBase85TTF( Sweet16mono_compressed_data_base85, config.SizePixels, &config, codepointRanges); } static void AddCustomFont() { const char* const filePath = Cvar_VariableString("r_guiFontFile"); if(filePath == NULL || filePath[0] == '\0') { return; } const int height = Cvar_VariableIntegerValue("r_guiFontHeight"); const char* name = filePath; for(int i = strlen(filePath) - 2; i > 0; i--) { if(filePath[i] == '/' || filePath[i] == '\\') { name = filePath + i + 1; break; } } ImFontConfig config; config.FontDataOwnedByAtlas = false; config.OversampleH = 1; config.OversampleV = 1; config.PixelSnapH = true; config.SizePixels = height; Com_sprintf(config.Name, sizeof(config.Name), "%s (%dpx)", name, height); void* data = NULL; const int dataSize = FS_ReadFile(filePath, &data); if(data == NULL || dataSize <= 0) { Com_Printf("^3WARNING: failed to open font file: %s\n", filePath); return; } ImGui::GetIO().Fonts->AddFontFromMemoryTTF(data, dataSize, config.SizePixels, &config, codepointRanges); FS_FreeFile(data); } void CL_IMGUI_Init() { Cmd_RegisterArray(imgui_cmds, MODULE_CLIENT); ImGui::CreateContext(); ImPlot::CreateContext(); ImGuiIO& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; io.IniFilename = "cnq3/imgui.ini"; io.GetClipboardTextFn = &GetClipboardText; io.SetClipboardTextFn = &SetClipboardText; io.MouseDrawCursor = false; // just use the operating system's AddProggyCleanFont(); AddSweet16MonoFont(); AddCustomFont(); const int fontIndex = Cvar_VariableIntegerValue("r_guiFont"); if(fontIndex >= 0 && fontIndex < io.Fonts->Fonts.Size) { io.FontDefault = io.Fonts->Fonts[fontIndex]; } else { io.FontDefault = io.Fonts->Fonts[0]; Cvar_Set("r_guiFont", "0"); } ImGUI_ApplyTheme(); memset(keyMap, 0xFF, sizeof(keyMap)); keyMap[K_CTRL] = ImGuiMod_Ctrl; keyMap[K_ALT] = ImGuiMod_Alt; keyMap[K_SHIFT] = ImGuiMod_Shift; keyMap[K_TAB] = ImGuiKey_Tab; keyMap[K_LEFTARROW] = ImGuiKey_LeftArrow; keyMap[K_RIGHTARROW] = ImGuiKey_RightArrow; keyMap[K_UPARROW] = ImGuiKey_UpArrow; keyMap[K_DOWNARROW] = ImGuiKey_DownArrow; keyMap[K_PGUP] = ImGuiKey_PageUp; keyMap[K_PGDN] = ImGuiKey_PageDown; keyMap[K_HOME] = ImGuiKey_Home; keyMap[K_END] = ImGuiKey_End; keyMap[K_INS] = ImGuiKey_Insert; keyMap[K_DEL] = ImGuiKey_Delete; keyMap[K_BACKSPACE] = ImGuiKey_Backspace; keyMap[K_SPACE] = ImGuiKey_Space; keyMap[K_ENTER] = ImGuiKey_Enter; keyMap[K_ESCAPE] = ImGuiKey_Escape; keyMap[K_CAPSLOCK] = ImGuiKey_CapsLock; keyMap[K_PAUSE] = ImGuiKey_Pause; keyMap[K_BACKSLASH] = ImGuiKey_Backslash; keyMap[K_KP_INS] = ImGuiKey_Keypad0; keyMap[K_KP_END] = ImGuiKey_Keypad1; keyMap[K_KP_DOWNARROW] = ImGuiKey_Keypad2; keyMap[K_KP_PGDN] = ImGuiKey_Keypad3; keyMap[K_KP_LEFTARROW] = ImGuiKey_Keypad4; keyMap[K_KP_5] = ImGuiKey_Keypad5; keyMap[K_KP_RIGHTARROW] = ImGuiKey_Keypad6; keyMap[K_KP_HOME] = ImGuiKey_Keypad7; keyMap[K_KP_UPARROW] = ImGuiKey_Keypad8; keyMap[K_KP_PGUP] = ImGuiKey_Keypad9; keyMap[K_KP_ENTER] = ImGuiKey_KeyPadEnter; keyMap[K_KP_SLASH] = ImGuiKey_KeypadDivide; keyMap[K_KP_MINUS] = ImGuiKey_KeypadSubtract; keyMap[K_KP_PLUS] = ImGuiKey_KeypadAdd; keyMap[K_KP_STAR] = ImGuiKey_KeypadMultiply; keyMap[K_KP_EQUALS] = ImGuiKey_KeypadEqual; for(int i = 0; i < 26; ++i) { keyMap['a' + i] = ImGuiKey_A + i; } for(int i = 0; i < 10; ++i) { keyMap['0' + i] = ImGuiKey_0 + i; } for(int i = 0; i < 12; ++i) { keyMap[K_F1 + i] = ImGuiKey_F1 + i; } } void CL_IMGUI_Frame() { if(Cvar_VariableIntegerValue("r_debugInput")) { cls.keyCatchers |= KEYCATCH_IMGUI; } else { cls.keyCatchers &= ~KEYCATCH_IMGUI; } static int64_t prevUS = 0; const int64_t currUS = Sys_Microseconds(); const int64_t elapsedUS = currUS - prevUS; prevUS = currUS; int x, y; Sys_GetCursorPosition(&x, &y); re.ComputeCursorPosition(&x, &y); ImGuiIO& io = ImGui::GetIO(); io.DeltaTime = (float)((double)elapsedUS / 1000000.0); io.MousePos[0] = x; io.MousePos[1] = y; } void CL_IMGUI_MouseEvent(int dx, int dy) { ImGui::GetIO().AddMousePosEvent(dx, dy); } qbool CL_IMGUI_KeyEvent(int key, qbool down, const char* cmd) { if(down) { if(cmd != NULL) { const char* const prefix = "keycatchgui"; if(Q_stristr(cmd, prefix) == cmd) { Cbuf_AddText(cmd + strlen(prefix)); Cbuf_AddText("\n"); return qtrue; } } } if(cls.keyCatchers & KEYCATCH_IMGUI) { if(down && (key == '`' || key == '~')) { // continue displaying the GUI but route input to the console Cvar_Set("r_debugInput", "0"); return qfalse; } unsigned int imguiKey; ImGuiIO& io = ImGui::GetIO(); switch(key) { case K_MOUSE1: case K_MOUSE2: case K_MOUSE3: case K_MOUSE4: case K_MOUSE5: io.AddMouseButtonEvent(key - K_MOUSE1, !!down); break; case K_MWHEELDOWN: case K_MWHEELUP: io.AddMouseWheelEvent(0.0f, key == K_MWHEELDOWN ? -1.0f : 1.0f); break; default: imguiKey = (unsigned int)keyMap[key]; if(imguiKey != 0xFFFFFFFF) { io.AddKeyEvent((ImGuiKey)imguiKey, !!down); } break; } return qtrue; } return qfalse; } void CL_IMGUI_CharEvent(char key) { ImGui::GetIO().AddInputCharacter(key); } void CL_IMGUI_Shutdown() { ImPlot::DestroyContext(); ImGui::DestroyContext(); Cmd_UnregisterArray(imgui_cmds); } qbool CL_IMGUI_IsCustomFontLoaded(const char** debugName) { if(ImGui::GetIO().Fonts->Fonts.Size != 3) { return qfalse; } if(debugName != NULL) { *debugName = ImGui::GetIO().Fonts->Fonts[2]->GetDebugName(); } return qtrue; }