diff --git a/src/client/client_imgui.cpp b/src/client/client_imgui.cpp index 6d8b5a0..afdb393 100644 --- a/src/client/client_imgui.cpp +++ b/src/client/client_imgui.cpp @@ -5,10 +5,18 @@ #include "imgui.h" #include "imgui_impl_glfw.h" #include "imgui_impl_opengl3.h" +#include "ui/console.h" #include #include +#include "common/logging.hpp" + +// Globals +static Logger logger; + +// End globals + static void error_callback(int error, const char* description) { fprintf(stderr, "Error %d: %s\n", error, description); @@ -79,6 +87,8 @@ int main(int argc, char** argv) glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW); + static Console console; + bool show_console = false; while(!glfwWindowShouldClose(window)) { glfwPollEvents(); // Process input. @@ -111,26 +121,19 @@ int main(int argc, char** argv) ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); - { static float f = 0.0f; static int counter = 0; - ImGui::Begin("Hello, world!"); // Create a window called "Hello, world!" and append into it. - - ImGui::Text("This is some useful text."); // Display some text (you can use a format strings too) + ImGui::Begin("Misc"); // Create a window called "Hello, world!" and append into it. ImGui::SliderFloat("float", &f, 0.0f, 1.0f); // Edit 1 float using a slider from 0.0f to 1.0f ImGui::ColorEdit3("clear color", (float*)&clear_color); // Edit 3 floats representing a color - if (ImGui::Button("Button")) { // Buttons return true when clicked (most widgets return true when edited/activated) - counter++; - } - ImGui::SameLine(); - ImGui::Text("counter = %d", counter); - ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); ImGui::End(); + + console.Draw("Debug Console", &show_console); } // Finalise rendering diff --git a/src/client/ui/console.h b/src/client/ui/console.h new file mode 100644 index 0000000..f78febf --- /dev/null +++ b/src/client/ui/console.h @@ -0,0 +1,373 @@ +#include "imgui.h" +#include // toupper +#include + +struct Console +{ + char InputBuf[256]; + ImVector Items; + ImVector Commands; + ImVector History; + int HistoryPos; // -1: new line, 0..History.Size-1 browsing history. + ImGuiTextFilter Filter; + bool AutoScroll; + bool ScrollToBottom; + + Console() + { + ClearLog(); + memset(InputBuf, 0, sizeof(InputBuf)); + HistoryPos = -1; + Commands.push_back("HELP"); + Commands.push_back("HISTORY"); + Commands.push_back("CLEAR"); + Commands.push_back("CLASSIFY"); // "classify" is only here to provide an example of "C"+[tab] completing to "CL" and displaying matches. + Commands.push_back("CONNECT"); + AutoScroll = true; + ScrollToBottom = false; + AddLog("Welcome to Dear ImGui!"); + } + ~Console() + { + ClearLog(); + for (int i = 0; i < History.Size; i++) + free(History[i]); + } + + // Portable helpers + static int Stricmp(const char* str1, const char* str2) { int d; while ((d = toupper(*str2) - toupper(*str1)) == 0 && *str1) { str1++; str2++; } return d; } + static int Strnicmp(const char* str1, const char* str2, int n) { int d = 0; while (n > 0 && (d = toupper(*str2) - toupper(*str1)) == 0 && *str1) { str1++; str2++; n--; } return d; } + static char* Strdup(const char *str) { size_t len = strlen(str) + 1; void* buf = malloc(len); IM_ASSERT(buf); return (char*)memcpy(buf, (const void*)str, len); } + static void Strtrim(char* str) { char* str_end = str + strlen(str); while (str_end > str && str_end[-1] == ' ') str_end--; *str_end = 0; } + + void ClearLog() + { + for (int i = 0; i < Items.Size; i++) + free(Items[i]); + Items.clear(); + } + + void Connect(const char *command_line) { + // Parse + bool gotHost = false, gotPort = false, gotParam = false; + char default_host[] = "127.0.0.1"; + char default_port[] = "2800"; + char* item, *sub_item; + char* host, *port; + char command[256]; + memset(command, 0, sizeof(command)); + strncpy(command, command_line, 256); + item = strtok(command, " "); + while (item != NULL) { + if (Strnicmp(item, "connect", 7) == 0) { + item = strtok(NULL, " "); + continue; + } + if (!gotParam) { + gotParam = true; + break; + // Everything after the the first token following connect is ignored + } + } + if (gotParam) { + sub_item = strtok(item, ":"); + while (sub_item != NULL) { + AddLog("[debug] sub_item is '%s'", sub_item); + if (!gotHost) { + host = sub_item; + gotHost = true; + sub_item = strtok(NULL, ":"); + continue; + } + if (!gotPort) { + port = sub_item; + gotPort = true; + break; + } + } + } + if (!gotHost) { + host = default_host; + } + if (!gotPort) { + port = default_port; + } + AddLog("[debug] Parsed host '%s' [default:%d] and port '%s' [default:%d] from connect command.", + host, !gotHost, port, !gotPort); + // Raise an event + + } + + void AddLog(const char* fmt, ...) IM_FMTARGS(2) + { + // FIXME-OPT + char buf[1024]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, IM_ARRAYSIZE(buf), fmt, args); + buf[IM_ARRAYSIZE(buf)-1] = 0; + va_end(args); + Items.push_back(Strdup(buf)); + } + + void Draw(const char* title, bool* p_open) + { + ImGui::SetNextWindowSize(ImVec2(520,600), ImGuiCond_FirstUseEver); + if (!ImGui::Begin(title, p_open)) + { + ImGui::End(); + return; + } + + // As a specific feature guaranteed by the library, after calling Begin() the last Item represent the title bar. So e.g. IsItemHovered() will return true when hovering the title bar. + // Here we create a context menu only available from the title bar. + if (ImGui::BeginPopupContextItem()) + { + if (ImGui::MenuItem("Close Console")) + *p_open = false; + ImGui::EndPopup(); + } + + ImGui::TextWrapped("Enter 'HELP' for help, press TAB to use text completion."); + + // TODO: display items starting from the bottom + + if (ImGui::SmallButton("Add Dummy Text")) { AddLog("%d some text", Items.Size); AddLog("some more text"); AddLog("display very important message here!"); } ImGui::SameLine(); + if (ImGui::SmallButton("Add Dummy Error")) { AddLog("[error] something went wrong"); } ImGui::SameLine(); + if (ImGui::SmallButton("Clear")) { ClearLog(); } ImGui::SameLine(); + bool copy_to_clipboard = ImGui::SmallButton("Copy"); + //static float t = 0.0f; if (ImGui::GetTime() - t > 0.02f) { t = ImGui::GetTime(); AddLog("Spam %f", t); } + + ImGui::Separator(); + + // Options menu + if (ImGui::BeginPopup("Options")) + { + ImGui::Checkbox("Auto-scroll", &AutoScroll); + ImGui::EndPopup(); + } + + // Options, Filter + if (ImGui::Button("Options")) + ImGui::OpenPopup("Options"); + ImGui::SameLine(); + Filter.Draw("Filter (\"incl,-excl\") (\"error\")", 180); + ImGui::Separator(); + + const float footer_height_to_reserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); // 1 separator, 1 input text + ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footer_height_to_reserve), false, ImGuiWindowFlags_HorizontalScrollbar); // Leave room for 1 separator + 1 InputText + if (ImGui::BeginPopupContextWindow()) + { + if (ImGui::Selectable("Clear")) ClearLog(); + ImGui::EndPopup(); + } + + // Display every line as a separate entry so we can change their color or add custom widgets. If you only want raw text you can use ImGui::TextUnformatted(log.begin(), log.end()); + // NB- if you have thousands of entries this approach may be too inefficient and may require user-side clipping to only process visible items. + // You can seek and display only the lines that are visible using the ImGuiListClipper helper, if your elements are evenly spaced and you have cheap random access to the elements. + // To use the clipper we could replace the 'for (int i = 0; i < Items.Size; i++)' loop with: + // ImGuiListClipper clipper(Items.Size); + // while (clipper.Step()) + // for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + // However, note that you can not use this code as is if a filter is active because it breaks the 'cheap random-access' property. We would need random-access on the post-filtered list. + // A typical application wanting coarse clipping and filtering may want to pre-compute an array of indices that passed the filtering test, recomputing this array when user changes the filter, + // and appending newly elements as they are inserted. This is left as a task to the user until we can manage to improve this example code! + // If your items are of variable size you may want to implement code similar to what ImGuiListClipper does. Or split your data into fixed height items to allow random-seeking into your list. + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4,1)); // Tighten spacing + if (copy_to_clipboard) + ImGui::LogToClipboard(); + for (int i = 0; i < Items.Size; i++) + { + const char* item = Items[i]; + if (!Filter.PassFilter(item)) + continue; + + // Normally you would store more information in your item (e.g. make Items[] an array of structure, store color/type etc.) + bool pop_color = false; + if (strstr(item, "[error]")) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f)); pop_color = true; } + else if (strncmp(item, "# ", 2) == 0) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.6f, 1.0f)); pop_color = true; } + ImGui::TextUnformatted(item); + if (pop_color) + ImGui::PopStyleColor(); + } + if (copy_to_clipboard) + ImGui::LogFinish(); + + if (ScrollToBottom || (AutoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) + ImGui::SetScrollHereY(1.0f); + ScrollToBottom = false; + + ImGui::PopStyleVar(); + ImGui::EndChild(); + ImGui::Separator(); + + // Command-line + bool reclaim_focus = false; + if (ImGui::InputText("Input", InputBuf, IM_ARRAYSIZE(InputBuf), ImGuiInputTextFlags_EnterReturnsTrue|ImGuiInputTextFlags_CallbackCompletion|ImGuiInputTextFlags_CallbackHistory, &TextEditCallbackStub, (void*)this)) + { + char* s = InputBuf; + Strtrim(s); + if (s[0]) + ExecCommand(s); + strcpy(s, ""); + reclaim_focus = true; + } + + // Auto-focus on window apparition + ImGui::SetItemDefaultFocus(); + if (reclaim_focus) + ImGui::SetKeyboardFocusHere(-1); // Auto focus previous widget + + ImGui::End(); + } + + void ExecCommand(const char* command_line) + { + AddLog("# %s\n", command_line); + + // Insert into history. First find match and delete it so it can be pushed to the back. This isn't trying to be smart or optimal. + HistoryPos = -1; + for (int i = History.Size-1; i >= 0; i--) + if (Stricmp(History[i], command_line) == 0) + { + free(History[i]); + History.erase(History.begin() + i); + break; + } + History.push_back(Strdup(command_line)); + + // Process command + if (Stricmp(command_line, "CLEAR") == 0) + { + ClearLog(); + } + else if (Stricmp(command_line, "HELP") == 0) + { + AddLog("Commands:"); + for (int i = 0; i < Commands.Size; i++) + AddLog("- %s", Commands[i]); + } + else if (Stricmp(command_line, "HISTORY") == 0) + { + int first = History.Size - 10; + for (int i = first > 0 ? first : 0; i < History.Size; i++) + AddLog("%3d: %s\n", i, History[i]); + } + else if (Strnicmp(command_line, "CONNECT", 7) == 0) { + Connect(command_line); + } + else + { + AddLog("Unknown command: '%s'\n", command_line); + } + + // On commad input, we scroll to bottom even if AutoScroll==false + ScrollToBottom = true; + } + + static int TextEditCallbackStub(ImGuiInputTextCallbackData* data) // In C++11 you are better off using lambdas for this sort of forwarding callbacks + { + Console* console = (Console*)data->UserData; + return console->TextEditCallback(data); + } + + int TextEditCallback(ImGuiInputTextCallbackData* data) + { + //AddLog("cursor: %d, selection: %d-%d", data->CursorPos, data->SelectionStart, data->SelectionEnd); + switch (data->EventFlag) + { + case ImGuiInputTextFlags_CallbackCompletion: + { + // Example of TEXT COMPLETION + + // Locate beginning of current word + const char* word_end = data->Buf + data->CursorPos; + const char* word_start = word_end; + while (word_start > data->Buf) + { + const char c = word_start[-1]; + if (c == ' ' || c == '\t' || c == ',' || c == ';') + break; + word_start--; + } + + // Build a list of candidates + ImVector candidates; + for (int i = 0; i < Commands.Size; i++) + if (Strnicmp(Commands[i], word_start, (int)(word_end-word_start)) == 0) + candidates.push_back(Commands[i]); + + if (candidates.Size == 0) + { + // No match + AddLog("No match for \"%.*s\"!\n", (int)(word_end-word_start), word_start); + } + else if (candidates.Size == 1) + { + // Single match. Delete the beginning of the word and replace it entirely so we've got nice casing + data->DeleteChars((int)(word_start-data->Buf), (int)(word_end-word_start)); + data->InsertChars(data->CursorPos, candidates[0]); + data->InsertChars(data->CursorPos, " "); + } + else + { + // Multiple matches. Complete as much as we can, so inputing "C" will complete to "CL" and display "CLEAR" and "CLASSIFY" + int match_len = (int)(word_end - word_start); + for (;;) + { + int c = 0; + bool all_candidates_matches = true; + for (int i = 0; i < candidates.Size && all_candidates_matches; i++) + if (i == 0) + c = toupper(candidates[i][match_len]); + else if (c == 0 || c != toupper(candidates[i][match_len])) + all_candidates_matches = false; + if (!all_candidates_matches) + break; + match_len++; + } + + if (match_len > 0) + { + data->DeleteChars((int)(word_start - data->Buf), (int)(word_end-word_start)); + data->InsertChars(data->CursorPos, candidates[0], candidates[0] + match_len); + } + + // List matches + AddLog("Possible matches:\n"); + for (int i = 0; i < candidates.Size; i++) + AddLog("- %s\n", candidates[i]); + } + + break; + } + case ImGuiInputTextFlags_CallbackHistory: + { + // Example of HISTORY + const int prev_history_pos = HistoryPos; + if (data->EventKey == ImGuiKey_UpArrow) + { + if (HistoryPos == -1) + HistoryPos = History.Size - 1; + else if (HistoryPos > 0) + HistoryPos--; + } + else if (data->EventKey == ImGuiKey_DownArrow) + { + if (HistoryPos != -1) + if (++HistoryPos >= History.Size) + HistoryPos = -1; + } + + // A better implementation would preserve the data on the current input line along with cursor position. + if (prev_history_pos != HistoryPos) + { + const char* history_str = (HistoryPos >= 0) ? History[HistoryPos] : ""; + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, history_str); + } + } + } + return 0; + } +}; diff --git a/src/common/logging.hpp b/src/common/logging.hpp new file mode 100644 index 0000000..83ce699 --- /dev/null +++ b/src/common/logging.hpp @@ -0,0 +1,116 @@ +#ifndef _LOGGING_H_ +#define _LOGGING_H_ + +#include +#include +#include +#include + +enum LogLevel { + vvvdebug, + debug, + info, + warning, + error +}; + +class Logger { + + LogLevel currentLevel; + FILE **fds; + int fd_count = 0; + public: + Logger(LogLevel level = info) { + this->currentLevel = level; + this->attachFd(stderr); + } + + void attachFd(FILE *fd) { + int newSize = this->fd_count + 1; + FILE **newfds = (FILE **)calloc(newSize, sizeof(FILE*)); + if (this->fds != NULL) { + memcpy(newfds, this->fds,this->fd_count * sizeof(FILE*)); + free(this->fds); + this->fds = newfds; + } + else { + this->fds = newfds; + } + this->fds[newSize-1] = fd; + this->fd_count = newSize; + } + + void removeFd(FILE *fd) { + int newSize = this->fd_count - 1; + if (newSize <= 0) { + free(this->fds); + this->fds = NULL; + this->fd_count = 0; + return; + } + FILE **newfds = (FILE **) calloc(newSize, sizeof(FILE*)); + int new_i = 0; + for (int i = 0 ; i < this->fd_count ; i++) { + if (fd == this->fds[i]) { + continue; + } else { + newfds[new_i] = this->fds[i]; + new_i++; + } + } + free(this->fds); + this->fds = newfds; + this->fd_count = newSize; + } + + const char* mapLevel(LogLevel l) { + switch (l) { + case vvvdebug: + return "vvvdebug"; + break; + case debug: + return "debug"; + break; + case info: + return "info"; + break; + case warning: + return "warning"; + break; + case error: + return "error"; + break; + default: + return "unknown"; + break; + } + } + + void setLevel(LogLevel l) { + this->currentLevel = l; + } + + void log(LogLevel level, char* str) { + this->log(level, "%s", str); + } + + void log(LogLevel level, const char *fmt, ...) { + if (level < this->currentLevel) { + return; + } + if (this->fd_count <= 0) { + // No attached fds. + return; + } + char buf[1024]; + memset(buf, 0, 1024*sizeof(char)); + va_list args; + vsnprintf(buf, 1024, fmt, args); + for (int i = 0; i < this->fd_count ; i++) { + fprintf(this->fds[i], "[%s]\t%s\n", this->mapLevel(level), buf); + fflush(this->fds[i]); + } + va_end(args); + } +}; +#endif // _LOGGING_H_