Merge branch 'shell'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -4,8 +4,14 @@
|
||||
<link type="text/rcss" href="styles/main.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="test">
|
||||
<p>Test!..</p>
|
||||
<div id="navbar">
|
||||
<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 id="page-content"></div>
|
||||
</body>
|
||||
</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 @@
|
||||
* {
|
||||
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;
|
||||
}
|
||||
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 <RmlUi/Core/ElementDocument.h>
|
||||
#include <RmlUi/Core/Event.h>
|
||||
#include <RmlUi/Debugger/Debugger.h>
|
||||
#include <RmlUi/Lua/Interpreter.h>
|
||||
#include <iostream>
|
||||
#include <RmlUi/Core/Context.h>
|
||||
#include <RmlUi/Core/Core.h>
|
||||
@@ -9,6 +11,11 @@
|
||||
#include <RmlUi_Backend.h>
|
||||
#include <RmlUi/Debugger.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_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<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->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;
|
||||
|
||||
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