Files
MosisService/designer/src/main.cpp

494 lines
17 KiB
C++

// Mosis Designer - Desktop designer and testing tool for Mosis virtual phone UI
#include <iostream>
#include <fstream>
#include <filesystem>
#include <string>
#include <thread>
#include <chrono>
#include <RmlUi/Core.h>
#include <RmlUi/Debugger.h>
#include <RmlUi/Lua.h>
#include <RmlUi/Lua/Interpreter.h>
#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<mosis::testing::ActionRecorder> g_recorder;
static std::unique_ptr<mosis::testing::ActionPlayer> 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<mosis::desktop::DesktopPlatform>();
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<mosis::DesktopKernel*>(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<mosis::testing::ActionRecorder>(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<mosis::testing::ActionPlayer>(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<mosis::desktop::HotReload> hot_reload;
if (!opts.dump_mode) {
hot_reload = std::make_unique<mosis::desktop::HotReload>(
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 << " <document.rml> [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);
}