// D:\Dev\Mosis\MosisService\designer\main.cpp // Mosis Designer - Desktop UI development tool with hot-reload #include #include #include #include #include #include "RmlUi_Renderer_GL3.h" #include "platform.h" #include "desktop_platform.h" #include "desktop_file_interface.h" #include "hot_reload.h" #include "testing/action_recorder.h" #include "testing/action_player.h" #include "testing/ui_inspector.h" #include "testing/visual_capture.h" #include #include #include #include #include #include namespace fs = std::filesystem; // Global state static GLFWwindow* g_window = nullptr; static Rml::Context* g_context = nullptr; static RenderInterface_GL3* g_render_interface = nullptr; static mosis::DesktopPlatform* g_platform = nullptr; static mosis::HotReload* g_hot_reload = nullptr; static std::string g_current_document_path; static bool g_needs_reload = false; // Resolution presets static int g_width = 540; static int g_height = 960; // Test mode enum class TestMode { Interactive, // Normal interactive mode with hot-reload Record, // Record user actions to JSON Playback, // Playback actions from JSON Screenshot, // Take a screenshot and exit DumpHierarchy // Dump UI hierarchy to JSON and exit }; static TestMode g_test_mode = TestMode::Interactive; static std::string g_test_input_path; // Input file for playback static std::string g_test_output_path; // Output file for record/screenshot/hierarchy static mosis::testing::ActionRecorder* g_action_recorder = nullptr; static mosis::testing::ActionPlayer* g_action_player = nullptr; // Forward declarations bool InitializeRmlUi(const std::string& assets_path); void ShutdownRmlUi(); bool LoadDocument(const std::string& path); void ReloadDocument(); // GLFW callbacks static void ErrorCallback(int error, const char* description) { std::cerr << "GLFW Error " << error << ": " << description << std::endl; } static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { if (action != GLFW_PRESS) return; // F5 - Reload if (key == GLFW_KEY_F5) { g_needs_reload = true; } // F12 - Toggle debugger else if (key == GLFW_KEY_F12) { Rml::Debugger::SetVisible(!Rml::Debugger::IsVisible()); } // Escape - Back navigation else if (key == GLFW_KEY_ESCAPE) { // Record back button action if recording if (g_action_recorder && g_action_recorder->IsRecording()) { g_action_recorder->RecordButton("back"); } if (g_context) { g_context->ProcessKeyDown(Rml::Input::KI_ESCAPE, 0); g_context->ProcessKeyUp(Rml::Input::KI_ESCAPE, 0); } } // R - Toggle recording (in interactive mode) else if (key == GLFW_KEY_R && g_test_mode == TestMode::Interactive) { if (!g_action_recorder) { g_action_recorder = new mosis::testing::ActionRecorder(g_width, g_height); } if (g_action_recorder->IsRecording()) { g_action_recorder->StopRecording(); // Generate filename with timestamp auto now = std::chrono::system_clock::now(); auto time_t = std::chrono::system_clock::to_time_t(now); std::tm tm_buf; localtime_s(&tm_buf, &time_t); char buffer[64]; std::strftime(buffer, sizeof(buffer), "recording_%Y%m%d_%H%M%S.json", &tm_buf); std::string filename = buffer; if (g_action_recorder->SaveToFile(filename)) { std::cout << "Recording saved to: " << filename << std::endl; } else { std::cerr << "Failed to save recording" << std::endl; } } else { g_action_recorder->StartRecording(); std::cout << "Recording started. Press R again to stop." << std::endl; } } } static void MouseButtonCallback(GLFWwindow* window, int button, int action, int mods) { if (!g_context) return; double xpos, ypos; glfwGetCursorPos(window, &xpos, &ypos); int key_modifier = 0; if (mods & GLFW_MOD_CONTROL) key_modifier |= Rml::Input::KM_CTRL; if (mods & GLFW_MOD_SHIFT) key_modifier |= Rml::Input::KM_SHIFT; if (mods & GLFW_MOD_ALT) key_modifier |= Rml::Input::KM_ALT; // Update mouse position before processing button event g_context->ProcessMouseMove(static_cast(xpos), static_cast(ypos), key_modifier); if (button == GLFW_MOUSE_BUTTON_LEFT) { if (action == GLFW_PRESS) { // Record mouse down in record mode if (g_action_recorder && g_action_recorder->IsRecording()) { g_action_recorder->RecordMouseDown(static_cast(xpos), static_cast(ypos)); } g_context->ProcessMouseButtonDown(0, key_modifier); } else if (action == GLFW_RELEASE) { // Record mouse up in record mode if (g_action_recorder && g_action_recorder->IsRecording()) { g_action_recorder->RecordMouseUp(static_cast(xpos), static_cast(ypos)); } g_context->ProcessMouseButtonUp(0, key_modifier); } } } static void CursorPosCallback(GLFWwindow* window, double xpos, double ypos) { if (!g_context) return; g_context->ProcessMouseMove(static_cast(xpos), static_cast(ypos), 0); } static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) { if (!g_context) return; g_context->ProcessMouseWheel(static_cast(-yoffset), 0); } // System interface for RmlUi class DesktopSystemInterface : public Rml::SystemInterface { public: double GetElapsedTime() override { return glfwGetTime(); } bool LogMessage(Rml::Log::Type type, const Rml::String& message) override { const char* type_str = ""; switch (type) { case Rml::Log::LT_ERROR: type_str = "[ERROR]"; break; case Rml::Log::LT_WARNING: type_str = "[WARN]"; break; case Rml::Log::LT_INFO: type_str = "[INFO]"; break; default: type_str = "[DEBUG]"; break; } std::cout << type_str << " " << message << std::endl; return true; } }; static DesktopSystemInterface g_system_interface; static void PrintUsage() { std::cout << "Usage: mosis-designer [options] [document.rml]\n" << "\nOptions:\n" << " --resolution WxH Set window resolution (default: 540x960)\n" << " --assets PATH Set assets directory (default: assets)\n" << "\nTest modes:\n" << " --record FILE Record user actions to JSON file\n" << " --playback FILE Playback actions from JSON file\n" << " --screenshot FILE Take screenshot and exit\n" << " --dump-hierarchy FILE Dump UI hierarchy to JSON and exit\n" << "\nKeys:\n" << " F5 Reload document\n" << " F12 Toggle RmlUi debugger\n" << " ESC Back navigation\n" << " R Start/Stop recording (in interactive mode)\n" << std::endl; } int main(int argc, char* argv[]) { std::cout << "Mosis Designer v0.1.0" << std::endl; // Parse arguments std::string document_path; std::string assets_path = "assets"; // Default relative to executable for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if (arg == "--help" || arg == "-h") { PrintUsage(); return 0; } else if (arg == "--resolution" && i + 1 < argc) { std::string res = argv[++i]; size_t x = res.find('x'); if (x != std::string::npos) { g_width = std::stoi(res.substr(0, x)); g_height = std::stoi(res.substr(x + 1)); } } else if (arg == "--assets" && i + 1 < argc) { assets_path = argv[++i]; } else if (arg == "--record" && i + 1 < argc) { g_test_mode = TestMode::Record; g_test_output_path = argv[++i]; } else if (arg == "--playback" && i + 1 < argc) { g_test_mode = TestMode::Playback; g_test_input_path = argv[++i]; } else if (arg == "--screenshot" && i + 1 < argc) { g_test_mode = TestMode::Screenshot; g_test_output_path = argv[++i]; } else if (arg == "--dump-hierarchy" && i + 1 < argc) { g_test_mode = TestMode::DumpHierarchy; g_test_output_path = argv[++i]; } else if (arg[0] != '-') { document_path = arg; } } // Default document if (document_path.empty()) { document_path = "apps/home/home.rml"; } // Make assets_path absolute assets_path = fs::absolute(assets_path).string(); std::cout << "Assets path: " << assets_path << std::endl; std::cout << "Resolution: " << g_width << "x" << g_height << std::endl; // Print mode info if (g_test_mode == TestMode::Interactive) { std::cout << "Press F5 to reload, F12 for debugger, ESC for back, R to record" << std::endl; } else if (g_test_mode == TestMode::Record) { std::cout << "Recording mode: actions will be saved to " << g_test_output_path << std::endl; } else if (g_test_mode == TestMode::Playback) { std::cout << "Playback mode: playing " << g_test_input_path << std::endl; } else if (g_test_mode == TestMode::Screenshot) { std::cout << "Screenshot mode: will save to " << g_test_output_path << std::endl; } else if (g_test_mode == TestMode::DumpHierarchy) { std::cout << "Hierarchy dump mode: will save to " << g_test_output_path << std::endl; } // Initialize GLFW glfwSetErrorCallback(ErrorCallback); if (!glfwInit()) { std::cerr << "Failed to initialize GLFW" << std::endl; return 1; } // Create window with OpenGL 3.3 core context glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); g_window = glfwCreateWindow(g_width, g_height, "Mosis Designer", nullptr, nullptr); if (!g_window) { std::cerr << "Failed to create GLFW window" << std::endl; glfwTerminate(); return 1; } glfwMakeContextCurrent(g_window); glfwSwapInterval(1); // VSync // Set callbacks glfwSetKeyCallback(g_window, KeyCallback); glfwSetMouseButtonCallback(g_window, MouseButtonCallback); glfwSetCursorPosCallback(g_window, CursorPosCallback); glfwSetScrollCallback(g_window, ScrollCallback); // Load OpenGL functions with GLAD if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cerr << "Failed to initialize GLAD" << std::endl; glfwDestroyWindow(g_window); glfwTerminate(); return 1; } std::cout << "OpenGL " << glGetString(GL_VERSION) << std::endl; // Create platform abstraction and set as global singleton auto platform = std::make_unique(g_window, g_width, g_height); platform->SetAssetsPath(assets_path); g_platform = platform.get(); mosis::SetPlatform(std::move(platform)); // Initialize RmlUi if (!InitializeRmlUi(assets_path)) { std::cerr << "Failed to initialize RmlUi" << std::endl; glfwDestroyWindow(g_window); glfwTerminate(); return 1; } // Load initial document if (!LoadDocument(document_path)) { std::cerr << "Failed to load document: " << document_path << std::endl; } // Initialize test mode components if (g_test_mode == TestMode::Record) { g_action_recorder = new mosis::testing::ActionRecorder(g_width, g_height); g_action_recorder->StartRecording(); std::cout << "Recording started. Close window to save." << std::endl; } else if (g_test_mode == TestMode::Playback) { g_action_player = new mosis::testing::ActionPlayer(g_context); if (!g_action_player->LoadFromFile(g_test_input_path)) { std::cerr << "Failed to load actions from: " << g_test_input_path << std::endl; delete g_action_player; g_action_player = nullptr; } else { g_action_player->Start(); std::cout << "Playback started..." << std::endl; } } // Set up hot-reload (only in interactive mode) if (g_test_mode == TestMode::Interactive) { g_hot_reload = new mosis::HotReload(assets_path, []() { g_needs_reload = true; }); g_hot_reload->Start(); std::cout << "Hot-reload enabled for: " << assets_path << std::endl; } // For screenshot/hierarchy modes, render one frame then capture if (g_test_mode == TestMode::Screenshot || g_test_mode == TestMode::DumpHierarchy) { // Render one frame glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); if (g_context) { g_context->Update(); g_render_interface->BeginFrame(); g_context->Render(); g_render_interface->EndFrame(0); } glfwSwapBuffers(g_window); if (g_test_mode == TestMode::Screenshot) { mosis::testing::VisualCapture capture(g_width, g_height); if (capture.CaptureScreenshot(g_test_output_path)) { std::cout << "Screenshot saved to: " << g_test_output_path << std::endl; } else { std::cerr << "Failed to save screenshot" << std::endl; } } else { mosis::testing::UIInspector inspector(g_context); if (inspector.SaveHierarchy(g_test_output_path)) { std::cout << "UI hierarchy saved to: " << g_test_output_path << std::endl; } else { std::cerr << "Failed to save hierarchy" << std::endl; } } // Cleanup and exit ShutdownRmlUi(); glfwDestroyWindow(g_window); glfwTerminate(); return 0; } // Main loop while (!glfwWindowShouldClose(g_window)) { glfwPollEvents(); // Handle hot-reload if (g_needs_reload) { g_needs_reload = false; ReloadDocument(); } // Update action playback if (g_action_player && g_action_player->IsPlaying()) { g_action_player->Update(); // Check if playback finished if (g_action_player->IsFinished()) { std::cout << "Playback complete" << std::endl; // Optionally exit after playback glfwSetWindowShouldClose(g_window, GLFW_TRUE); } } // Clear glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // Update and render if (g_context) { g_context->Update(); g_render_interface->BeginFrame(); g_context->Render(); g_render_interface->EndFrame(0); } glfwSwapBuffers(g_window); } // Save recording if in record mode if (g_action_recorder && g_action_recorder->IsRecording()) { g_action_recorder->StopRecording(); if (g_action_recorder->SaveToFile(g_test_output_path)) { std::cout << "Recording saved to: " << g_test_output_path << std::endl; } else { std::cerr << "Failed to save recording" << std::endl; } } // Cleanup delete g_action_recorder; delete g_action_player; if (g_hot_reload) { g_hot_reload->Stop(); delete g_hot_reload; } ShutdownRmlUi(); glfwDestroyWindow(g_window); glfwTerminate(); return 0; } bool InitializeRmlUi(const std::string& assets_path) { // Create render interface g_render_interface = new RenderInterface_GL3(); if (!*g_render_interface) { std::cerr << "Failed to create GL3 render interface" << std::endl; return false; } g_render_interface->SetViewport(g_width, g_height); // Initialize RmlUi Rml::SetSystemInterface(&g_system_interface); Rml::SetFileInterface(&g_platform->GetFileInterface()); Rml::SetRenderInterface(g_render_interface); if (!Rml::Initialise()) { std::cerr << "Failed to initialize RmlUi" << std::endl; return false; } // Initialize Lua bindings Rml::Lua::Initialise(); // Register loadScreen function for navigation lua_State* L = Rml::Lua::Interpreter::GetLuaState(); lua_pushcfunction(L, [](lua_State* L) -> int { const char* path = luaL_checkstring(L, 1); std::cout << "loadScreen called: " << path << std::endl; if (!g_context) { lua_pushboolean(L, false); return 1; } // Close existing documents (except debugger) while (g_context->GetNumDocuments() > 1) { auto* doc = g_context->GetDocument(0); if (doc && doc->GetSourceURL().find("__rmlui") == std::string::npos) { doc->Close(); } else { break; } } // Load new document auto* document = g_context->LoadDocument(path); if (document) { document->Show(); g_current_document_path = path; std::cout << "Loaded: " << path << std::endl; lua_pushboolean(L, true); } else { std::cerr << "Failed to load: " << path << std::endl; lua_pushboolean(L, false); } return 1; }); lua_setglobal(L, "loadScreen"); std::cout << "Registered Lua loadScreen function" << std::endl; // Load fonts std::vector fonts = { "fonts/LatoLatin-Regular.ttf", "fonts/LatoLatin-Bold.ttf", "fonts/LatoLatin-Light.ttf", }; for (const auto& font : fonts) { if (!Rml::LoadFontFace(font)) { std::cerr << "Warning: Failed to load font: " << font << std::endl; } } // Create context g_context = Rml::CreateContext("main", Rml::Vector2i(g_width, g_height)); if (!g_context) { std::cerr << "Failed to create RmlUi context" << std::endl; return false; } // Initialize debugger Rml::Debugger::Initialise(g_context); return true; } void ShutdownRmlUi() { if (g_context) { Rml::RemoveContext("main"); g_context = nullptr; } // Rml::Lua is shut down automatically when Rml::Shutdown() is called Rml::Shutdown(); delete g_render_interface; g_render_interface = nullptr; // Platform (and its file interface) is managed by the global singleton } bool LoadDocument(const std::string& path) { if (!g_context) return false; // Close existing documents while (g_context->GetNumDocuments() > 0) { auto* doc = g_context->GetDocument(0); if (doc) doc->Close(); } // Load new document auto* document = g_context->LoadDocument(path); if (!document) { std::cerr << "Failed to load: " << path << std::endl; return false; } document->Show(); g_current_document_path = path; std::cout << "Loaded: " << path << std::endl; return true; } void ReloadDocument() { std::cout << "Reloading..." << std::endl; // Reload stylesheets for (int i = 0; i < g_context->GetNumDocuments(); ++i) { auto* doc = g_context->GetDocument(i); if (doc) { doc->ReloadStyleSheet(); } } // Reload document if (!g_current_document_path.empty()) { LoadDocument(g_current_document_path); } std::cout << "Reload complete" << std::endl; }