work in progress

This commit is contained in:
2026-01-16 12:43:06 +01:00
parent 77a9579025
commit c8ee7defe8
236 changed files with 11405 additions and 28 deletions

View File

@@ -0,0 +1,276 @@
// Data models implementation
#include "data_models.h"
#include <iostream>
// Global data instances
SettingsData g_settings;
PhoneData g_phone;
BrowserData g_browser;
std::vector<Conversation> g_conversations;
std::vector<Contact> g_contacts;
int g_selected_conversation = -1;
int g_selected_contact = -1;
// Data model handles
Rml::DataModelHandle g_settings_model;
Rml::DataModelHandle g_phone_model;
Rml::DataModelHandle g_messages_model;
Rml::DataModelHandle g_contacts_model;
Rml::DataModelHandle g_browser_model;
void initializeSampleData()
{
// Initialize contacts
g_contacts = {
{1, "Alice Johnson", "+1 (555) 123-4567", "alice@example.com", "#E91E63", "A"},
{2, "Andrew Smith", "+1 (555) 234-5678", "andrew@example.com", "#9C27B0", "A"},
{3, "Bob Williams", "+1 (555) 345-6789", "bob@example.com", "#2196F3", "B"},
{4, "Brian Davis", "+1 (555) 456-7890", "brian@example.com", "#00BCD4", "B"},
{5, "Carol Martinez", "+1 (555) 567-8901", "carol@example.com", "#4CAF50", "C"},
{6, "David Lee", "+1 (555) 678-9012", "david@example.com", "#FF9800", "D"},
{7, "John Wilson", "+1 (555) 789-0123", "john@example.com", "#F44336", "J"},
{8, "Mom", "+1 (555) 890-1234", "mom@example.com", "#673AB7", "M"},
{9, "Mike Brown", "+1 (555) 901-2345", "mike@example.com", "#3F51B5", "M"},
{10, "Sarah Taylor", "+1 (555) 012-3456", "sarah@example.com", "#009688", "S"}
};
// Initialize conversations
g_conversations = {
{1, "John Wilson", "#4CAF50", "Hey, are you coming to the party tonight?", "2:30 PM", 2, {
{"them", "Hey!", "2:25 PM"},
{"them", "What are you up to?", "2:26 PM"},
{"me", "Not much, just working", "2:27 PM"},
{"them", "Cool! There's a party at Mike's tonight", "2:28 PM"},
{"them", "Hey, are you coming to the party tonight?", "2:30 PM"}
}},
{2, "Mom", "#673AB7", "Don't forget to call your grandmother!", "1:15 PM", 0, {
{"them", "Hi sweetie!", "1:00 PM"},
{"me", "Hi Mom!", "1:05 PM"},
{"them", "How are you doing?", "1:10 PM"},
{"me", "I'm good, how are you?", "1:12 PM"},
{"them", "Don't forget to call your grandmother!", "1:15 PM"}
}},
{3, "Alice Johnson", "#E91E63", "Thanks for the help with the project!", "Yesterday", 0, {
{"me", "Here's the file you needed", "Yesterday"},
{"them", "Thanks for the help with the project!", "Yesterday"}
}},
{4, "Bob Williams", "#2196F3", "Did you see the game last night?", "Yesterday", 0, {
{"them", "Did you see the game last night?", "Yesterday"}
}},
{5, "Work Group", "#FF9800", "Sarah: Meeting moved to 3pm", "Mon", 0, {
{"Sarah", "Meeting moved to 3pm", "Mon"}
}},
{6, "Sarah Taylor", "#009688", "See you at the coffee shop!", "Sun", 0, {
{"them", "See you at the coffee shop!", "Sun"}
}},
{7, "David Lee", "#F44336", "Great talking to you!", "Sat", 0, {
{"them", "Great talking to you!", "Sat"}
}}
};
// Initialize browser tabs
g_browser.tabs = {
{"example.com", "Example Domain", false},
{"rmlui.github.io", "RmlUi Documentation", false},
{"github.com", "GitHub", false}
};
std::cout << "Sample data initialized: " << g_contacts.size() << " contacts, "
<< g_conversations.size() << " conversations, "
<< g_browser.tabs.size() << " browser tabs" << std::endl;
}
void setupDataModels(Rml::Context* context)
{
// Messages data model
if (auto constructor = context->CreateDataModel("messages"))
{
if (auto msg_handle = constructor.RegisterStruct<Message>())
{
msg_handle.RegisterMember("from", &Message::from);
msg_handle.RegisterMember("text", &Message::text);
msg_handle.RegisterMember("time", &Message::time);
}
if (auto conv_handle = constructor.RegisterStruct<Conversation>())
{
conv_handle.RegisterMember("id", &Conversation::id);
conv_handle.RegisterMember("name", &Conversation::name);
conv_handle.RegisterMember("color", &Conversation::color);
conv_handle.RegisterMember("last_message", &Conversation::last_message);
conv_handle.RegisterMember("time", &Conversation::time);
conv_handle.RegisterMember("unread", &Conversation::unread);
conv_handle.RegisterMember("messages", &Conversation::messages);
}
constructor.RegisterArray<std::vector<Message>>();
constructor.RegisterArray<std::vector<Conversation>>();
constructor.Bind("conversations", &g_conversations);
constructor.Bind("selected_conversation", &g_selected_conversation);
constructor.BindEventCallback("select_conversation",
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
if (!args.empty()) {
g_selected_conversation = args[0].Get<int>();
std::cout << "Selected conversation: " << g_selected_conversation << std::endl;
handle.DirtyVariable("selected_conversation");
}
});
g_messages_model = constructor.GetModelHandle();
std::cout << "Messages data model created" << std::endl;
}
// Contacts data model
if (auto constructor = context->CreateDataModel("contacts"))
{
if (auto contact_handle = constructor.RegisterStruct<Contact>())
{
contact_handle.RegisterMember("id", &Contact::id);
contact_handle.RegisterMember("name", &Contact::name);
contact_handle.RegisterMember("phone", &Contact::phone);
contact_handle.RegisterMember("email", &Contact::email);
contact_handle.RegisterMember("color", &Contact::color);
contact_handle.RegisterMember("initial", &Contact::initial);
}
constructor.RegisterArray<std::vector<Contact>>();
constructor.Bind("contacts", &g_contacts);
constructor.Bind("selected_contact", &g_selected_contact);
constructor.BindEventCallback("select_contact",
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
if (!args.empty()) {
g_selected_contact = args[0].Get<int>();
std::cout << "Selected contact: " << g_selected_contact << std::endl;
handle.DirtyVariable("selected_contact");
}
});
g_contacts_model = constructor.GetModelHandle();
std::cout << "Contacts data model created" << std::endl;
}
// Settings data model
if (auto constructor = context->CreateDataModel("settings"))
{
constructor.Bind("wifi", &g_settings.wifi);
constructor.Bind("bluetooth", &g_settings.bluetooth);
constructor.Bind("airplane_mode", &g_settings.airplane_mode);
constructor.Bind("location", &g_settings.location);
constructor.Bind("dark_mode", &g_settings.dark_mode);
constructor.Bind("notifications", &g_settings.notifications);
constructor.Bind("do_not_disturb", &g_settings.do_not_disturb);
constructor.Bind("user_name", &g_settings.user_name);
constructor.Bind("user_email", &g_settings.user_email);
constructor.Bind("wifi_network", &g_settings.wifi_network);
constructor.Bind("battery_percent", &g_settings.battery_percent);
constructor.Bind("battery_remaining", &g_settings.battery_remaining);
constructor.Bind("storage_used", &g_settings.storage_used);
g_settings_model = constructor.GetModelHandle();
std::cout << "Settings data model created" << std::endl;
}
// Phone data model
if (auto constructor = context->CreateDataModel("phone"))
{
constructor.Bind("dial_number", &g_phone.dial_number);
constructor.Bind("is_calling", &g_phone.is_calling);
constructor.Bind("call_contact", &g_phone.call_contact);
constructor.Bind("call_duration", &g_phone.call_duration);
constructor.BindEventCallback("dial_press",
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
if (!args.empty()) {
g_phone.dial_number += args[0].Get<Rml::String>();
std::cout << "Dial: " << g_phone.dial_number << std::endl;
handle.DirtyVariable("dial_number");
}
});
constructor.BindEventCallback("dial_backspace",
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
if (!g_phone.dial_number.empty()) {
g_phone.dial_number.pop_back();
handle.DirtyVariable("dial_number");
}
});
constructor.BindEventCallback("dial_clear",
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
g_phone.dial_number.clear();
handle.DirtyVariable("dial_number");
});
constructor.BindEventCallback("make_call",
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
if (!g_phone.dial_number.empty()) {
g_phone.is_calling = true;
g_phone.call_contact = g_phone.dial_number;
std::cout << "Calling: " << g_phone.call_contact << std::endl;
handle.DirtyVariable("is_calling");
handle.DirtyVariable("call_contact");
}
});
constructor.BindEventCallback("end_call",
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
g_phone.is_calling = false;
g_phone.call_contact = "";
g_phone.call_duration = "00:00";
handle.DirtyVariable("is_calling");
handle.DirtyVariable("call_contact");
handle.DirtyVariable("call_duration");
});
g_phone_model = constructor.GetModelHandle();
std::cout << "Phone data model created" << std::endl;
}
// Browser data model
if (auto constructor = context->CreateDataModel("browser"))
{
if (auto tab_handle = constructor.RegisterStruct<BrowserTab>())
{
tab_handle.RegisterMember("url", &BrowserTab::url);
tab_handle.RegisterMember("title", &BrowserTab::title);
tab_handle.RegisterMember("is_loading", &BrowserTab::is_loading);
}
constructor.RegisterArray<std::vector<BrowserTab>>();
constructor.Bind("current_url", &g_browser.current_url);
constructor.Bind("page_title", &g_browser.page_title);
constructor.Bind("page_content", &g_browser.page_content);
constructor.Bind("tabs", &g_browser.tabs);
constructor.Bind("current_tab", &g_browser.current_tab);
constructor.Bind("can_go_back", &g_browser.can_go_back);
constructor.Bind("can_go_forward", &g_browser.can_go_forward);
constructor.BindEventCallback("navigate",
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
if (!args.empty()) {
g_browser.current_url = args[0].Get<Rml::String>();
g_browser.page_title = "Loading...";
g_browser.can_go_back = true;
std::cout << "Navigating to: " << g_browser.current_url << std::endl;
handle.DirtyVariable("current_url");
handle.DirtyVariable("page_title");
handle.DirtyVariable("can_go_back");
}
});
constructor.BindEventCallback("refresh",
[](Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args) {
std::cout << "Refreshing page" << std::endl;
});
g_browser_model = constructor.GetModelHandle();
std::cout << "Browser data model created" << std::endl;
}
std::cout << "All data models initialized" << std::endl;
}

