#include "ui.hpp" #include "ogl.hpp" #include "sandbox.hpp" #include #include // NOTE: also exists in imgui_internal.h // intentionally name it the same (including the fields), so if somebody else comes along and includes imgui_internal.h, they get a compile error and they can remove this struct ImRect { ImVec2 Min; ImVec2 Max; }; /// Helpers for displaying and coordinate space tracking of the sandbox image display. struct SandboxDisplay { float tile_size; int sandbox_width = 40; int sandbox_height = 100; ImVec2 size_screenspace; /* AVAILABLE AFTER CALLING show_image() */ ImVec2 origin; SandboxDisplay(float tile_size, const Sandbox& sb) : tile_size(tile_size) , sandbox_width{ sb.width } , sandbox_height{ sb.height } , size_screenspace(sandbox_width * tile_size, sandbox_height * tile_size) {} ImVec2 tr_sandbox2screen(Pt pt) const { return ImVec2(origin.x + tile_size * pt.x, origin.y - tile_size * pt.y); } // Assuming inclusive-inclusive rectangle ImRect tr_sandbox2screen(Rect r) const { return { ImVec2(origin.x + tile_size * r.bl.x, origin.y - tile_size * (r.tr.y + 1)), ImVec2(origin.x + tile_size * (r.tr.x + 1), origin.y - tile_size * r.bl.y), }; } // `pos` is +X right, +Y down from top left of the window's content region Pt tr_screen2sandbox(ImVec2 pos) const { // +X right, +Y up from `origin` ImVec2 translated(pos.x - origin.x, origin.y - pos.y); return Pt(translated.x / tile_size, translated.y / tile_size); } bool is_out_of_bounds(Pt pt) const { return pt.x < 0 || pt.x >= sandbox_width || pt.y < 0 || pt.y >= sandbox_height; } void show_image(ImTextureID texid) { auto img_topleft = ImGui::GetCursorScreenPos(); ImGui::InvisibleButton("sandbox", size_screenspace); const auto rect_min = ImGui::GetItemRectMin(); const auto rect_max = ImGui::GetItemRectMax(); ImGui::GetWindowDrawList()->AddImage(texid, rect_min, rect_max, ImVec2(0, 1), ImVec2(1, 0)); // bottom-left corner of the sandbox, in screen space origin = rect_min; origin.y += size_screenspace.y; } }; typedef int BrushType; enum BrushType_ { BrushType_Box, BrushType_Circle, }; static const char* TILE_NAMES = #define X(_0, str, _2) str "\0" #include "x/tile_types.inc" #undef X ; static const char* FLUID_NAMES = #define X(_0, str, _2) str "\0" #include "x/fluid_types.inc" #undef X ; static void edit_tile_palette(Tile& palette) { int tile = palette.so; ImGui::Combo("Tile", &tile, TILE_NAMES); palette.so = static_cast(tile); int fluid = palette.fl; ImGui::Combo("Fluid", &fluid, FLUID_NAMES); palette.fl = static_cast(fluid); int fmass = palette.fmass; ImGui::InputInt("Fluid mass", &fmass, 1); palette.fmass = std::clamp(fmass, 1, 255); } static void paint_sand(Sandbox& sb, const Tile& palette, Pt pt, BrushType type, int size) { switch (type) { case BrushType_Box: { // size: side length int y0 = pt.y - size / 2; int x0 = pt.x - size / 2; for (int y = y0; y < y0 + size; ++y) { for (int x = x0; x < x0 + size; ++x) { sb.set_sand(x, y, palette); } } } break; case BrushType_Circle: { // size: radius // TODO } break; default: { ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 0, 0, 255)); ImGui::Text("invalid brush type %d", type); ImGui::PopStyleVar(); } break; } } void ShowEverything() { ImGui::Begin("Options"); static float tile_size = 4.0f; static std::unique_ptr sandbox = std::make_unique(40, 100); static OglImage gl; ImGui::InputFloat("Tile size", &tile_size); { constexpr auto kModify = "Modify sandbox parameters"; static int new_width, new_height; ImGui::Text("size = (%d, %d)", sandbox->width, sandbox->height); if (ImGui::Button("Modify")) { ImGui::OpenPopup(kModify); new_width = sandbox->width; new_height = sandbox->height; } if (ImGui::BeginPopup(kModify)) { ImGui::InputInt("Sandbox width", &new_width); ImGui::InputInt("Sandbox height", &new_height); if (ImGui::Button("Confirm")) { sandbox = std::make_unique(new_width, new_height); ImGui::CloseCurrentPopup(); } ImGui::SameLine(); if (ImGui::Button("Cancel")) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } } static bool running = false; ImGui::Checkbox("Run", &running); static bool show_dirty_rect = true; ImGui::Checkbox("Show dirty rects", &show_dirty_rect); static bool hide_mouse = true; ImGui::Checkbox("Hide mouse", &hide_mouse); ImGui::Text("ncycle = %d", sandbox->ncycle); ImGui::SameLine(); bool step = ImGui::Button("Step"); static Tile palette{ .so = Tile::Ti_Sand }; static int brush_size = 1; static BrushType brush_type = BrushType_Box; if (ImGui::CollapsingHeader("Palette")) { edit_tile_palette(palette); } if (ImGui::CollapsingHeader("Brush")) { ImGui::SliderInt("Brush radius", &brush_size, 1, 20); ImGui::Combo("Brush type", &brush_type, "Box\0" "Circle"); } ImGui::End(); ImGui::Begin("Sandbox"); // Update texture to the current state of the the sandbox. // Do this every frame, because it's easier than trying to track when did the sandbox change, which may be caused by: // - Initialization // - Simulation step // - User interaction (painting, save/load, etc.) gl.upload(reinterpret_cast(sandbox->bitmap), sandbox->width, sandbox->height); SandboxDisplay dis(tile_size, *sandbox); dis.show_image(gl.as_imgui()); auto dl = ImGui::GetWindowDrawList(); if (show_dirty_rect) { auto [top_left, bottom_right] = dis.tr_sandbox2screen(sandbox->dirty_writeto); dl->AddRect(top_left, bottom_right, IM_COL32(255, 0, 0, 255)); } // Unfortunate technical limitation: update HAS to come after render // We don't get the screen pos of the ImGui::Image after having called it if (step || running) { sandbox->simulate_step(); } // HERE BE DRAGON: poor man's inner function do { // This checks for popup windows, drag&drop state, overlapping widgets, etc. // So it's necessary even if tr_screen2sandbox() can already detect if the mouse is out of bounds if (!ImGui::IsItemHovered()) break; if (hide_mouse) { ImGui::SetMouseCursor(ImGuiMouseCursor_None); } auto mouse_pt = dis.tr_screen2sandbox(ImGui::GetMousePos()); // This does trigger, if mouse is at very top of the image // There must be some discrepency between the math above, and ImGui's hover rect calculation. I'm not sure what's happening. if (dis.is_out_of_bounds(mouse_pt)) break; // Show the tile under mouse auto [top_left, bottom_right] = dis.tr_sandbox2screen(Rect(mouse_pt, mouse_pt)); dl->AddRect(top_left, bottom_right, IM_COL32_BLACK); if (ImGui::IsItemActive()) { paint_sand(*sandbox, palette, mouse_pt, brush_type, brush_size); } break; } while (false); ImGui::End(); ImGui::ShowDemoWindow(); }