// 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 #include #include #include #include "src/desktop_sandbox.h" #include "src/app_discovery.h" 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 std::string g_current_screen_url; // For hierarchy dump - tracks current screen static bool g_needs_reload = false; static std::unique_ptr g_sandbox; // 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; // Logging and hierarchy dump for testing static std::string g_log_file_path; // Log file path (--log) static std::string g_hierarchy_file_path; // Continuous hierarchy dump path (--hierarchy) static std::ofstream g_log_file; // Simulator mode static bool g_simulator_mode = false; static bool g_shell_mode = false; // Use persistent shell instead of direct documents static std::string g_test_apps_path; // Path to test-apps directory static std::string g_simulator_home_path; // Path to simulator home.rml static std::string g_shell_path; // Path to shell.rml for shell mode static std::string g_main_assets_path; // Path to main assets (for goHome) static std::vector g_discovered_apps; static std::string g_current_app_id; // Currently running app (empty = home) // Forward declarations bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height); void ShutdownRmlUi(); bool LoadDocument(const std::string& path); void ReloadDocument(); void PopulateSimulatorApps(); // Helper to set the 'document' global in Lua to the current document static void SetLuaDocumentGlobal(Rml::ElementDocument* doc) { if (!doc) return; lua_State* L = Rml::Lua::Interpreter::GetLuaState(); if (!L) return; // Push the document using RmlUi's Lua type system Rml::Lua::LuaType::push(L, doc, false); lua_setglobal(L, "document"); std::cout << "Set Lua 'document' global to: " << doc->GetTitle() << std::endl; } // 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"); } // In simulator mode with an app running, go back to home if (g_simulator_mode && !g_current_app_id.empty()) { lua_State* L = Rml::Lua::Interpreter::GetLuaState(); lua_getglobal(L, "simulator"); if (lua_istable(L, -1)) { lua_getfield(L, -1, "goHome"); if (lua_isfunction(L, -1)) { if (lua_pcall(L, 0, 0, 0) != LUA_OK) { std::cerr << "Error calling simulator.goHome: " << lua_tostring(L, -1) << std::endl; lua_pop(L, 1); } // Re-populate apps after returning home PopulateSimulatorApps(); } else { lua_pop(L, 1); } } lua_pop(L, 1); } else 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); // glfwGetCursorPos returns position in screen coordinates (same as window size) // which may differ from framebuffer size on high-DPI displays. // We need to scale to match the RmlUi context (which matches framebuffer). int winWidth, winHeight; glfwGetWindowSize(window, &winWidth, &winHeight); int fbWidth, fbHeight; glfwGetFramebufferSize(window, &fbWidth, &fbHeight); // Scale cursor position: screen coords -> framebuffer coords -> RmlUi context // On high DPI: winWidth=432, fbWidth=540, g_width=540 // Cursor in screen space needs to scale to framebuffer/context space int mouseX = static_cast(xpos * fbWidth / winWidth); int mouseY = static_cast(ypos * fbHeight / winHeight); // Debug logging for click events std::cout << "MouseButton: " << (action == GLFW_PRESS ? "DOWN" : "UP") << " at raw(" << xpos << "," << ypos << ") -> scaled(" << mouseX << "," << mouseY << ")" << " win=" << winWidth << "x" << winHeight << " fb=" << fbWidth << "x" << fbHeight << std::endl; if (g_log_file.is_open()) { g_log_file << "[DEBUG] MouseButton: " << (action == GLFW_PRESS ? "DOWN" : "UP") << " at raw(" << xpos << "," << ypos << ") -> scaled(" << mouseX << "," << mouseY << ")" << std::endl; g_log_file.flush(); } 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(mouseX, mouseY, key_modifier); if (button == GLFW_MOUSE_BUTTON_LEFT) { if (action == GLFW_PRESS) { if (g_action_recorder && g_action_recorder->IsRecording()) { g_action_recorder->RecordMouseDown(mouseX, mouseY); } g_context->ProcessMouseButtonDown(0, key_modifier); } else if (action == GLFW_RELEASE) { if (g_action_recorder && g_action_recorder->IsRecording()) { g_action_recorder->RecordMouseUp(mouseX, mouseY); } g_context->ProcessMouseButtonUp(0, key_modifier); } } } static void CursorPosCallback(GLFWwindow* window, double xpos, double ypos) { if (!g_context) return; // Scale from screen coordinates to framebuffer/RmlUi context coordinates int winWidth, winHeight; glfwGetWindowSize(window, &winWidth, &winHeight); int fbWidth, fbHeight; glfwGetFramebufferSize(window, &fbWidth, &fbHeight); int mouseX = static_cast(xpos * fbWidth / winWidth); int mouseY = static_cast(ypos * fbHeight / winHeight); g_context->ProcessMouseMove(mouseX, mouseY, 0); } static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) { if (!g_context) return; g_context->ProcessMouseWheel(static_cast(-yoffset), 0); } static void FramebufferSizeCallback(GLFWwindow* window, int width, int height) { // Update render interface viewport when framebuffer size changes (DPI scaling) if (g_render_interface) { g_render_interface->SetViewport(width, height); std::cout << "Framebuffer resized to: " << width << "x" << height << std::endl; } } // 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; // Also log to file if configured if (g_log_file.is_open()) { g_log_file << type_str << " " << message << std::endl; g_log_file.flush(); } 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: derived from document)\n" << " --log FILE Write all log messages to file\n" << " --hierarchy FILE Continuously dump UI hierarchy to JSON\n" << "\nSimulator mode:\n" << " --simulator Run in simulator mode (uses shell by default)\n" << " --no-shell Disable shell (use direct document loading)\n" << " --test-apps PATH Path to test-apps directory (default: ./test-apps)\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; // Will be derived from document path if not specified bool assets_path_specified = false; 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]; assets_path_specified = true; } else if (arg == "--log" && i + 1 < argc) { g_log_file_path = argv[++i]; } else if (arg == "--hierarchy" && i + 1 < argc) { g_hierarchy_file_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-after" && i + 1 < argc) { // Capture screenshot after playback completes (used with --playback) g_test_output_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 == "--simulator") { g_simulator_mode = true; g_shell_mode = true; // Shell mode is implicit in simulator mode } else if (arg == "--no-shell") { g_shell_mode = false; // Disable shell (use direct document loading) } else if (arg == "--test-apps" && i + 1 < argc) { g_test_apps_path = argv[++i]; } else if (arg[0] != '-') { document_path = arg; } } // Simulator mode setup if (g_simulator_mode) { // Default test-apps path if (g_test_apps_path.empty()) { // Get executable directory for relative path resolution fs::path exe_path = fs::absolute(argv[0]).parent_path(); // Try to find test-apps relative to executable or working directory std::vector search_paths = { "test-apps", "../test-apps", "../../test-apps", "../../../test-apps", exe_path / "test-apps", exe_path / "../test-apps", exe_path / "../../test-apps", exe_path / "../../../test-apps", // From build/Release -> MosisService }; for (const auto& path : search_paths) { if (fs::exists(path) && fs::is_directory(path)) { g_test_apps_path = fs::absolute(path).string(); break; } } } else { g_test_apps_path = fs::absolute(g_test_apps_path).string(); } // Find main assets home.rml for simulator mode // Look for src/main/assets/apps/home/home.rml relative to test-apps fs::path test_apps_fs = fs::path(g_test_apps_path); std::vector home_search_paths = { test_apps_fs.parent_path() / "src" / "main" / "assets" / "apps" / "home" / "home.rml", fs::path("src/main/assets/apps/home/home.rml"), fs::absolute("src/main/assets/apps/home/home.rml"), }; for (const auto& path : home_search_paths) { if (fs::exists(path)) { g_simulator_home_path = fs::absolute(path).string(); break; } } // Override document path to main home screen or shell if (!g_simulator_home_path.empty()) { // Also set the main assets path for proper resource loading g_main_assets_path = fs::path(g_simulator_home_path).parent_path().parent_path().parent_path().string(); // If shell mode, use shell instead of home if (g_shell_mode) { g_shell_path = (fs::path(g_main_assets_path) / "apps" / "shell" / "shell.rml").string(); if (fs::exists(g_shell_path)) { document_path = g_shell_path; std::cout << "Shell mode enabled" << std::endl; std::cout << "Shell: " << g_shell_path << std::endl; } else { std::cerr << "Warning: Could not find shell.rml, falling back to direct mode" << std::endl; document_path = g_simulator_home_path; g_shell_mode = false; } } else { document_path = g_simulator_home_path; } std::cout << "Simulator mode enabled" << std::endl; std::cout << "Test apps path: " << g_test_apps_path << std::endl; std::cout << "Home screen: " << g_simulator_home_path << std::endl; } else { std::cerr << "Warning: Could not find home.rml for simulator" << std::endl; g_simulator_mode = false; } } // Default document if (document_path.empty()) { document_path = "apps/home/home.rml"; } // Derive assets path from document path if not specified // The document path should be something like .../assets/apps/home/home.rml // We want to find the "assets" directory in the path if (!assets_path_specified) { fs::path doc_path = fs::absolute(document_path); fs::path current = doc_path.parent_path(); fs::path prev_path; // Walk up the directory tree looking for a folder that ends with "assets" // or contains typical asset folders like "apps", "ui", "fonts" while (!current.empty() && current != prev_path) { std::string folder_name = current.filename().string(); if (folder_name == "assets") { assets_path = current.string(); break; } // Check if this folder contains typical asset subfolders if (fs::exists(current / "apps") && fs::exists(current / "ui")) { assets_path = current.string(); break; } prev_path = current; current = current.parent_path(); } // Fall back options if no standard assets folder found if (assets_path.empty()) { // If the document exists, use its parent directory as assets path fs::path doc_path = fs::absolute(document_path); if (fs::exists(doc_path)) { assets_path = doc_path.parent_path().string(); } else { // Try executable's assets folder fs::path exe_path = fs::path(argv[0]).parent_path(); if (exe_path.empty()) { exe_path = "."; } fs::path exe_assets = fs::absolute(exe_path) / "assets"; if (fs::exists(exe_assets)) { assets_path = exe_assets.string(); } else { // Last resort: current directory assets_path = fs::absolute(".").string(); } } } } // Make document_path absolute if it's relative and exists if (!fs::path(document_path).is_absolute()) { fs::path abs_doc = fs::absolute(document_path); if (fs::exists(abs_doc)) { document_path = abs_doc.string(); } } // Make assets_path absolute assets_path = fs::absolute(assets_path).string(); std::cout << "Assets path: " << assets_path << std::endl; // Determine main assets path (where home.rml lives) // This is important for goHome() when running test apps g_main_assets_path = assets_path; // Check if we're running from test-apps folder fs::path assets_fs = fs::path(assets_path); if (assets_fs.filename().string().find("com.") == 0 || assets_fs.parent_path().filename() == "test-apps") { // Running a test app - find the main assets fs::path test_apps_root = assets_fs.parent_path(); if (test_apps_root.filename() != "test-apps") { test_apps_root = test_apps_root.parent_path(); } fs::path main_assets = test_apps_root.parent_path() / "src" / "main" / "assets"; if (fs::exists(main_assets / "apps" / "home" / "home.rml")) { g_main_assets_path = main_assets.string(); std::cout << "Main assets: " << g_main_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; } // Open log file if specified if (!g_log_file_path.empty()) { g_log_file.open(g_log_file_path, std::ios::out | std::ios::trunc); if (g_log_file.is_open()) { std::cout << "Logging to: " << g_log_file_path << std::endl; } else { std::cerr << "Warning: Failed to open log file: " << g_log_file_path << std::endl; } } // Log hierarchy file path if specified if (!g_hierarchy_file_path.empty()) { std::cout << "Hierarchy dump to: " << g_hierarchy_file_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); // Create window with document name in title std::string window_title = "Mosis Designer - " + fs::path(document_path).filename().string(); g_window = glfwCreateWindow(g_width, g_height, window_title.c_str(), 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); glfwSetFramebufferSizeCallback(g_window, FramebufferSizeCallback); // 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; // Get actual framebuffer size (may differ from window size on HiDPI displays) int fb_width, fb_height; glfwGetFramebufferSize(g_window, &fb_width, &fb_height); std::cout << "Framebuffer size: " << fb_width << "x" << fb_height << 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 (use framebuffer size for rendering, logical size for context) if (!InitializeRmlUi(assets_path, fb_width, fb_height)) { 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; } // Note: In simulator mode, the Lua script will call simulator.getApps() // when the document is ready, so we don't need to call PopulateSimulatorApps() here // 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); } // Capture screenshot BEFORE swap (glReadPixels reads from back buffer) if (g_test_mode == TestMode::Screenshot) { mosis::testing::VisualCapture capture(fb_width, fb_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; } } glfwSwapBuffers(g_window); // 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; // Capture screenshot if --screenshot-after was specified if (!g_test_output_path.empty()) { int fb_width, fb_height; glfwGetFramebufferSize(g_window, &fb_width, &fb_height); mosis::testing::VisualCapture capture(fb_width, fb_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; } } // Exit after playback glfwSetWindowShouldClose(g_window, GLFW_TRUE); } } // Clear glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // Update sandbox (process timers) if (g_sandbox) { g_sandbox->Update(); } // Update and render if (g_context) { g_context->Update(); g_render_interface->BeginFrame(); g_context->Render(); g_render_interface->EndFrame(0); } // Dump UI hierarchy each frame if configured if (!g_hierarchy_file_path.empty() && g_context) { mosis::testing::UIInspector inspector(g_context); inspector.SetCurrentScreen(g_current_screen_url); inspector.SaveHierarchy(g_hierarchy_file_path); } 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; } if (g_log_file.is_open()) { g_log_file.close(); } g_sandbox.reset(); ShutdownRmlUi(); glfwDestroyWindow(g_window); glfwTerminate(); return 0; } bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height) { // 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; } // Use framebuffer size (physical pixels) for the render interface viewport g_render_interface->SetViewport(fb_width, fb_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(); // Get Lua state lua_State* L = Rml::Lua::Interpreter::GetLuaState(); // Initialize sandbox with APIs (timers, JSON, crypto, fs) mosis::DesktopSandboxConfig sandbox_config; sandbox_config.data_root = assets_path + "/sandbox_data"; g_sandbox = std::make_unique(sandbox_config); g_sandbox->RegisterAPIs(L); // Register loadScreen function for navigation lua_pushcfunction(L, [](lua_State* L) -> int { const char* path = luaL_checkstring(L, 1); 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(); SetLuaDocumentGlobal(document); // Set 'document' global for Lua scripts g_current_document_path = path; g_current_screen_url = path; // Track current screen for hierarchy dump // Log using RmlUi logging so it appears in log file Rml::Log::Message(Rml::Log::LT_INFO, "Loaded screen: %s", path); lua_pushboolean(L, true); } else { Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to load screen: %s", path); lua_pushboolean(L, false); } return 1; }); lua_setglobal(L, "loadScreen"); // Register goHome function to return to home screen lua_pushcfunction(L, [](lua_State* L) -> int { if (!g_context) { lua_pushboolean(L, false); return 1; } std::cout << "goHome called - returning to home screen" << std::endl; // Reset sandbox back to home context if (g_sandbox && !g_current_app_id.empty()) { g_sandbox->UnregisterAPIs(L); g_sandbox->Reset(); mosis::DesktopSandboxConfig config; config.app_id = "com.mosis.home"; config.data_root = g_main_assets_path + "/sandbox_data"; g_sandbox = std::make_unique(config); g_sandbox->RegisterAPIs(L); std::cout << "Sandbox reset to home context" << std::endl; } // 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 home screen from main assets path std::string home_path = (fs::path(g_main_assets_path) / "apps" / "home" / "home.rml").string(); std::cout << "Loading home from: " << home_path << std::endl; auto* document = g_context->LoadDocument(home_path); if (document) { document->Show(); SetLuaDocumentGlobal(document); // Set 'document' global for Lua scripts g_current_document_path = home_path; g_current_screen_url = home_path; g_current_app_id = ""; // Clear current app Rml::Log::Message(Rml::Log::LT_INFO, "Returned to home screen"); lua_pushboolean(L, true); } else { Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to load home screen from: %s", home_path.c_str()); lua_pushboolean(L, false); } return 1; }); lua_setglobal(L, "goHome"); // Register switchAppSandbox function for third-party app launching lua_pushcfunction(L, [](lua_State* L) -> int { const char* app_id = luaL_checkstring(L, 1); const char* install_path = luaL_checkstring(L, 2); std::cout << "switchAppSandbox called for: " << app_id << " at " << install_path << std::endl; // Store current app ID g_current_app_id = app_id; // Reset sandbox for the new app if (g_sandbox) { g_sandbox->UnregisterAPIs(L); g_sandbox->Reset(); // Re-configure sandbox with app-specific data root mosis::DesktopSandboxConfig config; config.app_id = app_id; config.data_root = std::string(install_path) + "/sandbox_data"; g_sandbox = std::make_unique(config); g_sandbox->RegisterAPIs(L); std::cout << "Sandbox switched to: " << app_id << std::endl; } lua_pushboolean(L, true); return 1; }); lua_setglobal(L, "switchAppSandbox"); // Register loadAppContent function for shell-based app loading // loadAppContent(element, path) - loads RML content into an element's inner_rml lua_pushcfunction(L, [](lua_State* L) -> int { // Get element from first argument (RmlUi element userdata) Rml::Element* element = Rml::Lua::LuaType::check(L, 1); const char* path = luaL_checkstring(L, 2); if (!element) { std::cerr << "loadAppContent: Invalid element" << std::endl; lua_pushboolean(L, false); return 1; } // Resolve path relative to assets directory std::string full_path; if (fs::path(path).is_absolute()) { full_path = path; } else { full_path = (fs::path(g_main_assets_path) / path).string(); } // Read file content std::ifstream file(full_path); if (!file.is_open()) { std::cerr << "loadAppContent: Cannot open file: " << full_path << std::endl; lua_pushboolean(L, false); return 1; } std::stringstream buffer; buffer << file.rdbuf(); std::string content = buffer.str(); // Set as inner_rml element->SetInnerRML(content); Rml::Log::Message(Rml::Log::LT_INFO, "Loaded app content from: %s", path); lua_pushboolean(L, true); return 1; }); lua_setglobal(L, "loadAppContent"); std::cout << "Registered Lua loadScreen, goHome, switchAppSandbox, and loadAppContent functions" << std::endl; // Register simulator API (if in simulator mode) if (g_simulator_mode) { // Create simulator table lua_newtable(L); // simulator.launchApp(entry, path, id) lua_pushcfunction(L, [](lua_State* L) -> int { const char* entry = luaL_checkstring(L, 1); const char* app_path = luaL_checkstring(L, 2); const char* app_id = luaL_checkstring(L, 3); if (!g_context) { lua_pushboolean(L, false); return 1; } std::cout << "Simulator: Launching app " << app_id << std::endl; std::cout << " Entry: " << entry << std::endl; std::cout << " Path: " << app_path << std::endl; // Store current app ID g_current_app_id = app_id; // Reset sandbox for the new app if (g_sandbox) { g_sandbox->UnregisterAPIs(L); g_sandbox->Reset(); // Re-configure sandbox with app-specific data root mosis::DesktopSandboxConfig config; config.app_id = app_id; config.data_root = std::string(app_path) + "/sandbox_data"; g_sandbox = std::make_unique(config); g_sandbox->RegisterAPIs(L); } // Close existing documents while (g_context->GetNumDocuments() > 0) { auto* doc = g_context->GetDocument(0); if (doc) doc->Close(); } // Load the app auto* document = g_context->LoadDocument(entry); if (document) { document->Show(); SetLuaDocumentGlobal(document); // Set 'document' global for Lua scripts g_current_document_path = entry; g_current_screen_url = entry; std::cout << "Simulator: App launched successfully" << std::endl; lua_pushboolean(L, true); } else { std::cerr << "Simulator: Failed to load app: " << entry << std::endl; g_current_app_id.clear(); lua_pushboolean(L, false); } return 1; }); lua_setfield(L, -2, "launchApp"); // simulator.goHome() lua_pushcfunction(L, [](lua_State* L) -> int { if (!g_context || g_simulator_home_path.empty()) { lua_pushboolean(L, false); return 1; } std::cout << "Simulator: Returning to home" << std::endl; g_current_app_id.clear(); // Reset sandbox to default state if (g_sandbox) { g_sandbox->UnregisterAPIs(L); g_sandbox->Reset(); mosis::DesktopSandboxConfig config; config.data_root = "./sandbox_data"; g_sandbox = std::make_unique(config); g_sandbox->RegisterAPIs(L); } // Close existing documents while (g_context->GetNumDocuments() > 0) { auto* doc = g_context->GetDocument(0); if (doc) doc->Close(); } // Load simulator home auto* document = g_context->LoadDocument(g_simulator_home_path); if (document) { document->Show(); SetLuaDocumentGlobal(document); // Set 'document' global for Lua scripts g_current_document_path = g_simulator_home_path; g_current_screen_url = g_simulator_home_path; // Re-populate apps in home screen // This will be done by calling setApps from C++ after document loads lua_pushboolean(L, true); } else { lua_pushboolean(L, false); } return 1; }); lua_setfield(L, -2, "goHome"); // simulator.isInApp() lua_pushcfunction(L, [](lua_State* L) -> int { lua_pushboolean(L, !g_current_app_id.empty()); return 1; }); lua_setfield(L, -2, "isInApp"); // simulator.getCurrentAppId() lua_pushcfunction(L, [](lua_State* L) -> int { if (g_current_app_id.empty()) { lua_pushnil(L); } else { lua_pushstring(L, g_current_app_id.c_str()); } return 1; }); lua_setfield(L, -2, "getCurrentAppId"); // simulator.getApps() - returns the list of discovered apps lua_pushcfunction(L, [](lua_State* L) -> int { // Discover apps if not already done if (g_discovered_apps.empty() && !g_test_apps_path.empty()) { g_discovered_apps = mosis::AppDiscovery::DiscoverApps(g_test_apps_path); } // Create apps table lua_newtable(L); int app_index = 1; for (const auto& app : g_discovered_apps) { lua_pushinteger(L, app_index++); lua_newtable(L); lua_pushstring(L, app.id.c_str()); lua_setfield(L, -2, "id"); lua_pushstring(L, app.name.c_str()); lua_setfield(L, -2, "name"); lua_pushstring(L, app.GetIconPath().c_str()); lua_setfield(L, -2, "icon"); lua_pushstring(L, app.GetEntryPath().c_str()); lua_setfield(L, -2, "entry"); lua_pushstring(L, app.app_path.c_str()); lua_setfield(L, -2, "path"); lua_pushstring(L, app.description.c_str()); lua_setfield(L, -2, "description"); lua_settable(L, -3); // apps[index] = app_table } return 1; }); lua_setfield(L, -2, "getApps"); lua_setglobal(L, "simulator"); std::cout << "Registered simulator Lua API" << std::endl; // Also register mosis.apps API for compatibility with home.lua // Create mosis table (or get existing) lua_getglobal(L, "mosis"); if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_newtable(L); } // Create mosis.apps table lua_newtable(L); // mosis.apps.getInstalled() - returns installed apps lua_pushcfunction(L, [](lua_State* L) -> int { // Discover apps if not already done if (g_discovered_apps.empty() && !g_test_apps_path.empty()) { g_discovered_apps = mosis::AppDiscovery::DiscoverApps(g_test_apps_path); std::cout << "Discovered " << g_discovered_apps.size() << " apps in " << g_test_apps_path << std::endl; } // Create apps table (array format expected by home.lua) lua_newtable(L); int app_index = 1; for (const auto& app : g_discovered_apps) { lua_pushinteger(L, app_index++); lua_newtable(L); lua_pushstring(L, app.name.c_str()); lua_setfield(L, -2, "name"); lua_pushstring(L, app.id.c_str()); lua_setfield(L, -2, "package_id"); lua_pushstring(L, app.GetIconPath().c_str()); lua_setfield(L, -2, "icon"); lua_pushboolean(L, app.is_system_app); lua_setfield(L, -2, "is_system_app"); lua_pushstring(L, app.app_path.c_str()); lua_setfield(L, -2, "install_path"); lua_pushstring(L, app.entry.c_str()); lua_setfield(L, -2, "entry_point"); lua_settable(L, -3); // apps[index] = app_table } return 1; }); lua_setfield(L, -2, "getInstalled"); // mosis.apps.launch(package_id) - launch an app lua_pushcfunction(L, [](lua_State* L) -> int { const char* package_id = luaL_checkstring(L, 1); // Find the app for (const auto& app : g_discovered_apps) { if (app.id == package_id) { std::cout << "mosis.apps.launch: Starting " << package_id << std::endl; g_current_app_id = package_id; // Reset sandbox for the new app if (g_sandbox) { g_sandbox->UnregisterAPIs(L); g_sandbox->Reset(); mosis::DesktopSandboxConfig config; config.app_id = package_id; config.data_root = app.app_path + "/sandbox_data"; g_sandbox = std::make_unique(config); g_sandbox->RegisterAPIs(L); } lua_pushboolean(L, true); return 1; } } std::cerr << "mosis.apps.launch: App not found: " << package_id << std::endl; lua_pushboolean(L, false); return 1; }); lua_setfield(L, -2, "launch"); lua_setfield(L, -2, "apps"); // mosis.apps = apps_table lua_setglobal(L, "mosis"); std::cout << "Registered mosis.apps Lua API" << std::endl; } // Load fonts - search for fonts directory in multiple locations std::string fonts_root; std::vector font_search_paths = { assets_path + "/fonts", assets_path + "/../src/main/assets/fonts", // If assets_path is test-app dir assets_path + "/../../src/main/assets/fonts", std::filesystem::absolute("src/main/assets/fonts").string(), std::filesystem::absolute("assets/fonts").string(), // For simulator mode }; for (const auto& search_path : font_search_paths) { if (std::filesystem::exists(search_path + "/LatoLatin-Regular.ttf")) { fonts_root = search_path; break; } } if (fonts_root.empty()) { std::cerr << "Warning: Could not find fonts directory" << std::endl; } else { std::cout << "Fonts path: " << fonts_root << std::endl; std::vector fonts = { fonts_root + "/LatoLatin-Regular.ttf", fonts_root + "/LatoLatin-Bold.ttf", fonts_root + "/LatoLatin-Italic.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(); SetLuaDocumentGlobal(document); // Set 'document' global for Lua scripts g_current_document_path = path; g_current_screen_url = path; // Track current screen for hierarchy dump std::cout << "Loaded: " << path << std::endl; return true; } void ReloadDocument() { std::cout << "Reloading..." << std::endl; // Reset sandbox (clear timers, re-register APIs) if (g_sandbox) { lua_State* L = Rml::Lua::Interpreter::GetLuaState(); g_sandbox->UnregisterAPIs(L); g_sandbox->Reset(); g_sandbox->RegisterAPIs(L); } // 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); } // Re-populate simulator apps if on home screen if (g_simulator_mode && g_current_app_id.empty()) { PopulateSimulatorApps(); } std::cout << "Reload complete" << std::endl; } void PopulateSimulatorApps() { if (!g_simulator_mode || g_test_apps_path.empty()) return; // Discover apps g_discovered_apps = mosis::AppDiscovery::DiscoverApps(g_test_apps_path); std::cout << "Discovered " << g_discovered_apps.size() << " apps" << std::endl; // Get Lua state lua_State* L = Rml::Lua::Interpreter::GetLuaState(); // Call setApps(apps) in Lua lua_getglobal(L, "setApps"); if (!lua_isfunction(L, -1)) { lua_pop(L, 1); std::cerr << "Warning: setApps function not found in Lua" << std::endl; return; } // Create apps table lua_newtable(L); int app_index = 1; for (const auto& app : g_discovered_apps) { lua_pushinteger(L, app_index++); lua_newtable(L); lua_pushstring(L, app.id.c_str()); lua_setfield(L, -2, "id"); lua_pushstring(L, app.name.c_str()); lua_setfield(L, -2, "name"); lua_pushstring(L, app.GetIconPath().c_str()); lua_setfield(L, -2, "icon"); lua_pushstring(L, app.GetEntryPath().c_str()); lua_setfield(L, -2, "entry"); lua_pushstring(L, app.app_path.c_str()); lua_setfield(L, -2, "path"); lua_pushstring(L, app.description.c_str()); lua_setfield(L, -2, "description"); lua_settable(L, -3); // apps[index] = app_table } // Call setApps(apps) if (lua_pcall(L, 1, 0, 0) != LUA_OK) { std::cerr << "Error calling setApps: " << lua_tostring(L, -1) << std::endl; lua_pop(L, 1); } }