View File

@@ -0,0 +1,99 @@
// Data models for Mosis phone UI
#pragma once
#include <RmlUi/Core.h>
#include <string>
#include <vector>
// Settings data model
struct SettingsData {
bool wifi = true;
bool bluetooth = false;
bool airplane_mode = false;
bool location = true;
bool dark_mode = true;
bool notifications = true;
bool do_not_disturb = false;
// User profile
Rml::String user_name = "User Name";
Rml::String user_email = "user@example.com";
// Device info
Rml::String wifi_network = "Home_Network";
int battery_percent = 85;
Rml::String battery_remaining = "About 12h remaining";
Rml::String storage_used = "45.2 GB used of 128 GB";
};
// Phone state data model
struct PhoneData {
Rml::String dial_number = "";
bool is_calling = false;
Rml::String call_contact = "";
Rml::String call_duration = "00:00";
};
// Messages data model
struct Message {
Rml::String from;
Rml::String text;
Rml::String time;
};
struct Conversation {
int id;
Rml::String name;
Rml::String color;
Rml::String last_message;
Rml::String time;
int unread;
std::vector<Message> messages;
};
// Contact data model
struct Contact {
int id;
Rml::String name;
Rml::String phone;
Rml::String email;
Rml::String color;
Rml::String initial;
};
// Browser data model
struct BrowserTab {
Rml::String url;
Rml::String title;
bool is_loading;
};
struct BrowserData {
Rml::String current_url = "example.com";
Rml::String page_title = "Example Domain";
Rml::String page_content = "This domain is for use in illustrative examples.";
std::vector<BrowserTab> tabs;
int current_tab = 0;
bool can_go_back = false;
bool can_go_forward = false;
};
// Global data instances
extern SettingsData g_settings;
extern PhoneData g_phone;
extern BrowserData g_browser;
extern std::vector<Conversation> g_conversations;
extern std::vector<Contact> g_contacts;
extern int g_selected_conversation;
extern int g_selected_contact;
// Data model handles
extern Rml::DataModelHandle g_settings_model;
extern Rml::DataModelHandle g_phone_model;
extern Rml::DataModelHandle g_messages_model;
extern Rml::DataModelHandle g_contacts_model;
extern Rml::DataModelHandle g_browser_model;
// Functions
void initializeSampleData();
void setupDataModels(Rml::Context* context);

View File

@@ -0,0 +1,89 @@
// Desktop platform implementation (simplified - uses RmlUi backend for graphics)
#include "desktop_platform.h"
#include <iostream>
#include <chrono>
// Note: Graphics context and rendering is handled by RmlUi's backend.
// This platform implementation provides additional utilities and state management.
namespace mosis::desktop {
DesktopPlatform::DesktopPlatform()
: m_file_interface(std::make_unique<DesktopFileInterface>())
{
auto now = std::chrono::steady_clock::now();
m_start_time = std::chrono::duration<double>(now.time_since_epoch()).count();
}
DesktopPlatform::~DesktopPlatform() = default;
bool DesktopPlatform::Initialize(uint32_t width, uint32_t height, const char* title) {
m_width = width;
m_height = height;
// Graphics initialization is done by RmlUi Backend
return true;
}
void DesktopPlatform::Shutdown() {
// Graphics shutdown is done by RmlUi Backend
}
std::unique_ptr<IGraphicsContext> DesktopPlatform::CreateGraphicsContext() {
// Graphics context is managed by RmlUi Backend
return nullptr;
}
std::unique_ptr<IRenderTarget> DesktopPlatform::CreateRenderTarget(uint32_t width, uint32_t height) {
// Render targets are managed by RmlUi Backend
return nullptr;
}
IFileInterface& DesktopPlatform::GetFileInterface() {
return *m_file_interface;
}
void DesktopPlatform::Log(const std::string& message) {
std::cout << "[INFO] " << message << std::endl;
}
void DesktopPlatform::LogError(const std::string& message) {
std::cerr << "[ERROR] " << message << std::endl;
}
bool DesktopPlatform::PollEvents() {
// Events are handled by RmlUi Backend
return true;
}
void DesktopPlatform::SwapBuffers() {
// Swap is handled by RmlUi Backend
}
bool DesktopPlatform::ShouldClose() const {
return false; // Determined by RmlUi Backend
}
void DesktopPlatform::SetResolution(uint32_t width, uint32_t height) {
m_width = width;
m_height = height;
}
float DesktopPlatform::GetDpiScale() const {
return 1.0f;
}
double DesktopPlatform::GetElapsedTime() const {
auto now = std::chrono::steady_clock::now();
double current = std::chrono::duration<double>(now.time_since_epoch()).count();
return current - m_start_time;
}
bool DesktopPlatform::IsMouseButtonDown() const {
return false; // Input is handled through RmlUi
}
void DesktopPlatform::GetMousePosition(double& x, double& y) const {
x = y = 0; // Input is handled through RmlUi
}
} // namespace mosis::desktop

View File

@@ -0,0 +1,50 @@
// Desktop platform implementation using GLFW + OpenGL 3.3
// Note: Graphics context and rendering is handled by RmlUi's backend.
// This platform implementation provides file interface and utilities.
#pragma once
#include "platform.h"
#include "file_interface.h"
#include <memory>
namespace mosis::desktop {
class DesktopPlatform : public IPlatform {
uint32_t m_width = 540;
uint32_t m_height = 960;
std::unique_ptr<DesktopFileInterface> m_file_interface;
double m_start_time = 0.0;
public:
DesktopPlatform();
~DesktopPlatform() override;
// Initialize the platform
bool Initialize(uint32_t width, uint32_t height, const char* title);
void Shutdown();
// IPlatform implementation
std::unique_ptr<IGraphicsContext> CreateGraphicsContext() override;
std::unique_ptr<IRenderTarget> CreateRenderTarget(uint32_t width, uint32_t height) override;
IFileInterface& GetFileInterface() override;
void Log(const std::string& message) override;
void LogError(const std::string& message) override;
bool PollEvents() override;
void SwapBuffers() override;
bool ShouldClose() const override;
uint32_t GetWidth() const override { return m_width; }
uint32_t GetHeight() const override { return m_height; }
void SetResolution(uint32_t width, uint32_t height) override;
float GetDpiScale() const override;
double GetElapsedTime() const override;
// Input state (delegated to RmlUi backend)
bool IsMouseButtonDown() const;
void GetMousePosition(double& x, double& y) const;
};
} // namespace mosis::desktop

View File

