diff --git a/src/doompanning.cc b/src/doompanning.cc index 3ca24b0..3b5da1d 100644 --- a/src/doompanning.cc +++ b/src/doompanning.cc @@ -4,8 +4,6 @@ #include #include -#include -#include #include #include #include @@ -21,9 +19,10 @@ void dp_sdl_fatal(const char *const msg) struct DoomState { + bool alive = false; // FIXME for testing only }; -static_assert(std::is_trivial::value, "DoomState must be a trivial type"); +static_assert(std::is_trivially_copyable::value, "DoomState must be a trivially copyable type"); struct ControllerContext { @@ -33,9 +32,37 @@ struct ControllerContext SDL_Renderer *renderer; std::vector dooms; bool quit = false; + ExampleAppLog appLog; + int columns = 4; }; -void show_ui(ControllerContext *ctx) +struct ControllerActions +{ + int doomsToSpawn = 0; +}; + +void spawn_doom(ControllerContext &ctx) +{ + size_t nextId = ctx.dooms.size(); + + + + DoomState ds; + ds.alive = true; + ctx.dooms.emplace_back(ds); +} + +void perform_actions(ControllerContext &ctx, const ControllerActions &actions) +{ + if (actions.doomsToSpawn) + { + log_info("Spawning %d new dooms", actions.doomsToSpawn); + for (int i=0; iWorkPos.x + 666, main_viewport->WorkPos.y + 20), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver); + ctx.appLog.Draw("log"); + } + ImGui::SetNextWindowPos(ImVec2(main_viewport->WorkPos.x + 20, main_viewport->WorkPos.y + 20), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSize(ImVec2(550, 340), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(420, 340), ImGuiCond_FirstUseEver); ImGuiWindowFlags window_flags = ImGuiWindowFlags_MenuBar; + static std::array strbuf; auto &io = ImGui::GetIO(); - std::array windowTitle; - std::snprintf(windowTitle.data(), windowTitle.size(), - "doompanning - #dooms=%zu, %.2f ms/frame (%.1f fps)###doompanning", - 0ul, 1000.0f / io.Framerate, io.Framerate); + + aprintf(strbuf, "doompanning - #dooms=%zu, %.2f ms/frame (%.1f fps)###doompanning", + ctx.dooms.size(), 1000.0f / io.Framerate, io.Framerate); // Main body of the doompanning window starts here. - if (!ImGui::Begin(windowTitle.data(), nullptr, window_flags)) + if (!ImGui::Begin(strbuf.data(), nullptr, window_flags)) { // Early out if the window is collapsed, as an optimization. ImGui::End(); - return; + return {}; } - ImGui::PushItemWidth(ImGui::GetFontSize() * -12); - // Menu Bar if (ImGui::BeginMenuBar()) { if (ImGui::BeginMenu("Menu")) { - ImGui::MenuItem("Quit", "Ctrl+Q", &ctx->quit, true); + ImGui::MenuItem("Log Window", nullptr, &show_log_window); + ImGui::MenuItem("Quit", "Ctrl+Q", &ctx.quit, true); ImGui::EndMenu(); } @@ -90,15 +125,32 @@ void show_ui(ControllerContext *ctx) ImGui::EndMenuBar(); } + // Window contents + + ControllerActions result = {}; + static int doomsToSpawn = 1; + + ImGui::PushItemWidth(ImGui::GetFontSize() * -16); // affects stuff like slider widths + + ImGui::SliderInt("Layout columns##columns", &ctx.columns, 1, 32, "%d", ImGuiSliderFlags_AlwaysClamp); + + ImGui::SliderInt("##dooms", &doomsToSpawn, 1, 16, "%d", ImGuiSliderFlags_AlwaysClamp); + aprintf(strbuf, "Spawn %d more doom%s###spawnmore", doomsToSpawn, doomsToSpawn > 1 ? "s" : ""); + if (ImGui::SameLine(); ImGui::Button(strbuf.data())) + result.doomsToSpawn = doomsToSpawn; + ImGui::PopItemWidth(); ImGui::End(); + + return result; } -int doom_controller_loop(ControllerContext *ctx) +int doom_controller_loop(ControllerContext &ctx) { static constexpr ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); + ControllerActions actions = {}; - while (!ctx->quit) + while (!ctx.quit) { SDL_Event event; while (SDL_PollEvent(&event)) @@ -106,41 +158,64 @@ int doom_controller_loop(ControllerContext *ctx) ImGui_ImplSDL2_ProcessEvent(&event); if (event.type == SDL_QUIT) - ctx->quit = true; + ctx.quit = true; if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_CLOSE - && event.window.windowID == SDL_GetWindowID(ctx->window)) + && event.window.windowID == SDL_GetWindowID(ctx.window)) { - ctx->quit = true; + ctx.quit = true; } } + // Process input events not consumed by ImGui + if (auto &io = ImGui::GetIO(); + !io.WantCaptureKeyboard) + { + if (io.KeyCtrl && ImGui::IsKeyDown(ImGuiKey_Q)) + { + ctx.quit = true; + } + } + + perform_actions(ctx, actions); + // Start the Dear ImGui frame ImGui_ImplSDLRenderer_NewFrame(); ImGui_ImplSDL2_NewFrame(); ImGui::NewFrame(); - // Do ImGui things here - show_ui(ctx); + actions = run_ui(ctx); // Rendering ImGui::Render(); auto [r, g, b, a] = imvec4_to_rgba(clear_color); - SDL_SetRenderDrawColor(ctx->renderer, r, g, b, a); - SDL_RenderClear(ctx->renderer); + SDL_SetRenderDrawColor(ctx.renderer, r, g, b, a); + SDL_RenderClear(ctx.renderer); ImGui_ImplSDLRenderer_RenderDrawData(ImGui::GetDrawData()); - SDL_RenderPresent(ctx->renderer); + SDL_RenderPresent(ctx.renderer); } return 0; } +void log_to_context(log_Event *ev) +{ + auto ctx = reinterpret_cast(ev->udata); + ctx->appLog.AddLog(ev->fmt, ev->ap); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; +#ifndef NDEBUG + log_set_level(LOG_TRACE); +#else + log_set_level(LOG_DEBUG); +#endif + log_info("doompanning ctrl starting"); if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER)) @@ -150,7 +225,7 @@ int main(int argc, char *argv[]) SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1"); #endif - const auto windowFlags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; + const auto windowFlags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_OPENGL; auto window = SDL_CreateWindow("doompanning", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1280, 720, windowFlags); if (!window) dp_sdl_fatal("SDL_CreateWindow"); @@ -172,16 +247,20 @@ int main(int argc, char *argv[]) auto pubSock = make_ctrl_pub(CtrlUrl); auto subSock = make_ctrl_sub(DoomUrl); - ControllerContext ctx + ControllerContext ctx = { pubSock, subSock, window, renderer, - {} + {}, + {}, + {}, }; - int ret = doom_controller_loop(&ctx); + log_add_callback(log_to_context, &ctx, LOG_TRACE); + + int ret = doom_controller_loop(ctx); nng_close(pubSock); nng_close(subSock); diff --git a/src/dp_util.hpp b/src/dp_util.hpp index b544a81..4fdb6ff 100644 --- a/src/dp_util.hpp +++ b/src/dp_util.hpp @@ -1,8 +1,13 @@ #ifndef SRC_DP_UTIL_HPP #define SRC_DP_UTIL_HPP -#include +#include +#include +#include #include + +#include + #include "dp_types.h" inline std::tuple imvec4_to_rgba(const ImVec4 &color) @@ -16,4 +21,151 @@ inline std::tuple imvec4_to_rgba(const ImVec4 &color) }; } +template +[[maybe_unused]] const char *aprintf(std::array &buf, const char *fmt, ...) +{ + std::va_list args; + va_start(args, fmt); + std::vsnprintf(buf.data(), buf.size(), fmt, args); + va_end(args); + return buf.data(); +} + +// Usage: +// static ExampleAppLog my_log; +// my_log.AddLog("Hello %d world\n", 123); +// my_log.Draw("title"); +struct ExampleAppLog +{ + ImGuiTextBuffer Buf; + ImGuiTextFilter Filter; + ImVector LineOffsets; // Index to lines offset. We maintain this with AddLog() calls. + bool AutoScroll; // Keep scrolling if already at the bottom. + + ExampleAppLog() + { + AutoScroll = true; + Clear(); + } + + void Clear() + { + Buf.clear(); + LineOffsets.clear(); + LineOffsets.push_back(0); + } + + void AddLog(const char* fmt, ...) IM_FMTARGS(2) + { + int old_size = Buf.size(); + va_list args; + va_start(args, fmt); + Buf.appendfv(fmt, args); + va_end(args); + for (int new_size = Buf.size(); old_size < new_size; old_size++) + if (Buf[old_size] == '\n') + LineOffsets.push_back(old_size + 1); + } + + void AddLog(const char *fmt, va_list args) + { + int old_size = Buf.size(); + Buf.appendfv(fmt, args); + Buf.append("\n"); + for (int new_size = Buf.size(); old_size < new_size; old_size++) + if (Buf[old_size] == '\n') + LineOffsets.push_back(old_size + 1); + } + + void Draw(const char* title, bool* p_open = NULL) + { + if (!ImGui::Begin(title, p_open)) + { + ImGui::End(); + return; + } + + // Options menu + if (ImGui::BeginPopup("Options")) + { + ImGui::Checkbox("Auto-scroll", &AutoScroll); + ImGui::EndPopup(); + } + + // Main window + if (ImGui::Button("Options")) + ImGui::OpenPopup("Options"); + ImGui::SameLine(); + bool clear = ImGui::Button("Clear"); + ImGui::SameLine(); + bool copy = ImGui::Button("Copy"); + ImGui::SameLine(); + Filter.Draw("Filter", -100.0f); + + ImGui::Separator(); + + if (ImGui::BeginChild("scrolling", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar)) + { + if (clear) + Clear(); + if (copy) + ImGui::LogToClipboard(); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + const char* buf = Buf.begin(); + const char* buf_end = Buf.end(); + if (Filter.IsActive()) + { + // In this example we don't use the clipper when Filter is enabled. + // This is because we don't have random access to the result of our filter. + // A real application processing logs with ten of thousands of entries may want to store the result of + // search/filter.. especially if the filtering function is not trivial (e.g. reg-exp). + for (int line_no = 0; line_no < LineOffsets.Size; line_no++) + { + const char* line_start = buf + LineOffsets[line_no]; + const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end; + if (Filter.PassFilter(line_start, line_end)) + ImGui::TextUnformatted(line_start, line_end); + } + } + else + { + // The simplest and easy way to display the entire buffer: + // ImGui::TextUnformatted(buf_begin, buf_end); + // And it'll just work. TextUnformatted() has specialization for large blob of text and will fast-forward + // to skip non-visible lines. Here we instead demonstrate using the clipper to only process lines that are + // within the visible area. + // If you have tens of thousands of items and their processing cost is non-negligible, coarse clipping them + // on your side is recommended. Using ImGuiListClipper requires + // - A) random access into your data + // - B) items all being the same height, + // both of which we can handle since we have an array pointing to the beginning of each line of text. + // When using the filter (in the block of code above) we don't have random access into the data to display + // anymore, which is why we don't use the clipper. Storing or skimming through the search result would make + // it possible (and would be recommended if you want to search through tens of thousands of entries). + ImGuiListClipper clipper; + clipper.Begin(LineOffsets.Size); + while (clipper.Step()) + { + for (int line_no = clipper.DisplayStart; line_no < clipper.DisplayEnd; line_no++) + { + const char* line_start = buf + LineOffsets[line_no]; + const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end; + ImGui::TextUnformatted(line_start, line_end); + } + } + clipper.End(); + } + ImGui::PopStyleVar(); + + // Keep up at the bottom of the scroll region if we were already at the bottom at the beginning of the frame. + // Using a scrollbar or mouse-wheel will take away from the bottom edge. + if (AutoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) + ImGui::SetScrollHereY(1.0f); + } + ImGui::EndChild(); + ImGui::End(); + } +}; + #endif // SRC_DP_UTIL_HPP \ No newline at end of file