- Add DesktopSandbox class that integrates timer, JSON, crypto, and VirtualFS APIs - Fix mouse coordinate handling: GLFW reports window coordinates, not physical pixels - Fix font path resolution to search multiple locations for test apps - Fix screenshot capture timing (capture before buffer swap) - Fix test app CSS: use border-width instead of border:none, add display:block - Fix test app Lua: add document nil checks, use HTML entities for symbols - Update hot_reload to reset sandbox state on reload Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
756 lines
27 KiB
C++
756 lines
27 KiB
C++
// D:\Dev\Mosis\MosisService\designer\main.cpp
|
|
// Mosis Designer - Desktop UI development tool with hot-reload
|
|
|
|
#include <glad/glad.h>
|
|
#include <GLFW/glfw3.h>
|
|
#include <RmlUi/Core.h>
|
|
#include <RmlUi/Lua.h>
|
|
#include <RmlUi/Debugger.h>
|
|
#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 <RmlUi/Lua/Interpreter.h>
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <filesystem>
|
|
#include <memory>
|
|
#include <chrono>
|
|
#include <ctime>
|
|
#include "src/desktop_sandbox.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<mosis::DesktopSandbox> 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;
|
|
|
|
// 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();
|
|
|
|
// 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);
|
|
|
|
// GLFW cursor callbacks report coordinates in screen/window coordinates (logical pixels)
|
|
// which match the RmlUi context dimensions, so no scaling needed
|
|
int mouseX = static_cast<int>(xpos);
|
|
int mouseY = static_cast<int>(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(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;
|
|
|
|
// GLFW cursor callbacks report coordinates in screen/window coordinates (logical pixels)
|
|
// which match the RmlUi context dimensions, so no scaling needed
|
|
int mouseX = static_cast<int>(xpos);
|
|
int mouseY = static_cast<int>(ypos);
|
|
|
|
g_context->ProcessMouseMove(mouseX, mouseY, 0);
|
|
}
|
|
|
|
static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) {
|
|
if (!g_context) return;
|
|
g_context->ProcessMouseWheel(static_cast<float>(-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"
|
|
<< "\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" && 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";
|
|
}
|
|
|
|
// 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;
|
|
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<mosis::DesktopPlatform>(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;
|
|
}
|
|
|
|
// 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;
|
|
// 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 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<mosis::DesktopSandbox>(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();
|
|
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");
|
|
std::cout << "Registered Lua loadScreen function" << std::endl;
|
|
|
|
// Load fonts - search for fonts directory in multiple locations
|
|
std::string fonts_root;
|
|
std::vector<std::string> 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(),
|
|
};
|
|
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<std::string> 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();
|
|
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);
|
|
}
|
|
|
|
std::cout << "Reload complete" << std::endl;
|
|
}
|