diff --git a/assets/screens/messages.rml b/assets/screens/messages.rml
index dbfd303..35a38be 100644
--- a/assets/screens/messages.rml
+++ b/assets/screens/messages.rml
@@ -86,50 +86,9 @@
justify-content: center;
margin-left: 8px;
}
-
- /* Chat View Styles */
- .chat-header {
- display: flex;
- align-items: center;
- padding: 8px 0;
- }
-
- .chat-header-avatar {
- width: 36px;
- height: 36px;
- border-radius: 18px;
- background-color: #BB86FC;
- margin-right: 12px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 16px;
- color: #000000;
- }
-
- .chat-header-info {
- flex: 1;
- }
-
- .chat-header-name {
- font-size: 16px;
- font-weight: 500;
- color: #FFFFFF;
- }
-
- .chat-header-status {
- font-size: 12px;
- color: #B3B3B3;
- }
-
- .chat-messages {
- flex: 1;
- overflow: auto;
- padding: 16px;
- }
-
+
@@ -139,82 +98,16 @@
-
-
J
+
+
{{ conv.name | slice(0, 1) }}
-
Hey, are you coming to the party tonight?
-
-
2
-
-
-
-
M
-
-
-
Don't forget to call your grandmother!
-
-
-
-
-
A
-
-
-
Thanks for the help with the project!
-
-
-
-
-
B
-
-
-
Did you see the game last night?
-
-
-
-
-
W
-
-
-
Sarah: Meeting moved to 3pm
-
-
-
-
-
S
-
-
-
See you at the coffee shop!
-
-
-
-
-
D
-
-
-
Great talking to you!
+
{{ conv.last_message }}
+
{{ conv.unread }}
diff --git a/assets/screens/settings.rml b/assets/screens/settings.rml
index 98e489d..ebe68de 100644
--- a/assets/screens/settings.rml
+++ b/assets/screens/settings.rml
@@ -83,6 +83,7 @@
border-radius: 12px;
background-color: #666666;
position: relative;
+ cursor: pointer;
}
.settings-toggle.active {
@@ -147,7 +148,7 @@
}
-
+
@@ -161,8 +162,8 @@
U
-
User Name
-
user@example.com
+
{{ user_name }}
+
{{ user_email }}
>
@@ -174,9 +175,10 @@
W
Wi-Fi
-
Connected to Home_Network
+
Connected to {{ wifi_network }}
+
Off
-
@@ -184,9 +186,10 @@
B
Bluetooth
-
Off
+
On
+
Off
-
@@ -195,7 +198,7 @@
-
@@ -232,7 +235,7 @@
B
Battery
-
85% - About 12h remaining
+
{{ battery_percent }}% - {{ battery_remaining }}
>
@@ -240,7 +243,7 @@
S
Storage
-
45.2 GB used of 128 GB
+
{{ storage_used }}
>
@@ -269,9 +272,10 @@
L
Location
-
On - High accuracy
+
On - High accuracy
+
Off
-
diff --git a/assets/scripts/navigation.lua b/assets/scripts/navigation.lua
index c330e1d..e6d1d5f 100644
--- a/assets/scripts/navigation.lua
+++ b/assets/scripts/navigation.lua
@@ -6,8 +6,11 @@ local screens = {
home = "screens/home.rml",
lock = "screens/lock.rml",
dialer = "screens/dialer.rml",
+ calling = "screens/calling.rml",
contacts = "screens/contacts.rml",
+ contact_detail = "screens/contact_detail.rml",
messages = "screens/messages.rml",
+ chat = "screens/chat.rml",
settings = "screens/settings.rml",
browser = "screens/browser.rml"
}
diff --git a/assets/scripts/phone.lua b/assets/scripts/phone.lua
deleted file mode 100644
index 06780a3..0000000
--- a/assets/scripts/phone.lua
+++ /dev/null
@@ -1,80 +0,0 @@
--- Phone/Dialer functionality for Virtual Smartphone
-
--- Dial pad state
-local dial_number = ""
-local max_digits = 15
-
--- Add a digit to the dial display
-function dialPress(digit)
- if #dial_number < max_digits then
- dial_number = dial_number .. digit
- updateDialDisplay()
- print("Dialed: " .. digit .. " | Number: " .. dial_number)
- end
-end
-
--- Clear the last digit
-function dialBackspace()
- if #dial_number > 0 then
- dial_number = dial_number:sub(1, -2)
- updateDialDisplay()
- print("Backspace | Number: " .. dial_number)
- end
-end
-
--- Clear all digits
-function dialClear()
- dial_number = ""
- updateDialDisplay()
- print("Cleared dial pad")
-end
-
--- Update the dial display element
-function updateDialDisplay()
- local display = document:GetElementById("dial-display")
- if display then
- -- Format number with dashes for readability
- local formatted = formatPhoneNumber(dial_number)
- display.inner_rml = formatted
- end
-end
-
--- Format phone number for display
-function formatPhoneNumber(number)
- local len = #number
- if len == 0 then
- return ""
- elseif len <= 3 then
- return number
- elseif len <= 6 then
- return number:sub(1, 3) .. "-" .. number:sub(4)
- elseif len <= 10 then
- return "(" .. number:sub(1, 3) .. ") " .. number:sub(4, 6) .. "-" .. number:sub(7)
- else
- return "+1 (" .. number:sub(1, 3) .. ") " .. number:sub(4, 6) .. "-" .. number:sub(7, 10)
- end
-end
-
--- Make a call (simulated)
-function makeCall()
- if #dial_number > 0 then
- print("Calling: " .. dial_number)
- -- In a real app, this would initiate a call
- -- For now, just show feedback
- else
- print("No number to call")
- end
-end
-
--- End a call (simulated)
-function endCall()
- print("Call ended")
- dialClear()
-end
-
--- Get the current dial number
-function getDialNumber()
- return dial_number
-end
-
-print("Phone system initialized")
diff --git a/main.cpp b/main.cpp
index eab380a..406cc5a 100644
--- a/main.cpp
+++ b/main.cpp
@@ -2,10 +2,7 @@
#include
#include
#include
-#include
-#include
-#include
-#include
+
#include
#include
#include
@@ -14,7 +11,6 @@
#include
#include
#include
-#include
extern "C" {
#include
@@ -22,106 +18,21 @@ extern "C" {
#include
}
-// Global app state for Lua access
+#include "src/data_models.h"
+#include "src/system_interface.h"
+#include "src/utils.h"
+
+// Global app state
struct AppState {
Rml::Context* context = nullptr;
Rml::ElementDocument* document = nullptr;
std::filesystem::path assets_path;
} g_app;
-// Custom system interface to enable logging
-class LoggingSystemInterface : public Rml::SystemInterface {
-public:
- LoggingSystemInterface(Rml::SystemInterface* backend) : backend_(backend) {}
-
- double GetElapsedTime() override { return backend_->GetElapsedTime(); }
-
- bool LogMessage(Rml::Log::Type type, const Rml::String& message) override {
- const char* type_str = "";
- 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: type_str = "LOG"; break;
- }
- std::println("[RmlUi {}] {}", type_str, message);
- return true;
- }
-
- // Forward JoinPath to fix Windows path issues
- void JoinPath(Rml::String& translated_path, const Rml::String& document_path, const Rml::String& path) override {
- // Fix paths where colon was converted to pipe (D| -> D:)
- std::string fixed_path = path;
- if (fixed_path.length() >= 2 && fixed_path[1] == '|') {
- fixed_path[1] = ':';
- }
-
- std::string fixed_doc = document_path;
- if (fixed_doc.length() >= 2 && fixed_doc[1] == '|') {
- fixed_doc[1] = ':';
- }
-
- // Use std::filesystem to join paths properly and normalize (resolve ..)
- std::filesystem::path doc_dir = std::filesystem::path(fixed_doc).parent_path();
- std::filesystem::path result = (doc_dir / fixed_path).lexically_normal();
- translated_path = result.generic_string();
- std::println("JoinPath: {} + {} -> {}", fixed_doc, fixed_path, translated_path);
- }
-
-private:
- Rml::SystemInterface* backend_;
-};
-
static LoggingSystemInterface* g_logging_interface = nullptr;
-
-// Custom file interface to fix Windows path issues (D| -> D:)
-class WindowsFileInterface : public Rml::FileInterface {
-public:
- Rml::FileHandle Open(const Rml::String& path) override {
- // Fix paths where colon was converted to pipe (D| -> D:)
- std::string fixed_path = path;
- if (fixed_path.length() >= 2 && fixed_path[1] == '|') {
- fixed_path[1] = ':';
- }
- std::println("FileInterface::Open: {} -> {}", path, fixed_path);
-
- FILE* fp = fopen(fixed_path.c_str(), "rb");
- return reinterpret_cast(fp);
- }
-
- void Close(Rml::FileHandle file) override {
- fclose(reinterpret_cast(file));
- }
-
- size_t Read(void* buffer, size_t size, Rml::FileHandle file) override {
- return fread(buffer, 1, size, reinterpret_cast(file));
- }
-
- bool Seek(Rml::FileHandle file, long offset, int origin) override {
- return fseek(reinterpret_cast(file), offset, origin) == 0;
- }
-
- size_t Tell(Rml::FileHandle file) override {
- return ftell(reinterpret_cast(file));
- }
-};
-
static WindowsFileInterface* g_file_interface = nullptr;
-void load_fonts(const std::filesystem::path& dir)
-{
- for (const auto& file : std::filesystem::directory_iterator(dir))
- {
- if (file.path().extension() == ".ttf")
- {
- Rml::LoadFontFace(file.path().string());
- }
- }
-}
-
// Lua function: loadScreen(path) - loads a new RML document
-// Path is relative to assets folder
int lua_loadScreen(lua_State* L)
{
std::println("lua_loadScreen called!");
@@ -168,131 +79,12 @@ void registerLuaFunctions()
{
lua_State* L = Rml::Lua::Interpreter::GetLuaState();
- // Register global function
lua_pushcfunction(L, lua_loadScreen);
lua_setglobal(L, "loadScreen");
std::println("Registered Lua functions");
}
-void DumpElementTree(Rml::Element* element, std::ofstream& out, int depth = 0)
-{
- if (!element) return;
-
- std::string indent(depth * 2, ' ');
- std::string id = element->GetId().empty() ? "" : "#" + element->GetId();
-
- out << indent << element->GetTagName() << id
- << " @ (" << element->GetAbsoluteLeft() << ", " << element->GetAbsoluteTop() << ") "
- << element->GetOffsetWidth() << "x" << element->GetOffsetHeight() << "\n";
-
- for (int i = 0; i < element->GetNumChildren(); i++)
- DumpElementTree(element->GetChild(i), out, depth + 1);
-}
-
-// Offscreen framebuffer for screenshot capture
-struct OffscreenFBO {
- GLuint fbo = 0;
- GLuint texture = 0;
- GLuint depth_rbo = 0;
- int width = 0;
- int height = 0;
-
- bool create(int w, int h)
- {
- width = w;
- height = h;
-
- // Create framebuffer
- glGenFramebuffers(1, &fbo);
- glBindFramebuffer(GL_FRAMEBUFFER, fbo);
-
- // Create color texture
- glGenTextures(1, &texture);
- glBindTexture(GL_TEXTURE_2D, texture);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
-
- // Create depth/stencil renderbuffer
- glGenRenderbuffers(1, &depth_rbo);
- glBindRenderbuffer(GL_RENDERBUFFER, depth_rbo);
- glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
- glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depth_rbo);
-
- // Check completeness
- if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
- {
- std::println("Failed to create offscreen framebuffer");
- destroy();
- return false;
- }
-
- glBindFramebuffer(GL_FRAMEBUFFER, 0);
- return true;
- }
-
- void destroy()
- {
- if (texture) glDeleteTextures(1, &texture);
- if (depth_rbo) glDeleteRenderbuffers(1, &depth_rbo);
- if (fbo) glDeleteFramebuffers(1, &fbo);
- fbo = texture = depth_rbo = 0;
- }
-
- void bind()
- {
- glBindFramebuffer(GL_FRAMEBUFFER, fbo);
- glViewport(0, 0, width, height);
- }
-
- std::vector readPixels()
- {
- std::vector pixels(width * height * 3);
- glBindFramebuffer(GL_FRAMEBUFFER, fbo);
- glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, pixels.data());
- return pixels;
- }
-};
-
-void SavePNG(const std::filesystem::path& path, const std::vector& pixels, int width, int height)
-{
- // Flip vertically (OpenGL origin is bottom-left)
- std::vector flipped(width * height * 3);
- for (int y = 0; y < height; y++)
- {
- std::memcpy(&flipped[y * width * 3],
- &pixels[(height - 1 - y) * width * 3],
- width * 3);
- }
-
- FILE* fp = fopen(path.string().c_str(), "wb");
- if (!fp)
- {
- std::println("Failed to open {} for writing", path.string());
- return;
- }
-
- png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
- png_infop info = png_create_info_struct(png);
- png_init_io(png, fp);
- png_set_IHDR(png, info, width, height, 8, PNG_COLOR_TYPE_RGB,
- PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
- png_write_info(png, info);
-
- std::vector rows(height);
- for (int y = 0; y < height; y++)
- rows[y] = const_cast(&flipped[y * width * 3]);
- png_write_image(png, rows.data());
- png_write_end(png, nullptr);
-
- png_destroy_write_struct(&png, &info);
- fclose(fp);
-
- std::println("Screenshot saved to: {}", path.string());
-}
-
int main(const int argc, const char* argv[])
{
constexpr int window_width = 540;
@@ -312,7 +104,7 @@ int main(const int argc, const char* argv[])
return EXIT_FAILURE;
}
- if (!Backend::Initialize("Load Document Sample", window_width, window_height, true))
+ if (!Backend::Initialize("Mosis Designer", window_width, window_height, true))
{
return EXIT_FAILURE;
}
@@ -335,13 +127,16 @@ int main(const int argc, const char* argv[])
return EXIT_FAILURE;
}
+ // Initialize sample data and data models
+ initializeSampleData();
+ setupDataModels(g_app.context);
+
// Register custom Lua functions
registerLuaFunctions();
// Find the assets folder by checking for fonts/ subdirectory
g_app.assets_path = std::filesystem::absolute(file.parent_path());
- // Walk up the directory tree to find a folder containing a fonts/ subdirectory with .ttf files
std::filesystem::path fonts_path;
while (!g_app.assets_path.empty() && g_app.assets_path.has_parent_path())
{
@@ -383,7 +178,6 @@ int main(const int argc, const char* argv[])
// Dump mode: render and capture screenshot
if (dump_mode)
{
- // Create offscreen FBO at window dimensions
OffscreenFBO fbo;
if (!fbo.create(window_width, window_height))
{
@@ -414,26 +208,21 @@ int main(const int argc, const char* argv[])
dump_file.close();
}
- // Bind FBO and set up OpenGL state for rendering
+ // Bind FBO and render
fbo.bind();
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
-
- // Enable blending for proper text rendering
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
- // Render to FBO
g_app.context->Update();
g_app.context->Render();
- // Read pixels
auto pixels = fbo.readPixels();
fbo.destroy();
SavePNG(dump_folder / "screenshot.png", pixels, window_width, window_height);
- // Restore default framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, 0);
Rml::Shutdown();
@@ -441,6 +230,7 @@ int main(const int argc, const char* argv[])
return EXIT_SUCCESS;
}
+ // Watch for file changes
const HANDLE hNotif = FindFirstChangeNotification(g_app.assets_path.c_str(),
TRUE, FILE_NOTIFY_CHANGE_LAST_WRITE);
@@ -469,6 +259,9 @@ int main(const int argc, const char* argv[])
Backend::PresentFrame();
}
+ delete g_logging_interface;
+ delete g_file_interface;
+
Rml::Shutdown();
Backend::Shutdown();
diff --git a/src/data_models.cpp b/src/data_models.cpp
new file mode 100644
index 0000000..4e061a5
--- /dev/null
+++ b/src/data_models.cpp
@@ -0,0 +1,274 @@
+#include "data_models.h"
+#include
+
+// Global data instances
+SettingsData g_settings;
+PhoneData g_phone;
+BrowserData g_browser;
+std::vector g_conversations;
+std::vector 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::println("Sample data initialized: {} contacts, {} conversations, {} browser tabs",
+ g_contacts.size(), g_conversations.size(), g_browser.tabs.size());
+}
+
+void setupDataModels(Rml::Context* context)
+{
+ // Messages data model
+ if (auto constructor = context->CreateDataModel("messages"))
+ {
+ if (auto msg_handle = constructor.RegisterStruct())
+ {
+ msg_handle.RegisterMember("from", &Message::from);
+ msg_handle.RegisterMember("text", &Message::text);
+ msg_handle.RegisterMember("time", &Message::time);
+ }
+
+ if (auto conv_handle = constructor.RegisterStruct())
+ {
+ 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>();
+ constructor.RegisterArray>();
+
+ 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();
+ std::println("Selected conversation: {}", g_selected_conversation);
+ handle.DirtyVariable("selected_conversation");
+ }
+ });
+
+ g_messages_model = constructor.GetModelHandle();
+ std::println("Messages data model created");
+ }
+
+ // Contacts data model
+ if (auto constructor = context->CreateDataModel("contacts"))
+ {
+ if (auto contact_handle = constructor.RegisterStruct())
+ {
+ 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>();
+
+ 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();
+ std::println("Selected contact: {}", g_selected_contact);
+ handle.DirtyVariable("selected_contact");
+ }
+ });
+
+ g_contacts_model = constructor.GetModelHandle();
+ std::println("Contacts data model created");
+ }
+
+ // 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::println("Settings data model created");
+ }
+
+ // 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();
+ std::println("Dial: {}", g_phone.dial_number);
+ 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::println("Calling: {}", g_phone.call_contact);
+ 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::println("Phone data model created");
+ }
+
+ // Browser data model
+ if (auto constructor = context->CreateDataModel("browser"))
+ {
+ if (auto tab_handle = constructor.RegisterStruct())
+ {
+ tab_handle.RegisterMember("url", &BrowserTab::url);
+ tab_handle.RegisterMember("title", &BrowserTab::title);
+ tab_handle.RegisterMember("is_loading", &BrowserTab::is_loading);
+ }
+
+ constructor.RegisterArray>();
+
+ 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();
+ g_browser.page_title = "Loading...";
+ g_browser.can_go_back = true;
+ std::println("Navigating to: {}", g_browser.current_url);
+ 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::println("Refreshing page");
+ });
+
+ g_browser_model = constructor.GetModelHandle();
+ std::println("Browser data model created");
+ }
+
+ std::println("All data models initialized");
+}
diff --git a/src/data_models.h b/src/data_models.h
new file mode 100644
index 0000000..d37ffea
--- /dev/null
+++ b/src/data_models.h
@@ -0,0 +1,98 @@
+#pragma once
+
+#include
+#include
+#include
+
+// 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 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 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 g_conversations;
+extern std::vector 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);
diff --git a/src/system_interface.cpp b/src/system_interface.cpp
new file mode 100644
index 0000000..38a7124
--- /dev/null
+++ b/src/system_interface.cpp
@@ -0,0 +1,82 @@
+#include "system_interface.h"
+#include
+#include
+
+// LoggingSystemInterface implementation
+LoggingSystemInterface::LoggingSystemInterface(Rml::SystemInterface* backend)
+ : backend_(backend)
+{
+}
+
+double LoggingSystemInterface::GetElapsedTime()
+{
+ return backend_->GetElapsedTime();
+}
+
+bool LoggingSystemInterface::LogMessage(Rml::Log::Type type, const Rml::String& message)
+{
+ const char* type_str = "";
+ 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: type_str = "LOG"; break;
+ }
+ std::println("[RmlUi {}] {}", type_str, message);
+ return true;
+}
+
+void LoggingSystemInterface::JoinPath(Rml::String& translated_path, const Rml::String& document_path, const Rml::String& path)
+{
+ // Fix paths where colon was converted to pipe (D| -> D:)
+ std::string fixed_path = path;
+ if (fixed_path.length() >= 2 && fixed_path[1] == '|') {
+ fixed_path[1] = ':';
+ }
+
+ std::string fixed_doc = document_path;
+ if (fixed_doc.length() >= 2 && fixed_doc[1] == '|') {
+ fixed_doc[1] = ':';
+ }
+
+ // Use std::filesystem to join paths properly and normalize (resolve ..)
+ std::filesystem::path doc_dir = std::filesystem::path(fixed_doc).parent_path();
+ std::filesystem::path result = (doc_dir / fixed_path).lexically_normal();
+ translated_path = result.generic_string();
+ std::println("JoinPath: {} + {} -> {}", fixed_doc, fixed_path, translated_path);
+}
+
+// WindowsFileInterface implementation
+Rml::FileHandle WindowsFileInterface::Open(const Rml::String& path)
+{
+ // Fix paths where colon was converted to pipe (D| -> D:)
+ std::string fixed_path = path;
+ if (fixed_path.length() >= 2 && fixed_path[1] == '|') {
+ fixed_path[1] = ':';
+ }
+ std::println("FileInterface::Open: {} -> {}", path, fixed_path);
+
+ FILE* fp = fopen(fixed_path.c_str(), "rb");
+ return reinterpret_cast(fp);
+}
+
+void WindowsFileInterface::Close(Rml::FileHandle file)
+{
+ fclose(reinterpret_cast(file));
+}
+
+size_t WindowsFileInterface::Read(void* buffer, size_t size, Rml::FileHandle file)
+{
+ return fread(buffer, 1, size, reinterpret_cast(file));
+}
+
+bool WindowsFileInterface::Seek(Rml::FileHandle file, long offset, int origin)
+{
+ return fseek(reinterpret_cast(file), offset, origin) == 0;
+}
+
+size_t WindowsFileInterface::Tell(Rml::FileHandle file)
+{
+ return ftell(reinterpret_cast(file));
+}
diff --git a/src/system_interface.h b/src/system_interface.h
new file mode 100644
index 0000000..2ba039c
--- /dev/null
+++ b/src/system_interface.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include
+#include
+
+// Custom system interface to enable logging and fix Windows paths
+class LoggingSystemInterface : public Rml::SystemInterface {
+public:
+ LoggingSystemInterface(Rml::SystemInterface* backend);
+
+ double GetElapsedTime() override;
+ bool LogMessage(Rml::Log::Type type, const Rml::String& message) override;
+ void JoinPath(Rml::String& translated_path, const Rml::String& document_path, const Rml::String& path) override;
+
+private:
+ Rml::SystemInterface* backend_;
+};
+
+// Custom file interface to fix Windows path issues (D| -> D:)
+class WindowsFileInterface : public Rml::FileInterface {
+public:
+ Rml::FileHandle Open(const Rml::String& path) override;
+ void Close(Rml::FileHandle file) override;
+ size_t Read(void* buffer, size_t size, Rml::FileHandle file) override;
+ bool Seek(Rml::FileHandle file, long offset, int origin) override;
+ size_t Tell(Rml::FileHandle file) override;
+};
diff --git a/src/utils.cpp b/src/utils.cpp
new file mode 100644
index 0000000..eed1ada
--- /dev/null
+++ b/src/utils.cpp
@@ -0,0 +1,125 @@
+#include "utils.h"
+#include
+#include
+#include
+#include
+
+void load_fonts(const std::filesystem::path& dir)
+{
+ for (const auto& file : std::filesystem::directory_iterator(dir))
+ {
+ if (file.path().extension() == ".ttf")
+ {
+ Rml::LoadFontFace(file.path().string());
+ }
+ }
+}
+
+void DumpElementTree(Rml::Element* element, std::ofstream& out, int depth)
+{
+ if (!element) return;
+
+ std::string indent(depth * 2, ' ');
+ std::string id = element->GetId().empty() ? "" : "#" + element->GetId();
+
+ out << indent << element->GetTagName() << id
+ << " @ (" << element->GetAbsoluteLeft() << ", " << element->GetAbsoluteTop() << ") "
+ << element->GetOffsetWidth() << "x" << element->GetOffsetHeight() << "\n";
+
+ for (int i = 0; i < element->GetNumChildren(); i++)
+ DumpElementTree(element->GetChild(i), out, depth + 1);
+}
+
+void SavePNG(const std::filesystem::path& path, const std::vector& pixels, int width, int height)
+{
+ // Flip vertically (OpenGL origin is bottom-left)
+ std::vector flipped(width * height * 3);
+ for (int y = 0; y < height; y++)
+ {
+ std::memcpy(&flipped[y * width * 3],
+ &pixels[(height - 1 - y) * width * 3],
+ width * 3);
+ }
+
+ FILE* fp = fopen(path.string().c_str(), "wb");
+ if (!fp)
+ {
+ std::println("Failed to open {} for writing", path.string());
+ return;
+ }
+
+ png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
+ png_infop info = png_create_info_struct(png);
+ png_init_io(png, fp);
+ png_set_IHDR(png, info, width, height, 8, PNG_COLOR_TYPE_RGB,
+ PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
+ png_write_info(png, info);
+
+ std::vector rows(height);
+ for (int y = 0; y < height; y++)
+ rows[y] = const_cast(&flipped[y * width * 3]);
+ png_write_image(png, rows.data());
+ png_write_end(png, nullptr);
+
+ png_destroy_write_struct(&png, &info);
+ fclose(fp);
+
+ std::println("Screenshot saved to: {}", path.string());
+}
+
+bool OffscreenFBO::create(int w, int h)
+{
+ width = w;
+ height = h;
+
+ // Create framebuffer
+ glGenFramebuffers(1, &fbo);
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+
+ // Create color texture
+ glGenTextures(1, &texture);
+ glBindTexture(GL_TEXTURE_2D, texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
+
+ // Create depth/stencil renderbuffer
+ glGenRenderbuffers(1, &depth_rbo);
+ glBindRenderbuffer(GL_RENDERBUFFER, depth_rbo);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depth_rbo);
+
+ // Check completeness
+ if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
+ {
+ std::println("Failed to create offscreen framebuffer");
+ destroy();
+ return false;
+ }
+
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ return true;
+}
+
+void OffscreenFBO::destroy()
+{
+ if (texture) glDeleteTextures(1, &texture);
+ if (depth_rbo) glDeleteRenderbuffers(1, &depth_rbo);
+ if (fbo) glDeleteFramebuffers(1, &fbo);
+ fbo = texture = depth_rbo = 0;
+}
+
+void OffscreenFBO::bind()
+{
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glViewport(0, 0, width, height);
+}
+
+std::vector OffscreenFBO::readPixels()
+{
+ std::vector pixels(width * height * 3);
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, pixels.data());
+ return pixels;
+}
diff --git a/src/utils.h b/src/utils.h
new file mode 100644
index 0000000..0a0a45c
--- /dev/null
+++ b/src/utils.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+// Load all TTF fonts from a directory
+void load_fonts(const std::filesystem::path& dir);
+
+// Dump element tree to a file for debugging
+void DumpElementTree(Rml::Element* element, std::ofstream& out, int depth = 0);
+
+// Save pixels to a PNG file
+void SavePNG(const std::filesystem::path& path, const std::vector& pixels, int width, int height);
+
+// Offscreen framebuffer for screenshot capture
+struct OffscreenFBO {
+ unsigned int fbo = 0;
+ unsigned int texture = 0;
+ unsigned int depth_rbo = 0;
+ int width = 0;
+ int height = 0;
+
+ bool create(int w, int h);
+ void destroy();
+ void bind();
+ std::vector readPixels();
+};