complete milestone 1

This commit is contained in:
2026-01-16 22:17:12 +01:00
parent 8de36aa975
commit baa77d4c95
8 changed files with 524 additions and 78 deletions

View File

@@ -17,6 +17,7 @@
#include "testing/visual_capture.h"
#include <RmlUi/Lua/Interpreter.h>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <memory>
#include <chrono>
@@ -31,6 +32,7 @@ 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;
// Resolution presets
@@ -52,8 +54,13 @@ static std::string g_test_output_path; // Output file for record/screenshot/hie
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);
bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height);
void ShutdownRmlUi();
bool LoadDocument(const std::string& path);
void ReloadDocument();
@@ -119,25 +126,36 @@ static void MouseButtonCallback(GLFWwindow* window, int button, int action, int
double xpos, ypos;
glfwGetCursorPos(window, &xpos, &ypos);
// Convert from physical (GLFW) to logical (RmlUi) coordinates
// GLFW reports physical pixels, but RmlUi context uses logical pixels
float scaleX, scaleY;
glfwGetWindowContentScale(window, &scaleX, &scaleY);
int logicalX = static_cast<int>(xpos / scaleX);
int logicalY = static_cast<int>(ypos / scaleY);
// Debug log for click detection
Rml::Log::Message(Rml::Log::LT_INFO, "Mouse %s at physical(%.0f, %.0f) logical(%d, %d) button=%d",
action == GLFW_PRESS ? "down" : "up", xpos, ypos, logicalX, logicalY, button);
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(static_cast<int>(xpos), static_cast<int>(ypos), key_modifier);
// Update mouse position before processing button event (using logical coords)
g_context->ProcessMouseMove(logicalX, logicalY, key_modifier);
if (button == GLFW_MOUSE_BUTTON_LEFT) {
if (action == GLFW_PRESS) {
// Record mouse down in record mode
// Record mouse down in record mode (use logical coords)
if (g_action_recorder && g_action_recorder->IsRecording()) {
g_action_recorder->RecordMouseDown(static_cast<int>(xpos), static_cast<int>(ypos));
g_action_recorder->RecordMouseDown(logicalX, logicalY);
}
g_context->ProcessMouseButtonDown(0, key_modifier);
} else if (action == GLFW_RELEASE) {
// Record mouse up in record mode
if (g_action_recorder && g_action_recorder->IsRecording()) {
g_action_recorder->RecordMouseUp(static_cast<int>(xpos), static_cast<int>(ypos));
g_action_recorder->RecordMouseUp(logicalX, logicalY);
}
g_context->ProcessMouseButtonUp(0, key_modifier);
}
@@ -146,7 +164,14 @@ static void MouseButtonCallback(GLFWwindow* window, int button, int action, int
static void CursorPosCallback(GLFWwindow* window, double xpos, double ypos) {
if (!g_context) return;
g_context->ProcessMouseMove(static_cast<int>(xpos), static_cast<int>(ypos), 0);
// Convert from physical to logical coordinates
float scaleX, scaleY;
glfwGetWindowContentScale(window, &scaleX, &scaleY);
int logicalX = static_cast<int>(xpos / scaleX);
int logicalY = static_cast<int>(ypos / scaleY);
g_context->ProcessMouseMove(logicalX, logicalY, 0);
}
static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) {
@@ -154,13 +179,21 @@ static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) {
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) {
@@ -170,6 +203,12 @@ public:
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;
}
};
@@ -181,7 +220,9 @@ 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: assets)\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"
@@ -200,7 +241,8 @@ int main(int argc, char* argv[]) {
// Parse arguments
std::string document_path;
std::string assets_path = "assets"; // Default relative to executable
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];
@@ -216,6 +258,11 @@ int main(int argc, char* argv[]) {
}
} 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];
@@ -232,12 +279,41 @@ int main(int argc, char* argv[]) {
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();
// 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.has_parent_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;
}
current = current.parent_path();
}
// Fall back to "assets" relative to executable if not found
if (assets_path.empty()) {
assets_path = "assets";
}
}
// Make assets_path absolute
assets_path = fs::absolute(assets_path).string();
std::cout << "Assets path: " << assets_path << std::endl;
@@ -255,7 +331,22 @@ int main(int argc, char* argv[]) {
} 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()) {
@@ -269,7 +360,9 @@ int main(int argc, char* argv[]) {
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
g_window = glfwCreateWindow(g_width, g_height, "Mosis Designer", nullptr, nullptr);
// 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();
@@ -284,6 +377,7 @@ int main(int argc, char* argv[]) {
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)) {
@@ -295,14 +389,19 @@ int main(int argc, char* argv[]) {
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
if (!InitializeRmlUi(assets_path)) {
// 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();
@@ -356,7 +455,7 @@ int main(int argc, char* argv[]) {
glfwSwapBuffers(g_window);
if (g_test_mode == TestMode::Screenshot) {
mosis::testing::VisualCapture capture(g_width, g_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 {
@@ -412,6 +511,13 @@ int main(int argc, char* argv[]) {
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);
}
@@ -432,6 +538,9 @@ int main(int argc, char* argv[]) {
g_hot_reload->Stop();
delete g_hot_reload;
}
if (g_log_file.is_open()) {
g_log_file.close();
}
ShutdownRmlUi();
glfwDestroyWindow(g_window);
glfwTerminate();
@@ -440,14 +549,15 @@ int main(int argc, char* argv[]) {
}
bool InitializeRmlUi(const std::string& assets_path) {
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;
}
g_render_interface->SetViewport(g_width, g_height);
// 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);
@@ -466,7 +576,6 @@ bool InitializeRmlUi(const std::string& assets_path) {
lua_State* L = Rml::Lua::Interpreter::GetLuaState();
lua_pushcfunction(L, [](lua_State* L) -> int {
const char* path = luaL_checkstring(L, 1);
std::cout << "loadScreen called: " << path << std::endl;
if (!g_context) {
lua_pushboolean(L, false);
@@ -488,10 +597,12 @@ bool InitializeRmlUi(const std::string& assets_path) {
if (document) {
document->Show();
g_current_document_path = path;
std::cout << "Loaded: " << path << std::endl;
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 {
std::cerr << "Failed to load: " << path << std::endl;
Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to load screen: %s", path);
lua_pushboolean(L, false);
}
return 1;
@@ -557,8 +668,9 @@ bool LoadDocument(const std::string& path) {
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;
}

View File

@@ -1,6 +1,7 @@
// D:\Dev\Mosis\MosisService\designer\src\desktop_file_interface.cpp
#include "desktop_file_interface.h"
#include <cstdio>
#include <cctype>
#include <filesystem>
namespace fs = std::filesystem;
@@ -16,12 +17,20 @@ void DesktopFileInterface::SetAssetsPath(const std::string& path) {
}
std::string DesktopFileInterface::ResolvePath(const std::string& path) const {
std::string resolved = path;
// Handle URL-encoded Windows drive letters (D| -> D:)
// RmlUi sometimes encodes the colon in Windows paths
if (resolved.size() >= 2 && std::isalpha(resolved[0]) && resolved[1] == '|') {
resolved[1] = ':';
}
// If path is absolute, use it directly
if (fs::path(path).is_absolute()) {
return path;
if (fs::path(resolved).is_absolute()) {
return resolved;
}
// Otherwise, prepend assets path
return m_assets_path + path;
return m_assets_path + resolved;
}
Rml::FileHandle DesktopFileInterface::Open(const Rml::String& path) {

View File

@@ -2,11 +2,27 @@
#include "ui_inspector.h"
#include <fstream>
#include <sstream>
#include <filesystem>
namespace mosis::testing {
using json = nlohmann::json;
// Helper to normalize RmlUi URLs for comparison
// RmlUi uses '|' instead of ':' for Windows drive letters
static std::string NormalizeUrl(const std::string& url) {
std::string normalized = url;
// Replace | with : (for Windows drive letters)
for (char& c : normalized) {
if (c == '|') c = ':';
}
// Normalize backslashes to forward slashes
for (char& c : normalized) {
if (c == '\\') c = '/';
}
return normalized;
}
UIInspector::UIInspector(Rml::Context* context)
: m_context(context)
{
@@ -24,10 +40,16 @@ json UIInspector::ElementToJson(Rml::Element* element) const {
j["id"] = id;
}
// Classes
// Classes - split into array
std::string class_str = element->GetAttribute<Rml::String>("class", "");
if (!class_str.empty()) {
j["classes"] = class_str;
json classes_arr = json::array();
std::istringstream iss(class_str);
std::string cls;
while (iss >> cls) {
classes_arr.push_back(cls);
}
j["classes"] = classes_arr;
}
// Bounds
@@ -75,32 +97,63 @@ json UIInspector::ElementToJson(Rml::Element* element) const {
json UIInspector::DumpHierarchy() const {
json result;
// Get current timestamp
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << std::put_time(std::localtime(&time_t), "%Y-%m-%dT%H:%M:%S");
result["timestamp"] = ss.str();
// Resolution
auto dimensions = m_context->GetDimensions();
result["resolution"] = {
{"width", dimensions.x},
{"height", dimensions.y}
};
// Current screen (document URL)
std::string screen = "";
if (m_context->GetNumDocuments() > 0) {
auto* doc = m_context->GetDocument(0);
// Current screen - use override if set, otherwise detect from documents
std::string screen = m_current_screen;
Rml::ElementDocument* main_doc = nullptr;
// Normalize the current screen path for comparison
std::string normalized_current = NormalizeUrl(m_current_screen);
// Find the document matching the current screen, or first non-debugger document
for (int i = 0; i < m_context->GetNumDocuments(); i++) {
auto* doc = m_context->GetDocument(i);
if (doc) {
screen = doc->GetSourceURL();
std::string url = doc->GetSourceURL();
// Skip debugger documents
if (url.find("__rmlui") == std::string::npos) {
// Normalize the document URL for comparison
std::string normalized_url = NormalizeUrl(url);
// If we have a current screen override, match it
if (!m_current_screen.empty()) {
if (normalized_url.find(normalized_current) != std::string::npos ||
normalized_current.find(normalized_url) != std::string::npos) {
main_doc = doc;
break;
}
} else if (!main_doc) {
// No override, use first non-debugger document
screen = url;
main_doc = doc;
}
}
}
}
result["screen"] = screen;
// Elements - dump all documents
// Elements - dump the main document body (expected format for HierarchyReader)
if (main_doc) {
result["elements"] = ElementToJson(main_doc);
} else {
result["elements"] = json::object();
}
// Also include all documents for more detailed inspection
result["documents"] = json::array();
for (int i = 0; i < m_context->GetNumDocuments(); i++) {
auto* doc = m_context->GetDocument(i);
@@ -108,24 +161,51 @@ json UIInspector::DumpHierarchy() const {
json doc_json;
doc_json["url"] = doc->GetSourceURL();
doc_json["title"] = doc->GetTitle();
// Dump document root (ElementDocument inherits from Element)
doc_json["body"] = ElementToJson(doc);
result["documents"].push_back(doc_json);
}
}
return result;
}
bool UIInspector::SaveHierarchy(const std::string& path) const {
json hierarchy = DumpHierarchy();
std::ofstream file(path);
if (!file) return false;
file << hierarchy.dump(2);
std::string content = hierarchy.dump(2);
// Use temporary file + rename for atomic writes
std::string temp_path = path + ".tmp";
{
std::ofstream file(temp_path, std::ios::binary | std::ios::trunc);
if (!file) {
return false;
}
file.write(content.c_str(), content.size());
file.flush();
if (!file.good()) {
file.close();
return false;
}
file.close();
}
// Delete the old file first to avoid rename conflicts on Windows
std::error_code ec;
std::filesystem::remove(path, ec); // Ignore errors if file doesn't exist
// Rename temp file to final path
std::filesystem::rename(temp_path, path, ec);
if (ec) {
// Fallback: copy and delete if rename fails
std::filesystem::copy_file(temp_path, path,
std::filesystem::copy_options::overwrite_existing, ec);
std::filesystem::remove(temp_path, ec);
}
return true;
}

View File

@@ -12,9 +12,12 @@ public:
UIInspector(Rml::Context* context);
~UIInspector() = default;
// Set current screen URL override (to handle stale document detection)
void SetCurrentScreen(const std::string& screenUrl) { m_current_screen = screenUrl; }
// Dump entire UI hierarchy to JSON
nlohmann::json DumpHierarchy() const;
// Save hierarchy to file
bool SaveHierarchy(const std::string& path) const;
@@ -42,6 +45,7 @@ private:
Rml::Element* FindElementRecursive(Rml::Element* root, const std::string& selector) const;
Rml::Context* m_context;
std::string m_current_screen; // Override for screen URL detection
};
} // namespace mosis::testing