494 lines
17 KiB
C++
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);
|
|
}
|