From 8432bbb986a8f0c8841f9eaab6fd9bc0e6464934 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 19 Jan 2026 10:22:32 +0100 Subject: [PATCH] add sandbox support to desktop designer, fix mouse coordinates and UI issues - 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 --- designer/CMakeLists.txt | 12 ++ designer/main.cpp | 135 +++++++++++---- designer/src/desktop_sandbox.cpp | 167 +++++++++++++++++++ designer/src/desktop_sandbox.h | 33 ++++ designer/src/hot_reload.cpp | 34 +++- designer/src/hot_reload.h | 1 + test-apps/com.mosis.sandbox-test/app.lua | 27 +-- test-apps/com.mosis.sandbox-test/styles.rcss | 16 +- 8 files changed, 372 insertions(+), 53 deletions(-) create mode 100644 designer/src/desktop_sandbox.cpp create mode 100644 designer/src/desktop_sandbox.h diff --git a/designer/CMakeLists.txt b/designer/CMakeLists.txt index 56fd14e..011635c 100644 --- a/designer/CMakeLists.txt +++ b/designer/CMakeLists.txt @@ -55,6 +55,14 @@ target_compile_definitions(mosis-kernel PUBLIC RMLUI_GL3_CUSTOM_LOADER="glad_loader.h" ) +# Sandbox sources (reuse from MosisService) +set(SANDBOX_SOURCES + ../src/main/cpp/sandbox/timer_manager.cpp + ../src/main/cpp/sandbox/json_api.cpp + ../src/main/cpp/sandbox/crypto_api.cpp + ../src/main/cpp/sandbox/virtual_fs.cpp +) + # Designer executable add_executable(mosis-designer main.cpp @@ -62,6 +70,7 @@ add_executable(mosis-designer src/desktop_file_interface.cpp src/hot_reload.cpp src/platform_singleton.cpp + src/desktop_sandbox.cpp src/testing/action_recorder.cpp src/testing/action_player.cpp src/testing/ui_inspector.cpp @@ -69,6 +78,8 @@ add_executable(mosis-designer # Local backend with input recording hooks src/backend/RmlUi_Backend_GLFW_GL3.cpp src/backend/RmlUi_Platform_GLFW.cpp + # Sandbox APIs + ${SANDBOX_SOURCES} ) target_include_directories(mosis-designer PRIVATE @@ -77,6 +88,7 @@ target_include_directories(mosis-designer PRIVATE src/backend ../src/main/kernel/include ../src/main/cpp + ../src/main/cpp/sandbox ) target_link_libraries(mosis-designer PRIVATE diff --git a/designer/main.cpp b/designer/main.cpp index 3437f48..08bd251 100644 --- a/designer/main.cpp +++ b/designer/main.cpp @@ -22,6 +22,7 @@ #include #include #include +#include "src/desktop_sandbox.h" namespace fs = std::filesystem; @@ -34,6 +35,7 @@ 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 g_sandbox; // Resolution presets static int g_width = 540; @@ -126,36 +128,28 @@ 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(xpos / scaleX); - int logicalY = static_cast(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); + // 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(xpos); + int mouseY = static_cast(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 (using logical coords) - g_context->ProcessMouseMove(logicalX, logicalY, key_modifier); + // Update mouse position before processing button event + g_context->ProcessMouseMove(mouseX, mouseY, key_modifier); if (button == GLFW_MOUSE_BUTTON_LEFT) { if (action == GLFW_PRESS) { - // Record mouse down in record mode (use logical coords) if (g_action_recorder && g_action_recorder->IsRecording()) { - g_action_recorder->RecordMouseDown(logicalX, logicalY); + g_action_recorder->RecordMouseDown(mouseX, mouseY); } 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(logicalX, logicalY); + g_action_recorder->RecordMouseUp(mouseX, mouseY); } g_context->ProcessMouseButtonUp(0, key_modifier); } @@ -165,13 +159,12 @@ static void MouseButtonCallback(GLFWwindow* window, int button, int action, int static void CursorPosCallback(GLFWwindow* window, double xpos, double ypos) { if (!g_context) return; - // Convert from physical to logical coordinates - float scaleX, scaleY; - glfwGetWindowContentScale(window, &scaleX, &scaleY); - int logicalX = static_cast(xpos / scaleX); - int logicalY = static_cast(ypos / scaleY); + // 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(xpos); + int mouseY = static_cast(ypos); - g_context->ProcessMouseMove(logicalX, logicalY, 0); + g_context->ProcessMouseMove(mouseX, mouseY, 0); } static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) { @@ -291,10 +284,11 @@ int main(int argc, char* argv[]) { 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.has_parent_path()) { + while (!current.empty() && current != prev_path) { std::string folder_name = current.filename().string(); if (folder_name == "assets") { assets_path = current.string(); @@ -305,12 +299,38 @@ int main(int argc, char* argv[]) { assets_path = current.string(); break; } + prev_path = current; current = current.parent_path(); } - // Fall back to "assets" relative to executable if not found + // Fall back options if no standard assets folder found if (assets_path.empty()) { - assets_path = "assets"; + // 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(); } } @@ -452,8 +472,7 @@ int main(int argc, char* argv[]) { g_render_interface->EndFrame(0); } - glfwSwapBuffers(g_window); - + // 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)) { @@ -470,6 +489,8 @@ int main(int argc, char* argv[]) { } } + glfwSwapBuffers(g_window); + // Cleanup and exit ShutdownRmlUi(); glfwDestroyWindow(g_window); @@ -503,6 +524,11 @@ int main(int argc, char* argv[]) { 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(); @@ -541,6 +567,7 @@ int main(int argc, char* argv[]) { if (g_log_file.is_open()) { g_log_file.close(); } + g_sandbox.reset(); ShutdownRmlUi(); glfwDestroyWindow(g_window); glfwTerminate(); @@ -572,8 +599,16 @@ bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height // Initialize Lua bindings Rml::Lua::Initialise(); - // Register loadScreen function for navigation + // 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(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); @@ -610,15 +645,33 @@ bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height lua_setglobal(L, "loadScreen"); std::cout << "Registered Lua loadScreen function" << std::endl; - // Load fonts - std::vector fonts = { - "fonts/LatoLatin-Regular.ttf", - "fonts/LatoLatin-Bold.ttf", - "fonts/LatoLatin-Light.ttf", + // Load fonts - search for fonts directory in multiple locations + std::string fonts_root; + std::vector 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& font : fonts) { - if (!Rml::LoadFontFace(font)) { - std::cerr << "Warning: Failed to load font: " << font << std::endl; + 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 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; + } } } @@ -676,7 +729,15 @@ bool LoadDocument(const std::string& path) { 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); diff --git a/designer/src/desktop_sandbox.cpp b/designer/src/desktop_sandbox.cpp new file mode 100644 index 0000000..fa9886c --- /dev/null +++ b/designer/src/desktop_sandbox.cpp @@ -0,0 +1,167 @@ +#include "desktop_sandbox.h" +#include "timer_manager.h" +#include "json_api.h" +#include "crypto_api.h" +#include "virtual_fs.h" +#include +#include +#include + +namespace fs = std::filesystem; + +namespace mosis { + +DesktopSandbox::DesktopSandbox(const DesktopSandboxConfig& config) + : m_config(config) { + // Create timer manager + m_timer_manager = std::make_unique(); + + // Create virtual filesystem + // Ensure data root exists + std::error_code ec; + fs::create_directories(m_config.data_root, ec); + + m_vfs = std::make_unique( + m_config.app_id, + m_config.data_root, + VirtualFSLimits{} + ); + + std::cout << "DesktopSandbox initialized for app: " << m_config.app_id << std::endl; + std::cout << " Data root: " << m_config.data_root << std::endl; +} + +DesktopSandbox::~DesktopSandbox() { + std::cout << "DesktopSandbox destroyed" << std::endl; +} + +// crypto.sha256(data) -> hash +// Convenience wrapper for crypto.hash("sha256", data) +static int lua_crypto_sha256(lua_State* L) { + // Get the crypto table + lua_getglobal(L, "crypto"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return luaL_error(L, "crypto table not found"); + } + + // Get crypto.hash function + lua_getfield(L, -1, "hash"); + if (!lua_isfunction(L, -1)) { + lua_pop(L, 2); + return luaL_error(L, "crypto.hash function not found"); + } + + // Call hash("sha256", data) + lua_pushstring(L, "sha256"); + lua_pushvalue(L, 1); // Push the original data argument + lua_call(L, 2, 1); + + // Remove the crypto table from stack, keep result + lua_remove(L, -2); + return 1; +} + +// crypto.sha512(data) -> hash +static int lua_crypto_sha512(lua_State* L) { + lua_getglobal(L, "crypto"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return luaL_error(L, "crypto table not found"); + } + + lua_getfield(L, -1, "hash"); + if (!lua_isfunction(L, -1)) { + lua_pop(L, 2); + return luaL_error(L, "crypto.hash function not found"); + } + + lua_pushstring(L, "sha512"); + lua_pushvalue(L, 1); + lua_call(L, 2, 1); + + lua_remove(L, -2); + return 1; +} + +void DesktopSandbox::RegisterAPIs(lua_State* L) { + std::cout << "Registering sandbox APIs..." << std::endl; + + // Register timer API (setTimeout, setInterval, clearTimeout, clearInterval) + RegisterTimerAPI(L, m_timer_manager.get(), m_config.app_id); + std::cout << " Timer API registered" << std::endl; + + // Register JSON API (json.encode, json.decode) + RegisterJsonAPI(L, JsonLimits{}); + std::cout << " JSON API registered" << std::endl; + + // Register crypto API (crypto.randomBytes, crypto.hash, crypto.hmac) + RegisterCryptoAPI(L); + + // Add convenience wrappers for common hash algorithms + lua_getglobal(L, "crypto"); + if (lua_istable(L, -1)) { + lua_pushcfunction(L, lua_crypto_sha256); + lua_setfield(L, -2, "sha256"); + + lua_pushcfunction(L, lua_crypto_sha512); + lua_setfield(L, -2, "sha512"); + } + lua_pop(L, 1); + std::cout << " Crypto API registered" << std::endl; + + // Register virtual filesystem API (fs.read, fs.write, fs.delete, etc.) + RegisterVirtualFS(L, m_vfs.get()); + std::cout << " VirtualFS API registered" << std::endl; + + std::cout << "All sandbox APIs registered" << std::endl; +} + +void DesktopSandbox::UnregisterAPIs(lua_State* L) { + // Clear the global tables by setting them to nil + // This ensures clean state on hot-reload + lua_pushnil(L); + lua_setglobal(L, "setTimeout"); + + lua_pushnil(L); + lua_setglobal(L, "clearTimeout"); + + lua_pushnil(L); + lua_setglobal(L, "setInterval"); + + lua_pushnil(L); + lua_setglobal(L, "clearInterval"); + + lua_pushnil(L); + lua_setglobal(L, "json"); + + lua_pushnil(L); + lua_setglobal(L, "crypto"); + + lua_pushnil(L); + lua_setglobal(L, "fs"); + + std::cout << "Sandbox APIs unregistered" << std::endl; +} + +void DesktopSandbox::Update() { + // Process any pending timers + if (m_timer_manager) { + m_timer_manager->ProcessTimers(); + } +} + +void DesktopSandbox::Reset() { + // Clear all timers for the app + if (m_timer_manager) { + m_timer_manager->ClearAppTimers(m_config.app_id); + std::cout << "Timers cleared for hot-reload" << std::endl; + } + + // Optionally clear temp files + if (m_vfs) { + m_vfs->ClearTemp(); + } +} + +} // namespace mosis diff --git a/designer/src/desktop_sandbox.h b/designer/src/desktop_sandbox.h new file mode 100644 index 0000000..e47c24b --- /dev/null +++ b/designer/src/desktop_sandbox.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include + +struct lua_State; + +namespace mosis { + +class TimerManager; +class VirtualFS; + +struct DesktopSandboxConfig { + std::string app_id = "com.mosis.designer"; + std::string data_root = "./sandbox_data"; +}; + +class DesktopSandbox { +public: + explicit DesktopSandbox(const DesktopSandboxConfig& config); + ~DesktopSandbox(); + + void RegisterAPIs(lua_State* L); + void UnregisterAPIs(lua_State* L); + void Update(); // Process timers - call each frame + void Reset(); // For hot-reload + +private: + DesktopSandboxConfig m_config; + std::unique_ptr m_timer_manager; + std::unique_ptr m_vfs; +}; + +} // namespace mosis diff --git a/designer/src/hot_reload.cpp b/designer/src/hot_reload.cpp index ebcd586..685ad6c 100644 --- a/designer/src/hot_reload.cpp +++ b/designer/src/hot_reload.cpp @@ -22,14 +22,24 @@ void HotReload::Start() { m_running = true; #ifdef _WIN32 + // Create a manual-reset event for signaling shutdown + m_stop_event = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!m_stop_event) { + std::cerr << "Failed to create stop event" << std::endl; + m_running = false; + return; + } + m_notification_handle = FindFirstChangeNotificationW( m_watch_path.c_str(), TRUE, // Watch subtree FILE_NOTIFY_CHANGE_LAST_WRITE ); - + if (m_notification_handle == INVALID_HANDLE_VALUE) { std::cerr << "Failed to set up file watching" << std::endl; + CloseHandle(m_stop_event); + m_stop_event = nullptr; m_running = false; return; } @@ -39,6 +49,12 @@ void HotReload::Start() { void HotReload::Stop() { m_running = false; +#ifdef _WIN32 + // Signal the stop event to wake up the watch thread + if (m_stop_event) { + SetEvent(m_stop_event); + } +#endif if (m_watch_thread.joinable()) { m_watch_thread.join(); } @@ -47,21 +63,33 @@ void HotReload::Stop() { FindCloseChangeNotification(m_notification_handle); m_notification_handle = nullptr; } + if (m_stop_event) { + CloseHandle(m_stop_event); + m_stop_event = nullptr; + } #endif } void HotReload::WatchThread() { while (m_running) { #ifdef _WIN32 - DWORD result = WaitForSingleObject(m_notification_handle, 100); // 100ms timeout + // Wait on both the file change notification and the stop event + HANDLE handles[2] = { m_notification_handle, m_stop_event }; + DWORD result = WaitForMultipleObjects(2, handles, FALSE, INFINITE); + if (result == WAIT_OBJECT_0) { + // File change notification // Debounce - wait a bit for file writes to complete std::this_thread::sleep_for(std::chrono::milliseconds(100)); - if (m_callback) { + if (m_callback && m_running) { m_callback(); } FindNextChangeNotification(m_notification_handle); + } else if (result == WAIT_OBJECT_0 + 1) { + // Stop event signaled - exit the loop + break; } + // WAIT_FAILED or other errors will just loop and check m_running #else // TODO: Linux inotify / macOS FSEvents implementation std::this_thread::sleep_for(std::chrono::milliseconds(100)); diff --git a/designer/src/hot_reload.h b/designer/src/hot_reload.h index 08c6cb5..d499c00 100644 --- a/designer/src/hot_reload.h +++ b/designer/src/hot_reload.h @@ -29,6 +29,7 @@ private: #ifdef _WIN32 void* m_notification_handle = nullptr; // HANDLE + void* m_stop_event = nullptr; // HANDLE for signaling shutdown #endif }; diff --git a/test-apps/com.mosis.sandbox-test/app.lua b/test-apps/com.mosis.sandbox-test/app.lua index 0f8658d..9f81bc9 100644 --- a/test-apps/com.mosis.sandbox-test/app.lua +++ b/test-apps/com.mosis.sandbox-test/app.lua @@ -7,9 +7,12 @@ local logCounter = 0 local function log(msg) logCounter = logCounter + 1 table.insert(results, string.format("[%03d] %s", logCounter, msg)) - local el = document:GetElementById("results") - if el then - el.inner_rml = table.concat(results, "\n") + -- document may not be available during initial script load + if document then + local el = document:GetElementById("results") + if el then + el.inner_rml = table.concat(results, "\n") + end end end @@ -23,12 +26,14 @@ function goBack() end local function setStatus(id, status, success) + -- document may not be available during initial script load + if not document then return end local el = document:GetElementById(id) if el then if success then - el.inner_rml = "✓ " .. status + el.inner_rml = "✓ " .. status else - el.inner_rml = "✗ " .. status + el.inner_rml = "✗ " .. status end end end @@ -151,8 +156,8 @@ function testStorage() local success = true local msg = "" - -- Test write - local writeOk = fs.write("test.txt", "Hello from sandbox!") + -- Test write (VirtualFS requires /data/, /cache/, /temp/, or /shared/ prefix) + local writeOk = fs.write("/data/test.txt", "Hello from sandbox!") if writeOk then log("Write successful") else @@ -162,7 +167,7 @@ function testStorage() -- Test read if success then - local content = fs.read("test.txt") + local content = fs.read("/data/test.txt") if content == "Hello from sandbox!" then log("Read successful: " .. content) else @@ -173,9 +178,9 @@ function testStorage() -- Test list if success then - local files = fs.list("/") + local files = fs.list("/data") if files then - log("Files in root: " .. #files) + log("Files in /data: " .. #files) for _, f in ipairs(files) do log(" - " .. f) end @@ -184,7 +189,7 @@ function testStorage() -- Test delete if success then - local deleteOk = fs.delete("test.txt") + local deleteOk = fs.delete("/data/test.txt") if deleteOk then log("Delete successful") else diff --git a/test-apps/com.mosis.sandbox-test/styles.rcss b/test-apps/com.mosis.sandbox-test/styles.rcss index 7e41fcc..d557d30 100644 --- a/test-apps/com.mosis.sandbox-test/styles.rcss +++ b/test-apps/com.mosis.sandbox-test/styles.rcss @@ -3,6 +3,8 @@ body { font-size: 16dp; background-color: #121212; color: #ffffff; + width: 100%; + height: 100%; } .app-bar { @@ -38,10 +40,14 @@ body { } .content { + display: block; padding: 16dp; + width: auto; + box-sizing: border-box; } .card { + display: block; background-color: #1e1e1e; border-radius: 12dp; padding: 16dp; @@ -49,16 +55,22 @@ body { } .card-title { + display: block; font-size: 18dp; font-weight: bold; margin-bottom: 8dp; color: #bb86fc; } +.card div { + display: block; +} + button { + display: block; background-color: #bb86fc; color: #000000; - border: none; + border-width: 0; border-radius: 8dp; padding: 12dp 24dp; font-size: 14dp; @@ -75,7 +87,7 @@ button:active { } #results { - font-family: monospace; + font-family: LatoLatin; font-size: 12dp; background-color: #0d0d0d; padding: 12dp;