@@ -0,0 +1,93 @@
// Hot-reload file watcher implementation
#include "hot_reload.h"
#include <iostream>
#ifdef _WIN32
#include <Windows.h>
#endif
namespace mosis::desktop {
HotReload::HotReload(const std::filesystem::path& watch_path, ReloadCallback callback)
: m_watch_path(watch_path)
, m_callback(std::move(callback))
, m_last_change(std::chrono::steady_clock::now())
{}
HotReload::~HotReload() {
Stop();
}
void HotReload::Start() {
if (m_running) return;
#ifdef _WIN32
m_notification_handle = FindFirstChangeNotificationW(
m_watch_path.c_str(),
TRUE, // Watch subtree
FILE_NOTIFY_CHANGE_LAST_WRITE
);
if (m_notification_handle == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to create file change notification for: "
<< m_watch_path << std::endl;
return;
}
#endif
m_running = true;
m_watch_thread = std::thread(&HotReload::WatchThread, this);
}
void HotReload::Stop() {
m_running = false;
if (m_watch_thread.joinable()) {
m_watch_thread.join();
}
#ifdef _WIN32
if (m_notification_handle && m_notification_handle != INVALID_HANDLE_VALUE) {
FindCloseChangeNotification(m_notification_handle);
m_notification_handle = nullptr;
}
#endif
}
bool HotReload::CheckForChanges() {
if (m_change_detected) {
auto now = std::chrono::steady_clock::now();
if (now - m_last_change >= m_debounce_delay) {
m_change_detected = false;
if (m_callback) {
m_callback();
}
return true;
}
}
return false;
}
void HotReload::WatchThread() {
while (m_running) {
#ifdef _WIN32
DWORD result = WaitForSingleObject(m_notification_handle, 100); // 100ms timeout
if (result == WAIT_OBJECT_0) {
m_last_change = std::chrono::steady_clock::now();
m_change_detected = true;
// Reset the notification
if (!FindNextChangeNotification(m_notification_handle)) {
std::cerr << "FindNextChangeNotification failed" << std::endl;
break;
}
}
#else
// TODO: Linux inotify / macOS FSEvents implementation
// For now, just sleep to avoid busy loop
std::this_thread::sleep_for(std::chrono::milliseconds(100));
#endif
}
}
} // namespace mosis::desktop

44
designer/src/hot_reload.h Normal file
View File

@@ -0,0 +1,44 @@
// Hot-reload file watcher for development
#pragma once
#include <filesystem>
#include <functional>
#include <thread>
#include <atomic>
#include <chrono>
namespace mosis::desktop {
class HotReload {
public:
using ReloadCallback = std::function<void()>;
HotReload(const std::filesystem::path& watch_path, ReloadCallback callback);
~HotReload();
void Start();
void Stop();
bool IsRunning() const { return m_running; }
// Check for changes (call from main thread if not using threaded mode)
bool CheckForChanges();
private:
void WatchThread();
std::filesystem::path m_watch_path;
ReloadCallback m_callback;
std::thread m_watch_thread;
std::atomic<bool> m_running{false};
std::atomic<bool> m_change_detected{false};
#ifdef _WIN32
void* m_notification_handle = nullptr; // HANDLE
#endif
// Debounce settings
std::chrono::milliseconds m_debounce_delay{100};
std::chrono::steady_clock::time_point m_last_change;
};
} // namespace mosis::desktop

View File

@@ -0,0 +1,330 @@
// Desktop kernel implementation
#include "kernel_impl.h"
#include "platform.h"
#include "file_interface.h"
#include "log.h"
#include <RmlUi/Core.h>
#include <RmlUi/Lua.h>
#include <RmlUi/Lua/Interpreter.h>
#include <mutex>
#include <vector>
#include <functional>
#include <atomic>
#include <iostream>
#include <chrono>
#include <algorithm>
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}
namespace mosis {
// Global kernel instance for Lua access
static DesktopKernel* g_kernel_instance = nullptr;
// Implementation class
class DesktopKernel::Impl {
public:
KernelConfig config;
Rml::Context* context = nullptr;
Rml::ElementDocument* document = nullptr;
std::vector<std::shared_ptr<IServiceListener>> listeners;
std::vector<std::function<void()>> tasks;
std::mutex mutex;
std::atomic<bool> running{false};
std::atomic<bool> reload_requested{false};
std::string current_document_path;
// Status bar time
std::string status_time;
Rml::DataModelHandle status_time_handle;
explicit Impl(const KernelConfig& cfg) : config(cfg) {}
};
DesktopKernel::DesktopKernel(const KernelConfig& config)
: m_impl(std::make_unique<Impl>(config))
{
g_kernel_instance = this;
}
DesktopKernel::~DesktopKernel() {
Stop();
g_kernel_instance = nullptr;
}
void DesktopKernel::AddListener(std::shared_ptr<IServiceListener> listener) {
std::lock_guard lock(m_impl->mutex);
m_impl->listeners.push_back(listener);
if (m_impl->running) {
listener->OnServiceInitialized(true);
}
}
void DesktopKernel::RemoveListener(std::shared_ptr<IServiceListener> listener) {
std::lock_guard lock(m_impl->mutex);
m_impl->listeners.erase(
std::remove(m_impl->listeners.begin(), m_impl->listeners.end(), listener),
m_impl->listeners.end()
);
}
void DesktopKernel::OnTouchDown(float x, float y) {
std::lock_guard lock(m_impl->mutex);
auto px = static_cast<int>(x * m_impl->config.width);
auto py = static_cast<int>(y * m_impl->config.height);
m_impl->tasks.emplace_back([this, px, py]() {
if (m_impl->context) {
m_impl->context->ProcessMouseMove(px, py, 0);
m_impl->context->ProcessMouseButtonDown(0, 0);
}
});
}
void DesktopKernel::OnTouchMove(float x, float y) {
std::lock_guard lock(m_impl->mutex);
auto px = static_cast<int>(x * m_impl->config.width);
auto py = static_cast<int>(y * m_impl->config.height);
m_impl->tasks.emplace_back([this, px, py]() {
if (m_impl->context) {
m_impl->context->ProcessMouseMove(px, py, 0);
}
});
}
void DesktopKernel::OnTouchUp(float x, float y) {
std::lock_guard lock(m_impl->mutex);
auto px = static_cast<int>(x * m_impl->config.width);
auto py = static_cast<int>(y * m_impl->config.height);
m_impl->tasks.emplace_back([this, px, py]() {
if (m_impl->context) {
m_impl->context->ProcessMouseMove(px, py, 0);
m_impl->context->ProcessMouseButtonUp(0, 0);
}
});
}
void DesktopKernel::OnBackButton() {
std::lock_guard lock(m_impl->mutex);
m_impl->tasks.emplace_back([this]() {
lua_State* L = Rml::Lua::Interpreter::GetLuaState();
lua_getglobal(L, "goBack");
if (lua_isfunction(L, -1)) {
lua_pcall(L, 0, 0, 0);
} else {
lua_pop(L, 1);
}
});
}
void DesktopKernel::OnHomeButton() {
std::lock_guard lock(m_impl->mutex);
m_impl->tasks.emplace_back([this]() {
lua_State* L = Rml::Lua::Interpreter::GetLuaState();
lua_getglobal(L, "goHome");
if (lua_isfunction(L, -1)) {
lua_pcall(L, 0, 0, 0);
} else {
lua_pop(L, 1);
}
});
}
void DesktopKernel::OnRecentsButton() {
std::cout << "Recents button pressed (not implemented)" << std::endl;
}
void DesktopKernel::RequestReload() {
m_impl->reload_requested = true;
}
void DesktopKernel::LoadDocument(const std::string& path) {
std::lock_guard lock(m_impl->mutex);
m_impl->tasks.emplace_back([this, path]() {
auto& file_interface = GetPlatform().GetFileInterface();
std::string full_path = file_interface.ResolvePath(path);
if (m_impl->document) {
m_impl->context->UnloadDocument(m_impl->document);
m_impl->document = nullptr;
}
m_impl->document = m_impl->context->LoadDocument(full_path);
if (m_impl->document) {
m_impl->document->Show();
m_impl->current_document_path = full_path;
std::cout << "Loaded document: " << path << std::endl;
} else {
std::cerr << "Failed to load document: " << path << std::endl;
}
});
}
void DesktopKernel::Start() {
if (m_impl->running) return;
m_impl->running = true;
std::lock_guard lock(m_impl->mutex);
for (auto& listener : m_impl->listeners) {
listener->OnServiceInitialized(true);
}
}
void DesktopKernel::Stop() {
m_impl->running = false;
}
bool DesktopKernel::IsRunning() const {
return m_impl->running;
}
void DesktopKernel::Update() {
if (!m_impl->running || !m_impl->context) return;
// Process pending tasks
{
std::lock_guard lock(m_impl->mutex);
for (const auto& task : m_impl->tasks) {
task();
}
m_impl->tasks.clear();
}
// Handle reload request
if (m_impl->reload_requested) {
m_impl->reload_requested = false;
if (!m_impl->current_document_path.empty()) {
std::cout << "Reloading document..." << std::endl;
if (m_impl->document) {
m_impl->context->UnloadDocument(m_impl->document);
m_impl->document = nullptr;
}
m_impl->context->Update();
m_impl->document = m_impl->context->LoadDocument(m_impl->current_document_path);
if (m_impl->document) {
m_impl->document->ReloadStyleSheet();
m_impl->document->Show();
std::cout << "Document reloaded" << std::endl;
}
}
}
// Update status bar time
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
std::tm tm{};
#ifdef _WIN32
localtime_s(&tm, &time_t);
#else
localtime_r(&time_t, &tm);
#endif
char buffer[16];
std::strftime(buffer, sizeof(buffer), "%H:%M", &tm);
m_impl->status_time = buffer;
if (m_impl->status_time_handle) {
m_impl->status_time_handle.DirtyVariable("time");
}
m_impl->context->Update();
}
void DesktopKernel::Render() {
if (!m_impl->running || !m_impl->context) return;
m_impl->context->Render();
std::lock_guard lock(m_impl->mutex);
for (auto& listener : m_impl->listeners) {
listener->OnFrameAvailable();
}
}
void DesktopKernel::SetContext(Rml::Context* context) {
m_impl->context = context;
// Setup status data model
if (m_impl->context) {
if (auto constructor = m_impl->context->CreateDataModel("status-data")) {
constructor.Bind("time", &m_impl->status_time);
m_impl->status_time_handle = constructor.GetModelHandle();
}
}
}
Rml::Context* DesktopKernel::GetContext() const {
return m_impl->context;
}
void DesktopKernel::SetDocument(Rml::ElementDocument* doc) {
m_impl->document = doc;
}
Rml::ElementDocument* DesktopKernel::GetDocument() const {
return m_impl->document;
}
void DesktopKernel::SetCurrentDocumentPath(const std::string& path) {
m_impl->current_document_path = path;
}
int DesktopKernel::LuaLoadScreen(lua_State* L) {
if (!g_kernel_instance) {
lua_pushboolean(L, false);
return 1;
}
const char* path = luaL_checkstring(L, 1);
Log(std::string("Loading screen: ") + path);
auto& file_interface = GetPlatform().GetFileInterface();
std::string full_path = file_interface.ResolvePath(path);
if (!file_interface.FileExists(full_path)) {
Log(std::string("Screen not found: ") + full_path);
lua_pushboolean(L, false);
return 1;
}
auto* impl = g_kernel_instance->m_impl.get();
// Unload current document
if (impl->document) {
impl->context->UnloadDocument(impl->document);
impl->document = nullptr;
}
// Load new document
impl->document = impl->context->LoadDocument(full_path);
if (impl->document) {
impl->document->Show();
impl->current_document_path = full_path;
Log(std::string("Loaded screen: ") + path);
lua_pushboolean(L, true);
} else {
Log(std::string("Failed to load screen: ") + path);
lua_pushboolean(L, false);
}
return 1;
}
void DesktopKernel::RegisterLuaFunctions() {
lua_State* L = Rml::Lua::Interpreter::GetLuaState();
lua_pushcfunction(L, LuaLoadScreen);
lua_setglobal(L, "loadScreen");
std::cout << "Registered Lua loadScreen function" << std::endl;
}
// Factory function
std::unique_ptr<IKernel> CreateKernel(const KernelConfig& config) {
return std::make_unique<DesktopKernel>(config);
}
} // namespace mosis

