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 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,14 @@ target_compile_definitions(mosis-kernel PUBLIC
|
|||||||
RMLUI_GL3_CUSTOM_LOADER="glad_loader.h"
|
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
|
# Designer executable
|
||||||
add_executable(mosis-designer
|
add_executable(mosis-designer
|
||||||
main.cpp
|
main.cpp
|
||||||
@@ -62,6 +70,7 @@ add_executable(mosis-designer
|
|||||||
src/desktop_file_interface.cpp
|
src/desktop_file_interface.cpp
|
||||||
src/hot_reload.cpp
|
src/hot_reload.cpp
|
||||||
src/platform_singleton.cpp
|
src/platform_singleton.cpp
|
||||||
|
src/desktop_sandbox.cpp
|
||||||
src/testing/action_recorder.cpp
|
src/testing/action_recorder.cpp
|
||||||
src/testing/action_player.cpp
|
src/testing/action_player.cpp
|
||||||
src/testing/ui_inspector.cpp
|
src/testing/ui_inspector.cpp
|
||||||
@@ -69,6 +78,8 @@ add_executable(mosis-designer
|
|||||||
# Local backend with input recording hooks
|
# Local backend with input recording hooks
|
||||||
src/backend/RmlUi_Backend_GLFW_GL3.cpp
|
src/backend/RmlUi_Backend_GLFW_GL3.cpp
|
||||||
src/backend/RmlUi_Platform_GLFW.cpp
|
src/backend/RmlUi_Platform_GLFW.cpp
|
||||||
|
# Sandbox APIs
|
||||||
|
${SANDBOX_SOURCES}
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(mosis-designer PRIVATE
|
target_include_directories(mosis-designer PRIVATE
|
||||||
@@ -77,6 +88,7 @@ target_include_directories(mosis-designer PRIVATE
|
|||||||
src/backend
|
src/backend
|
||||||
../src/main/kernel/include
|
../src/main/kernel/include
|
||||||
../src/main/cpp
|
../src/main/cpp
|
||||||
|
../src/main/cpp/sandbox
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(mosis-designer PRIVATE
|
target_link_libraries(mosis-designer PRIVATE
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
#include "src/desktop_sandbox.h"
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
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_document_path;
|
||||||
static std::string g_current_screen_url; // For hierarchy dump - tracks current screen
|
static std::string g_current_screen_url; // For hierarchy dump - tracks current screen
|
||||||
static bool g_needs_reload = false;
|
static bool g_needs_reload = false;
|
||||||
|
static std::unique_ptr<mosis::DesktopSandbox> g_sandbox;
|
||||||
|
|
||||||
// Resolution presets
|
// Resolution presets
|
||||||
static int g_width = 540;
|
static int g_width = 540;
|
||||||
@@ -126,36 +128,28 @@ static void MouseButtonCallback(GLFWwindow* window, int button, int action, int
|
|||||||
double xpos, ypos;
|
double xpos, ypos;
|
||||||
glfwGetCursorPos(window, &xpos, &ypos);
|
glfwGetCursorPos(window, &xpos, &ypos);
|
||||||
|
|
||||||
// Convert from physical (GLFW) to logical (RmlUi) coordinates
|
// GLFW cursor callbacks report coordinates in screen/window coordinates (logical pixels)
|
||||||
// GLFW reports physical pixels, but RmlUi context uses logical pixels
|
// which match the RmlUi context dimensions, so no scaling needed
|
||||||
float scaleX, scaleY;
|
int mouseX = static_cast<int>(xpos);
|
||||||
glfwGetWindowContentScale(window, &scaleX, &scaleY);
|
int mouseY = static_cast<int>(ypos);
|
||||||
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;
|
int key_modifier = 0;
|
||||||
if (mods & GLFW_MOD_CONTROL) key_modifier |= Rml::Input::KM_CTRL;
|
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_SHIFT) key_modifier |= Rml::Input::KM_SHIFT;
|
||||||
if (mods & GLFW_MOD_ALT) key_modifier |= Rml::Input::KM_ALT;
|
if (mods & GLFW_MOD_ALT) key_modifier |= Rml::Input::KM_ALT;
|
||||||
|
|
||||||
// Update mouse position before processing button event (using logical coords)
|
// Update mouse position before processing button event
|
||||||
g_context->ProcessMouseMove(logicalX, logicalY, key_modifier);
|
g_context->ProcessMouseMove(mouseX, mouseY, key_modifier);
|
||||||
|
|
||||||
if (button == GLFW_MOUSE_BUTTON_LEFT) {
|
if (button == GLFW_MOUSE_BUTTON_LEFT) {
|
||||||
if (action == GLFW_PRESS) {
|
if (action == GLFW_PRESS) {
|
||||||
// Record mouse down in record mode (use logical coords)
|
|
||||||
if (g_action_recorder && g_action_recorder->IsRecording()) {
|
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);
|
g_context->ProcessMouseButtonDown(0, key_modifier);
|
||||||
} else if (action == GLFW_RELEASE) {
|
} else if (action == GLFW_RELEASE) {
|
||||||
// Record mouse up in record mode
|
|
||||||
if (g_action_recorder && g_action_recorder->IsRecording()) {
|
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);
|
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) {
|
static void CursorPosCallback(GLFWwindow* window, double xpos, double ypos) {
|
||||||
if (!g_context) return;
|
if (!g_context) return;
|
||||||
|
|
||||||
// Convert from physical to logical coordinates
|
// GLFW cursor callbacks report coordinates in screen/window coordinates (logical pixels)
|
||||||
float scaleX, scaleY;
|
// which match the RmlUi context dimensions, so no scaling needed
|
||||||
glfwGetWindowContentScale(window, &scaleX, &scaleY);
|
int mouseX = static_cast<int>(xpos);
|
||||||
int logicalX = static_cast<int>(xpos / scaleX);
|
int mouseY = static_cast<int>(ypos);
|
||||||
int logicalY = static_cast<int>(ypos / scaleY);
|
|
||||||
|
|
||||||
g_context->ProcessMouseMove(logicalX, logicalY, 0);
|
g_context->ProcessMouseMove(mouseX, mouseY, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) {
|
static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) {
|
||||||
@@ -291,10 +284,11 @@ int main(int argc, char* argv[]) {
|
|||||||
if (!assets_path_specified) {
|
if (!assets_path_specified) {
|
||||||
fs::path doc_path = fs::absolute(document_path);
|
fs::path doc_path = fs::absolute(document_path);
|
||||||
fs::path current = doc_path.parent_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"
|
// Walk up the directory tree looking for a folder that ends with "assets"
|
||||||
// or contains typical asset folders like "apps", "ui", "fonts"
|
// 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();
|
std::string folder_name = current.filename().string();
|
||||||
if (folder_name == "assets") {
|
if (folder_name == "assets") {
|
||||||
assets_path = current.string();
|
assets_path = current.string();
|
||||||
@@ -305,12 +299,38 @@ int main(int argc, char* argv[]) {
|
|||||||
assets_path = current.string();
|
assets_path = current.string();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
prev_path = current;
|
||||||
current = current.parent_path();
|
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()) {
|
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);
|
g_render_interface->EndFrame(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
glfwSwapBuffers(g_window);
|
// Capture screenshot BEFORE swap (glReadPixels reads from back buffer)
|
||||||
|
|
||||||
if (g_test_mode == TestMode::Screenshot) {
|
if (g_test_mode == TestMode::Screenshot) {
|
||||||
mosis::testing::VisualCapture capture(fb_width, fb_height);
|
mosis::testing::VisualCapture capture(fb_width, fb_height);
|
||||||
if (capture.CaptureScreenshot(g_test_output_path)) {
|
if (capture.CaptureScreenshot(g_test_output_path)) {
|
||||||
@@ -470,6 +489,8 @@ int main(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
glfwSwapBuffers(g_window);
|
||||||
|
|
||||||
// Cleanup and exit
|
// Cleanup and exit
|
||||||
ShutdownRmlUi();
|
ShutdownRmlUi();
|
||||||
glfwDestroyWindow(g_window);
|
glfwDestroyWindow(g_window);
|
||||||
@@ -503,6 +524,11 @@ int main(int argc, char* argv[]) {
|
|||||||
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
|
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
// Update sandbox (process timers)
|
||||||
|
if (g_sandbox) {
|
||||||
|
g_sandbox->Update();
|
||||||
|
}
|
||||||
|
|
||||||
// Update and render
|
// Update and render
|
||||||
if (g_context) {
|
if (g_context) {
|
||||||
g_context->Update();
|
g_context->Update();
|
||||||
@@ -541,6 +567,7 @@ int main(int argc, char* argv[]) {
|
|||||||
if (g_log_file.is_open()) {
|
if (g_log_file.is_open()) {
|
||||||
g_log_file.close();
|
g_log_file.close();
|
||||||
}
|
}
|
||||||
|
g_sandbox.reset();
|
||||||
ShutdownRmlUi();
|
ShutdownRmlUi();
|
||||||
glfwDestroyWindow(g_window);
|
glfwDestroyWindow(g_window);
|
||||||
glfwTerminate();
|
glfwTerminate();
|
||||||
@@ -572,8 +599,16 @@ bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height
|
|||||||
// Initialize Lua bindings
|
// Initialize Lua bindings
|
||||||
Rml::Lua::Initialise();
|
Rml::Lua::Initialise();
|
||||||
|
|
||||||
// Register loadScreen function for navigation
|
// Get Lua state
|
||||||
lua_State* L = Rml::Lua::Interpreter::GetLuaState();
|
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 {
|
lua_pushcfunction(L, [](lua_State* L) -> int {
|
||||||
const char* path = luaL_checkstring(L, 1);
|
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");
|
lua_setglobal(L, "loadScreen");
|
||||||
std::cout << "Registered Lua loadScreen function" << std::endl;
|
std::cout << "Registered Lua loadScreen function" << std::endl;
|
||||||
|
|
||||||
// Load fonts
|
// Load fonts - search for fonts directory in multiple locations
|
||||||
std::vector<std::string> fonts = {
|
std::string fonts_root;
|
||||||
"fonts/LatoLatin-Regular.ttf",
|
std::vector<std::string> font_search_paths = {
|
||||||
"fonts/LatoLatin-Bold.ttf",
|
assets_path + "/fonts",
|
||||||
"fonts/LatoLatin-Light.ttf",
|
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) {
|
for (const auto& search_path : font_search_paths) {
|
||||||
if (!Rml::LoadFontFace(font)) {
|
if (std::filesystem::exists(search_path + "/LatoLatin-Regular.ttf")) {
|
||||||
std::cerr << "Warning: Failed to load font: " << font << std::endl;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,7 +729,15 @@ bool LoadDocument(const std::string& path) {
|
|||||||
|
|
||||||
void ReloadDocument() {
|
void ReloadDocument() {
|
||||||
std::cout << "Reloading..." << std::endl;
|
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
|
// Reload stylesheets
|
||||||
for (int i = 0; i < g_context->GetNumDocuments(); ++i) {
|
for (int i = 0; i < g_context->GetNumDocuments(); ++i) {
|
||||||
auto* doc = g_context->GetDocument(i);
|
auto* doc = g_context->GetDocument(i);
|
||||||
|
|||||||
167
designer/src/desktop_sandbox.cpp
Normal file
167
designer/src/desktop_sandbox.cpp
Normal file
@@ -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 <lua.hpp>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace mosis {
|
||||||
|
|
||||||
|
DesktopSandbox::DesktopSandbox(const DesktopSandboxConfig& config)
|
||||||
|
: m_config(config) {
|
||||||
|
// Create timer manager
|
||||||
|
m_timer_manager = std::make_unique<TimerManager>();
|
||||||
|
|
||||||
|
// Create virtual filesystem
|
||||||
|
// Ensure data root exists
|
||||||
|
std::error_code ec;
|
||||||
|
fs::create_directories(m_config.data_root, ec);
|
||||||
|
|
||||||
|
m_vfs = std::make_unique<VirtualFS>(
|
||||||
|
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
|
||||||
33
designer/src/desktop_sandbox.h
Normal file
33
designer/src/desktop_sandbox.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
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<TimerManager> m_timer_manager;
|
||||||
|
std::unique_ptr<VirtualFS> m_vfs;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mosis
|
||||||
@@ -22,14 +22,24 @@ void HotReload::Start() {
|
|||||||
|
|
||||||
m_running = true;
|
m_running = true;
|
||||||
#ifdef _WIN32
|
#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_notification_handle = FindFirstChangeNotificationW(
|
||||||
m_watch_path.c_str(),
|
m_watch_path.c_str(),
|
||||||
TRUE, // Watch subtree
|
TRUE, // Watch subtree
|
||||||
FILE_NOTIFY_CHANGE_LAST_WRITE
|
FILE_NOTIFY_CHANGE_LAST_WRITE
|
||||||
);
|
);
|
||||||
|
|
||||||
if (m_notification_handle == INVALID_HANDLE_VALUE) {
|
if (m_notification_handle == INVALID_HANDLE_VALUE) {
|
||||||
std::cerr << "Failed to set up file watching" << std::endl;
|
std::cerr << "Failed to set up file watching" << std::endl;
|
||||||
|
CloseHandle(m_stop_event);
|
||||||
|
m_stop_event = nullptr;
|
||||||
m_running = false;
|
m_running = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -39,6 +49,12 @@ void HotReload::Start() {
|
|||||||
|
|
||||||
void HotReload::Stop() {
|
void HotReload::Stop() {
|
||||||
m_running = false;
|
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()) {
|
if (m_watch_thread.joinable()) {
|
||||||
m_watch_thread.join();
|
m_watch_thread.join();
|
||||||
}
|
}
|
||||||
@@ -47,21 +63,33 @@ void HotReload::Stop() {
|
|||||||
FindCloseChangeNotification(m_notification_handle);
|
FindCloseChangeNotification(m_notification_handle);
|
||||||
m_notification_handle = nullptr;
|
m_notification_handle = nullptr;
|
||||||
}
|
}
|
||||||
|
if (m_stop_event) {
|
||||||
|
CloseHandle(m_stop_event);
|
||||||
|
m_stop_event = nullptr;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void HotReload::WatchThread() {
|
void HotReload::WatchThread() {
|
||||||
while (m_running) {
|
while (m_running) {
|
||||||
#ifdef _WIN32
|
#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) {
|
if (result == WAIT_OBJECT_0) {
|
||||||
|
// File change notification
|
||||||
// Debounce - wait a bit for file writes to complete
|
// Debounce - wait a bit for file writes to complete
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
if (m_callback) {
|
if (m_callback && m_running) {
|
||||||
m_callback();
|
m_callback();
|
||||||
}
|
}
|
||||||
FindNextChangeNotification(m_notification_handle);
|
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
|
#else
|
||||||
// TODO: Linux inotify / macOS FSEvents implementation
|
// TODO: Linux inotify / macOS FSEvents implementation
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ private:
|
|||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
void* m_notification_handle = nullptr; // HANDLE
|
void* m_notification_handle = nullptr; // HANDLE
|
||||||
|
void* m_stop_event = nullptr; // HANDLE for signaling shutdown
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ local logCounter = 0
|
|||||||
local function log(msg)
|
local function log(msg)
|
||||||
logCounter = logCounter + 1
|
logCounter = logCounter + 1
|
||||||
table.insert(results, string.format("[%03d] %s", logCounter, msg))
|
table.insert(results, string.format("[%03d] %s", logCounter, msg))
|
||||||
local el = document:GetElementById("results")
|
-- document may not be available during initial script load
|
||||||
if el then
|
if document then
|
||||||
el.inner_rml = table.concat(results, "\n")
|
local el = document:GetElementById("results")
|
||||||
|
if el then
|
||||||
|
el.inner_rml = table.concat(results, "\n")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -23,12 +26,14 @@ function goBack()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function setStatus(id, status, success)
|
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)
|
local el = document:GetElementById(id)
|
||||||
if el then
|
if el then
|
||||||
if success then
|
if success then
|
||||||
el.inner_rml = "✓ " .. status
|
el.inner_rml = "✓ " .. status
|
||||||
else
|
else
|
||||||
el.inner_rml = "✗ " .. status
|
el.inner_rml = "✗ " .. status
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -151,8 +156,8 @@ function testStorage()
|
|||||||
local success = true
|
local success = true
|
||||||
local msg = ""
|
local msg = ""
|
||||||
|
|
||||||
-- Test write
|
-- Test write (VirtualFS requires /data/, /cache/, /temp/, or /shared/ prefix)
|
||||||
local writeOk = fs.write("test.txt", "Hello from sandbox!")
|
local writeOk = fs.write("/data/test.txt", "Hello from sandbox!")
|
||||||
if writeOk then
|
if writeOk then
|
||||||
log("Write successful")
|
log("Write successful")
|
||||||
else
|
else
|
||||||
@@ -162,7 +167,7 @@ function testStorage()
|
|||||||
|
|
||||||
-- Test read
|
-- Test read
|
||||||
if success then
|
if success then
|
||||||
local content = fs.read("test.txt")
|
local content = fs.read("/data/test.txt")
|
||||||
if content == "Hello from sandbox!" then
|
if content == "Hello from sandbox!" then
|
||||||
log("Read successful: " .. content)
|
log("Read successful: " .. content)
|
||||||
else
|
else
|
||||||
@@ -173,9 +178,9 @@ function testStorage()
|
|||||||
|
|
||||||
-- Test list
|
-- Test list
|
||||||
if success then
|
if success then
|
||||||
local files = fs.list("/")
|
local files = fs.list("/data")
|
||||||
if files then
|
if files then
|
||||||
log("Files in root: " .. #files)
|
log("Files in /data: " .. #files)
|
||||||
for _, f in ipairs(files) do
|
for _, f in ipairs(files) do
|
||||||
log(" - " .. f)
|
log(" - " .. f)
|
||||||
end
|
end
|
||||||
@@ -184,7 +189,7 @@ function testStorage()
|
|||||||
|
|
||||||
-- Test delete
|
-- Test delete
|
||||||
if success then
|
if success then
|
||||||
local deleteOk = fs.delete("test.txt")
|
local deleteOk = fs.delete("/data/test.txt")
|
||||||
if deleteOk then
|
if deleteOk then
|
||||||
log("Delete successful")
|
log("Delete successful")
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ body {
|
|||||||
font-size: 16dp;
|
font-size: 16dp;
|
||||||
background-color: #121212;
|
background-color: #121212;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-bar {
|
.app-bar {
|
||||||
@@ -38,10 +40,14 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
display: block;
|
||||||
padding: 16dp;
|
padding: 16dp;
|
||||||
|
width: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
display: block;
|
||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
border-radius: 12dp;
|
border-radius: 12dp;
|
||||||
padding: 16dp;
|
padding: 16dp;
|
||||||
@@ -49,16 +55,22 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
|
display: block;
|
||||||
font-size: 18dp;
|
font-size: 18dp;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 8dp;
|
margin-bottom: 8dp;
|
||||||
color: #bb86fc;
|
color: #bb86fc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card div {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
display: block;
|
||||||
background-color: #bb86fc;
|
background-color: #bb86fc;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
border: none;
|
border-width: 0;
|
||||||
border-radius: 8dp;
|
border-radius: 8dp;
|
||||||
padding: 12dp 24dp;
|
padding: 12dp 24dp;
|
||||||
font-size: 14dp;
|
font-size: 14dp;
|
||||||
@@ -75,7 +87,7 @@ button:active {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#results {
|
#results {
|
||||||
font-family: monospace;
|
font-family: LatoLatin;
|
||||||
font-size: 12dp;
|
font-size: 12dp;
|
||||||
background-color: #0d0d0d;
|
background-color: #0d0d0d;
|
||||||
padding: 12dp;
|
padding: 12dp;
|
||||||
|
|||||||
Reference in New Issue
Block a user