Merge branch 'shell'
This commit is contained in:
@@ -16,13 +16,21 @@ add_library(lunasvg::lunasvg ALIAS PkgConfig::LUNASVG)
|
|||||||
|
|
||||||
find_package(RmlUi CONFIG REQUIRED)
|
find_package(RmlUi CONFIG REQUIRED)
|
||||||
|
|
||||||
add_executable(${PROJECT_NAME}
|
add_library(rmlui_backend STATIC
|
||||||
src/main.cpp
|
|
||||||
vendor/rmlui_backend/RmlUi_Backend_GLFW_GL3.cpp
|
vendor/rmlui_backend/RmlUi_Backend_GLFW_GL3.cpp
|
||||||
vendor/rmlui_backend/RmlUi_Platform_GLFW.cpp
|
vendor/rmlui_backend/RmlUi_Platform_GLFW.cpp
|
||||||
vendor/rmlui_backend/RmlUi_Renderer_GL3.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
|
target_include_directories(${PROJECT_NAME} PRIVATE
|
||||||
src
|
src
|
||||||
vendor/rmlui_backend
|
vendor/rmlui_backend
|
||||||
@@ -30,6 +38,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(${PROJECT_NAME} PRIVATE
|
target_link_libraries(${PROJECT_NAME} PRIVATE
|
||||||
|
rmlui_backend
|
||||||
RmlUi::RmlUi
|
RmlUi::RmlUi
|
||||||
RmlUi::Debugger
|
RmlUi::Debugger
|
||||||
RmlUi::Lua
|
RmlUi::Lua
|
||||||
|
|||||||
@@ -4,8 +4,14 @@
|
|||||||
<link type="text/rcss" href="styles/main.rcss" />
|
<link type="text/rcss" href="styles/main.rcss" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="test">
|
<div id="navbar">
|
||||||
<p>Test!..</p>
|
<h1>Navigation</h1>
|
||||||
|
<div class="nav-buttons">
|
||||||
|
<button id="nav-test">Test Page</button>
|
||||||
|
<button id="nav-about">About</button>
|
||||||
|
<button id="nav-lua-test" onclick="NavigateTo('lua-test')">Lua Test</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="page-content"></div>
|
||||||
</body>
|
</body>
|
||||||
</rml>
|
</rml>
|
||||||
4
assets/pages/about.rml
Normal file
4
assets/pages/about.rml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<div id="page-root" class="page">
|
||||||
|
<h1>About</h1>
|
||||||
|
<p>This is a static page</p>
|
||||||
|
</div>
|
||||||
5
assets/pages/lua-test.rml
Normal file
5
assets/pages/lua-test.rml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<div id="page-root">
|
||||||
|
<h1>Lua test page</h1>
|
||||||
|
<p>This button uses <code>NavigateTo('about')</code> lua code instead of a registered C++ event</p>
|
||||||
|
<button onclick="NavigateTo('about')">Go to About Page</button>
|
||||||
|
</div>
|
||||||
5
assets/pages/testPage.rml
Normal file
5
assets/pages/testPage.rml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<div id="page-root" data-model="testPage" class="page">
|
||||||
|
<h1>Test page</h1>
|
||||||
|
<p>{{greeting}}</p>
|
||||||
|
<button id="test-btn">Button clicked: {{counter}} times</button>
|
||||||
|
</div>
|
||||||
@@ -1,71 +1,314 @@
|
|||||||
* {
|
/* ==========================================================================
|
||||||
|
RCSS User Agent Defaults (Plain HTML Style)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* Core Application Window Foundation */
|
||||||
|
body {
|
||||||
|
display: block;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
font-family: "rmlui-debugger-font";
|
font-family: "rmlui-debugger-font";
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div
|
/* Generic Structural Layout Defaults */
|
||||||
{
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
div, section, article, header, footer, main, nav, aside {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
p
|
span, em, strong, a {
|
||||||
{
|
display: inline;
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1
|
/* Typographic Defaults */
|
||||||
{
|
p {
|
||||||
display: block;
|
display: block;
|
||||||
|
margin: 1em 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
em
|
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;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
strong
|
strong {
|
||||||
{
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
select
|
a {
|
||||||
{
|
color: #0000ee;
|
||||||
text-align: left;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
tabset tabs
|
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;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Tables
|
||||||
|
========================================================================== */
|
||||||
table {
|
table {
|
||||||
box-sizing: border-box;
|
|
||||||
display: table;
|
display: table;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
box-sizing: border-box;
|
|
||||||
display: table-row;
|
display: table-row;
|
||||||
}
|
}
|
||||||
td {
|
|
||||||
box-sizing: border-box;
|
td, th {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
|
padding: 4px 8px;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #eaeaea;
|
||||||
|
}
|
||||||
|
|
||||||
col {
|
col {
|
||||||
box-sizing: border-box;
|
|
||||||
display: table-column;
|
display: table-column;
|
||||||
}
|
}
|
||||||
|
|
||||||
colgroup {
|
colgroup {
|
||||||
display: table-column-group;
|
display: table-column-group;
|
||||||
}
|
}
|
||||||
|
|
||||||
thead, tbody, tfoot {
|
thead, tbody, tfoot {
|
||||||
display: table-row-group;
|
display: table-row-group;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
/* ==========================================================================
|
||||||
--main-bg-color: #ffffff;
|
Form Inputs & Interactive UI Components
|
||||||
--text-color: #000000;
|
========================================================================== */
|
||||||
|
button, input, select, textarea {
|
||||||
width: 100vw;
|
display: inline-block;
|
||||||
height: 100vh;
|
font-family: "rmlui-debugger-font";
|
||||||
background-color: var(--main-bg-color);
|
font-size: 14px;
|
||||||
color: var(--text-color);
|
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;
|
||||||
}
|
}
|
||||||
74
src/App.hpp
74
src/App.hpp
@@ -1,74 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <RmlUi/Core/Context.h>
|
|
||||||
#include <RmlUi/Core/ElementDocument.h>
|
|
||||||
#include <RmlUi/Core/Factory.h>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
struct ApplicationData {
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
class App {
|
|
||||||
public:
|
|
||||||
|
|
||||||
App(Rml::Context *ctx, ApplicationData *appData) : ctx(ctx), appData(appData), doc(nullptr) {}
|
|
||||||
|
|
||||||
void SetupListeners(std::function<void(Rml::ElementDocument*)> bindCallback) {
|
|
||||||
onBindListeners = bindCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RegisterListener(Rml::Element* element, std::unique_ptr<Rml::EventListener> 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<std::unique_ptr<Rml::EventListener>> listeners;
|
|
||||||
std::function<void(Rml::ElementDocument*)> onBindListeners;
|
|
||||||
};
|
|
||||||
|
|
||||||
181
src/app.cpp
Normal file
181
src/app.cpp
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
#include "app.hpp"
|
||||||
|
#include <RmlUi/Core/Core.h>
|
||||||
|
#include <RmlUi/Core/Factory.h>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
App::App(Rml::Context *ctx, ApplicationData *appData)
|
||||||
|
: ctx(ctx), appData(appData), doc(nullptr) {}
|
||||||
|
|
||||||
|
|
||||||
|
App::~App() {
|
||||||
|
Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::SetupListeners(std::function<void(Rml::ElementDocument*)> bindCallback) {
|
||||||
|
onBindListeners = bindCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::RegisterListener(Rml::Element* element, std::unique_ptr<Rml::EventListener> 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<PageBase>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/app.hpp
Normal file
56
src/app.hpp
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <RmlUi/Core/Context.h>
|
||||||
|
#include <RmlUi/Core/ElementDocument.h>
|
||||||
|
#include <RmlUi/Core/EventListener.h>
|
||||||
|
#include <string>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <functional>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include "page.hpp"
|
||||||
|
|
||||||
|
struct ApplicationData {};
|
||||||
|
|
||||||
|
class App {
|
||||||
|
public:
|
||||||
|
App(Rml::Context* ctx, ApplicationData* appData);
|
||||||
|
~App();
|
||||||
|
|
||||||
|
void SetupListeners(std::function<void(Rml::ElementDocument*)> bindCallback);
|
||||||
|
void RegisterListener(Rml::Element* element, std::unique_ptr<Rml::EventListener> listener);
|
||||||
|
|
||||||
|
template <typename T = PageBase>
|
||||||
|
void RegisterPage(const std::string &pageName) {
|
||||||
|
static_assert(std::is_base_of<PageBase, T>::value, "T must derive from PageBase");
|
||||||
|
pages[pageName] = std::make_unique<T>(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<std::unique_ptr<Rml::EventListener>> listeners;
|
||||||
|
std::function<void(Rml::ElementDocument*)> onBindListeners;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<PageBase>> 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;
|
||||||
|
};
|
||||||
35
src/lua_app_api.cpp
Normal file
35
src/lua_app_api.cpp
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#include "lui_app_api.hpp"
|
||||||
|
#include <RmlUi/Lua/Interpreter.h>
|
||||||
|
#include <RmlUi/Lua/Lua.h>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include "lua.h"
|
||||||
|
}
|
||||||
|
|
||||||
|
int AppLuaApi::Lua_NavigateTo(lua_State *L) {
|
||||||
|
AppLuaApi* apiInstance = static_cast<AppLuaApi*>(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");
|
||||||
|
}
|
||||||
16
src/lui_app_api.hpp
Normal file
16
src/lui_app_api.hpp
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#include <RmlUi/Lua/Lua.h>
|
||||||
|
#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;
|
||||||
|
};
|
||||||
36
src/main.cpp
36
src/main.cpp
@@ -1,6 +1,8 @@
|
|||||||
#include "App.hpp"
|
|
||||||
#include <GLFW/glfw3.h>
|
#include <GLFW/glfw3.h>
|
||||||
|
#include <RmlUi/Core/ElementDocument.h>
|
||||||
|
#include <RmlUi/Core/Event.h>
|
||||||
#include <RmlUi/Debugger/Debugger.h>
|
#include <RmlUi/Debugger/Debugger.h>
|
||||||
|
#include <RmlUi/Lua/Interpreter.h>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <RmlUi/Core/Context.h>
|
#include <RmlUi/Core/Context.h>
|
||||||
#include <RmlUi/Core/Core.h>
|
#include <RmlUi/Core/Core.h>
|
||||||
@@ -9,6 +11,11 @@
|
|||||||
#include <RmlUi_Backend.h>
|
#include <RmlUi_Backend.h>
|
||||||
#include <RmlUi/Debugger.h>
|
#include <RmlUi/Debugger.h>
|
||||||
#include <RmlUi/Lua/Lua.h>
|
#include <RmlUi/Lua/Lua.h>
|
||||||
|
#include <memory>
|
||||||
|
#include "app.hpp"
|
||||||
|
#include "test_page.hpp"
|
||||||
|
#include "ButtonClickListener.hpp"
|
||||||
|
#include "lui_app_api.hpp"
|
||||||
|
|
||||||
static bool is_initalized = false;
|
static bool is_initalized = false;
|
||||||
static bool is_running = false;
|
static bool is_running = false;
|
||||||
@@ -44,6 +51,10 @@ bool InitalizeApplication() {
|
|||||||
return false;
|
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";
|
std::cout << "Application initalized...\n";
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -55,7 +66,7 @@ bool f8_was_pressed = false;
|
|||||||
void handle_input() {
|
void handle_input() {
|
||||||
GLFWwindow *window = glfwGetCurrentContext();
|
GLFWwindow *window = glfwGetCurrentContext();
|
||||||
if(window) {
|
if(window) {
|
||||||
// Reload main page
|
// Reload ui
|
||||||
bool f5_state = glfwGetKey(window, GLFW_KEY_F5);
|
bool f5_state = glfwGetKey(window, GLFW_KEY_F5);
|
||||||
if (f5_state == GLFW_PRESS && !f5_was_pressed) {
|
if (f5_state == GLFW_PRESS && !f5_was_pressed) {
|
||||||
f5_was_pressed = true;
|
f5_was_pressed = true;
|
||||||
@@ -84,7 +95,25 @@ int main() {
|
|||||||
is_running = true;
|
is_running = true;
|
||||||
|
|
||||||
std::cout << "Loading UI...\n";
|
std::cout << "Loading UI...\n";
|
||||||
|
|
||||||
|
app->RegisterPage<TestPagePage>("testPage");
|
||||||
|
|
||||||
|
app->SetupListeners([](Rml::ElementDocument *doc) {
|
||||||
|
if (auto *btn = doc->GetElementById("nav-test")) {
|
||||||
|
app->RegisterListener(btn, std::make_unique<ButtonClickListener>([](Rml::Event&){
|
||||||
|
app->NavigateTo("testPage");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto *btn = doc->GetElementById("nav-about")) {
|
||||||
|
app->RegisterListener(btn, std::make_unique<ButtonClickListener>([](Rml::Event&){
|
||||||
|
app->NavigateTo("about");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app->LoadUi();
|
app->LoadUi();
|
||||||
|
app->NavigateTo("lua-test");
|
||||||
std::cout << "UI Loaded\n";
|
std::cout << "UI Loaded\n";
|
||||||
|
|
||||||
while (is_running) {
|
while (is_running) {
|
||||||
@@ -98,8 +127,9 @@ int main() {
|
|||||||
Backend::PresentFrame();
|
Backend::PresentFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
delete app;
|
app->Shutdown();
|
||||||
Rml::Shutdown();
|
Rml::Shutdown();
|
||||||
|
delete app;
|
||||||
Backend::Shutdown();
|
Backend::Shutdown();
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
0
src/page.cpp
Normal file
0
src/page.cpp
Normal file
49
src/page.hpp
Normal file
49
src/page.hpp
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <RmlUi/Core/Context.h>
|
||||||
|
#include <RmlUi/Core/DataModelHandle.h>
|
||||||
|
#include <RmlUi/Core/Element.h>
|
||||||
|
#include <RmlUi/Core/ElementDocument.h>
|
||||||
|
#include <RmlUi/Core/Factory.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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<Rml::EventListener> 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<std::unique_ptr<Rml::EventListener>> pageListeners;
|
||||||
|
|
||||||
|
Rml::Element *root = nullptr;
|
||||||
|
Rml::DataModelHandle model;
|
||||||
|
bool hasModel = false;
|
||||||
|
};
|
||||||
27
src/test_page.cpp
Normal file
27
src/test_page.cpp
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#include "test_page.hpp"
|
||||||
|
#include <RmlUi/Core/DataModelHandle.h>
|
||||||
|
#include <RmlUi/Core/Element.h>
|
||||||
|
#include <iostream>
|
||||||
|
#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<ButtonClickListener>([this](Rml::Event&) {
|
||||||
|
data.counter++;
|
||||||
|
std::cout << "Button clicked, counter is now: " << data.counter << "\n";
|
||||||
|
DirtyVariable("counter");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestPagePage::OnExit() {}
|
||||||
20
src/test_page.hpp
Normal file
20
src/test_page.hpp
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "page.hpp"
|
||||||
|
#include <RmlUi/Core/DataModelHandle.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user