Compare commits

..

2 Commits

Author SHA1 Message Date
ea44f0bba4 Add simulator mode with app discovery and mosis.apps API
Simulator mode (--simulator flag):
- Starts from main home.rml instead of separate simulator home
- Discovers apps from test-apps folder automatically
- Shows discovered apps in home screen grid

mosis.apps API for Lua:
- getInstalled() returns array of discovered apps
- launch(package_id) starts an app with its own sandbox

goHome improvements:
- Uses g_main_assets_path to find home.rml correctly
- Works when running test apps directly
- Properly clears current app state

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 15:01:40 +01:00
984e8715d7 Fix desktop designer click handling and add goHome API
Designer click handling:
- Fix DPI scaling in MouseButtonCallback and CursorPosCallback
- Scale coordinates from window space to framebuffer/RmlUi context
- Remove window resizing in ResizeToPhone (caused DPI mismatches)

Test framework:
- Fix SendMouseDown to use MOUSEEVENTF_MOVE before button down
- Remove double-scaling in ScaleToPhysical (WindowController handles it)
- All 5 UI navigation tests now pass

Kernel API:
- Add goHome() Lua function to return to home screen
- Stops any running third-party apps before navigating

Test app:
- Update sandbox-test to use goHome() instead of goBack()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 14:52:15 +01:00
5 changed files with 321 additions and 86 deletions

View File

