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:
@@ -71,6 +71,7 @@ add_executable(mosis-designer
|
|||||||
src/hot_reload.cpp
|
src/hot_reload.cpp
|
||||||
src/platform_singleton.cpp
|
src/platform_singleton.cpp
|
||||||
src/desktop_sandbox.cpp
|
src/desktop_sandbox.cpp
|
||||||
|
src/app_discovery.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
|
||||||
@@ -111,3 +112,10 @@ add_custom_command(TARGET mosis-designer POST_BUILD
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/../src/main/assets
|
${CMAKE_CURRENT_SOURCE_DIR}/../src/main/assets
|
||||||
$<TARGET_FILE_DIR:mosis-designer>/assets
|
$<TARGET_FILE_DIR:mosis-designer>/assets
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Copy simulator assets
|
||||||
|
add_custom_command(TARGET mosis-designer POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/assets/simulator
|
||||||
|
$<TARGET_FILE_DIR:mosis-designer>/simulator
|
||||||
|
)
|
||||||
|
|||||||
154
designer/assets/simulator/home.rcss
Normal file
154
designer/assets/simulator/home.rcss
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
/* Simulator Home Screen Styles */
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: LatoLatin;
|
||||||
|
background-color: #1a1a2e;
|
||||||
|
color: #ffffff;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Bar */
|
||||||
|
.status-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 24dp;
|
||||||
|
padding: 0 12dp;
|
||||||
|
background-color: #0f0f1a;
|
||||||
|
font-size: 12dp;
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-time {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-wifi, .status-battery {
|
||||||
|
font-size: 10dp;
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Home Content */
|
||||||
|
.home-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16dp;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-header h1 {
|
||||||
|
font-size: 24dp;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 0 4dp 0;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-header .subtitle {
|
||||||
|
font-size: 14dp;
|
||||||
|
color: #888888;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* App Grid */
|
||||||
|
.app-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16dp;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-apps {
|
||||||
|
text-align: center;
|
||||||
|
color: #666666;
|
||||||
|
padding: 32dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-apps p {
|
||||||
|
margin: 8dp 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-apps .hint {
|
||||||
|
font-size: 12dp;
|
||||||
|
color: #555555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* App Icon */
|
||||||
|
.app-icon {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 80dp;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon:hover .app-icon-image {
|
||||||
|
transform: scale(1.1);
|
||||||
|
background-color: #3d3d5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-image {
|
||||||
|
width: 56dp;
|
||||||
|
height: 56dp;
|
||||||
|
border-radius: 12dp;
|
||||||
|
background-color: #2d2d44;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.1s, background-color 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-image img {
|
||||||
|
width: 48dp;
|
||||||
|
height: 48dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-placeholder {
|
||||||
|
font-size: 24dp;
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-label {
|
||||||
|
font-size: 11dp;
|
||||||
|
color: #cccccc;
|
||||||
|
margin-top: 6dp;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 80dp;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation Bar */
|
||||||
|
.nav-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 24dp;
|
||||||
|
height: 40dp;
|
||||||
|
background-color: #0f0f1a;
|
||||||
|
align-items: center;
|
||||||
|
border-top-width: 1dp;
|
||||||
|
border-top-color: #2d2d44;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-hint {
|
||||||
|
font-size: 11dp;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
36
designer/assets/simulator/home.rml
Normal file
36
designer/assets/simulator/home.rml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<rml>
|
||||||
|
<head>
|
||||||
|
<title>Mosis Simulator</title>
|
||||||
|
<link type="text/rcss" href="home.rcss"/>
|
||||||
|
<script src="simulator.lua"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="status-bar">
|
||||||
|
<span class="status-time" id="status-time">12:00</span>
|
||||||
|
<span class="status-icons">
|
||||||
|
<span class="status-wifi">●</span>
|
||||||
|
<span class="status-battery">■</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="home-content">
|
||||||
|
<div class="home-header">
|
||||||
|
<h1>Test Apps</h1>
|
||||||
|
<p class="subtitle">Tap an app to launch</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="app-grid" id="app-grid">
|
||||||
|
<!-- Apps will be populated dynamically by Lua -->
|
||||||
|
<div class="no-apps" id="no-apps">
|
||||||
|
<p>No apps found</p>
|
||||||
|
<p class="hint">Place apps in test-apps/ folder</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-bar">
|
||||||
|
<div class="nav-hint">ESC = Back</div>
|
||||||
|
<div class="nav-hint">F5 = Reload</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</rml>
|
||||||
158
designer/assets/simulator/simulator.lua
Normal file
158
designer/assets/simulator/simulator.lua
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
-- Simulator Home Screen Logic
|
||||||
|
|
||||||
|
local apps = {}
|
||||||
|
|
||||||
|
-- Helper to get document (may not be available immediately)
|
||||||
|
local function getDoc()
|
||||||
|
if document then
|
||||||
|
return document
|
||||||
|
end
|
||||||
|
if rmlui and rmlui.contexts and rmlui.contexts.main then
|
||||||
|
local ctx = rmlui.contexts.main
|
||||||
|
if ctx.documents then
|
||||||
|
for i, doc in ipairs(ctx.documents) do
|
||||||
|
if doc then
|
||||||
|
return doc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Populate the app grid with discovered apps
|
||||||
|
function populateAppGrid()
|
||||||
|
local doc = getDoc()
|
||||||
|
if not doc then
|
||||||
|
print("[Simulator] Document not available for populateAppGrid")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local grid = doc:GetElementById("app-grid")
|
||||||
|
if not grid then
|
||||||
|
print("[Simulator] app-grid element not found")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Clear existing content
|
||||||
|
grid.inner_rml = ""
|
||||||
|
|
||||||
|
if #apps == 0 then
|
||||||
|
grid.inner_rml = [[
|
||||||
|
<div class="no-apps">
|
||||||
|
<p>No apps found</p>
|
||||||
|
<p class="hint">Place apps in test-apps/ folder</p>
|
||||||
|
</div>
|
||||||
|
]]
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Build app icons
|
||||||
|
local html = ""
|
||||||
|
for i, app in ipairs(apps) do
|
||||||
|
local icon_html
|
||||||
|
if app.icon and app.icon ~= "" then
|
||||||
|
icon_html = string.format('<img src="%s"/>', app.icon)
|
||||||
|
else
|
||||||
|
icon_html = '<span class="app-icon-placeholder">■</span>'
|
||||||
|
end
|
||||||
|
|
||||||
|
html = html .. string.format([[
|
||||||
|
<div class="app-icon" onclick="launchApp('%s')">
|
||||||
|
<div class="app-icon-image">%s</div>
|
||||||
|
<span class="app-icon-label">%s</span>
|
||||||
|
</div>
|
||||||
|
]], app.id, icon_html, app.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
grid.inner_rml = html
|
||||||
|
print("[Simulator] Populated " .. #apps .. " apps")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get apps from C++ and populate the grid
|
||||||
|
function refreshApps()
|
||||||
|
if simulator and simulator.getApps then
|
||||||
|
apps = simulator.getApps()
|
||||||
|
print("[Simulator] Got " .. #apps .. " apps from C++")
|
||||||
|
populateAppGrid()
|
||||||
|
else
|
||||||
|
print("[Simulator] simulator.getApps not available")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Called from C++ after apps are discovered (backup method)
|
||||||
|
function setApps(app_list)
|
||||||
|
apps = app_list
|
||||||
|
populateAppGrid()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Launch an app by ID
|
||||||
|
function launchApp(app_id)
|
||||||
|
print("[Simulator] Launching app: " .. app_id)
|
||||||
|
|
||||||
|
-- Find the app
|
||||||
|
for _, app in ipairs(apps) do
|
||||||
|
if app.id == app_id then
|
||||||
|
-- Call C++ function to launch the app
|
||||||
|
if simulator and simulator.launchApp then
|
||||||
|
simulator.launchApp(app.entry, app.path, app.id)
|
||||||
|
else
|
||||||
|
print("[Simulator] Error: simulator.launchApp not available")
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
print("[Simulator] App not found: " .. app_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update the time display
|
||||||
|
function updateTime()
|
||||||
|
local doc = getDoc()
|
||||||
|
if not doc then return end
|
||||||
|
|
||||||
|
local timeEl = doc:GetElementById("status-time")
|
||||||
|
if timeEl then
|
||||||
|
-- Use os.date if available, otherwise show static time
|
||||||
|
local time_str = "12:00"
|
||||||
|
if os and os.date then
|
||||||
|
time_str = os.date("%H:%M")
|
||||||
|
end
|
||||||
|
timeEl.inner_rml = time_str
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Initialize
|
||||||
|
print("[Simulator] Home screen loaded")
|
||||||
|
|
||||||
|
-- Try immediate initialization first
|
||||||
|
local doc = getDoc()
|
||||||
|
if doc then
|
||||||
|
print("[Simulator] Document available immediately")
|
||||||
|
refreshApps()
|
||||||
|
updateTime()
|
||||||
|
elseif setInterval then
|
||||||
|
print("[Simulator] Document not ready, setting up timer")
|
||||||
|
-- Use a one-time timer to refresh apps after document is ready
|
||||||
|
local initTimerId = nil
|
||||||
|
local attempts = 0
|
||||||
|
initTimerId = setInterval(function()
|
||||||
|
attempts = attempts + 1
|
||||||
|
local d = getDoc()
|
||||||
|
if d then
|
||||||
|
print("[Simulator] Document ready after " .. attempts .. " attempts")
|
||||||
|
clearInterval(initTimerId)
|
||||||
|
refreshApps()
|
||||||
|
updateTime()
|
||||||
|
elseif attempts > 50 then
|
||||||
|
-- Give up after 5 seconds
|
||||||
|
print("[Simulator] Gave up waiting for document")
|
||||||
|
clearInterval(initTimerId)
|
||||||
|
end
|
||||||
|
end, 100)
|
||||||
|
|
||||||
|
-- Update time every minute
|
||||||
|
setInterval(updateTime, 60000)
|
||||||
|
else
|
||||||
|
print("[Simulator] No setInterval and no document - cannot init")
|
||||||
|
end
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include "src/desktop_sandbox.h"
|
#include "src/desktop_sandbox.h"
|
||||||
|
#include "src/app_discovery.h"
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
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::string g_hierarchy_file_path; // Continuous hierarchy dump path (--hierarchy)
|
||||||
static std::ofstream g_log_file;
|
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
|
// Forward declarations
|
||||||
bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height);
|
bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height);
|
||||||
void ShutdownRmlUi();
|
void ShutdownRmlUi();
|
||||||
bool LoadDocument(const std::string& path);
|
bool LoadDocument(const std::string& path);
|
||||||
void ReloadDocument();
|
void ReloadDocument();
|
||||||
|
void PopulateSimulatorApps();
|
||||||
|
|
||||||
// GLFW callbacks
|
// GLFW callbacks
|
||||||
static void ErrorCallback(int error, const char* description) {
|
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()) {
|
if (g_action_recorder && g_action_recorder->IsRecording()) {
|
||||||
g_action_recorder->RecordButton("back");
|
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->ProcessKeyDown(Rml::Input::KI_ESCAPE, 0);
|
||||||
g_context->ProcessKeyUp(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"
|
<< " --assets PATH Set assets directory (default: derived from document)\n"
|
||||||
<< " --log FILE Write all log messages to file\n"
|
<< " --log FILE Write all log messages to file\n"
|
||||||
<< " --hierarchy FILE Continuously dump UI hierarchy to JSON\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"
|
<< "\nTest modes:\n"
|
||||||
<< " --record FILE Record user actions to JSON file\n"
|
<< " --record FILE Record user actions to JSON file\n"
|
||||||
<< " --playback FILE Playback actions from 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) {
|
} else if (arg == "--dump-hierarchy" && i + 1 < argc) {
|
||||||
g_test_mode = TestMode::DumpHierarchy;
|
g_test_mode = TestMode::DumpHierarchy;
|
||||||
g_test_output_path = argv[++i];
|
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] != '-') {
|
} else if (arg[0] != '-') {
|
||||||
document_path = arg;
|
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
|
// Default document
|
||||||
if (document_path.empty()) {
|
if (document_path.empty()) {
|
||||||
document_path = "apps/home/home.rml";
|
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;
|
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
|
// Initialize test mode components
|
||||||
if (g_test_mode == TestMode::Record) {
|
if (g_test_mode == TestMode::Record) {
|
||||||
g_action_recorder = new mosis::testing::ActionRecorder(g_width, g_height);
|
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");
|
lua_setglobal(L, "loadScreen");
|
||||||
std::cout << "Registered Lua loadScreen function" << std::endl;
|
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
|
// Load fonts - search for fonts directory in multiple locations
|
||||||
std::string fonts_root;
|
std::string fonts_root;
|
||||||
std::vector<std::string> font_search_paths = {
|
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", // If assets_path is test-app dir
|
||||||
assets_path + "/../../src/main/assets/fonts",
|
assets_path + "/../../src/main/assets/fonts",
|
||||||
std::filesystem::absolute("src/main/assets/fonts").string(),
|
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) {
|
for (const auto& search_path : font_search_paths) {
|
||||||
if (std::filesystem::exists(search_path + "/LatoLatin-Regular.ttf")) {
|
if (std::filesystem::exists(search_path + "/LatoLatin-Regular.ttf")) {
|
||||||
@@ -767,5 +1022,63 @@ void ReloadDocument() {
|
|||||||
LoadDocument(g_current_document_path);
|
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;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
91
designer/src/app_discovery.cpp
Normal file
91
designer/src/app_discovery.cpp
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// D:\Dev\Mosis\MosisService\designer\src\app_discovery.cpp
|
||||||
|
#include "app_discovery.h"
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace mosis {
|
||||||
|
|
||||||
|
std::vector<AppInfo> AppDiscovery::DiscoverApps(const std::string& apps_directory) {
|
||||||
|
std::vector<AppInfo> apps;
|
||||||
|
|
||||||
|
if (!fs::exists(apps_directory) || !fs::is_directory(apps_directory)) {
|
||||||
|
std::cerr << "AppDiscovery: Directory not found: " << apps_directory << std::endl;
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "AppDiscovery: Scanning " << apps_directory << std::endl;
|
||||||
|
|
||||||
|
for (const auto& entry : fs::directory_iterator(apps_directory)) {
|
||||||
|
if (!entry.is_directory()) continue;
|
||||||
|
|
||||||
|
std::string app_dir = entry.path().string();
|
||||||
|
AppInfo info;
|
||||||
|
|
||||||
|
if (LoadAppManifest(app_dir, info)) {
|
||||||
|
apps.push_back(std::move(info));
|
||||||
|
std::cout << "AppDiscovery: Found app '" << apps.back().name
|
||||||
|
<< "' (" << apps.back().id << ")" << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "AppDiscovery: Found " << apps.size() << " apps" << std::endl;
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppDiscovery::LoadAppManifest(const std::string& app_directory, AppInfo& info) {
|
||||||
|
fs::path manifest_path = fs::path(app_directory) / "manifest.json";
|
||||||
|
|
||||||
|
if (!fs::exists(manifest_path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::ifstream file(manifest_path);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
std::cerr << "AppDiscovery: Cannot open " << manifest_path << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
json j;
|
||||||
|
file >> j;
|
||||||
|
|
||||||
|
// Required fields
|
||||||
|
if (!j.contains("id") || !j.contains("name") || !j.contains("entry")) {
|
||||||
|
std::cerr << "AppDiscovery: Missing required fields in " << manifest_path << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
info.id = j["id"].get<std::string>();
|
||||||
|
info.name = j["name"].get<std::string>();
|
||||||
|
info.entry = j["entry"].get<std::string>();
|
||||||
|
|
||||||
|
// Optional fields
|
||||||
|
info.version = j.value("version", "1.0.0");
|
||||||
|
info.icon = j.value("icon", "icon.tga");
|
||||||
|
info.description = j.value("description", "");
|
||||||
|
|
||||||
|
// Normalize path separators
|
||||||
|
info.app_path = app_directory;
|
||||||
|
std::replace(info.app_path.begin(), info.app_path.end(), '\\', '/');
|
||||||
|
|
||||||
|
// Verify entry file exists
|
||||||
|
fs::path entry_path = fs::path(app_directory) / info.entry;
|
||||||
|
if (!fs::exists(entry_path)) {
|
||||||
|
std::cerr << "AppDiscovery: Entry file not found: " << entry_path << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (const std::exception& e) {
|
||||||
|
std::cerr << "AppDiscovery: Error parsing " << manifest_path << ": " << e.what() << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mosis
|
||||||
32
designer/src/app_discovery.h
Normal file
32
designer/src/app_discovery.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// D:\Dev\Mosis\MosisService\designer\src\app_discovery.h
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace mosis {
|
||||||
|
|
||||||
|
struct AppInfo {
|
||||||
|
std::string id; // e.g., "com.mosis.sandbox-test"
|
||||||
|
std::string name; // Display name
|
||||||
|
std::string version; // Version string
|
||||||
|
std::string entry; // Entry point (e.g., "main.rml")
|
||||||
|
std::string icon; // Icon filename (e.g., "icon.tga")
|
||||||
|
std::string description; // App description
|
||||||
|
std::string app_path; // Full path to app directory
|
||||||
|
|
||||||
|
// Computed paths
|
||||||
|
std::string GetEntryPath() const { return app_path + "/" + entry; }
|
||||||
|
std::string GetIconPath() const { return app_path + "/" + icon; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class AppDiscovery {
|
||||||
|
public:
|
||||||
|
// Scan a directory for apps (folders with manifest.json)
|
||||||
|
static std::vector<AppInfo> DiscoverApps(const std::string& apps_directory);
|
||||||
|
|
||||||
|
// Load a single app's manifest
|
||||||
|
static bool LoadAppManifest(const std::string& app_directory, AppInfo& info);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mosis
|
||||||
Reference in New Issue
Block a user