diff --git a/CMakeLists.txt b/CMakeLists.txt index 6fd5850..1f62039 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,9 @@ set(RMLUI_SOURCE_DIR ${rmlui_SOURCE_DIR}) add_executable(mosis-designer main.cpp + src/data_models.cpp + src/system_interface.cpp + src/utils.cpp ${rmlui_SOURCE_DIR}/Backends/RmlUi_Backend_GLFW_GL3.cpp ${rmlui_SOURCE_DIR}/Backends/RmlUi_Platform_GLFW.cpp ${rmlui_SOURCE_DIR}/Backends/RmlUi_Renderer_GL3.cpp diff --git a/assets/screens/browser.rml b/assets/screens/browser.rml index 3287716..b930a4f 100644 --- a/assets/screens/browser.rml +++ b/assets/screens/browser.rml @@ -40,6 +40,11 @@ .browser-nav-btn.disabled { color: #444444; + cursor: default; + } + + .browser-nav-btn.disabled:hover { + background-color: transparent; } .browser-url-bar { @@ -92,6 +97,10 @@ .browser-page-link { color: #1a0dab; + cursor: pointer; + } + + .browser-page-link:hover { text-decoration: underline; } @@ -101,6 +110,11 @@ .browser-search-item { margin-bottom: 24px; + cursor: pointer; + } + + .browser-search-item:hover { + background-color: #f0f0f0; } .browser-search-title { @@ -161,7 +175,7 @@ } - +
12:30 @@ -174,34 +188,43 @@
-
-
-
- +
+ +
+
+ +
+
+ 🔒 + +
+
+ +
+
+
-
-
-
Example Domain
+
{{ page_title }}
- This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission. + {{ page_content }}
- More information... + More information...
Related Links
-
+
IANA — IANA-managed Reserved Domains
www.iana.org > domains > reserved
Certain domains are set aside and unavailable for registration. Learn about reserved top-level domains.
-
+
RFC 2606 - Reserved Top Level DNS Names
tools.ietf.org > html > rfc2606
This document describes some domain names that are reserved for documentation purposes.
@@ -217,7 +240,7 @@ Home
- 3 + {{ tabs.size }} Tabs
diff --git a/assets/screens/calling.rml b/assets/screens/calling.rml new file mode 100644 index 0000000..76ff3c0 --- /dev/null +++ b/assets/screens/calling.rml @@ -0,0 +1,126 @@ + + + + + + + + Calling + + + +
Calling...
+
Unknown
+
+ +
?
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
diff --git a/assets/screens/chat.rml b/assets/screens/chat.rml new file mode 100644 index 0000000..877c465 --- /dev/null +++ b/assets/screens/chat.rml @@ -0,0 +1,164 @@ + + + + + + + + Chat + + + + +
+
+
J
+
+
John Wilson
+
Online
+
+
+
+
+ + +
+
Hey!
+
What are you up to?
+
Not much, just working
+
Cool! There's a party at Mike's tonight
+
Hey, are you coming to the party tonight?
+
+ + +
+
+ +
+ +
+
+ +
diff --git a/assets/screens/contact_detail.rml b/assets/screens/contact_detail.rml new file mode 100644 index 0000000..fa5e88e --- /dev/null +++ b/assets/screens/contact_detail.rml @@ -0,0 +1,161 @@ + + + + + + + + + Contact + + + + +
+
+ Contact +
+
+ + +
+
?
+
Contact Name
+ +
+
+
+ +
+ Call +
+
+
+ +
+ Message +
+
+
+ + +
+
+ +
+
Mobile
+
+1 (555) 000-0000
+
+
+
+ +
+
Email
+
email@example.com
+
+
+
+ +
diff --git a/assets/screens/contacts.rml b/assets/screens/contacts.rml index 0d86905..a38c3ea 100644 --- a/assets/screens/contacts.rml +++ b/assets/screens/contacts.rml @@ -67,9 +67,29 @@ color: #B3B3B3; margin-top: 2px; } + + .contact-call-btn { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 20px; + } + + .contact-call-btn:hover { + background-color: rgba(76, 175, 80, 0.2); + } + + .contact-call-btn img { + width: 20px; + height: 20px; + pointer-events: none; + } - +
@@ -85,94 +105,16 @@
- -
A
-
-
A
-
-
Alice Johnson
-
+1 (555) 123-4567
-
-
-
-
A
-
-
Andrew Smith
-
+1 (555) 234-5678
-
-
- - -
B
-
-
B
-
-
Bob Williams
-
+1 (555) 345-6789
-
-
-
-
B
-
-
Brian Davis
-
+1 (555) 456-7890
-
-
- - -
C
-
-
C
-
-
Carol Martinez
-
+1 (555) 567-8901
-
-
- - -
D
-
-
D
-
-
David Lee
-
+1 (555) 678-9012
-
-
- - -
J
-
-
J
-
-
John Wilson
-
+1 (555) 789-0123
-
-
- - -
M
-
-
M
-
-
Mom
-
+1 (555) 890-1234
-
-
-
-
M
-
-
Mike Brown
-
+1 (555) 901-2345
-
-
- - -
S
-
-
S
-
-
Sarah Taylor
-
+1 (555) 012-3456
+
+
+
{{ contact.initial }}
+
+
{{ contact.name }}
+
{{ contact.phone }}
+
+
+ +
diff --git a/assets/screens/dialer.rml b/assets/screens/dialer.rml index 044aca8..c187ed1 100644 --- a/assets/screens/dialer.rml +++ b/assets/screens/dialer.rml @@ -4,7 +4,6 @@ - Phone - +
@@ -114,69 +113,71 @@
-
Keypad
-
Recent
-
Contacts
+
Keypad
+
Recent
+
Contacts
-
+
{{ dial_number }}
-
+
1
-
+
2 ABC
-
+
3 DEF
-
+
4 GHI
-
+
5 JKL
-
+
6 MNO
-
+
7 PQRS
-
+
8 TUV
-
+
9 WXYZ
-
+
*
-
+
0 +
-
+
#
- +
-
+
+
+
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) }}
- John Wilson - 2:30 PM + {{ conv.name }} + {{ conv.time }}
-
Hey, are you coming to the party tonight?
-
-
2
-
- -
-
M
-
-
- Mom - 1:15 PM -
-
Don't forget to call your grandmother!
-
-
- -
-
A
-
-
- Alice Johnson - Yesterday -
-
Thanks for the help with the project!
-
-
- -
-
B
-
-
- Bob Williams - Yesterday -
-
Did you see the game last night?
-
-
- -
-
W
-
-
- Work Group - Mon -
-
Sarah: Meeting moved to 3pm
-
-
- -
-
S
-
-
- Sarah Taylor - Sun -
-
See you at the coffee shop!
-
-
- -
-
D
-
-
- David Lee - Sat -
-
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
>
@@ -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 @@
Airplane Mode
-
+
@@ -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(); +};