View File

@@ -0,0 +1,57 @@
// Desktop kernel header
#pragma once
#include "service_interface.h"
#include <RmlUi/Core/Types.h>
#include <memory>
#include <string>
// Forward declarations
namespace Rml {
class Context;
class ElementDocument;
}
struct lua_State;
namespace mosis {
// Desktop kernel implementation
class DesktopKernel : public IKernel {
public:
explicit DesktopKernel(const KernelConfig& config);
~DesktopKernel() override;
// IKernel implementation
void AddListener(std::shared_ptr<IServiceListener> listener) override;
void RemoveListener(std::shared_ptr<IServiceListener> listener) override;
void OnTouchDown(float x, float y) override;
void OnTouchMove(float x, float y) override;
void OnTouchUp(float x, float y) override;
void OnBackButton() override;
void OnHomeButton() override;
void OnRecentsButton() override;
void RequestReload() override;
void LoadDocument(const std::string& path) override;
void Start() override;
void Stop() override;
bool IsRunning() const override;
void Update() override;
void Render() override;
// Desktop-specific methods
void SetContext(Rml::Context* context);
Rml::Context* GetContext() const;
void SetDocument(Rml::ElementDocument* doc);
Rml::ElementDocument* GetDocument() const;
void SetCurrentDocumentPath(const std::string& path);
// Static Lua function registration
static int LuaLoadScreen(lua_State* L);
static void RegisterLuaFunctions();
private:
class Impl;
std::unique_ptr<Impl> m_impl;
};
} // namespace mosis

22
designer/src/log.h Normal file
View File

@@ -0,0 +1,22 @@
// Logging utility for Mosis Designer
#pragma once
#include <string>
#include <fstream>
#include <iostream>
namespace mosis {
// Global log file - defined in main.cpp
extern std::ofstream* g_log_file_ptr;
// Log function that writes to both stdout and file
inline void Log(const std::string& message) {
std::cout << message << std::endl;
if (g_log_file_ptr && g_log_file_ptr->is_open()) {
*g_log_file_ptr << message << std::endl;
g_log_file_ptr->flush();
}
}
} // namespace mosis

379
designer/src/main.cpp Normal file
View File

