From 7c6e3a0d89251e05866a9b3eb35176261ca8c313 Mon Sep 17 00:00:00 2001 From: chris bell Date: Sat, 11 Jul 2026 21:51:33 -0500 Subject: [PATCH] Shell + page loading And: - some basic stylesheet i found for basic html elements - a global lua method for navigating pages --- CMakeLists.txt | 13 +- assets/main.rml | 10 +- assets/pages/about.rml | 4 + assets/pages/lua-test.rml | 5 + assets/pages/testPage.rml | 5 + assets/styles/main.rcss | 375 +++++++++++++++++++++++++++++++------- src/App.hpp | 74 -------- src/app.cpp | 181 ++++++++++++++++++ src/app.hpp | 56 ++++++ src/lua_app_api.cpp | 35 ++++ src/lui_app_api.hpp | 16 ++ src/main.cpp | 36 +++- src/page.cpp | 0 src/page.hpp | 49 +++++ src/test_page.cpp | 27 +++ src/test_page.hpp | 20 ++ 16 files changed, 759 insertions(+), 147 deletions(-) create mode 100644 assets/pages/about.rml create mode 100644 assets/pages/lua-test.rml create mode 100644 assets/pages/testPage.rml delete mode 100644 src/App.hpp create mode 100644 src/app.cpp create mode 100644 src/app.hpp create mode 100644 src/lua_app_api.cpp create mode 100644 src/lui_app_api.hpp create mode 100644 src/page.cpp create mode 100644 src/page.hpp create mode 100644 src/test_page.cpp create mode 100644 src/test_page.hpp 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!..

+ +
\ 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

+ +
\ 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}}

+ +
\ 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