// Mosis Designer - Desktop designer and testing tool for Mosis virtual phone UI #include #include #include #include #include #include #include #include #include #include #include "RmlUi_Backend.h" // Local backend with input recording hooks #include "platform.h" #include "file_interface.h" #include "service_interface.h" #include "kernel_impl.h" #include "data_models.h" #include "hot_reload.h" #include "desktop_platform.h" #include "testing/ui_inspector.h" #include "testing/action_recorder.h" #include "testing/action_player.h" // Command-line options struct Options { std::string document_path; uint32_t width = 540; uint32_t height = 960; bool dump_mode = false; bool debug_mode = false; std::string output_dir = "dump"; std::string log_file; // If set, write logs to this file std::string hierarchy_file; // If set, dump UI hierarchy to this file each frame std::string record_file; // If set, record actions to JSON file std::string playback_file; // If set, play back actions from JSON file }; // Global log file stream static std::ofstream g_log_file; // Pointer for shared logging (used by kernel_impl.cpp via log.h) namespace mosis { std::ofstream* g_log_file_ptr = nullptr; } // Log function that writes to both stdout and file void LogMessage(const std::string& message) { std::cout << message << std::endl; if (g_log_file.is_open()) { g_log_file << message << std::endl; g_log_file.flush(); } } // Forward declarations void PrintUsage(const char* program); Options ParseOptions(int argc, const char* argv[]); void LoadFonts(const std::filesystem::path& fonts_path); std::filesystem::path FindAssetsPath(const std::filesystem::path& start_path); // Custom system interface with logging class DesignerSystemInterface : public Rml::SystemInterface { Rml::SystemInterface* m_backend_interface; public: explicit DesignerSystemInterface(Rml::SystemInterface* backend) : m_backend_interface(backend) {} double GetElapsedTime() override { return m_backend_interface ? m_backend_interface->GetElapsedTime() : 0.0; } bool LogMessage(Rml::Log::Type type, const Rml::String& message) override { const char* type_str = "INFO"; switch (type) { case Rml::Log::LT_ERROR: type_str = "ERROR"; break; case Rml::Log::LT_WARNING: type_str = "WARNING"; break; case Rml::Log::LT_INFO: type_str = "INFO"; break; case Rml::Log::LT_DEBUG: type_str = "DEBUG"; break; default: break; } std::cout << "[RmlUi " << type_str << "] " << message << std::endl; return true; } }; // Global state static DesignerSystemInterface* g_system_interface = nullptr; static mosis::DesktopFileInterface* g_file_interface = nullptr; static mosis::IKernel* g_kernel = nullptr; static std::filesystem::path g_assets_path; static mosis::testing::UIInspector g_ui_inspector; // Recording/playback state static std::unique_ptr g_recorder; static std::unique_ptr g_player; static std::string g_record_file_path; // Key callback for F5 (recording control) bool HandleKeyDown(Rml::Context* context, Rml::Input::KeyIdentifier key, int key_modifier, float native_dp_ratio, bool priority) { // F5: Toggle recording / Save recording if (key == Rml::Input::KI_F5 && g_recorder) { if (g_recorder->IsRecording()) { g_recorder->StopRecording(); if (g_recorder->SaveToFile(g_record_file_path)) { LogMessage("Recording saved to: " + g_record_file_path); } else { LogMessage("ERROR: Failed to save recording to: " + g_record_file_path); } } else { g_recorder->StartRecording(); LogMessage("Recording started (press F5 to stop and save)"); } return true; // Consumed } // F6: Pause/resume playback if (key == Rml::Input::KI_F6 && g_player) { if (g_player->IsPlaying()) { g_player->Pause(); LogMessage("Playback paused"); } else if (g_player->IsPaused()) { g_player->Resume(); LogMessage("Playback resumed"); } return true; // Consumed } return false; // Not consumed, let RmlUi handle it } int main(int argc, const char* argv[]) { // Parse command-line options first Options opts = ParseOptions(argc, argv); // Open log file if specified if (!opts.log_file.empty()) { g_log_file.open(opts.log_file, std::ios::out | std::ios::trunc); if (!g_log_file.is_open()) { std::cerr << "Warning: Could not open log file: " << opts.log_file << std::endl; } else { mosis::g_log_file_ptr = &g_log_file; } } LogMessage("Mosis Designer v0.1.0"); if (opts.document_path.empty()) { PrintUsage(argv[0]); return EXIT_FAILURE; } std::filesystem::path document_file = opts.document_path; if (!std::filesystem::exists(document_file)) { std::cerr << "File not found: " << opts.document_path << std::endl; return EXIT_FAILURE; } document_file = std::filesystem::absolute(document_file); // Initialize the RmlUi backend (GLFW + OpenGL) if (!Backend::Initialize("Mosis Designer", opts.width, opts.height, true)) { std::cerr << "Failed to initialize backend" << std::endl; return EXIT_FAILURE; } // Find assets path g_assets_path = FindAssetsPath(document_file.parent_path()); LogMessage("Assets path: " + g_assets_path.generic_string()); // Setup custom interfaces g_system_interface = new DesignerSystemInterface(Backend::GetSystemInterface()); g_file_interface = new mosis::DesktopFileInterface(g_assets_path.string()); // Set platform for kernel auto platform = std::make_unique(); platform->GetFileInterface().SetAssetsPath(g_assets_path.string()); mosis::SetPlatform(std::move(platform)); Rml::SetSystemInterface(g_system_interface); Rml::SetFileInterface(g_file_interface); Rml::SetRenderInterface(Backend::GetRenderInterface()); // Initialize RmlUi Rml::Initialise(); Rml::Lua::Initialise(); LogMessage("RmlUi and Lua initialized"); // Create context Rml::Context* context = Rml::CreateContext("main", Rml::Vector2i(opts.width, opts.height)); if (!context) { std::cerr << "Failed to create RmlUi context" << std::endl; Rml::Shutdown(); Backend::Shutdown(); return EXIT_FAILURE; } // Enable debugger in debug mode if (opts.debug_mode) { Rml::Debugger::Initialise(context); Rml::Debugger::SetVisible(true); } // Load fonts std::filesystem::path fonts_path = g_assets_path / "fonts"; LoadFonts(fonts_path); // Initialize sample data and data models initializeSampleData(); setupDataModels(context); // Create and configure kernel mosis::KernelConfig kernel_config; kernel_config.width = opts.width; kernel_config.height = opts.height; kernel_config.initial_document = document_file.string(); kernel_config.threaded = false; auto kernel = mosis::CreateKernel(kernel_config); g_kernel = kernel.get(); // Set context and register Lua functions auto* desktop_kernel = dynamic_cast(g_kernel); if (desktop_kernel) { desktop_kernel->SetContext(context); mosis::DesktopKernel::RegisterLuaFunctions(); } // Load the initial document std::string document_path_str = document_file.generic_string(); LogMessage("Loading document: " + document_path_str); Rml::ElementDocument* document = context->LoadDocument(document_path_str); if (document) { document->Show(); if (desktop_kernel) { desktop_kernel->SetDocument(document); desktop_kernel->SetCurrentDocumentPath(document_path_str); } LogMessage("Document loaded successfully"); } else { LogMessage("ERROR: Failed to load document!"); } // Start kernel kernel->Start(); // Initialize recording if enabled if (!opts.record_file.empty()) { g_recorder = std::make_unique(opts.width, opts.height); g_record_file_path = opts.record_file; // Set up input callbacks for recording Backend::SetMouseButtonCallback([](int x, int y, int button, bool pressed) { if (g_recorder && g_recorder->IsRecording() && button == 0) { // Left mouse button only if (pressed) { g_recorder->RecordMouseDown(x, y); } else { g_recorder->RecordMouseUp(x, y); } } }); Backend::SetMouseMoveCallback([](int x, int y) { if (g_recorder && g_recorder->IsRecording()) { g_recorder->RecordMouseMove(x, y); } }); Backend::SetKeyCallback([](int key, bool pressed) { if (g_recorder && g_recorder->IsRecording()) { g_recorder->RecordKey(key, pressed); } }); LogMessage("Recording mode enabled. Press F5 to start recording."); } // Initialize playback if enabled if (!opts.playback_file.empty()) { g_player = std::make_unique(context); if (g_player->LoadFromFile(opts.playback_file)) { LogMessage("Loaded playback file: " + opts.playback_file); g_player->Start(); } else { LogMessage("ERROR: Failed to load playback file: " + opts.playback_file); g_player.reset(); } } // Setup hot-reload std::unique_ptr hot_reload; if (!opts.dump_mode) { hot_reload = std::make_unique( g_assets_path, [&kernel]() { std::cout << "File change detected, requesting reload..." << std::endl; kernel->RequestReload(); } ); hot_reload->Start(); std::cout << "Hot-reload enabled for: " << g_assets_path.generic_string() << std::endl; } // Main loop bool running = true; while (running) { // Process hot-reload if (hot_reload) { hot_reload->CheckForChanges(); } // Process events and update (with key callback for F5/F6 control) running = Backend::ProcessEvents(context, HandleKeyDown); // Update playback if active if (g_player && g_player->IsPlaying()) { g_player->Update(); if (g_player->IsFinished()) { LogMessage("Playback complete"); } } // Update kernel (processes tasks, updates time, etc.) kernel->Update(); // Render Backend::BeginFrame(); kernel->Render(); Backend::PresentFrame(); // Dump hierarchy if enabled (quiet mode to avoid spamming console) if (!opts.hierarchy_file.empty() && desktop_kernel) { Rml::ElementDocument* doc = desktop_kernel->GetDocument(); if (doc) { g_ui_inspector.SaveToFile(doc, opts.hierarchy_file, true); } } } // Cleanup // Stop and save recording if still active if (g_recorder && g_recorder->IsRecording()) { g_recorder->StopRecording(); if (g_recorder->SaveToFile(g_record_file_path)) { LogMessage("Recording saved on exit to: " + g_record_file_path); } } g_recorder.reset(); g_player.reset(); kernel->Stop(); kernel.reset(); g_kernel = nullptr; if (hot_reload) { hot_reload->Stop(); } if (opts.debug_mode) { Rml::Debugger::Shutdown(); } delete g_system_interface; delete g_file_interface; Rml::Shutdown(); Backend::Shutdown(); std::cout << "Mosis Designer shutdown complete" << std::endl; return EXIT_SUCCESS; } void PrintUsage(const char* program) { std::cout << "Usage: " << program << " [options]" << std::endl; std::cout << std::endl; std::cout << "Options:" << std::endl; std::cout << " --resolution WxH Set phone resolution (default: 540x960)" << std::endl; std::cout << " --dump Dump mode: screenshot + hierarchy, then exit" << std::endl; std::cout << " --debug Enable RmlUi debugger" << std::endl; std::cout << " --output DIR Output directory for dump mode (default: dump)" << std::endl; std::cout << " --log FILE Write log output to file (for automated testing)" << std::endl; std::cout << " --hierarchy FILE Continuously dump UI hierarchy to JSON file" << std::endl; std::cout << " --record FILE Record actions to JSON file (F5 to start/stop)" << std::endl; std::cout << " --playback FILE Play back recorded actions from JSON file" << std::endl; std::cout << std::endl; std::cout << "Recording Controls:" << std::endl; std::cout << " F5 Start/stop recording (when --record is enabled)" << std::endl; std::cout << " F6 Pause/resume playback (when --playback is enabled)" << std::endl; std::cout << std::endl; std::cout << "Examples:" << std::endl; std::cout << " " << program << " assets/apps/home/home.rml" << std::endl; std::cout << " " << program << " assets/apps/home/home.rml --resolution 720x1280" << std::endl; std::cout << " " << program << " assets/apps/home/home.rml --dump" << std::endl; std::cout << " " << program << " assets/apps/home/home.rml --record test.json" << std::endl; std::cout << " " << program << " assets/apps/home/home.rml --playback test.json" << std::endl; } Options ParseOptions(int argc, const char* argv[]) { Options opts; for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if (arg == "--resolution" && i + 1 < argc) { std::string res = argv[++i]; size_t x_pos = res.find('x'); if (x_pos != std::string::npos) { opts.width = std::stoi(res.substr(0, x_pos)); opts.height = std::stoi(res.substr(x_pos + 1)); } } else if (arg == "--dump") { opts.dump_mode = true; } else if (arg == "--debug") { opts.debug_mode = true; } else if (arg == "--output" && i + 1 < argc) { opts.output_dir = argv[++i]; } else if (arg == "--log" && i + 1 < argc) { opts.log_file = argv[++i]; } else if (arg == "--hierarchy" && i + 1 < argc) { opts.hierarchy_file = argv[++i]; } else if (arg == "--record" && i + 1 < argc) { opts.record_file = argv[++i]; } else if (arg == "--playback" && i + 1 < argc) { opts.playback_file = argv[++i]; } else if (arg[0] != '-') { opts.document_path = arg; } } return opts; } void LoadFonts(const std::filesystem::path& fonts_path) { if (!std::filesystem::exists(fonts_path)) { std::cerr << "Fonts directory not found: " << fonts_path << std::endl; return; } int count = 0; for (const auto& entry : std::filesystem::directory_iterator(fonts_path)) { if (entry.path().extension() == ".ttf") { std::string font_path = entry.path().generic_string(); if (Rml::LoadFontFace(font_path)) { std::cout << "Loaded font: " << entry.path().filename() << std::endl; ++count; } } } // Also try Roboto subdirectory std::filesystem::path roboto_path = fonts_path / "Roboto"; if (std::filesystem::exists(roboto_path)) { for (const auto& entry : std::filesystem::recursive_directory_iterator(roboto_path)) { if (entry.path().extension() == ".ttf") { std::string font_path = entry.path().generic_string(); if (Rml::LoadFontFace(font_path)) { std::cout << "Loaded font: " << entry.path().filename() << std::endl; ++count; } } } } std::cout << "Loaded " << count << " fonts" << std::endl; } std::filesystem::path FindAssetsPath(const std::filesystem::path& start_path) { std::filesystem::path current = std::filesystem::absolute(start_path); // Walk up the directory tree looking for a fonts/ subdirectory while (!current.empty() && current.has_parent_path()) { std::filesystem::path fonts_path = current / "fonts"; if (std::filesystem::exists(fonts_path) && std::filesystem::is_directory(fonts_path)) { // Check if it contains TTF files for (const auto& entry : std::filesystem::directory_iterator(fonts_path)) { if (entry.path().extension() == ".ttf") { return current; } } } current = current.parent_path(); } // Fall back to start path return std::filesystem::absolute(start_path); }