@@ -0,0 +1,379 @@
// Mosis Designer - Desktop designer and testing tool for Mosis virtual phone UI
#include <iostream>
#include <fstream>
#include <filesystem>
#include <string>
#include <thread>
#include <chrono>
#include <RmlUi/Core.h>
#include <RmlUi/Debugger.h>
#include <RmlUi/Lua.h>
#include <RmlUi/Lua/Interpreter.h>
#include <RmlUi_Backend.h>
#include "platform.h"
#include "file_interface.h"
#include "service_interface.h"
#include "kernel_impl.h"
#include "data_models.h"
#include "hot_reload.h"
#include "desktop_platform.h"
#include "testing/ui_inspector.h"
// Command-line options
struct Options {
std::string document_path;
uint32_t width = 540;
uint32_t height = 960;
bool dump_mode = false;
bool debug_mode = false;
std::string output_dir = "dump";
std::string log_file; // If set, write logs to this file
std::string hierarchy_file; // If set, dump UI hierarchy to this file each frame
};
// Global log file stream
static std::ofstream g_log_file;
// Pointer for shared logging (used by kernel_impl.cpp via log.h)
namespace mosis {
std::ofstream* g_log_file_ptr = nullptr;
}
// Log function that writes to both stdout and file
void LogMessage(const std::string& message) {
std::cout << message << std::endl;
if (g_log_file.is_open()) {
g_log_file << message << std::endl;
g_log_file.flush();
}
}
// Forward declarations
void PrintUsage(const char* program);
Options ParseOptions(int argc, const char* argv[]);
void LoadFonts(const std::filesystem::path& fonts_path);
std::filesystem::path FindAssetsPath(const std::filesystem::path& start_path);
// Custom system interface with logging
class DesignerSystemInterface : public Rml::SystemInterface {
Rml::SystemInterface* m_backend_interface;
public:
explicit DesignerSystemInterface(Rml::SystemInterface* backend)
: m_backend_interface(backend) {}
double GetElapsedTime() override {
return m_backend_interface ? m_backend_interface->GetElapsedTime() : 0.0;
}
bool LogMessage(Rml::Log::Type type, const Rml::String& message) override {
const char* type_str = "INFO";
switch (type) {
case Rml::Log::LT_ERROR: type_str = "ERROR"; break;
case Rml::Log::LT_WARNING: type_str = "WARNING"; break;
case Rml::Log::LT_INFO: type_str = "INFO"; break;
case Rml::Log::LT_DEBUG: type_str = "DEBUG"; break;
default: break;
}
std::cout << "[RmlUi " << type_str << "] " << message << std::endl;
return true;
}
};
// Global state
static DesignerSystemInterface* g_system_interface = nullptr;
static mosis::DesktopFileInterface* g_file_interface = nullptr;
static mosis::IKernel* g_kernel = nullptr;
static std::filesystem::path g_assets_path;
static mosis::testing::UIInspector g_ui_inspector;
int main(int argc, const char* argv[])
{
// Parse command-line options first
Options opts = ParseOptions(argc, argv);
// Open log file if specified
if (!opts.log_file.empty()) {
g_log_file.open(opts.log_file, std::ios::out | std::ios::trunc);
if (!g_log_file.is_open()) {
std::cerr << "Warning: Could not open log file: " << opts.log_file << std::endl;
} else {
mosis::g_log_file_ptr = &g_log_file;
}
}
LogMessage("Mosis Designer v0.1.0");
if (opts.document_path.empty()) {
PrintUsage(argv[0]);
return EXIT_FAILURE;
}
std::filesystem::path document_file = opts.document_path;
if (!std::filesystem::exists(document_file)) {
std::cerr << "File not found: " << opts.document_path << std::endl;
return EXIT_FAILURE;
}
document_file = std::filesystem::absolute(document_file);
// Initialize the RmlUi backend (GLFW + OpenGL)
if (!Backend::Initialize("Mosis Designer", opts.width, opts.height, true)) {
std::cerr << "Failed to initialize backend" << std::endl;
return EXIT_FAILURE;
}
// Find assets path
g_assets_path = FindAssetsPath(document_file.parent_path());
LogMessage("Assets path: " + g_assets_path.generic_string());
// Setup custom interfaces
g_system_interface = new DesignerSystemInterface(Backend::GetSystemInterface());
g_file_interface = new mosis::DesktopFileInterface(g_assets_path.string());
// Set platform for kernel
auto platform = std::make_unique<mosis::desktop::DesktopPlatform>();
platform->GetFileInterface().SetAssetsPath(g_assets_path.string());
mosis::SetPlatform(std::move(platform));
Rml::SetSystemInterface(g_system_interface);
Rml::SetFileInterface(g_file_interface);
Rml::SetRenderInterface(Backend::GetRenderInterface());
// Initialize RmlUi
Rml::Initialise();
Rml::Lua::Initialise();
LogMessage("RmlUi and Lua initialized");
// Create context
Rml::Context* context = Rml::CreateContext("main", Rml::Vector2i(opts.width, opts.height));
if (!context) {
std::cerr << "Failed to create RmlUi context" << std::endl;
Rml::Shutdown();
Backend::Shutdown();
return EXIT_FAILURE;
}
// Enable debugger in debug mode
if (opts.debug_mode) {
Rml::Debugger::Initialise(context);
Rml::Debugger::SetVisible(true);
}
// Load fonts
std::filesystem::path fonts_path = g_assets_path / "fonts";
LoadFonts(fonts_path);
// Initialize sample data and data models
initializeSampleData();
setupDataModels(context);
// Create and configure kernel
mosis::KernelConfig kernel_config;
kernel_config.width = opts.width;
kernel_config.height = opts.height;
kernel_config.initial_document = document_file.string();
kernel_config.threaded = false;
auto kernel = mosis::CreateKernel(kernel_config);
g_kernel = kernel.get();
// Set context and register Lua functions
auto* desktop_kernel = dynamic_cast<mosis::DesktopKernel*>(g_kernel);
if (desktop_kernel) {
desktop_kernel->SetContext(context);
mosis::DesktopKernel::RegisterLuaFunctions();
}
// Load the initial document
std::string document_path_str = document_file.generic_string();
LogMessage("Loading document: " + document_path_str);
Rml::ElementDocument* document = context->LoadDocument(document_path_str);
if (document) {
document->Show();
if (desktop_kernel) {
desktop_kernel->SetDocument(document);
desktop_kernel->SetCurrentDocumentPath(document_path_str);
}
LogMessage("Document loaded successfully");
} else {
LogMessage("ERROR: Failed to load document!");
}
// Start kernel
kernel->Start();
// Setup hot-reload
std::unique_ptr<mosis::desktop::HotReload> hot_reload;
if (!opts.dump_mode) {
hot_reload = std::make_unique<mosis::desktop::HotReload>(
g_assets_path,
[&kernel]() {
std::cout << "File change detected, requesting reload..." << std::endl;
kernel->RequestReload();
}
);
hot_reload->Start();
std::cout << "Hot-reload enabled for: " << g_assets_path.generic_string() << std::endl;
}
// Main loop
bool running = true;
while (running) {
// Process hot-reload
if (hot_reload) {
hot_reload->CheckForChanges();
}
// Process events and update
running = Backend::ProcessEvents(context);
// Update kernel (processes tasks, updates time, etc.)
kernel->Update();
// Render
Backend::BeginFrame();
kernel->Render();
Backend::PresentFrame();
// Dump hierarchy if enabled (quiet mode to avoid spamming console)
if (!opts.hierarchy_file.empty() && desktop_kernel) {
Rml::ElementDocument* doc = desktop_kernel->GetDocument();
if (doc) {
g_ui_inspector.SaveToFile(doc, opts.hierarchy_file, true);
}
}
}
// Cleanup
kernel->Stop();
kernel.reset();
g_kernel = nullptr;
if (hot_reload) {
hot_reload->Stop();
}
if (opts.debug_mode) {
Rml::Debugger::Shutdown();
}
delete g_system_interface;
delete g_file_interface;
Rml::Shutdown();
Backend::Shutdown();
std::cout << "Mosis Designer shutdown complete" << std::endl;
return EXIT_SUCCESS;
}
void PrintUsage(const char* program)
{
std::cout << "Usage: " << program << " <document.rml> [options]" << std::endl;
std::cout << std::endl;
std::cout << "Options:" << std::endl;
std::cout << " --resolution WxH Set phone resolution (default: 540x960)" << std::endl;
std::cout << " --dump Dump mode: screenshot + hierarchy, then exit" << std::endl;
std::cout << " --debug Enable RmlUi debugger" << std::endl;
std::cout << " --output DIR Output directory for dump mode (default: dump)" << std::endl;
std::cout << " --log FILE Write log output to file (for automated testing)" << std::endl;
std::cout << " --hierarchy FILE Continuously dump UI hierarchy to JSON file" << std::endl;
std::cout << std::endl;
std::cout << "Examples:" << std::endl;
std::cout << " " << program << " assets/apps/home/home.rml" << std::endl;
std::cout << " " << program << " assets/apps/home/home.rml --resolution 720x1280" << std::endl;
std::cout << " " << program << " assets/apps/home/home.rml --dump" << std::endl;
}
Options ParseOptions(int argc, const char* argv[])
{
Options opts;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--resolution" && i + 1 < argc) {
std::string res = argv[++i];
size_t x_pos = res.find('x');
if (x_pos != std::string::npos) {
opts.width = std::stoi(res.substr(0, x_pos));
opts.height = std::stoi(res.substr(x_pos + 1));
}
} else if (arg == "--dump") {
opts.dump_mode = true;
} else if (arg == "--debug") {
opts.debug_mode = true;
} else if (arg == "--output" && i + 1 < argc) {
opts.output_dir = argv[++i];
} else if (arg == "--log" && i + 1 < argc) {
opts.log_file = argv[++i];
} else if (arg == "--hierarchy" && i + 1 < argc) {
opts.hierarchy_file = argv[++i];
} else if (arg[0] != '-') {
opts.document_path = arg;
}
}
return opts;
}
void LoadFonts(const std::filesystem::path& fonts_path)
{
if (!std::filesystem::exists(fonts_path)) {
std::cerr << "Fonts directory not found: " << fonts_path << std::endl;
return;
}
int count = 0;
for (const auto& entry : std::filesystem::directory_iterator(fonts_path)) {
if (entry.path().extension() == ".ttf") {
std::string font_path = entry.path().generic_string();
if (Rml::LoadFontFace(font_path)) {
std::cout << "Loaded font: " << entry.path().filename() << std::endl;
++count;
}
}
}
// Also try Roboto subdirectory
std::filesystem::path roboto_path = fonts_path / "Roboto";
if (std::filesystem::exists(roboto_path)) {
for (const auto& entry : std::filesystem::recursive_directory_iterator(roboto_path)) {
if (entry.path().extension() == ".ttf") {
std::string font_path = entry.path().generic_string();
if (Rml::LoadFontFace(font_path)) {
std::cout << "Loaded font: " << entry.path().filename() << std::endl;
++count;
}
}
}
}
std::cout << "Loaded " << count << " fonts" << std::endl;
}
std::filesystem::path FindAssetsPath(const std::filesystem::path& start_path)
{
std::filesystem::path current = std::filesystem::absolute(start_path);
// Walk up the directory tree looking for a fonts/ subdirectory
while (!current.empty() && current.has_parent_path()) {
std::filesystem::path fonts_path = current / "fonts";
if (std::filesystem::exists(fonts_path) && std::filesystem::is_directory(fonts_path)) {
// Check if it contains TTF files
for (const auto& entry : std::filesystem::directory_iterator(fonts_path)) {
if (entry.path().extension() == ".ttf") {
return current;
}
}
}
current = current.parent_path();
}
// Fall back to start path
return std::filesystem::absolute(start_path);
}

