add simulator mode to desktop designer for testing apps
- Add --simulator flag to launch home screen showing discovered apps - Create app discovery system to scan test-apps/ directory - Build simulator home screen with dark phone-like UI - Add Lua API: simulator.launchApp, simulator.goHome, simulator.getApps - ESC key returns to home when inside an app - Apps displayed with icons in grid layout Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include "src/desktop_sandbox.h"
|
||||
#include "src/app_discovery.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@@ -61,11 +62,19 @@ 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;
|
||||
|
||||
// Simulator mode
|
||||
static bool g_simulator_mode = false;
|
||||
static std::string g_test_apps_path; // Path to test-apps directory
|
||||
static std::string g_simulator_home_path; // Path to simulator home.rml
|
||||
static std::vector<mosis::AppInfo> g_discovered_apps;
|
||||
static std::string g_current_app_id; // Currently running app (empty = home)
|
||||
|
||||
// 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();
|
||||
void PopulateSimulatorApps();
|
||||
|
||||
// GLFW callbacks
|
||||
static void ErrorCallback(int error, const char* description) {
|
||||
@@ -89,7 +98,26 @@ static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, i
|
||||
if (g_action_recorder && g_action_recorder->IsRecording()) {
|
||||
g_action_recorder->RecordButton("back");
|
||||
}
|
||||
if (g_context) {
|
||||
|
||||
// In simulator mode with an app running, go back to home
|
||||
if (g_simulator_mode && !g_current_app_id.empty()) {
|
||||
lua_State* L = Rml::Lua::Interpreter::GetLuaState();
|
||||
lua_getglobal(L, "simulator");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_getfield(L, -1, "goHome");
|
||||
if (lua_isfunction(L, -1)) {
|
||||
if (lua_pcall(L, 0, 0, 0) != LUA_OK) {
|
||||
std::cerr << "Error calling simulator.goHome: " << lua_tostring(L, -1) << std::endl;
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
// Re-populate apps after returning home
|
||||
PopulateSimulatorApps();
|
||||
} else {
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
} else if (g_context) {
|
||||
g_context->ProcessKeyDown(Rml::Input::KI_ESCAPE, 0);
|
||||
g_context->ProcessKeyUp(Rml::Input::KI_ESCAPE, 0);
|
||||
}
|
||||
@@ -216,6 +244,9 @@ static void PrintUsage() {
|
||||
<< " --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"
|
||||
<< "\nSimulator mode:\n"
|
||||
<< " --simulator Run in simulator mode (app launcher)\n"
|
||||
<< " --test-apps PATH Path to test-apps directory (default: ./test-apps)\n"
|
||||
<< "\nTest modes:\n"
|
||||
<< " --record FILE Record user actions to JSON file\n"
|
||||
<< " --playback FILE Playback actions from JSON file\n"
|
||||
@@ -271,11 +302,68 @@ int main(int argc, char* argv[]) {
|
||||
} else if (arg == "--dump-hierarchy" && i + 1 < argc) {
|
||||
g_test_mode = TestMode::DumpHierarchy;
|
||||
g_test_output_path = argv[++i];
|
||||
} else if (arg == "--simulator") {
|
||||
g_simulator_mode = true;
|
||||
} else if (arg == "--test-apps" && i + 1 < argc) {
|
||||
g_test_apps_path = argv[++i];
|
||||
} else if (arg[0] != '-') {
|
||||
document_path = arg;
|
||||
}
|
||||
}
|
||||
|
||||
// Simulator mode setup
|
||||
if (g_simulator_mode) {
|
||||
// Default test-apps path
|
||||
if (g_test_apps_path.empty()) {
|
||||
// Get executable directory for relative path resolution
|
||||
fs::path exe_path = fs::absolute(argv[0]).parent_path();
|
||||
|
||||
// Try to find test-apps relative to executable or working directory
|
||||
std::vector<fs::path> search_paths = {
|
||||
"test-apps",
|
||||
"../test-apps",
|
||||
"../../test-apps",
|
||||
"../../../test-apps",
|
||||
exe_path / "test-apps",
|
||||
exe_path / "../test-apps",
|
||||
exe_path / "../../test-apps",
|
||||
exe_path / "../../../test-apps", // From build/Release -> MosisService
|
||||
};
|
||||
for (const auto& path : search_paths) {
|
||||
if (fs::exists(path) && fs::is_directory(path)) {
|
||||
g_test_apps_path = fs::absolute(path).string();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
g_test_apps_path = fs::absolute(g_test_apps_path).string();
|
||||
}
|
||||
|
||||
// Simulator home screen path
|
||||
std::vector<std::string> sim_search_paths = {
|
||||
"simulator/home.rml",
|
||||
"../designer/assets/simulator/home.rml",
|
||||
fs::absolute("simulator/home.rml").string(),
|
||||
};
|
||||
for (const auto& path : sim_search_paths) {
|
||||
if (fs::exists(path)) {
|
||||
g_simulator_home_path = fs::absolute(path).string();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Override document path to simulator home
|
||||
if (!g_simulator_home_path.empty()) {
|
||||
document_path = g_simulator_home_path;
|
||||
std::cout << "Simulator mode enabled" << std::endl;
|
||||
std::cout << "Test apps path: " << g_test_apps_path << std::endl;
|
||||
std::cout << "Simulator home: " << g_simulator_home_path << std::endl;
|
||||
} else {
|
||||
std::cerr << "Warning: Could not find simulator home.rml" << std::endl;
|
||||
g_simulator_mode = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Default document
|
||||
if (document_path.empty()) {
|
||||
document_path = "apps/home/home.rml";
|
||||
@@ -436,6 +524,9 @@ int main(int argc, char* argv[]) {
|
||||
std::cerr << "Failed to load document: " << document_path << std::endl;
|
||||
}
|
||||
|
||||
// Note: In simulator mode, the Lua script will call simulator.getApps()
|
||||
// when the document is ready, so we don't need to call PopulateSimulatorApps() here
|
||||
|
||||
// Initialize test mode components
|
||||
if (g_test_mode == TestMode::Record) {
|
||||
g_action_recorder = new mosis::testing::ActionRecorder(g_width, g_height);
|
||||
@@ -661,6 +752,169 @@ 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;
|
||||
|
||||
// Register simulator API (if in simulator mode)
|
||||
if (g_simulator_mode) {
|
||||
// Create simulator table
|
||||
lua_newtable(L);
|
||||
|
||||
// simulator.launchApp(entry, path, id)
|
||||
lua_pushcfunction(L, [](lua_State* L) -> int {
|
||||
const char* entry = luaL_checkstring(L, 1);
|
||||
const char* app_path = luaL_checkstring(L, 2);
|
||||
const char* app_id = luaL_checkstring(L, 3);
|
||||
|
||||
if (!g_context) {
|
||||
lua_pushboolean(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "Simulator: Launching app " << app_id << std::endl;
|
||||
std::cout << " Entry: " << entry << std::endl;
|
||||
std::cout << " Path: " << app_path << std::endl;
|
||||
|
||||
// Store current app ID
|
||||
g_current_app_id = app_id;
|
||||
|
||||
// Reset sandbox for the new app
|
||||
if (g_sandbox) {
|
||||
g_sandbox->UnregisterAPIs(L);
|
||||
g_sandbox->Reset();
|
||||
|
||||
// Re-configure sandbox with app-specific data root
|
||||
mosis::DesktopSandboxConfig config;
|
||||
config.app_id = app_id;
|
||||
config.data_root = std::string(app_path) + "/sandbox_data";
|
||||
g_sandbox = std::make_unique<mosis::DesktopSandbox>(config);
|
||||
g_sandbox->RegisterAPIs(L);
|
||||
}
|
||||
|
||||
// Close existing documents
|
||||
while (g_context->GetNumDocuments() > 0) {
|
||||
auto* doc = g_context->GetDocument(0);
|
||||
if (doc) doc->Close();
|
||||
}
|
||||
|
||||
// Load the app
|
||||
auto* document = g_context->LoadDocument(entry);
|
||||
if (document) {
|
||||
document->Show();
|
||||
g_current_document_path = entry;
|
||||
g_current_screen_url = entry;
|
||||
std::cout << "Simulator: App launched successfully" << std::endl;
|
||||
lua_pushboolean(L, true);
|
||||
} else {
|
||||
std::cerr << "Simulator: Failed to load app: " << entry << std::endl;
|
||||
g_current_app_id.clear();
|
||||
lua_pushboolean(L, false);
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "launchApp");
|
||||
|
||||
// simulator.goHome()
|
||||
lua_pushcfunction(L, [](lua_State* L) -> int {
|
||||
if (!g_context || g_simulator_home_path.empty()) {
|
||||
lua_pushboolean(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "Simulator: Returning to home" << std::endl;
|
||||
g_current_app_id.clear();
|
||||
|
||||
// Reset sandbox to default state
|
||||
if (g_sandbox) {
|
||||
g_sandbox->UnregisterAPIs(L);
|
||||
g_sandbox->Reset();
|
||||
|
||||
mosis::DesktopSandboxConfig config;
|
||||
config.data_root = "./sandbox_data";
|
||||
g_sandbox = std::make_unique<mosis::DesktopSandbox>(config);
|
||||
g_sandbox->RegisterAPIs(L);
|
||||
}
|
||||
|
||||
// Close existing documents
|
||||
while (g_context->GetNumDocuments() > 0) {
|
||||
auto* doc = g_context->GetDocument(0);
|
||||
if (doc) doc->Close();
|
||||
}
|
||||
|
||||
// Load simulator home
|
||||
auto* document = g_context->LoadDocument(g_simulator_home_path);
|
||||
if (document) {
|
||||
document->Show();
|
||||
g_current_document_path = g_simulator_home_path;
|
||||
g_current_screen_url = g_simulator_home_path;
|
||||
|
||||
// Re-populate apps in home screen
|
||||
// This will be done by calling setApps from C++ after document loads
|
||||
lua_pushboolean(L, true);
|
||||
} else {
|
||||
lua_pushboolean(L, false);
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "goHome");
|
||||
|
||||
// simulator.isInApp()
|
||||
lua_pushcfunction(L, [](lua_State* L) -> int {
|
||||
lua_pushboolean(L, !g_current_app_id.empty());
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "isInApp");
|
||||
|
||||
// simulator.getCurrentAppId()
|
||||
lua_pushcfunction(L, [](lua_State* L) -> int {
|
||||
if (g_current_app_id.empty()) {
|
||||
lua_pushnil(L);
|
||||
} else {
|
||||
lua_pushstring(L, g_current_app_id.c_str());
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "getCurrentAppId");
|
||||
|
||||
// simulator.getApps() - returns the list of discovered apps
|
||||
lua_pushcfunction(L, [](lua_State* L) -> int {
|
||||
// Discover apps if not already done
|
||||
if (g_discovered_apps.empty() && !g_test_apps_path.empty()) {
|
||||
g_discovered_apps = mosis::AppDiscovery::DiscoverApps(g_test_apps_path);
|
||||
}
|
||||
|
||||
// Create apps table
|
||||
lua_newtable(L);
|
||||
int app_index = 1;
|
||||
for (const auto& app : g_discovered_apps) {
|
||||
lua_pushinteger(L, app_index++);
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushstring(L, app.id.c_str());
|
||||
lua_setfield(L, -2, "id");
|
||||
|
||||
lua_pushstring(L, app.name.c_str());
|
||||
lua_setfield(L, -2, "name");
|
||||
|
||||
lua_pushstring(L, app.GetIconPath().c_str());
|
||||
lua_setfield(L, -2, "icon");
|
||||
|
||||
lua_pushstring(L, app.GetEntryPath().c_str());
|
||||
lua_setfield(L, -2, "entry");
|
||||
|
||||
lua_pushstring(L, app.app_path.c_str());
|
||||
lua_setfield(L, -2, "path");
|
||||
|
||||
lua_pushstring(L, app.description.c_str());
|
||||
lua_setfield(L, -2, "description");
|
||||
|
||||
lua_settable(L, -3); // apps[index] = app_table
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "getApps");
|
||||
|
||||
lua_setglobal(L, "simulator");
|
||||
std::cout << "Registered simulator Lua API" << std::endl;
|
||||
}
|
||||
|
||||
// Load fonts - search for fonts directory in multiple locations
|
||||
std::string fonts_root;
|
||||
std::vector<std::string> font_search_paths = {
|
||||
@@ -668,6 +922,7 @@ bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height
|
||||
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(),
|
||||
std::filesystem::absolute("assets/fonts").string(), // For simulator mode
|
||||
};
|
||||
for (const auto& search_path : font_search_paths) {
|
||||
if (std::filesystem::exists(search_path + "/LatoLatin-Regular.ttf")) {
|
||||
@@ -761,11 +1016,69 @@ void ReloadDocument() {
|
||||
doc->ReloadStyleSheet();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Reload document
|
||||
if (!g_current_document_path.empty()) {
|
||||
LoadDocument(g_current_document_path);
|
||||
}
|
||||
|
||||
|
||||
// Re-populate simulator apps if on home screen
|
||||
if (g_simulator_mode && g_current_app_id.empty()) {
|
||||
PopulateSimulatorApps();
|
||||
}
|
||||
|
||||
std::cout << "Reload complete" << std::endl;
|
||||
}
|
||||
|
||||
void PopulateSimulatorApps() {
|
||||
if (!g_simulator_mode || g_test_apps_path.empty()) return;
|
||||
|
||||
// Discover apps
|
||||
g_discovered_apps = mosis::AppDiscovery::DiscoverApps(g_test_apps_path);
|
||||
std::cout << "Discovered " << g_discovered_apps.size() << " apps" << std::endl;
|
||||
|
||||
// Get Lua state
|
||||
lua_State* L = Rml::Lua::Interpreter::GetLuaState();
|
||||
|
||||
// Call setApps(apps) in Lua
|
||||
lua_getglobal(L, "setApps");
|
||||
if (!lua_isfunction(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
std::cerr << "Warning: setApps function not found in Lua" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// Create apps table
|
||||
lua_newtable(L);
|
||||
int app_index = 1;
|
||||
for (const auto& app : g_discovered_apps) {
|
||||
lua_pushinteger(L, app_index++);
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushstring(L, app.id.c_str());
|
||||
lua_setfield(L, -2, "id");
|
||||
|
||||
lua_pushstring(L, app.name.c_str());
|
||||
lua_setfield(L, -2, "name");
|
||||
|
||||
lua_pushstring(L, app.GetIconPath().c_str());
|
||||
lua_setfield(L, -2, "icon");
|
||||
|
||||
lua_pushstring(L, app.GetEntryPath().c_str());
|
||||
lua_setfield(L, -2, "entry");
|
||||
|
||||
lua_pushstring(L, app.app_path.c_str());
|
||||
lua_setfield(L, -2, "path");
|
||||
|
||||
lua_pushstring(L, app.description.c_str());
|
||||
lua_setfield(L, -2, "description");
|
||||
|
||||
lua_settable(L, -3); // apps[index] = app_table
|
||||
}
|
||||
|
||||
// Call setApps(apps)
|
||||
if (lua_pcall(L, 1, 0, 0) != LUA_OK) {
|
||||
std::cerr << "Error calling setApps: " << lua_tostring(L, -1) << std::endl;
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user