@@ -10,19 +10,20 @@
using namespace mosis::test;
// Helper: Scale coordinates from hierarchy (logical) to window (physical) space
// Helper: Scale coordinates from hierarchy to phone space
// The WindowController expects coordinates in phone space (540x960) and handles DPI scaling internally
void ScaleToPhysical(TestContext& ctx, int& x, int& y) {
// The hierarchy reports coordinates in RmlUi's logical space
// which may be DPI-scaled. We need to convert to physical window coordinates.
// The hierarchy reports coordinates in RmlUi's context space (540x960)
// which matches the phone resolution. No scaling needed since
// WindowController::SendClick handles DPI scaling internally.
// Just validate the hierarchy dimensions match expected phone size.
int hierarchyWidth = ctx.hierarchy.GetWidth();
int hierarchyHeight = ctx.hierarchy.GetHeight();
int windowWidth = ctx.window.GetInfo().clientWidth;
int windowHeight = ctx.window.GetInfo().clientHeight;
if (hierarchyWidth > 0 && hierarchyHeight > 0) {
x = static_cast<int>(x * static_cast<float>(windowWidth) / hierarchyWidth);
y = static_cast<int>(y * static_cast<float>(windowHeight) / hierarchyHeight);
if (hierarchyWidth != 540 || hierarchyHeight != 960) {
std::cerr << " Warning: Unexpected hierarchy size " << hierarchyWidth << "x" << hierarchyHeight << std::endl;
}
// Coordinates stay in phone space (540x960) - no scaling needed
}
// Helper: Click on an element found by ID in the hierarchy

View File

@@ -107,33 +107,47 @@ LPARAM WindowController::PhoneToClientLParam(int phoneX, int phoneY) {
bool WindowController::SendMouseDown(int phoneX, int phoneY) {
if (!m_hwnd) return false;
// Convert phone coordinates to client coordinates
// Convert phone coordinates to client coordinates (using window scale)
int clientX = static_cast<int>(phoneX * m_info.scaleX);
int clientY = static_cast<int>(phoneY * m_info.scaleY);
// Calculate screen coordinates
int screenX = m_info.clientX + clientX;
int screenY = m_info.clientY + clientY;
// Get DPI info for debugging
UINT dpi = GetDpiForWindow(m_hwnd);
// Calculate screen coordinates from client position
// On DPI-aware systems, Windows APIs return consistent coordinate spaces
int screenX = m_info.clientX + clientX;
int screenY = m_info.clientY + clientY;
// Ensure window is foreground before clicking
SetForegroundWindow(m_hwnd);
Sleep(10); // Small delay
Sleep(10);
// Use SendInput for GLFW compatibility
SetCursorPos(screenX, screenY);
Sleep(10); // Small delay for cursor move
// Get screen dimensions for absolute coordinate conversion
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
INPUT input = {};
input.type = INPUT_MOUSE;
input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
SendInput(1, &input, sizeof(INPUT));
// Convert to normalized absolute coordinates (0-65535)
LONG absX = static_cast<LONG>((screenX * 65535) / screenWidth);
LONG absY = static_cast<LONG>((screenY * 65535) / screenHeight);
std::cout << "MouseDown at phone(" << phoneX << "," << phoneY << ") -> screen("
<< screenX << "," << screenY << ") dpi=" << dpi << std::endl;
// Send mouse move then button down
INPUT moveInput = {};
moveInput.type = INPUT_MOUSE;
moveInput.mi.dx = absX;
moveInput.mi.dy = absY;
moveInput.mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE;
SendInput(1, &moveInput, sizeof(INPUT));
Sleep(20);
INPUT clickInput = {};
clickInput.type = INPUT_MOUSE;
clickInput.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
SendInput(1, &clickInput, sizeof(INPUT));
std::cout << "MouseDown at phone(" << phoneX << "," << phoneY << ") -> client("
<< clientX << "," << clientY << ") screen(" << screenX << "," << screenY
<< ") dpi=" << dpi << std::endl;
return true;
}
@@ -226,38 +240,20 @@ bool WindowController::Close() {
bool WindowController::ResizeToPhone() {
if (!m_hwnd) return false;
// Get current window rect to preserve position
// Skip resizing - the designer creates the window at the correct size
// and resizing causes DPI scaling mismatches between window and framebuffer.
// Just move window to top-left for consistent test positioning.
RECT currentRect;
::GetWindowRect(m_hwnd, &currentRect);
int currentWidth = currentRect.right - currentRect.left;
int currentHeight = currentRect.bottom - currentRect.top;
// Calculate window size needed for phone-sized client area
RECT desiredRect = {0, 0, PHONE_WIDTH, PHONE_HEIGHT};
DWORD style = ::GetWindowLong(m_hwnd, GWL_STYLE);
DWORD exStyle = ::GetWindowLong(m_hwnd, GWL_EXSTYLE);
::AdjustWindowRectEx(&desiredRect, style, FALSE, exStyle);
std::cout << "ResizeToPhone: keeping current size " << currentWidth << "x" << currentHeight
<< ", moving to (0,0)" << std::endl;
int newWidth = desiredRect.right - desiredRect.left;
int newHeight = desiredRect.bottom - desiredRect.top;
// Check screen dimensions
int screenWidth = ::GetSystemMetrics(SM_CXSCREEN);
int screenHeight = ::GetSystemMetrics(SM_CYSCREEN);
std::cout << "ResizeToPhone: screen=" << screenWidth << "x" << screenHeight
<< ", needed=" << newWidth << "x" << newHeight << std::endl;
// If screen is too small, we can't resize to full phone size
if (newHeight > screenHeight) {
std::cout << " Warning: Screen too small for full phone resolution, keeping current size" << std::endl;
return true; // Not an error, just can't resize
}
// Move window to top-left to ensure it fits on screen
int newX = 0;
int newY = 0;
std::cout << " Moving to (" << newX << "," << newY << ") size " << newWidth << "x" << newHeight << std::endl;
::MoveWindow(m_hwnd, newX, newY, newWidth, newHeight, TRUE);
// Move window to top-left for consistent positioning
::MoveWindow(m_hwnd, 0, 0, currentWidth, currentHeight, TRUE);
// Re-acquire window info
std::this_thread::sleep_for(std::chrono::milliseconds(100));
@@ -274,26 +270,36 @@ bool WindowController::SendClickFromBottom(int clientX, int offsetFromBottom) {
int screenX = m_info.clientX + clientX;
int screenY = m_info.clientY + clientY;
// Save current cursor position
POINT oldPos;
GetCursorPos(&oldPos);
// Ensure window is active
SetForegroundWindow(m_hwnd);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// Move cursor and click
SetCursorPos(screenX, screenY);
// Get screen dimensions for absolute coordinate conversion
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
INPUT input = {};
input.type = INPUT_MOUSE;
input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
SendInput(1, &input, sizeof(INPUT));
// Convert to normalized absolute coordinates (0-65535)
LONG absX = static_cast<LONG>((screenX * 65535) / screenWidth);
LONG absY = static_cast<LONG>((screenY * 65535) / screenHeight);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// Use SendInput with absolute move + click
INPUT inputs[3] = {};
input.mi.dwFlags = MOUSEEVENTF_LEFTUP;
SendInput(1, &input, sizeof(INPUT));
// Move cursor
inputs[0].type = INPUT_MOUSE;
inputs[0].mi.dx = absX;
inputs[0].mi.dy = absY;
inputs[0].mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE;
// Mouse down
inputs[1].type = INPUT_MOUSE;
inputs[1].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
// Mouse up
inputs[2].type = INPUT_MOUSE;
inputs[2].mi.dwFlags = MOUSEEVENTF_LEFTUP;
SendInput(3, inputs, sizeof(INPUT));
std::cout << "ClickFromBottom at client(" << clientX << "," << clientY
<< ") -> screen(" << screenX << "," << screenY << ")" << std::endl;

View File

@@ -66,6 +66,7 @@ static std::ofstream g_log_file;
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::string g_main_assets_path; // Path to main assets (for goHome)
static std::vector<mosis::AppInfo> g_discovered_apps;
static std::string g_current_app_id; // Currently running app (empty = home)
@@ -156,10 +157,30 @@ static void MouseButtonCallback(GLFWwindow* window, int button, int action, int
double xpos, ypos;
glfwGetCursorPos(window, &xpos, &ypos);
// 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<int>(xpos);
int mouseY = static_cast<int>(ypos);
// glfwGetCursorPos returns position in screen coordinates (same as window size)
// which may differ from framebuffer size on high-DPI displays.
// We need to scale to match the RmlUi context (which matches framebuffer).
int winWidth, winHeight;
glfwGetWindowSize(window, &winWidth, &winHeight);
int fbWidth, fbHeight;
glfwGetFramebufferSize(window, &fbWidth, &fbHeight);
// Scale cursor position: screen coords -> framebuffer coords -> RmlUi context
// On high DPI: winWidth=432, fbWidth=540, g_width=540
// Cursor in screen space needs to scale to framebuffer/context space
int mouseX = static_cast<int>(xpos * fbWidth / winWidth);
int mouseY = static_cast<int>(ypos * fbHeight / winHeight);
// Debug logging for click events
std::cout << "MouseButton: " << (action == GLFW_PRESS ? "DOWN" : "UP")
<< " at raw(" << xpos << "," << ypos << ") -> scaled(" << mouseX << "," << mouseY << ")"
<< " win=" << winWidth << "x" << winHeight << " fb=" << fbWidth << "x" << fbHeight << std::endl;
if (g_log_file.is_open()) {
g_log_file << "[DEBUG] MouseButton: " << (action == GLFW_PRESS ? "DOWN" : "UP")
<< " at raw(" << xpos << "," << ypos << ") -> scaled(" << mouseX << "," << mouseY << ")" << std::endl;
g_log_file.flush();
}
int key_modifier = 0;
if (mods & GLFW_MOD_CONTROL) key_modifier |= Rml::Input::KM_CTRL;
@@ -187,10 +208,15 @@ static void MouseButtonCallback(GLFWwindow* window, int button, int action, int
static void CursorPosCallback(GLFWwindow* window, double xpos, double ypos) {
if (!g_context) return;
// 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<int>(xpos);
int mouseY = static_cast<int>(ypos);
// Scale from screen coordinates to framebuffer/RmlUi context coordinates
int winWidth, winHeight;
glfwGetWindowSize(window, &winWidth, &winHeight);
int fbWidth, fbHeight;
glfwGetFramebufferSize(window, &fbWidth, &fbHeight);
int mouseX = static_cast<int>(xpos * fbWidth / winWidth);
int mouseY = static_cast<int>(ypos * fbHeight / winHeight);
g_context->ProcessMouseMove(mouseX, mouseY, 0);
}
@@ -339,27 +365,31 @@ int main(int argc, char* argv[]) {
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(),
// Find main assets home.rml for simulator mode
// Look for src/main/assets/apps/home/home.rml relative to test-apps
fs::path test_apps_fs = fs::path(g_test_apps_path);
std::vector<fs::path> home_search_paths = {
test_apps_fs.parent_path() / "src" / "main" / "assets" / "apps" / "home" / "home.rml",
fs::path("src/main/assets/apps/home/home.rml"),
fs::absolute("src/main/assets/apps/home/home.rml"),
};
for (const auto& path : sim_search_paths) {
for (const auto& path : home_search_paths) {
if (fs::exists(path)) {
g_simulator_home_path = fs::absolute(path).string();
break;
}
}
// Override document path to simulator home
// Override document path to main home screen
if (!g_simulator_home_path.empty()) {
document_path = g_simulator_home_path;
// Also set the main assets path for proper resource loading
g_main_assets_path = fs::path(g_simulator_home_path).parent_path().parent_path().parent_path().string();
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;
std::cout << "Home screen: " << g_simulator_home_path << std::endl;
} else {
std::cerr << "Warning: Could not find simulator home.rml" << std::endl;
std::cerr << "Warning: Could not find home.rml for simulator" << std::endl;
g_simulator_mode = false;
}
}
@@ -428,6 +458,26 @@ int main(int argc, char* argv[]) {
// Make assets_path absolute
assets_path = fs::absolute(assets_path).string();
std::cout << "Assets path: " << assets_path << std::endl;
// Determine main assets path (where home.rml lives)
// This is important for goHome() when running test apps
g_main_assets_path = assets_path;
// Check if we're running from test-apps folder
fs::path assets_fs = fs::path(assets_path);
if (assets_fs.filename().string().find("com.") == 0 ||
assets_fs.parent_path().filename() == "test-apps") {
// Running a test app - find the main assets
fs::path test_apps_root = assets_fs.parent_path();
if (test_apps_root.filename() != "test-apps") {
test_apps_root = test_apps_root.parent_path();
}
fs::path main_assets = test_apps_root.parent_path() / "src" / "main" / "assets";
if (fs::exists(main_assets / "apps" / "home" / "home.rml")) {
g_main_assets_path = main_assets.string();
std::cout << "Main assets: " << g_main_assets_path << std::endl;
}
}
std::cout << "Resolution: " << g_width << "x" << g_height << std::endl;
// Print mode info
@@ -750,7 +800,46 @@ bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height
return 1;
});
lua_setglobal(L, "loadScreen");
std::cout << "Registered Lua loadScreen function" << std::endl;
// Register goHome function to return to home screen
lua_pushcfunction(L, [](lua_State* L) -> int {
if (!g_context) {
lua_pushboolean(L, false);
return 1;
}
std::cout << "goHome called - returning to home screen" << std::endl;
// Close existing documents (except debugger)
while (g_context->GetNumDocuments() > 1) {
auto* doc = g_context->GetDocument(0);
if (doc && doc->GetSourceURL().find("__rmlui") == std::string::npos) {
doc->Close();
} else {
break;
}
}
// Load home screen from main assets path
std::string home_path = (fs::path(g_main_assets_path) / "apps" / "home" / "home.rml").string();
std::cout << "Loading home from: " << home_path << std::endl;
auto* document = g_context->LoadDocument(home_path);
if (document) {
document->Show();
g_current_document_path = home_path;
g_current_screen_url = home_path;
g_current_app_id = ""; // Clear current app
Rml::Log::Message(Rml::Log::LT_INFO, "Returned to home screen");
lua_pushboolean(L, true);
} else {
Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to load home screen from: %s", home_path.c_str());
lua_pushboolean(L, false);
}
return 1;
});
lua_setglobal(L, "goHome");
std::cout << "Registered Lua loadScreen and goHome functions" << std::endl;
// Register simulator API (if in simulator mode)
if (g_simulator_mode) {
@@ -913,6 +1002,93 @@ bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height
lua_setglobal(L, "simulator");
std::cout << "Registered simulator Lua API" << std::endl;
// Also register mosis.apps API for compatibility with home.lua
// Create mosis table (or get existing)
lua_getglobal(L, "mosis");
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_newtable(L);
}
// Create mosis.apps table
lua_newtable(L);
// mosis.apps.getInstalled() - returns installed 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);
std::cout << "Discovered " << g_discovered_apps.size() << " apps in " << g_test_apps_path << std::endl;
}
// Create apps table (array format expected by home.lua)
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.name.c_str());
lua_setfield(L, -2, "name");
lua_pushstring(L, app.id.c_str());
lua_setfield(L, -2, "package_id");
lua_pushstring(L, app.GetIconPath().c_str());
lua_setfield(L, -2, "icon");
lua_pushboolean(L, false); // Not a system app
lua_setfield(L, -2, "is_system_app");
lua_pushstring(L, app.app_path.c_str());
lua_setfield(L, -2, "install_path");
lua_pushstring(L, app.entry.c_str());
lua_setfield(L, -2, "entry_point");
lua_settable(L, -3); // apps[index] = app_table
}
return 1;
});
lua_setfield(L, -2, "getInstalled");
// mosis.apps.launch(package_id) - launch an app
lua_pushcfunction(L, [](lua_State* L) -> int {
const char* package_id = luaL_checkstring(L, 1);
// Find the app
for (const auto& app : g_discovered_apps) {
if (app.id == package_id) {
std::cout << "mosis.apps.launch: Starting " << package_id << std::endl;
g_current_app_id = package_id;
// Reset sandbox for the new app
if (g_sandbox) {
g_sandbox->UnregisterAPIs(L);
g_sandbox->Reset();
mosis::DesktopSandboxConfig config;
config.app_id = package_id;
config.data_root = app.app_path + "/sandbox_data";
g_sandbox = std::make_unique<mosis::DesktopSandbox>(config);
g_sandbox->RegisterAPIs(L);
}
lua_pushboolean(L, true);
return 1;
}
}
std::cerr << "mosis.apps.launch: App not found: " << package_id << std::endl;
lua_pushboolean(L, false);
return 1;
});
lua_setfield(L, -2, "launch");
lua_setfield(L, -2, "apps"); // mosis.apps = apps_table
lua_setglobal(L, "mosis");
std::cout << "Registered mosis.apps Lua API" << std::endl;
}
// Load fonts - search for fonts directory in multiple locations

View File

@@ -257,13 +257,65 @@ static int LuaLoadScreen(lua_State* L)
return 1;
}
// Lua function to go back to home screen
static int LuaGoHome(lua_State* L)
{
if (!g_context)
{
lua_pushboolean(L, false);
return 1;
}
Logger::Log("goHome called - returning to home screen");
// Stop any running third-party app
if (g_sandbox_manager) {
auto running_apps = g_sandbox_manager->GetRunningApps();
for (const auto& app_id : running_apps) {
// Don't stop system apps
if (app_id.find("com.mosis.") == 0 && app_id != "com.mosis.home") {
Logger::Log(std::format("Stopping app: {}", app_id));
g_sandbox_manager->StopApp(app_id);
}
}
}
// Load home screen
const char* home_path = "apps/home/home.rml";
// Unload current document
if (g_document)
{
g_context->UnloadDocument(g_document);
g_document = nullptr;
}
// Load home document
g_document = g_context->LoadDocument(home_path);
if (g_document)
{
g_document->Show();
Logger::Log("Returned to home screen");
lua_pushboolean(L, true);
}
else
{
Logger::Log("Failed to load home screen");
lua_pushboolean(L, false);
}
return 1;
}
// Register Lua functions for navigation
static void RegisterLuaFunctions(const std::string& current_app_id, bool is_system_app)
{
lua_State* L = Rml::Lua::Interpreter::GetLuaState();
lua_pushcfunction(L, LuaLoadScreen);
lua_setglobal(L, "loadScreen");
Logger::Log("Registered Lua loadScreen function");
lua_pushcfunction(L, LuaGoHome);
lua_setglobal(L, "goHome");
Logger::Log("Registered Lua loadScreen and goHome functions");
// Register app management APIs
if (g_app_manager && g_update_service) {

View File

@@ -6,8 +6,8 @@
</head>
<body>
<div class="app-bar">
<div class="app-bar-nav btn-icon" onclick="goBack()">
<span class="icon"></span>
<div class="app-bar-nav btn-icon" onclick="goHome()">
<span class="icon">&lt;</span>
</div>
<div class="app-bar-title">Sandbox Test</div>
</div>