View File

@@ -0,0 +1,131 @@
// Action player implementation
#include "action_player.h"
#include "service_interface.h"
#include <iostream>
#include <thread>
namespace mosis::testing {
void ActionPlayer::LoadActions(const std::vector<Action>& actions) {
m_actions = actions;
Reset();
}
void ActionPlayer::LoadFromFile(const std::string& path) {
ActionRecorder recorder;
recorder.LoadFromFile(path);
m_actions = recorder.GetActions();
Reset();
}
void ActionPlayer::Play() {
if (m_actions.empty()) {
std::cout << "No actions to play" << std::endl;
return;
}
m_playing = true;
std::cout << "Playback started" << std::endl;
}
void ActionPlayer::Pause() {
m_playing = false;
std::cout << "Playback paused at action " << m_current_index << std::endl;
}
void ActionPlayer::Stop() {
m_playing = false;
Reset();
std::cout << "Playback stopped" << std::endl;
}
void ActionPlayer::Reset() {
m_current_index = 0;
m_elapsed_time_ms = 0;
}
void ActionPlayer::StepForward() {
if (m_current_index < m_actions.size()) {
ExecuteAction(m_actions[m_current_index]);
++m_current_index;
}
}
void ActionPlayer::Update(double delta_time_ms) {
if (!m_playing || m_current_index >= m_actions.size()) {
return;
}
m_elapsed_time_ms += delta_time_ms;
// Execute all actions whose timestamp has passed
while (m_current_index < m_actions.size()) {
const auto& action = m_actions[m_current_index];
if (action.timestamp_ms <= m_elapsed_time_ms) {
ExecuteAction(action);
++m_current_index;
} else {
break;
}
}
// Check if finished
if (m_current_index >= m_actions.size()) {
m_playing = false;
std::cout << "Playback finished" << std::endl;
}
}
void ActionPlayer::ExecuteAction(const Action& action) {
if (!m_kernel) {
std::cerr << "No kernel set for action player" << std::endl;
return;
}
switch (action.type) {
case ActionType::Tap:
std::cout << "Execute tap at (" << action.x << ", " << action.y << ")" << std::endl;
m_kernel->OnTouchDown(static_cast<float>(action.x), static_cast<float>(action.y));
m_kernel->OnTouchUp(static_cast<float>(action.x), static_cast<float>(action.y));
break;
case ActionType::Swipe:
std::cout << "Execute swipe from (" << action.x1 << ", " << action.y1
<< ") to (" << action.x2 << ", " << action.y2 << ")" << std::endl;
// Simplified swipe - just start and end
m_kernel->OnTouchDown(static_cast<float>(action.x1), static_cast<float>(action.y1));
m_kernel->OnTouchMove(static_cast<float>(action.x2), static_cast<float>(action.y2));
m_kernel->OnTouchUp(static_cast<float>(action.x2), static_cast<float>(action.y2));
break;
case ActionType::LongPress:
std::cout << "Execute long press at (" << action.x << ", " << action.y
<< ") for " << action.duration_ms << "ms" << std::endl;
m_kernel->OnTouchDown(static_cast<float>(action.x), static_cast<float>(action.y));
// In a real implementation, we'd hold for duration
m_kernel->OnTouchUp(static_cast<float>(action.x), static_cast<float>(action.y));
break;
case ActionType::Button:
std::cout << "Execute button: " << action.button << std::endl;
if (action.button == "back") {
m_kernel->OnBackButton();
} else if (action.button == "home") {
m_kernel->OnHomeButton();
} else if (action.button == "recents") {
m_kernel->OnRecentsButton();
}
break;
case ActionType::Wait:
std::cout << "Wait " << action.duration_ms << "ms" << std::endl;
// Wait is handled by timestamp comparison
break;
}
// Call callback if set
if (m_action_callback) {
m_action_callback(action, m_current_index);
}
}
} // namespace mosis::testing

View File

@@ -0,0 +1,60 @@
// Action player for replaying recorded UI interactions
#pragma once
#include "action_recorder.h"
#include <functional>
namespace mosis {
class IKernel;
}
namespace mosis::testing {
// Callback for when an action is executed
using ActionCallback = std::function<void(const Action&, size_t index)>;
// Plays back recorded actions
class ActionPlayer {
public:
ActionPlayer() = default;
// Set the kernel for executing actions
void SetKernel(IKernel* kernel) { m_kernel = kernel; }
// Load actions to play
void LoadActions(const std::vector<Action>& actions);
void LoadFromFile(const std::string& path);
// Playback control
void Play();
void Pause();
void Stop();
void Reset();
// Step through one action at a time
void StepForward();
// Update (call each frame)
void Update(double delta_time_ms);
// State
bool IsPlaying() const { return m_playing; }
bool IsFinished() const { return m_current_index >= m_actions.size(); }
size_t GetCurrentIndex() const { return m_current_index; }
size_t GetActionCount() const { return m_actions.size(); }
// Callbacks
void SetActionCallback(ActionCallback callback) { m_action_callback = callback; }
private:
void ExecuteAction(const Action& action);
IKernel* m_kernel = nullptr;
std::vector<Action> m_actions;
size_t m_current_index = 0;
double m_elapsed_time_ms = 0;
bool m_playing = false;
ActionCallback m_action_callback;
};
} // namespace mosis::testing

View File

@@ -0,0 +1,191 @@
// Action recorder implementation
#include "action_recorder.h"
#include <fstream>
#include <iostream>
namespace mosis::testing {
nlohmann::json Action::ToJson() const {
nlohmann::json j;
switch (type) {
case ActionType::Tap:
j["type"] = "tap";
j["x"] = x;
j["y"] = y;
break;
case ActionType::Swipe:
j["type"] = "swipe";
j["x1"] = x1;
j["y1"] = y1;
j["x2"] = x2;
j["y2"] = y2;
j["duration"] = duration_ms;
break;
case ActionType::LongPress:
j["type"] = "long_press";
j["x"] = x;
j["y"] = y;
j["duration"] = duration_ms;
break;
case ActionType::Button:
j["type"] = "button";
j["button"] = button;
break;
case ActionType::Wait:
j["type"] = "wait";
j["duration"] = duration_ms;
break;
}
j["timestamp"] = timestamp_ms;
return j;
}
Action Action::FromJson(const nlohmann::json& j) {
Action action;
action.timestamp_ms = j.value("timestamp", 0);
std::string type_str = j.value("type", "");
if (type_str == "tap") {
action.type = ActionType::Tap;
action.x = j.value("x", 0.0);
action.y = j.value("y", 0.0);
} else if (type_str == "swipe") {
action.type = ActionType::Swipe;
action.x1 = j.value("x1", 0.0);
action.y1 = j.value("y1", 0.0);
action.x2 = j.value("x2", 0.0);
action.y2 = j.value("y2", 0.0);
action.duration_ms = j.value("duration", 0);
} else if (type_str == "long_press") {
action.type = ActionType::LongPress;
action.x = j.value("x", 0.0);
action.y = j.value("y", 0.0);
action.duration_ms = j.value("duration", 0);
} else if (type_str == "button") {
action.type = ActionType::Button;
action.button = j.value("button", "");
} else if (type_str == "wait") {
action.type = ActionType::Wait;
action.duration_ms = j.value("duration", 0);
}
return action;
}
void ActionRecorder::StartRecording() {
m_recording = true;
m_start_time = std::chrono::steady_clock::now();
m_actions.clear();
std::cout << "Action recording started" << std::endl;
}
void ActionRecorder::StopRecording() {
m_recording = false;
std::cout << "Action recording stopped. Recorded " << m_actions.size() << " actions" << std::endl;
}
void ActionRecorder::RecordTap(double x, double y) {
if (!m_recording) return;
Action action;
action.type = ActionType::Tap;
action.x = x;
action.y = y;
action.timestamp_ms = GetTimestamp();
m_actions.push_back(action);
}
void ActionRecorder::RecordSwipe(double x1, double y1, double x2, double y2, int duration_ms) {
if (!m_recording) return;
Action action;
action.type = ActionType::Swipe;
action.x1 = x1;
action.y1 = y1;
action.x2 = x2;
action.y2 = y2;
action.duration_ms = duration_ms;
action.timestamp_ms = GetTimestamp();
m_actions.push_back(action);
}
void ActionRecorder::RecordLongPress(double x, double y, int duration_ms) {
if (!m_recording) return;
Action action;
action.type = ActionType::LongPress;
action.x = x;
action.y = y;
action.duration_ms = duration_ms;
action.timestamp_ms = GetTimestamp();
m_actions.push_back(action);
}
void ActionRecorder::RecordButton(const std::string& button) {
if (!m_recording) return;
Action action;
action.type = ActionType::Button;
action.button = button;
action.timestamp_ms = GetTimestamp();
m_actions.push_back(action);
}
void ActionRecorder::RecordWait(int duration_ms) {
if (!m_recording) return;
Action action;
action.type = ActionType::Wait;
action.duration_ms = duration_ms;
action.timestamp_ms = GetTimestamp();
m_actions.push_back(action);
}
void ActionRecorder::SaveToFile(const std::string& path) const {
nlohmann::json j;
j["actions"] = nlohmann::json::array();
for (const auto& action : m_actions) {
j["actions"].push_back(action.ToJson());
}
std::ofstream file(path);
if (file) {
file << j.dump(2);
std::cout << "Saved " << m_actions.size() << " actions to " << path << std::endl;
} else {
std::cerr << "Failed to save actions to " << path << std::endl;
}
}
void ActionRecorder::LoadFromFile(const std::string& path) {
std::ifstream file(path);
if (!file) {
std::cerr << "Failed to load actions from " << path << std::endl;
return;
}
nlohmann::json j;
file >> j;
m_actions.clear();
for (const auto& action_json : j["actions"]) {
m_actions.push_back(Action::FromJson(action_json));
}
std::cout << "Loaded " << m_actions.size() << " actions from " << path << std::endl;
}
void ActionRecorder::Clear() {
m_actions.clear();
}
int64_t ActionRecorder::GetTimestamp() const {
auto now = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_start_time);
return duration.count();
}
} // namespace mosis::testing

