diff --git a/CMakeLists.txt b/CMakeLists.txt
index e4c2326..3379655 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -16,13 +16,21 @@ add_library(lunasvg::lunasvg ALIAS PkgConfig::LUNASVG)
find_package(RmlUi CONFIG REQUIRED)
-add_executable(${PROJECT_NAME}
- src/main.cpp
+add_library(rmlui_backend STATIC
vendor/rmlui_backend/RmlUi_Backend_GLFW_GL3.cpp
vendor/rmlui_backend/RmlUi_Platform_GLFW.cpp
vendor/rmlui_backend/RmlUi_Renderer_GL3.cpp
)
+add_executable(${PROJECT_NAME}
+ src/main.cpp
+ src/app.cpp
+ src/lua_app_api.cpp
+
+ src/page.cpp
+ src/test_page.cpp
+)
+
target_include_directories(${PROJECT_NAME} PRIVATE
src
vendor/rmlui_backend
@@ -30,6 +38,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE
)
target_link_libraries(${PROJECT_NAME} PRIVATE
+ rmlui_backend
RmlUi::RmlUi
RmlUi::Debugger
RmlUi::Lua
diff --git a/assets/main.rml b/assets/main.rml
index de9d38a..8e7c3a3 100644
--- a/assets/main.rml
+++ b/assets/main.rml
@@ -4,8 +4,14 @@
-
-
Test!..
+
+
Navigation
+
+ Test Page
+ About
+ Lua Test
+
+
\ No newline at end of file
diff --git a/assets/pages/about.rml b/assets/pages/about.rml
new file mode 100644
index 0000000..8e0207c
--- /dev/null
+++ b/assets/pages/about.rml
@@ -0,0 +1,4 @@
+
+
About
+
This is a static page
+
\ No newline at end of file
diff --git a/assets/pages/lua-test.rml b/assets/pages/lua-test.rml
new file mode 100644
index 0000000..6ba9034
--- /dev/null
+++ b/assets/pages/lua-test.rml
@@ -0,0 +1,5 @@
+
+
Lua test page
+
This button uses NavigateTo('about') lua code instead of a registered C++ event
+
Go to About Page
+
\ No newline at end of file
diff --git a/assets/pages/testPage.rml b/assets/pages/testPage.rml
new file mode 100644
index 0000000..0843462
--- /dev/null
+++ b/assets/pages/testPage.rml
@@ -0,0 +1,5 @@
+
+
Test page
+
{{greeting}}
+
Button clicked: {{counter}} times
+
\ No newline at end of file
diff --git a/assets/styles/main.rcss b/assets/styles/main.rcss
index a7ae0e8..64b4e5c 100644
--- a/assets/styles/main.rcss
+++ b/assets/styles/main.rcss
@@ -1,71 +1,314 @@
-* {
- font-family: "rmlui-debugger-font";
-}
-
-div
-{
- display: block;
-}
-
-p
-{
- display: block;
-}
-
-h1
-{
- display: block;
-}
-
-em
-{
- font-style: italic;
-}
-
-strong
-{
- font-weight: bold;
-}
-
-select
-{
- text-align: left;
-}
-
-tabset tabs
-{
- display: block;
-}
-
-table {
- box-sizing: border-box;
- display: table;
-}
-tr {
- box-sizing: border-box;
- display: table-row;
-}
-td {
- box-sizing: border-box;
- display: table-cell;
-}
-col {
- box-sizing: border-box;
- display: table-column;
-}
-colgroup {
- display: table-column-group;
-}
-thead, tbody, tfoot {
- display: table-row-group;
-}
+/* ==========================================================================
+ RCSS User Agent Defaults (Plain HTML Style)
+ ========================================================================== */
+/* Core Application Window Foundation */
body {
- --main-bg-color: #ffffff;
- --text-color: #000000;
-
+ display: block;
width: 100vw;
height: 100vh;
- background-color: var(--main-bg-color);
- color: var(--text-color);
+ font-family: "rmlui-debugger-font";
+ font-size: 16px;
+ background-color: #ffffff;
+ color: #000000;
+ margin: 0px;
+ padding: 0px;
+}
+
+/* Generic Structural Layout Defaults */
+* {
+ box-sizing: border-box;
+}
+
+div, section, article, header, footer, main, nav, aside {
+ display: block;
+}
+
+span, em, strong, a {
+ display: inline;
+}
+
+/* Typographic Defaults */
+p {
+ display: block;
+ margin: 1em 0px;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ display: block;
+ font-weight: bold;
+ margin: 0.83em 0px;
+}
+
+h1 { font-size: 2em; }
+h2 { font-size: 1.5em; }
+h3 { font-size: 1.17em; }
+h4 { font-size: 1em; }
+h5 { font-size: 0.83em; }
+h6 { font-size: 0.67em; }
+
+em {
+ font-style: italic;
+}
+
+strong {
+ font-weight: bold;
+}
+
+a {
+ color: #0000ee;
+ text-decoration: underline;
+}
+
+a:hover {
+ color: #551a8b;
+}
+
+blockquote {
+ display: block;
+ margin: 1em 40px;
+ padding-left: 10px;
+ border-left: 2px #cccccc;
+ color: #555555;
+}
+
+code, pre {
+ font-family: "rmlui-debugger-font";
+ background-color: #606060;
+ color: #000000;
+ padding: 3px;
+ font-style: italic;
+}
+
+pre {
+ display: block;
+ white-space: pre;
+ margin: 1em 0px;
+ padding: 8px;
+}
+
+/* Lists */
+ul, ol {
+ display: block;
+ margin: 1em 0px;
+ padding-left: 40px;
+}
+
+li {
+ display: block;
+}
+
+/* ==========================================================================
+ Tables
+ ========================================================================== */
+table {
+ display: table;
+}
+
+tr {
+ display: table-row;
+}
+
+td, th {
+ display: table-cell;
+ padding: 4px 8px;
+ text-align: left;
+}
+
+th {
+ font-weight: bold;
+ background-color: #eaeaea;
+}
+
+col {
+ display: table-column;
+}
+
+colgroup {
+ display: table-column-group;
+}
+
+thead, tbody, tfoot {
+ display: table-row-group;
+}
+
+/* ==========================================================================
+ Form Inputs & Interactive UI Components
+ ========================================================================== */
+button, input, select, textarea {
+ display: inline-block;
+ font-family: "rmlui-debugger-font";
+ font-size: 14px;
+ margin: 2px;
+ vertical-align: middle;
+}
+
+/* Traditional gray bevel button style */
+button, input[type="submit"], input[type="button"] {
+ padding: 4px 12px;
+ min-width: 40px;
+ background-color: #e1e1e1;
+ color: #000000;
+ border: 1px #767676;
+ text-align: center;
+}
+
+button:hover, input[type="submit"]:hover, input[type="button"]:hover {
+ background-color: #d0d0d0;
+ border: 1px #4f4f4f;
+}
+
+button:active, input[type="submit"]:active, input[type="button"]:active {
+ background-color: #b5b5b5;
+ border: 1px #000000;
+}
+
+/* Standard text field styling */
+input[type="text"], input[type="password"], textarea {
+ padding: 4px;
+ background-color: #ffffff;
+ color: #000000;
+ border: 1px #767676;
+}
+
+input[type="text"]:focus, input[type="password"]:focus, textarea:focus {
+ border: 1px #005aff;
+}
+
+textarea {
+ display: block;
+ white-space: pre-wrap;
+ width: 300px;
+ height: 100px;
+}
+
+/* Select Box & Dropdowns */
+select {
+ padding: 4px;
+ background-color: #ffffff;
+ color: #000000;
+ border: 1px #767676;
+ text-align: left;
+}
+
+/* ==========================================================================
+ RmlUi Specific Elements (Tabsets, Progress Bars, etc.)
+ ========================================================================== */
+tabset {
+ display: block;
+}
+
+tabset tabs {
+ display: block;
+ border-bottom: 1px #cccccc;
+}
+
+/* Simple default layout style for built-in tabs */
+tab {
+ display: inline-block;
+ padding: 6px 12px;
+ background-color: #f0f0f0;
+ border: 1px #cccccc;
+ margin-right: 2px;
+}
+
+tab:selected {
+ background-color: #ffffff;
+ border-bottom: 1px #ffffff; /* Overlap the bottom border line */
+ font-weight: bold;
+}
+
+panel {
+ display: block;
+ padding: 10px;
+ background-color: #ffffff;
+}
+
+/* Standard HTML5 / RmlUi Progress Bar Fallback */
+progress {
+ display: inline-block;
+ width: 160px;
+ height: 20px;
+ background-color: #e6e6e6;
+ border: 1px #b0b0b0;
+}
+
+progress-value {
+ display: block;
+ height: 100%;
+ background-color: #0078d7; /* Classic Windows Blue fill */
+}
+
+/* ==========================================================================
+ Utility & Modern Layout Helpers
+ ========================================================================== */
+.flex {
+ display: flex;
+}
+
+.flex-row {
+ flex-direction: row;
+}
+
+.flex-col {
+ flex-direction: column;
+}
+
+.justify-center {
+ justify-content: center;
+}
+
+.align-center {
+ align-items: center;
+}
+
+.hidden {
+ display: none;
+}
+
+
+
+
+
+#navbar {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+
+ width: 100%;
+ height: 60px;
+ padding: 0px 20px;
+ background-color: #2b2b2b;
+}
+
+#navbar h1 {
+ color: #ffffff;
+ font-size: 24px;
+ margin: 0px;
+}
+
+#navbar .nav-buttons {
+ display: flex;
+ flex-direction: row;
+ gap: 12px;
+}
+
+#navbar button {
+ padding: 8px 16px;
+ background-color: #3c3c3c;
+ color: #e0e0e0;
+ border: 1px #666666;
+}
+
+#navbar button:hover {
+ background-color: #55555a;
+}
+
+/* Page Content Base */
+#page-content {
+ display: block;
+ padding: 20px;
+ background-color: #1a1a1a;
+ color: #f0f0f0;
}
\ No newline at end of file
diff --git a/src/App.hpp b/src/App.hpp
deleted file mode 100644
index d619f1f..0000000
--- a/src/App.hpp
+++ /dev/null
@@ -1,74 +0,0 @@
-#pragma once
-
-#include
-#include
-#include
-#include
-#include
-
-struct ApplicationData {
-
-};
-
-class App {
-public:
-
- App(Rml::Context *ctx, ApplicationData *appData) : ctx(ctx), appData(appData), doc(nullptr) {}
-
- void SetupListeners(std::function bindCallback) {
- onBindListeners = bindCallback;
- }
-
- void RegisterListener(Rml::Element* element, std::unique_ptr listener) {
- if (element && listener) {
- element->AddEventListener(Rml::EventId::Click, listener.get());
- listeners.push_back(std::move(listener));
- }
- }
-
- bool LoadUi() {
- if (!ctx) return false;
-
- Rml::ElementDocument *doc = ctx->LoadDocument(GetAssetsPath() + "/main.rml");
- if (!doc) return false;
-
- doc->Show();
-
- listeners.clear();
- if (onBindListeners) {
- onBindListeners(doc);
- }
-
- return true;
- }
-
- void Reload() {
- Rml::Factory::ClearStyleSheetCache();
- Rml::Factory::ClearTemplateCache();
-
- if (doc) {
- doc->Close();
- doc = nullptr;
- }
- LoadUi();
- }
-
- static inline std::string GetAssetsPath() {
- std::string install_path = std::string(ASSETS_DIR) + "/main.rml";
- if (std::filesystem::exists(install_path)) {
- return std::string(ASSETS_DIR);
- }
- if (std::filesystem::exists("./assets/main.rml")) {
- return "./assets";
- }
- return ".";
- }
-
-private:
- ApplicationData *appData;
- Rml::Context* ctx;
- Rml::ElementDocument* doc;
- std::vector> listeners;
- std::function onBindListeners;
-};
-
diff --git a/src/app.cpp b/src/app.cpp
new file mode 100644
index 0000000..6f28135
--- /dev/null
+++ b/src/app.cpp
@@ -0,0 +1,181 @@
+#include "app.hpp"
+#include
+#include
+#include
+#include
+#include
+#include
+
+App::App(Rml::Context *ctx, ApplicationData *appData)
+ : ctx(ctx), appData(appData), doc(nullptr) {}
+
+
+App::~App() {
+ Shutdown();
+}
+
+void App::SetupListeners(std::function bindCallback) {
+ onBindListeners = bindCallback;
+}
+
+void App::RegisterListener(Rml::Element* element, std::unique_ptr listener) {
+ if (element && listener) {
+ element->AddEventListener(Rml::EventId::Click, listener.get());
+ listeners.push_back(std::move(listener));
+ }
+}
+
+PageBase* App::GetOrCreatePage(const std::string& pageName) {
+ if (auto it = pages.find(pageName); it != pages.end()) {
+ return it->second.get();
+ }
+
+ std::string path = GetAssetsPath() + "/pages/" + pageName + ".rml";
+ if (!std::filesystem::exists(path)) {
+ return nullptr;
+ }
+
+ auto page = std::make_unique(pageName, this);
+ PageBase* raw = page.get();
+ pages.emplace(pageName, std::move(page));
+ return raw;
+}
+
+bool App::ReadFile(const std::string& path, std::string& outContent) {
+ std::ifstream file(path, std::ios::in | std::ios::binary);
+ if (!file) return false;
+
+ std::ostringstream ss;
+ ss << file.rdbuf();
+ outContent = ss.str();
+ return true;
+}
+
+bool App::NavigateTo(const std::string &pageName) {
+
+ if (!pageOutlet) {
+ std::cout << "[ERROR] no page outlet - is main.rml missing id=\"page-content\"?\n";
+ return false;
+ }
+
+ PageBase* page = GetOrCreatePage(pageName);
+ if (!page) {
+ std::cout << "[ERROR] no page registered or found on disk for '" << pageName << "'\n";
+ return false;
+ }
+
+ std::string path = GetAssetsPath() + "/pages/" + pageName + ".rml";
+ std::string content;
+ if (!ReadFile(path, content)) {
+ std::cout << "[ERROR] could not read page file: " << path << "\n";
+ return false;
+ }
+
+ TeardownCurrentPage();
+
+ if (Rml::DataModelConstructor constructor = ctx->CreateDataModel(pageName)) {
+ page->SetupDataModel(constructor);
+ page->model = constructor.GetModelHandle();
+ page->hasModel = true;
+ } else {
+ std::cout << "[WARN] failed to create data model for page '" << pageName << "'\n";
+ }
+
+ pageOutlet->SetInnerRML(content);
+
+ page->root = pageOutlet->GetElementById("page-root");
+ if (!page->root) {
+ std::cout << "[WARN] page '" << pageName << "' has no element with id=\"page-root\"\n";
+ }
+
+ currentPage = page;
+ page->OnEnter(page->root);
+
+ return true;
+}
+
+bool App::LoadUi() {
+ if (!ctx) return false;
+
+ doc = ctx->LoadDocument(GetAssetsPath() + "/main.rml");
+ if (!doc) return false;
+
+ doc->Show();
+
+ pageOutlet = doc->GetElementById("page-content");
+ if (!pageOutlet) {
+ std::cout << "[WARN] main.rml has no element with id=\"page-content\"\n";
+ }
+
+ listeners.clear();
+ if (onBindListeners) {
+ onBindListeners(doc);
+ }
+
+ return true;
+}
+
+void App::Reload() {
+ Rml::Factory::ClearStyleSheetCache();
+ Rml::Factory::ClearTemplateCache();
+
+ std::string previousPage = currentPage ? currentPage->GetName() : "";
+
+ TeardownCurrentPage();
+
+ if (doc) {
+ doc->Close();
+ doc = nullptr;
+ pageOutlet = nullptr;
+ }
+
+ LoadUi();
+
+ if (!previousPage.empty()) {
+ NavigateTo(previousPage);
+ }
+}
+
+std::string App::GetAssetsPath() {
+ std::string install_path = std::string(ASSETS_DIR) + "/main.rml";
+ if (std::filesystem::exists(install_path)) {
+ return std::string(ASSETS_DIR);
+ }
+ if (std::filesystem::exists("./assets/main.rml")) {
+ return "./assets";
+ }
+ return ".";
+}
+
+void App::TeardownCurrentPage() {
+ if (!currentPage) return;
+
+ currentPage->OnExit();
+
+ if (currentPage->hasModel) {
+ ctx->RemoveDataModel(currentPage->GetName());
+ currentPage->hasModel = false;
+ }
+
+ currentPage->root = nullptr;
+
+ if (pageOutlet) {
+ pageOutlet->SetInnerRML("");
+ }
+
+ currentPage->pageListeners.clear();
+ currentPage = nullptr;
+}
+
+void App::Shutdown() {
+ if (hasShutdown) return;
+ hasShutdown = true;
+
+ TeardownCurrentPage(); // uses ctx->RemoveDataModel - must run while ctx is still valid
+
+ if (doc) {
+ doc->Close(); // deferred destruction, but marks it for cleanup on next Update/Shutdown
+ doc = nullptr;
+ pageOutlet = nullptr;
+ }
+}
\ No newline at end of file
diff --git a/src/app.hpp b/src/app.hpp
new file mode 100644
index 0000000..24d381e
--- /dev/null
+++ b/src/app.hpp
@@ -0,0 +1,56 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "page.hpp"
+
+struct ApplicationData {};
+
+class App {
+public:
+ App(Rml::Context* ctx, ApplicationData* appData);
+ ~App();
+
+ void SetupListeners(std::function bindCallback);
+ void RegisterListener(Rml::Element* element, std::unique_ptr listener);
+
+ template
+ void RegisterPage(const std::string &pageName) {
+ static_assert(std::is_base_of::value, "T must derive from PageBase");
+ pages[pageName] = std::make_unique(pageName, this);
+ }
+
+ bool NavigateTo(const std::string &pageName);
+
+ bool LoadUi();
+ void Reload();
+
+ static std::string GetAssetsPath();
+
+ void Shutdown();
+
+private:
+ ApplicationData *appData;
+ Rml::Context* ctx;
+ Rml::ElementDocument* doc;
+ Rml::Element* pageOutlet = nullptr;
+
+ std::vector> listeners;
+ std::function onBindListeners;
+
+ std::unordered_map> pages;
+ PageBase* currentPage = nullptr;
+
+ PageBase* GetOrCreatePage(const std::string& pageName);
+ static bool ReadFile(const std::string& path, std::string& outContent);
+ void TeardownCurrentPage();
+
+ bool hasShutdown = false;
+};
\ No newline at end of file
diff --git a/src/lua_app_api.cpp b/src/lua_app_api.cpp
new file mode 100644
index 0000000..f0abc94
--- /dev/null
+++ b/src/lua_app_api.cpp
@@ -0,0 +1,35 @@
+#include "lui_app_api.hpp"
+#include
+#include
+#include
+
+extern "C" {
+ #include "lua.h"
+}
+
+int AppLuaApi::Lua_NavigateTo(lua_State *L) {
+ AppLuaApi* apiInstance = static_cast(lua_touserdata(L, lua_upvalueindex(1)));
+
+ if (!apiInstance || !apiInstance->app) {
+ std::cout << "[LUA ERROR] API context or App pointer is invalid.\n";
+ return 0;
+ }
+
+ if (lua_isstring(L, 1)) {
+ std::string pageName = lua_tostring(L, 1);
+ apiInstance->app->NavigateTo(pageName);
+ } else {
+ std::cout << "[LUA ERROR] NavigateTo expects a string argument for pageName.\n";
+ }
+
+ return 0;
+}
+
+void AppLuaApi::RegisterLuaBindings() {
+ if (!L) return;
+
+ lua_pushlightuserdata(L, this);
+
+ lua_pushcclosure(L, Lua_NavigateTo, 1);
+ lua_setglobal(L, "NavigateTo");
+}
\ No newline at end of file
diff --git a/src/lui_app_api.hpp b/src/lui_app_api.hpp
new file mode 100644
index 0000000..19297e0
--- /dev/null
+++ b/src/lui_app_api.hpp
@@ -0,0 +1,16 @@
+#include
+#include "app.hpp"
+
+class AppLuaApi {
+public:
+
+ AppLuaApi(App *app, lua_State *L) : app(app), L(L) {}
+
+ void RegisterLuaBindings();
+
+ static int Lua_NavigateTo(lua_State *L);
+
+private:
+ App *app;
+ lua_State *L;
+};
\ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
index c55a8e4..b3085da 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,6 +1,8 @@
-#include "App.hpp"
#include
+#include
+#include
#include
+#include
#include
#include
#include
@@ -9,6 +11,11 @@
#include
#include
#include
+#include
+#include "app.hpp"
+#include "test_page.hpp"
+#include "ButtonClickListener.hpp"
+#include "lui_app_api.hpp"
static bool is_initalized = false;
static bool is_running = false;
@@ -44,6 +51,10 @@ bool InitalizeApplication() {
return false;
}
+ lua_State* L = Rml::Lua::Interpreter::GetLuaState();
+ AppLuaApi *lua_app_api = new AppLuaApi(app, L);
+ lua_app_api->RegisterLuaBindings();
+
std::cout << "Application initalized...\n";
return true;
@@ -55,7 +66,7 @@ bool f8_was_pressed = false;
void handle_input() {
GLFWwindow *window = glfwGetCurrentContext();
if(window) {
- // Reload main page
+ // Reload ui
bool f5_state = glfwGetKey(window, GLFW_KEY_F5);
if (f5_state == GLFW_PRESS && !f5_was_pressed) {
f5_was_pressed = true;
@@ -84,7 +95,25 @@ int main() {
is_running = true;
std::cout << "Loading UI...\n";
+
+ app->RegisterPage("testPage");
+
+ app->SetupListeners([](Rml::ElementDocument *doc) {
+ if (auto *btn = doc->GetElementById("nav-test")) {
+ app->RegisterListener(btn, std::make_unique([](Rml::Event&){
+ app->NavigateTo("testPage");
+ }));
+ }
+
+ if (auto *btn = doc->GetElementById("nav-about")) {
+ app->RegisterListener(btn, std::make_unique([](Rml::Event&){
+ app->NavigateTo("about");
+ }));
+ }
+ });
+
app->LoadUi();
+ app->NavigateTo("lua-test");
std::cout << "UI Loaded\n";
while (is_running) {
@@ -98,8 +127,9 @@ int main() {
Backend::PresentFrame();
}
- delete app;
+ app->Shutdown();
Rml::Shutdown();
+ delete app;
Backend::Shutdown();
return 0;
diff --git a/src/page.cpp b/src/page.cpp
new file mode 100644
index 0000000..e69de29
diff --git a/src/page.hpp b/src/page.hpp
new file mode 100644
index 0000000..5198149
--- /dev/null
+++ b/src/page.hpp
@@ -0,0 +1,49 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+class App;
+
+class PageBase
+{
+public:
+ PageBase(std::string pageName, App* app) : pageName(std::move(pageName)), app(app) {}
+ virtual ~PageBase() = default;
+
+ const std::string& GetName() const { return pageName; }
+ bool TryLoadRmlPage(std::string path = "");
+
+ virtual void SetupDataModel(Rml::DataModelConstructor constructor) {}
+ virtual void OnEnter(Rml::Element *root) {}
+ virtual void OnExit() {}
+
+protected:
+ App* GetApp() const {return app;}
+ void RegisterListener(Rml::Element* element, std::unique_ptr listener) {
+ if (element && listener) {
+ element->AddEventListener(Rml::EventId::Click, listener.get());
+ pageListeners.push_back(std::move(listener));
+ }
+ }
+
+ void DirtyVariable(const std::string& name) {
+ if (hasModel) model.DirtyVariable(name);
+ }
+
+private:
+ friend class App;
+
+ std::string pageName;
+ App *app;
+
+ std::vector> pageListeners;
+
+ Rml::Element *root = nullptr;
+ Rml::DataModelHandle model;
+ bool hasModel = false;
+};
\ No newline at end of file
diff --git a/src/test_page.cpp b/src/test_page.cpp
new file mode 100644
index 0000000..86eab3a
--- /dev/null
+++ b/src/test_page.cpp
@@ -0,0 +1,27 @@
+#include "test_page.hpp"
+#include
+#include
+#include
+#include "ButtonClickListener.hpp"
+
+void TestPagePage::SetupDataModel(Rml::DataModelConstructor constructor) {
+ constructor.Bind("greeting", &data.greeting);
+ constructor.Bind("counter", &data.counter);
+}
+
+void TestPagePage::OnEnter(Rml::Element *root) {
+ if (!root) {
+ std::cout << "Was not root\n";
+ return;
+ }
+
+ if (Rml::Element *btn = root->GetElementById("test-btn")) {
+ RegisterListener(btn, std::make_unique([this](Rml::Event&) {
+ data.counter++;
+ std::cout << "Button clicked, counter is now: " << data.counter << "\n";
+ DirtyVariable("counter");
+ }));
+ }
+}
+
+void TestPagePage::OnExit() {}
\ No newline at end of file
diff --git a/src/test_page.hpp b/src/test_page.hpp
new file mode 100644
index 0000000..00ff8d8
--- /dev/null
+++ b/src/test_page.hpp
@@ -0,0 +1,20 @@
+#pragma once
+#include "page.hpp"
+#include
+
+struct TestPageData {
+ std::string greeting = "Hello from the test page!";
+ int counter = 0;
+};
+
+class TestPagePage : public PageBase {
+public:
+ using PageBase::PageBase;
+
+ void SetupDataModel(Rml::DataModelConstructor constructor) override;
+ void OnEnter(Rml::Element *root) override;
+ void OnExit() override;
+
+private:
+ TestPageData data;
+};
\ No newline at end of file