Merge branch 'shell'

This commit is contained in:
2026-07-11 21:52:21 -05:00
16 changed files with 759 additions and 147 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -0,0 +1,4 @@
<div id="page-root" class="page">
<h1>About</h1>
<p>This is a static page</p>
</div>

View 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>

View 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>

View File

@@ -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;
}

View File

@@ -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
View 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
View 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
View 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
View 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;
};

View File

@@ -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
View File

49
src/page.hpp Normal file
View 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
View 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
View 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;
};