View File

@@ -0,0 +1,69 @@
// Action recorder for UI testing automation
#pragma once
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
#include <chrono>
namespace mosis::testing {
// Action types for recording user interactions
enum class ActionType {
Tap,
Swipe,
LongPress,
Button,
Wait
};
// Represents a single recorded action
struct Action {
ActionType type;
double x = 0, y = 0; // For tap, long_press
double x1 = 0, y1 = 0; // For swipe start
double x2 = 0, y2 = 0; // For swipe end
int duration_ms = 0; // For swipe, long_press, wait
std::string button; // For button actions (back, home, recents)
int64_t timestamp_ms = 0; // Time offset from recording start
nlohmann::json ToJson() const;
static Action FromJson(const nlohmann::json& j);
};
// Records user interactions into a sequence of actions
class ActionRecorder {
public:
ActionRecorder() = default;
// Start/stop recording
void StartRecording();
void StopRecording();
bool IsRecording() const { return m_recording; }
// Record individual actions
void RecordTap(double x, double y);
void RecordSwipe(double x1, double y1, double x2, double y2, int duration_ms);
void RecordLongPress(double x, double y, int duration_ms);
void RecordButton(const std::string& button);
void RecordWait(int duration_ms);
// Get recorded actions
const std::vector<Action>& GetActions() const { return m_actions; }
// Save/load to JSON
void SaveToFile(const std::string& path) const;
void LoadFromFile(const std::string& path);
// Clear recorded actions
void Clear();
private:
int64_t GetTimestamp() const;
bool m_recording = false;
std::vector<Action> m_actions;
std::chrono::steady_clock::time_point m_start_time;
};
} // namespace mosis::testing

View File

@@ -0,0 +1,183 @@
// UI Inspector implementation
#include "ui_inspector.h"
#include <RmlUi/Core.h>
#include <fstream>
#include <iostream>
#include <filesystem>
namespace mosis::testing {
nlohmann::json UIInspector::DumpDocument(Rml::ElementDocument* document) const {
nlohmann::json j;
if (!document) {
return j;
}
// Add metadata
j["timestamp"] = std::time(nullptr);
j["screen"] = document->GetSourceURL();
j["resolution"] = {
{"width", document->GetContext()->GetDimensions().x},
{"height", document->GetContext()->GetDimensions().y}
};
// Dump element tree
j["elements"] = DumpElement(document);
return j;
}
nlohmann::json UIInspector::DumpElement(Rml::Element* element) const {
nlohmann::json j;
if (!element) {
return j;
}
j["tag"] = element->GetTagName();
j["id"] = element->GetId();
// Get classes
nlohmann::json classes = nlohmann::json::array();
Rml::String class_attr = element->GetAttribute<Rml::String>("class", "");
if (!class_attr.empty()) {
// Split by space
size_t start = 0;
size_t end;
while ((end = class_attr.find(' ', start)) != std::string::npos) {
if (end > start) {
classes.push_back(class_attr.substr(start, end - start));
}
start = end + 1;
}
if (start < class_attr.size()) {
classes.push_back(class_attr.substr(start));
}
}
j["classes"] = classes;
// Get bounds
auto bounds = GetBounds(element);
j["bounds"] = bounds.ToJson();
// Visibility
j["visible"] = IsVisible(element);
// Children
nlohmann::json children = nlohmann::json::array();
for (int i = 0; i < element->GetNumChildren(); ++i) {
Rml::Element* child = element->GetChild(i);
if (child && child->GetTagName() != "#text") {
children.push_back(DumpElement(child));
}
}
// Text content (only for leaf elements without children to avoid huge JSON)
if (children.empty()) {
std::string text = GetText(element);
// Only store short text to avoid huge JSON and escaping issues
if (!text.empty() && text.length() < 200) {
j["text"] = text;
} else {
j["text"] = nlohmann::json(); // null
}
} else {
j["text"] = nlohmann::json(); // null for non-leaf elements
j["children"] = children;
}
return j;
}
Rml::Element* UIInspector::FindById(Rml::ElementDocument* document, const std::string& id) const {
if (!document) return nullptr;
return document->GetElementById(id);
}
std::vector<Rml::Element*> UIInspector::FindByClass(Rml::ElementDocument* document, const std::string& class_name) const {
std::vector<Rml::Element*> results;
if (!document) return results;
Rml::ElementList elements;
document->GetElementsByClassName(elements, class_name);
for (auto* element : elements) {
results.push_back(element);
}
return results;
}
bool UIInspector::IsVisible(Rml::Element* element) const {
if (!element) return false;
// Check if element has zero size
auto box = element->GetBox();
if (box.GetSize().x <= 0 || box.GetSize().y <= 0) {
return false;
}
// Check display property
auto display = element->GetProperty<Rml::Style::Display>("display");
if (display == Rml::Style::Display::None) {
return false;
}
// Check visibility property
auto visibility = element->GetProperty<Rml::Style::Visibility>("visibility");
if (visibility == Rml::Style::Visibility::Hidden) {
return false;
}
return true;
}
ElementBounds UIInspector::GetBounds(Rml::Element* element) const {
ElementBounds bounds = {0, 0, 0, 0};
if (!element) return bounds;
auto abs_offset = element->GetAbsoluteOffset(Rml::BoxArea::Border);
auto box = element->GetBox();
bounds.x = abs_offset.x;
bounds.y = abs_offset.y;
bounds.width = box.GetSize(Rml::BoxArea::Border).x;
bounds.height = box.GetSize(Rml::BoxArea::Border).y;
return bounds;
}
std::string UIInspector::GetText(Rml::Element* element) const {
if (!element) return "";
return element->GetInnerRML();
}
void UIInspector::SaveToFile(Rml::ElementDocument* document, const std::string& path, bool quiet) const {
nlohmann::json j = DumpDocument(document);
// Write to temp file first, then rename for atomic update
std::string tempPath = path + ".tmp";
std::ofstream file(tempPath);
if (file) {
file << j.dump(2);
file.close(); // Ensure file is closed and flushed
// Atomic rename (on Windows, need to remove destination first)
std::error_code ec;
std::filesystem::remove(path, ec);
std::filesystem::rename(tempPath, path, ec);
if (ec && !quiet) {
std::cerr << "Failed to rename temp file: " << ec.message() << std::endl;
} else if (!quiet) {
std::cout << "Saved UI hierarchy to " << path << std::endl;
}
} else {
std::cerr << "Failed to save UI hierarchy to " << path << std::endl;
}
}
} // namespace mosis::testing

View File

@@ -0,0 +1,53 @@
// UI Inspector for dumping element hierarchy
#pragma once
#include <nlohmann/json.hpp>
#include <string>
namespace Rml {
class Element;
class ElementDocument;
}
namespace mosis::testing {
// Element bounds
struct ElementBounds {
float x, y, width, height;
nlohmann::json ToJson() const {
return {{"x", x}, {"y", y}, {"width", width}, {"height", height}};
}
};
// Inspects and dumps UI element hierarchy
class UIInspector {
public:
UIInspector() = default;
// Dump the element tree of a document to JSON
nlohmann::json DumpDocument(Rml::ElementDocument* document) const;
// Dump a single element and its children
nlohmann::json DumpElement(Rml::Element* element) const;
// Find element by ID
Rml::Element* FindById(Rml::ElementDocument* document, const std::string& id) const;
// Find elements by class
std::vector<Rml::Element*> FindByClass(Rml::ElementDocument* document, const std::string& class_name) const;
// Check if element is visible
bool IsVisible(Rml::Element* element) const;
// Get element bounds (in screen coordinates)
ElementBounds GetBounds(Rml::Element* element) const;
// Get element text content
std::string GetText(Rml::Element* element) const;
// Save hierarchy to file (quiet=true suppresses log message)
void SaveToFile(Rml::ElementDocument* document, const std::string& path, bool quiet = false) const;
};
} // namespace mosis::testing

