#pragma once #include "IUI_Enums.h" #include #include #include #include #include #include #include #include #include #include namespace grove { /** * @brief ImGui implementation of IUI interface * * Provides full windowing system with docking, tabs, splits, and floating windows. * Handles hybrid percentage + pixel sizing with automatic constraint enforcement. */ class ImGuiUI : public IUI { private: // ======================================== // CORE STATE // ======================================== GLFWwindow* window = nullptr; bool initialized = false; bool should_close = false; int frame_count = 0; // Screen/parent sizes for percentage calculations ImVec2 screen_size = {1400, 900}; ImVec2 previous_screen_size = {0, 0}; // ======================================== // WINDOW MANAGEMENT // ======================================== struct WindowInfo { std::string id; std::string title; std::string parent; DockPosition dock_position = DockPosition::CENTER; bool is_open = true; bool is_floating = false; bool resizable = true; bool closeable = true; // Size system ImVec2 size = {400, 300}; ImVec2 min_size = {100, 100}; ImVec2 max_size = {2000, 1500}; ImVec2 position = {0, 0}; // Percentage tracking std::string size_width_percent = ""; std::string size_height_percent = ""; std::string min_width_percent = ""; std::string min_height_percent = ""; std::string max_width_percent = ""; std::string max_height_percent = ""; // Content DataType data_type = DataType::CUSTOM; json content_data; }; std::map windows; struct DockInfo { std::string id; DockType type = DockType::DOCK; DockPosition position = DockPosition::LEFT; std::string parent; bool collapsible = true; bool resizable = true; ImVec2 size = {300, 200}; ImVec2 min_size = {100, 100}; ImVec2 max_size = {1000, 800}; std::vector child_windows; }; std::map docks; // ======================================== // CALLBACKS // ======================================== std::map> request_callbacks; std::map> custom_request_callbacks; // ======================================== // MESSAGE SYSTEM // ======================================== struct LogMessage { EventLevel level; std::string message; std::chrono::steady_clock::time_point timestamp; }; std::vector log_messages; static constexpr size_t MAX_LOG_MESSAGES = 100; public: ImGuiUI() = default; ~ImGuiUI() override { shutdown(); } // ======================================== // LIFECYCLE IMPLEMENTATION // ======================================== void initialize(const json& config) override { if (initialized) return; // Initialize GLFW if (!glfwInit()) { throw std::runtime_error("Failed to initialize GLFW"); } // OpenGL 3.3 Core glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // Create window std::string title = config.value("title", "Warfactory ImGui UI"); auto window_size = config.value("window_size", json{{"width", 1400}, {"height", 900}}); if (window_size.is_object()) { screen_size.x = window_size.value("width", 1400); screen_size.y = window_size.value("height", 900); } else { screen_size.x = 1400; screen_size.y = 900; } window = glfwCreateWindow( static_cast(screen_size.x), static_cast(screen_size.y), title.c_str(), nullptr, nullptr ); if (!window) { glfwTerminate(); throw std::runtime_error("Failed to create GLFW window"); } glfwMakeContextCurrent(window); glfwSwapInterval(1); // VSync // Initialize ImGui IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Note: Docking features depend on ImGui docking branch // Using manual docking simulation for compatibility // Basic style setup ImGui::StyleColorsDark(); // Platform/Renderer backends ImGui_ImplGlfw_InitForOpenGL(window, true); ImGui_ImplOpenGL3_Init("#version 330 core"); initialized = true; } bool update() override { if (!initialized || !window) return false; if (glfwWindowShouldClose(window)) { should_close = true; return false; } // Update screen size for percentage calculations int fb_width, fb_height; glfwGetFramebufferSize(window, &fb_width, &fb_height); ImVec2 new_screen_size = {static_cast(fb_width), static_cast(fb_height)}; // Detect screen size changes and recalculate if needed if (new_screen_size.x != previous_screen_size.x || new_screen_size.y != previous_screen_size.y) { if (frame_count > 0) { // Skip first frame (initialization) debug("🔄 Screen size changed: " + std::to_string((int)new_screen_size.x) + "x" + std::to_string((int)new_screen_size.y)); recalculateAllSizes(); } previous_screen_size = screen_size; } screen_size = new_screen_size; frame_count++; // Poll events glfwPollEvents(); // Start ImGui frame ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); // Render all windows renderAllWindows(); // Render ImGui ImGui::Render(); // OpenGL rendering glViewport(0, 0, static_cast(screen_size.x), static_cast(screen_size.y)); glClearColor(0.45f, 0.55f, 0.60f, 1.00f); glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); glfwSwapBuffers(window); return !should_close; } void shutdown() override { if (!initialized) return; if (window) { ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); glfwDestroyWindow(window); glfwTerminate(); window = nullptr; } initialized = false; } private: // ======================================== // SIZE CALCULATION HELPERS // ======================================== /** * @brief Parse size value - handles both pixels and percentages */ float parseSize(const json& size_value, float parent_size, float default_size) { try { if (size_value.is_number()) { return size_value.get(); } if (size_value.is_string()) { std::string size_str = size_value.get(); if (!size_str.empty() && size_str.back() == '%') { float percent = std::stof(size_str.substr(0, size_str.length() - 1)); return (percent / 100.0f) * parent_size; } else { // String but not percentage - try to parse as number return std::stof(size_str); } } } catch (...) { // Any JSON or parsing error - return default } // Neither number nor string or error - return default return default_size; } /** * @brief Calculate effective size with hybrid constraints */ ImVec2 calculateEffectiveSize(const WindowInfo& win, ImVec2 parent_size) { // Use already parsed sizes (converted in showData) float target_width = win.size.x; float target_height = win.size.y; // Calculate constraint bounds float min_width = win.min_size.x; float min_height = win.min_size.y; float max_width = win.max_size.x; float max_height = win.max_size.y; // Apply constraints (clamp) float final_width = std::max(min_width, std::min(target_width, max_width)); float final_height = std::max(min_height, std::min(target_height, max_height)); return {final_width, final_height}; } /** * @brief Calculate window position based on docking */ ImVec2 calculateDockedPosition(const WindowInfo& win, ImVec2 size) { if (win.parent.empty() || win.is_floating) { // For windows without parent, use explicit position or calculate smart default debug("🔍 Checking position for '" + win.id + "': pos=" + std::to_string(win.position.x) + "," + std::to_string(win.position.y) + " floating=" + (win.is_floating ? "true" : "false")); // Only use explicit position if it was actually set by user (not just default values) if (win.position.x > 10 && win.position.y > 10) { debug("📌 Using explicit position for '" + win.id + "'"); return win.position; // Use explicit position } else { // Simple approach: use actual window sizes from economy_main window float left_edge_end = 252; // Real end of economy_main (we know it's 252px wide) float top_edge_end = 88; // Real end of toolbar + margin // Find the right sidebar start by looking for info_panel_main float right_edge_start = 1050; // We know info_panel starts at 1050px debug("🔧 Simple positioning for window '" + win.id + "': left_end=" + std::to_string(left_edge_end) + "px, right_start=" + std::to_string(right_edge_start) + "px, top_end=" + std::to_string(top_edge_end) + "px"); // Position directly against the real edge of existing windows float x = left_edge_end; // Directly touching end of left sidebar (252px) float y = top_edge_end; // Directly below toolbar (88px) // If window would overlap with right sidebar, push it left to touch right edge if (x + size.x > right_edge_start) { x = right_edge_start - size.x; // Touch right sidebar windows } debug("🎯 Calculated position for '" + win.id + "': " + std::to_string(x) + "," + std::to_string(y) + " (touching real window edges)"); return {x, y}; } } // Find parent dock auto dock_it = docks.find(win.parent); if (dock_it == docks.end()) { return {0, 0}; // Parent dock not found } const DockInfo& dock = dock_it->second; // Calculate dock area based on position switch (dock.position) { case DockPosition::LEFT: return {0, 80}; // Left edge but below toolbar (72px + margin) case DockPosition::RIGHT: return {screen_size.x - dock.size.x, 80}; // Right edge but below toolbar case DockPosition::TOP: // Top edge - if dock spans full width, start at 0, else offset if (dock.size.x >= screen_size.x * 0.9f) { return {0, 0}; // Full width toolbar starts at screen edge } else { return {280, 0}; // Partial width toolbar starts after sidebar } case DockPosition::BOTTOM: return {0, screen_size.y - dock.size.y}; // Bottom edge case DockPosition::CENTER: default: return {screen_size.x * 0.5f - size.x * 0.5f, screen_size.y * 0.5f - size.y * 0.5f}; // Center } } // ======================================== // RECALCULATION METHODS // ======================================== void recalculateAllSizes() { // Recalculate dock sizes for (auto& [dock_id, dock] : docks) { // Recalculate dock size if it uses percentages recalculateDockSize(dock); } // Recalculate window sizes for (auto& [window_id, win] : windows) { recalculateWindowSize(win); } } void recalculateDockSize(DockInfo& dock) { // Re-parse dock size with new screen size // This would need the original JSON config, for now just log debug("📐 Recalculating dock: " + dock.id); // TODO: Store original percentage strings to recalculate properly } void recalculateWindowSize(WindowInfo& win) { // Re-parse window size with new screen/parent sizes debug("📐 Recalculating window: " + win.id); // Recalculate width if percentage if (!win.size_width_percent.empty()) { float parent_width = screen_size.x; if (!win.parent.empty() && docks.find(win.parent) != docks.end()) { parent_width = docks[win.parent].size.x; } win.size.x = parseSize(win.size_width_percent, parent_width, 400); } // Recalculate height if percentage if (!win.size_height_percent.empty()) { float parent_height = screen_size.y; if (!win.parent.empty() && docks.find(win.parent) != docks.end()) { parent_height = docks[win.parent].size.y; } win.size.y = parseSize(win.size_height_percent, parent_height, 300); } } // ======================================== // RENDERING IMPLEMENTATION // ======================================== void renderAllWindows() { // Log screen size for debugging (only first frame to avoid spam) if (frame_count == 1) { debug("🖥️ Screen Size: " + std::to_string((int)screen_size.x) + "x" + std::to_string((int)screen_size.y) + "px"); info("🏗️ Manual docking system active (simulated docking layout)"); } for (auto& [window_id, win] : windows) { if (!win.is_open) continue; if (frame_count <= 5) { // Log first 5 frames for each window debug("🪟 Window: " + window_id + " (" + win.title + ")"); debug(" 📐 Target Size: " + std::to_string((int)win.size.x) + "x" + std::to_string((int)win.size.y) + "px"); debug(" 📏 Size %: width='" + win.size_width_percent + "' height='" + win.size_height_percent + "'"); debug(" ⚖️ Constraints: min=" + std::to_string((int)win.min_size.x) + "x" + std::to_string((int)win.min_size.y) + " max=" + std::to_string((int)win.max_size.x) + "x" + std::to_string((int)win.max_size.y)); debug(" 🔗 Docking: parent='" + win.parent + "' position=" + std::to_string((int)win.dock_position)); } // Calculate effective size with constraints ImVec2 effective_size = calculateEffectiveSize(win, screen_size); if (frame_count <= 5) { debug(" ✅ Effective Size: " + std::to_string((int)effective_size.x) + "x" + std::to_string((int)effective_size.y) + "px"); } // Set window constraints ImGui::SetNextWindowSizeConstraints(win.min_size, win.max_size); // Set window size if (win.is_floating) { // For floating windows, force initial size and position ImGuiCond size_condition = (frame_count <= 3) ? ImGuiCond_Always : ImGuiCond_FirstUseEver; ImGui::SetNextWindowSize(effective_size, size_condition); // Calculate smart position that avoids dock overlaps ImVec2 floating_position = calculateDockedPosition(win, effective_size); ImGuiCond position_condition = (frame_count <= 3) ? ImGuiCond_Always : ImGuiCond_FirstUseEver; ImGui::SetNextWindowPos(floating_position, position_condition); if (frame_count <= 5) { debug(" 🎈 Floating Position: " + std::to_string((int)floating_position.x) + "," + std::to_string((int)floating_position.y)); } } else { // For docked windows, calculate position and force it during initial frames ImVec2 dock_position = calculateDockedPosition(win, effective_size); ImGuiCond condition = (frame_count <= 3) ? ImGuiCond_Always : ImGuiCond_FirstUseEver; ImGui::SetNextWindowSize(effective_size, condition); ImGui::SetNextWindowPos(dock_position, condition); if (frame_count <= 5) { debug(" 📍 Docked Position: " + std::to_string((int)dock_position.x) + "," + std::to_string((int)dock_position.y)); } } // Window flags ImGuiWindowFlags flags = ImGuiWindowFlags_None; if (!win.resizable) flags |= ImGuiWindowFlags_NoResize; // Render window if (ImGui::Begin(win.title.c_str(), win.closeable ? &win.is_open : nullptr, flags)) { // Log actual ImGui window size after rendering (first 5 frames only) if (frame_count <= 5) { ImVec2 current_size = ImGui::GetWindowSize(); ImVec2 current_pos = ImGui::GetWindowPos(); debug(" 🎯 ImGui Actual: pos=" + std::to_string((int)current_pos.x) + "," + std::to_string((int)current_pos.y) + " size=" + std::to_string((int)current_size.x) + "x" + std::to_string((int)current_size.y) + "px"); } renderWindowContent(win); } ImGui::End(); } } void renderWindowContent(const WindowInfo& win) { switch (win.data_type) { case DataType::ECONOMY: renderEconomyContent(win.content_data); break; case DataType::MAP: renderMapContent(win.content_data); break; case DataType::INVENTORY: renderInventoryContent(win.content_data); break; case DataType::CONSOLE: renderConsoleContent(win.content_data); break; case DataType::PERFORMANCE: renderPerformanceContent(win.content_data); break; case DataType::COMPANIES: renderCompaniesContent(win.content_data); break; case DataType::ALERTS: renderAlertsContent(win.content_data); break; case DataType::SETTINGS: renderSettingsContent(win.content_data); break; default: renderGenericContent(win.content_data); break; } } public: // ======================================== // IUI INTERFACE IMPLEMENTATION - DATA DISPLAY // ======================================== void showData(DataType dataType, const json& data) override { // Extract window configuration json window_config = data.value("window", json{}); json content = data.value("content", data); // Generate ID if not provided std::string window_id = window_config.value("id", "window_" + std::to_string(windows.size())); // Create or update window info WindowInfo& win = windows[window_id]; win.id = window_id; win.title = window_config.value("title", toString(dataType)); win.data_type = dataType; win.content_data = content; win.is_open = true; // Parse parent first (needed for size calculations) win.parent = window_config.value("parent", ""); // Parse size configuration with percentage support if (window_config.contains("size")) { auto size_config = window_config["size"]; if (size_config.is_object()) { if (size_config.contains("width")) { auto width_val = size_config["width"]; if (width_val.is_string()) { win.size_width_percent = width_val.get(); debug("🔧 Processing width percentage '" + win.size_width_percent + "' for window '" + win.id + "' with parent='" + win.parent + "'"); // Calculate parent size for percentage - use dock size if docked float parent_width = screen_size.x; if (!win.parent.empty() && docks.find(win.parent) != docks.end()) { parent_width = docks[win.parent].size.x; debug("🔍 Found parent dock '" + win.parent + "' with width=" + std::to_string((int)parent_width) + "px"); } else if (!win.parent.empty()) { debug("❌ Parent dock '" + win.parent + "' not found! Using screen width."); } win.size.x = parseSize(width_val, parent_width, 400); } else if (width_val.is_number()) { win.size.x = width_val.get(); } else { win.size.x = 400; // Default fallback } } if (size_config.contains("height")) { auto height_val = size_config["height"]; if (height_val.is_string()) { win.size_height_percent = height_val.get(); float parent_height = screen_size.y; if (!win.parent.empty() && docks.find(win.parent) != docks.end()) { parent_height = docks[win.parent].size.y; } win.size.y = parseSize(height_val, parent_height, 300); } else if (height_val.is_number()) { win.size.y = height_val.get(); } else { win.size.y = 300; // Default fallback } } } } // Parse constraints if (window_config.contains("min_size")) { auto min_config = window_config["min_size"]; if (min_config.is_object()) { if (min_config.contains("width")) { win.min_size.x = parseSize(min_config["width"], screen_size.x, 100); } else { win.min_size.x = 100; } if (min_config.contains("height")) { win.min_size.y = parseSize(min_config["height"], screen_size.y, 100); } else { win.min_size.y = 100; } } } if (window_config.contains("max_size")) { auto max_config = window_config["max_size"]; if (max_config.is_object()) { if (max_config.contains("width")) { win.max_size.x = parseSize(max_config["width"], screen_size.x, 2000); } else { win.max_size.x = 2000; } if (max_config.contains("height")) { win.max_size.y = parseSize(max_config["height"], screen_size.y, 1500); } else { win.max_size.y = 1500; } } } // Parse other properties win.is_floating = window_config.value("floating", false); win.resizable = window_config.value("resizable", true); win.closeable = window_config.value("closeable", true); // Parse dock position if specified if (window_config.contains("dock")) { std::string dock_str = window_config["dock"].get(); if (dock_str == "left") win.dock_position = DockPosition::LEFT; else if (dock_str == "right") win.dock_position = DockPosition::RIGHT; else if (dock_str == "top") win.dock_position = DockPosition::TOP; else if (dock_str == "bottom") win.dock_position = DockPosition::BOTTOM; else if (dock_str == "tab") win.dock_position = DockPosition::CENTER; // tabs go in center else win.dock_position = DockPosition::CENTER; } if (window_config.contains("position")) { auto pos = window_config["position"]; if (pos.is_object()) { win.position.x = pos.value("x", 0); win.position.y = pos.value("y", 0); } } } void showDataCustom(const std::string& customType, const json& data) override { // Treat as generic data with custom type in title json modified_data = data; if (!modified_data.contains("window")) { modified_data["window"] = json{}; } if (!modified_data["window"].contains("title")) { modified_data["window"]["title"] = customType; } showData(DataType::CUSTOM, modified_data); } // ======================================== // IUI INTERFACE IMPLEMENTATION - REQUESTS & EVENTS // ======================================== void onRequest(RequestType requestType, std::function callback) override; void onRequestCustom(const std::string& customType, std::function callback) override; void showEvent(EventLevel level, const std::string& message) override; // ======================================== // WINDOW MANAGEMENT IMPLEMENTATION // ======================================== void createDock(const std::string& dockId, DockType type, DockPosition position, const json& config = {}) override; void createSplit(const std::string& dockId, Orientation orientation, const json& config = {}) override; void closeWindow(const std::string& windowId) override; void focusWindow(const std::string& windowId) override; // ======================================== // STATE MANAGEMENT // ======================================== json getState() const override; void setState(const json& state) override; private: // ======================================== // CONTENT RENDERING IMPLEMENTATIONS // ======================================== void renderEconomyContent(const json& content); void renderMapContent(const json& content); void renderInventoryContent(const json& content); void renderConsoleContent(const json& content); void renderPerformanceContent(const json& content); void renderCompaniesContent(const json& content); void renderAlertsContent(const json& content); void renderSettingsContent(const json& content); void renderGenericContent(const json& content); void renderLogConsole(); }; } // namespace grove