View File

@@ -0,0 +1,244 @@
// Visual capture implementation
#include "visual_capture.h"
#include <png.h>
#include <fstream>
#include <iostream>
#include <cmath>
#include <algorithm>
// OpenGL header (from RmlUi backend)
#include <RmlUi_Include_GL3.h>
namespace mosis::testing {
ImageData VisualCapture::CaptureFramebuffer(uint32_t width, uint32_t height) const {
ImageData image;
image.width = width;
image.height = height;
image.pixels.resize(width * height * 4);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, image.pixels.data());
// Flip vertically (OpenGL origin is bottom-left)
FlipVertically(image);
return image;
}
bool VisualCapture::SavePNG(const ImageData& image, const std::string& path) const {
if (!image.IsValid()) {
std::cerr << "Invalid image data" << std::endl;
return false;
}
FILE* fp = fopen(path.c_str(), "wb");
if (!fp) {
std::cerr << "Failed to open file for writing: " << path << std::endl;
return false;
}
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!png) {
fclose(fp);
return false;
}
png_infop info = png_create_info_struct(png);
if (!info) {
png_destroy_write_struct(&png, nullptr);
fclose(fp);
return false;
}
if (setjmp(png_jmpbuf(png))) {
png_destroy_write_struct(&png, &info);
fclose(fp);
return false;
}
png_init_io(png, fp);
png_set_IHDR(png, info, image.width, image.height, 8,
PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png, info);
// Write rows
std::vector<png_bytep> rows(image.height);
for (uint32_t y = 0; y < image.height; ++y) {
rows[y] = const_cast<png_bytep>(image.pixels.data() + y * image.width * 4);
}
png_write_image(png, rows.data());
png_write_end(png, nullptr);
png_destroy_write_struct(&png, &info);
fclose(fp);
std::cout << "Saved screenshot to " << path << std::endl;
return true;
}
ImageData VisualCapture::LoadPNG(const std::string& path) const {
ImageData image;
FILE* fp = fopen(path.c_str(), "rb");
if (!fp) {
std::cerr << "Failed to open file for reading: " << path << std::endl;
return image;
}
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!png) {
fclose(fp);
return image;
}
png_infop info = png_create_info_struct(png);
if (!info) {
png_destroy_read_struct(&png, nullptr, nullptr);
fclose(fp);
return image;
}
if (setjmp(png_jmpbuf(png))) {
png_destroy_read_struct(&png, &info, nullptr);
fclose(fp);
return image;
}
png_init_io(png, fp);
png_read_info(png, info);
image.width = png_get_image_width(png, info);
image.height = png_get_image_height(png, info);
png_byte color_type = png_get_color_type(png, info);
png_byte bit_depth = png_get_bit_depth(png, info);
// Convert to RGBA
if (bit_depth == 16) {
png_set_strip_16(png);
}
if (color_type == PNG_COLOR_TYPE_PALETTE) {
png_set_palette_to_rgb(png);
}
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) {
png_set_expand_gray_1_2_4_to_8(png);
}
if (png_get_valid(png, info, PNG_INFO_tRNS)) {
png_set_tRNS_to_alpha(png);
}
if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE) {
png_set_filler(png, 0xFF, PNG_FILLER_AFTER);
}
if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) {
png_set_gray_to_rgb(png);
}
png_read_update_info(png, info);
// Read rows
image.pixels.resize(image.width * image.height * 4);
std::vector<png_bytep> rows(image.height);
for (uint32_t y = 0; y < image.height; ++y) {
rows[y] = image.pixels.data() + y * image.width * 4;
}
png_read_image(png, rows.data());
png_destroy_read_struct(&png, &info, nullptr);
fclose(fp);
return image;
}
CompareResult VisualCapture::Compare(const ImageData& actual, const ImageData& expected, double threshold) const {
CompareResult result;
if (!actual.IsValid() || !expected.IsValid()) {
return result;
}
if (actual.width != expected.width || actual.height != expected.height) {
std::cerr << "Image dimensions don't match" << std::endl;
return result;
}
uint32_t total_pixels = actual.width * actual.height;
uint32_t diff_pixels = 0;
for (uint32_t i = 0; i < actual.pixels.size(); i += 4) {
int dr = std::abs(static_cast<int>(actual.pixels[i]) - static_cast<int>(expected.pixels[i]));
int dg = std::abs(static_cast<int>(actual.pixels[i+1]) - static_cast<int>(expected.pixels[i+1]));
int db = std::abs(static_cast<int>(actual.pixels[i+2]) - static_cast<int>(expected.pixels[i+2]));
int da = std::abs(static_cast<int>(actual.pixels[i+3]) - static_cast<int>(expected.pixels[i+3]));
// If any channel differs significantly, count as different
if (dr > 2 || dg > 2 || db > 2 || da > 2) {
++diff_pixels;
}
}
result.diff_pixels = diff_pixels;
result.diff_percent = static_cast<double>(diff_pixels) / total_pixels;
result.match = result.diff_percent <= threshold;
if (!result.match) {
result.diff_image = GenerateDiff(actual, expected);
}
return result;
}
ImageData VisualCapture::GenerateDiff(const ImageData& actual, const ImageData& expected) const {
ImageData diff;
if (!actual.IsValid() || !expected.IsValid()) {
return diff;
}
diff.width = actual.width;
diff.height = actual.height;
diff.pixels.resize(actual.pixels.size());
for (uint32_t i = 0; i < actual.pixels.size(); i += 4) {
int dr = std::abs(static_cast<int>(actual.pixels[i]) - static_cast<int>(expected.pixels[i]));
int dg = std::abs(static_cast<int>(actual.pixels[i+1]) - static_cast<int>(expected.pixels[i+1]));
int db = std::abs(static_cast<int>(actual.pixels[i+2]) - static_cast<int>(expected.pixels[i+2]));
if (dr > 2 || dg > 2 || db > 2) {
// Highlight difference in red
diff.pixels[i] = 255;
diff.pixels[i+1] = 0;
diff.pixels[i+2] = 0;
diff.pixels[i+3] = 255;
} else {
// Dim the matching pixels
diff.pixels[i] = actual.pixels[i] / 3;
diff.pixels[i+1] = actual.pixels[i+1] / 3;
diff.pixels[i+2] = actual.pixels[i+2] / 3;
diff.pixels[i+3] = 255;
}
}
return diff;
}
void VisualCapture::FlipVertically(ImageData& image) const {
if (!image.IsValid()) return;
size_t row_size = image.width * 4;
std::vector<uint8_t> temp_row(row_size);
for (uint32_t y = 0; y < image.height / 2; ++y) {
uint8_t* top = image.pixels.data() + y * row_size;
uint8_t* bottom = image.pixels.data() + (image.height - 1 - y) * row_size;
memcpy(temp_row.data(), top, row_size);
memcpy(top, bottom, row_size);
memcpy(bottom, temp_row.data(), row_size);
}
}
} // namespace mosis::testing

View File

@@ -0,0 +1,51 @@
// Visual capture for screenshots and image comparison
#pragma once
#include <string>
#include <vector>
#include <cstdint>
namespace mosis::testing {
// PNG image data
struct ImageData {
uint32_t width = 0;
uint32_t height = 0;
std::vector<uint8_t> pixels; // RGBA format
bool IsValid() const { return width > 0 && height > 0 && !pixels.empty(); }
};
// Result of image comparison
struct CompareResult {
bool match = false;
double diff_percent = 0.0;
uint32_t diff_pixels = 0;
ImageData diff_image;
};
// Captures and compares screenshots
class VisualCapture {
public:
VisualCapture() = default;
// Capture current framebuffer to image
ImageData CaptureFramebuffer(uint32_t width, uint32_t height) const;
// Save image to PNG file
bool SavePNG(const ImageData& image, const std::string& path) const;
// Load PNG file to image
ImageData LoadPNG(const std::string& path) const;
// Compare two images
CompareResult Compare(const ImageData& actual, const ImageData& expected, double threshold = 0.01) const;
// Generate diff image (highlights differences)
ImageData GenerateDiff(const ImageData& actual, const ImageData& expected) const;
// Utility: flip image vertically (for OpenGL coordinate conversion)
void FlipVertically(ImageData& image) const;
};
} // namespace mosis::testing