Refactored stuff into new files, added a test modal for adding critters

This commit is contained in:
2026-08-05 22:48:44 -05:00
parent 9e96151a42
commit 11fe8ca54d
9 changed files with 414 additions and 232 deletions

View File

@@ -26,6 +26,9 @@ find_package(RmlUi CONFIG REQUIRED)
add_executable(${PROJECT_NAME}
src/main.cpp
src/database.cpp
src/ui.cpp
vendor/rmlui_backend/RmlUi_Backend_GLFW_GL3.cpp
vendor/rmlui_backend/RmlUi_Platform_GLFW.cpp
vendor/rmlui_backend/RmlUi_Renderer_GL3.cpp

Binary file not shown.

View File

@@ -7,13 +7,46 @@
<body data-model="app_data">
<h1>{{page_title}}</h1>
<h2>Critter Roster</h2>
<button data-event-click="is_dialog_open = true">Add New Critter</button>
<!-- Critter Cards -->
<div class="critter-list">
<div data-for="entry : critters" class="critter-card">
<h3>#{{entry.key}} - {{entry.value.name}}</h3>
<p><strong>Species:</strong> {{entry.value.species}}</p>
<p><strong>Gender:</strong> {{entry.value.gender}}</p>
<p><strong>Notes:</strong> {{entry.value.notes}}</p>
</div>
</div>
<!-- Modal Dialog Overlay -->
<div class="modal-overlay" data-visible="is_dialog_open">
<div class="modal-content">
<h2>Add New Critter</h2>
<p class="error" data-visible="error_message != ''">{{error_message}}</p>
<label>Name:</label>
<input type="text" data-value="form.name" />
<label>Species:</label>
<input type="text" data-value="form.species" />
<label>Gender (M/F):</label>
<input type="text" data-value="form.gender" />
<label>Mother ID (Optional):</label>
<input type="text" data-value="form.mother_id" />
<label>Father ID (Optional):</label>
<input type="text" data-value="form.father_id" />
<label>Notes:</label>
<input type="text" data-value="form.notes" />
<div class="modal-buttons">
<button data-event-click="submit_add_critter">Save</button>
<button data-event-click="is_dialog_open = false">Cancel</button>
</div>
</div>
</div>
</body>

View File

@@ -266,3 +266,52 @@ progress-value {
display: none;
}
/* ===================================
Specific app stuff
=================================== */
/* Critter cards */
/* Modals */
.modal-overlay {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background-color: #2b2b2b;
color: #ffffff;
padding: 20px;
border-radius: 8px;
width: 350px;
}
.modal-content label {
display: block;
margin-top: 8px;
}
.modal-content input {
width: 100%;
margin-bottom: 8px;
}
.error {
color: #ff5555;
font-weight: bold;
}
.modal-buttons {
margin-top: 15px;
display: flex;
justify-content: space-between;
}

133
src/database.cpp Normal file
View File

@@ -0,0 +1,133 @@
#include "database.hpp"
#include <SQLiteCpp/Database.h>
#include <iostream>
bool db_get_all_critters(std::map<int, Critter> &critters) {
try {
SQLite::Database db("./assets/local.db", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
SQLite::Statement q(db, "SELECT * FROM critters");
critters.clear();
while (q.executeStep()) {
int id = q.getColumn(0);
critters.try_emplace(
id,
id,
q.getColumn(1).getText(),
q.getColumn(2).getText(),
q.getColumn(3).getInt(),
q.getColumn(4).getInt(),
q.getColumn(5).getText(),
q.getColumn(6).getText()
);
}
}
catch (const std::exception &e) {
std::cout << "[ERROR] DB exception: " << e.what() << std::endl;
return false;
}
return true;
}
bool db_add_critter(const Critter &c, int &out_id) {
try {
SQLite::Database db("./assets/local.db", SQLite::OPEN_READWRITE);
SQLite::Statement q(db, "INSERT INTO critters (name, gender, mother_id, father_id, notes, species) VALUES (?, ?, ?, ?, ?, ?)");
q.bind(1, c.name);
q.bind(2, c.gender);
q.bind(3, c.mother_id);
q.bind(4, c.father_id);
q.bind(5, c.notes);
q.bind(6, c.species);
q.exec();
out_id = static_cast<int>(db.getLastInsertRowid());
return true;
}
catch (const std::exception &e) {
std::cout << "[ERROR] DB add exception: " << e.what() << std::endl;
return false;
}
}
bool db_delete_critter(int id) {
try {
SQLite::Database db("./assets/local.db", SQLite::OPEN_READWRITE);
SQLite::Statement q(db, "DELETE FROM critters WHERE id = ?");
q.bind(1, id);
q.exec();
return true;
}
catch (const std::exception &e) {
std::cout << "[ERROR] DB delete exception: " << e.what() << std::endl;
return false;
}
}
void db_print_all_critters(const std::map<int, Critter> &critters) {
std::cout << "Current loaded critters:";
for (const auto &[key, c] : critters) {
std::cout << "\nCritter " << key << "\n";
std::cout << "ID: " << c.id << std::endl;
std::cout << "Name: " << c.name << std::endl;
std::cout << "Gender: " << c.gender << std::endl;
std::cout << "Mother ID: " << c.mother_id << std::endl;
std::cout << "Father ID: " << c.father_id << std::endl;
std::cout << "Notes: " << c.notes << std::endl;
std::cout << "Species: " << c.species << std::endl;
}
std::cout << "\n";
}
bool validate_critter(const Critter &c, const std::map<int, Critter> &all_critters, std::string &out_error) {
if (c.mother_id != 0 && c.father_id != 0 && c.mother_id == c.father_id) {
out_error = "Mother ID and Father ID cannot be the same.";
return false;
}
if (c.mother_id != 0) {
auto it = all_critters.find(c.mother_id);
if (it == all_critters.end()) {
out_error = "Mother ID " + std::to_string(c.mother_id) + " does not exist.";
return false;
}
const Critter &mother = it->second;
if (mother.gender != "F" && mother.gender != "Female") {
out_error = "Mother must have female gender ('F' or 'Female').";
return false;
}
if (mother.species != c.species) {
out_error = "Mother species ('" + mother.species + "') must match critter species ('" + c.species + "').";
return false;
}
}
if (c.father_id != 0) {
auto it = all_critters.find(c.father_id);
if (it == all_critters.end()) {
out_error = "Father ID " + std::to_string(c.father_id) + " does not exist.";
return false;
}
const Critter &father = it->second;
if (father.gender != "M" && father.gender != "Male") {
out_error = "Father must have male gender ('M' or 'Male').";
return false;
}
if (father.species != c.species) {
out_error = "Father species ('" + father.species + "') must match critter species ('" + c.species + "').";
return false;
}
}
return true;
}

View File

@@ -1,104 +1,11 @@
#pragma once
#include "critter.hpp"
#include <SQLiteCpp/Database.h>
#include <iostream>
#include <map>
#include <string>
inline static bool db_get_all_critters(std::map<int, Critter> &critters) {
try {
SQLite::Database db("./assets/local.db", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
SQLite::Statement q(db, "SELECT * FROM critters");
critters.clear();
while (q.executeStep()) {
int id = q.getColumn(0);
critters.try_emplace(
id,
id,
q.getColumn(1).getText(),
q.getColumn(2).getText(),
q.getColumn(3).getInt(),
q.getColumn(4).getInt(),
q.getColumn(5).getText(),
q.getColumn(6).getText()
);
}
}
catch (const std::exception &e) {
std::cout << "[ERROR] DB exception: " << e.what() << std::endl;
return false;
}
return true;
}
inline static void db_print_all_critters(std::map<int, Critter> &critters) {
std::cout << "Current loaded critters:";
for (auto c : critters) {
std::cout << "\nCritter " << c.first << "\n";
std::cout << "ID: " << c.second.id << std::endl;
std::cout << "Name:" << c.second.name << std::endl;
std::cout << "Gender: " << c.second.gender << std::endl;
std::cout << "Mother ID: " << c.second.mother_id << std::endl;
std::cout << "Father ID: " << c.second.father_id << std::endl;
std::cout << "Notes: " << c.second.notes << std::endl;
std::cout << "Species: " << c.second.species << std::endl;
}
std::cout << "\n";
}
inline static bool validate_critter(const Critter &c, std::map<int, Critter> &all_critters, std::string &out_error) {
if (c.mother_id != 0 && c.father_id != 0 && c.mother_id == c.father_id) {
out_error = "Mother ID and Father ID cannot be the same.";
return false;
}
// Mother Validation
if (c.mother_id != 0) {
auto it = all_critters.find(c.mother_id);
if (it == all_critters.end()) {
out_error = "Mother ID " + std::to_string(c.mother_id) + " does not exist.";
return false;
}
const Critter& mother = it->second;
if (mother.gender != "F" && mother.gender != "Female") {
out_error = "Mother must have female gender ('F' or 'Female').";
return false;
}
if (mother.species != c.species) {
out_error = "Mother species ('" + mother.species + "') must match critter species ('" + c.species + "').";
return false;
}
}
// Father Validation
if (c.father_id != 0) {
auto it = all_critters.find(c.father_id);
if (it == all_critters.end()) {
out_error = "Father ID " + std::to_string(c.father_id) + " does not exist.";
return false;
}
const Critter& father = it->second;
if (father.gender != "M" && father.gender != "Male") {
out_error = "Father must have male gender ('M' or 'Male').";
return false;
}
if (father.species != c.species) {
out_error = "Father species ('" + father.species + "') must match critter species ('" + c.species + "').";
return false;
}
}
return true;
}
inline static bool db_try_add_critter() {
}
bool db_get_all_critters(std::map<int, Critter> &critters);
bool db_add_critter(const Critter &c, int &out_id);
bool db_delete_critter(int id);
void db_print_all_critters(const std::map<int, Critter> &critters);
bool validate_critter(const Critter &c, const std::map<int, Critter> &all_critters, std::string &out_error);

View File

@@ -1,162 +1,96 @@
#include <RmlUi/Config/Config.h>
#include <RmlUi/Core/Core.h>
#include <RmlUi/Core/DataModelHandle.h>
#include <RmlUi/Core/ElementDocument.h>
#include <RmlUi/Core/Math.h>
#include <RmlUi/Lua/Interpreter.h>
#include <RmlUi/Lua/Lua.h>
#include <SQLiteCpp/Database.h>
#include <iostream>
#include <RmlUi/Core.h>
#include <RmlUi_Backend.h>
#include <RmlUi/Lua/Lua.h>
#include <RmlUi/Core/Context.h>
#include <lua.h>
#include <RmlUi/Debugger/Debugger.h>
#include "critter.hpp"
#include <SQLiteCpp/SQLiteCpp.h>
#include "database.hpp"
#include "ui.hpp"
#include <RmlUi/Core.h>
#include <RmlUi/Debugger/Debugger.h>
#include <RmlUi/Lua/Lua.h>
#include <RmlUi_Backend.h>
#include <iostream>
#include <map>
#include <vector>
#include "database.hpp"
static bool is_initalized = false;
static bool is_running = false;
static Rml::Context *ctx;
static std::map<int, Critter> critters = {};
static std::vector<CritterEntry> critter_list = {};
static Rml::Context *ctx = nullptr;
static std::map<int, Critter> critters;
static std::vector<CritterEntry> critter_list;
static AppData app_data;
struct AppData {
Rml::String page_title = "Main";
} app_data;
void SyncMapToList() {
critter_list.clear();
for (const auto &[id, critter] : critters) {
critter_list.push_back({id, critter});
}
void shutdown() {
Rml::Shutdown();
Backend::Shutdown();
}
void Shutdown() {
Rml::Shutdown();
Backend::Shutdown();
}
bool initialize() {
if (!Backend::Initialize("CritterFolio", 800, 600, true)) {
std::cout << "[ERROR] Failed to initialize backend.\n";
return false;
}
std::cout << "[INFO] RmlUI Backend initialized.\n";
bool SetupDataBinding(Rml::Context *context, Rml::DataModelHandle &model_handle) {
Rml::DataModelConstructor dmc = ctx->CreateDataModel("app_data");
if (!dmc) {
std::cout << "[ERROR] could not create 'app_data' data binding.";
return false;
}
Rml::SetSystemInterface(Backend::GetSystemInterface());
Rml::SetRenderInterface(Backend::GetRenderInterface());
Rml::Initialise();
Rml::Lua::Initialise();
if (auto handle = dmc.RegisterStruct<Critter>()) {
handle.RegisterMember("id", &Critter::id);
handle.RegisterMember("name", &Critter::name);
handle.RegisterMember("gender", &Critter::gender);
handle.RegisterMember("species", &Critter::species);
handle.RegisterMember("notes", &Critter::notes);
handle.RegisterMember("mother_id", &Critter::mother_id);
handle.RegisterMember("father_id", &Critter::father_id);
}
std::cout << "[INFO] Loading local DB...\n";
if (!db_get_all_critters(critters)) {
std::cout << "[ERROR] Local DB failed to initialize.\n";
return false;
}
std::cout << "[INFO] Local DB initialized.\n";
if (auto handle = dmc.RegisterStruct<CritterEntry>()) {
handle.RegisterMember("key", &CritterEntry::key);
handle.RegisterMember("value", &CritterEntry::value);
}
sync_map_to_list(critters, critter_list);
dmc.RegisterArray<std::vector<CritterEntry>>();
dmc.Bind("critters", &critter_list);
dmc.Bind("page_title", &app_data.page_title);
model_handle = dmc.GetModelHandle();
return true;
}
bool Initalize() {
if (!Backend::Initialize("CritterFolio", 800, 600, true)) {
std::cout << "[ERROR] Failed to initalize the backend.";
return false;
}
std::cout << "[INFO] RmlUI Backend initalized.\n";
Rml::SetSystemInterface(Backend::GetSystemInterface());
Rml::SetRenderInterface(Backend::GetRenderInterface());
Rml::Initialise();
Rml::Lua::Initialise();
// TODO: Add more error handling for initalization
std::cout << "[INFO] Loading local DB...\n";
bool db_result = db_get_all_critters(critters);
if (!db_result) {
std::cout << "[ERROR] Local DB failed to initalize.\n";
return false;
}
std::cout << "[INFO] Local DB initalized.\n";
SyncMapToList();
// ------- DB TESTING
// db_print_all_critters(critters);
// ------- END DB TESTING
ctx = Rml::CreateContext("main", Rml::Vector2i(800, 600));
if (!ctx) {
std::cout << "[ERROR] Failed to create context.";
return false;
}
std::cout << "[INFO] Main context initalized.\n";
Rml::Debugger::Initialise(ctx);
// lua_State *L = Rml::Lua::Interpreter::GetLuaState();
return true;
}
void Update(Rml::DataModelHandle model_handle) {
ctx = Rml::CreateContext("main", Rml::Vector2i(800, 600));
if (!ctx) {
std::cout << "[ERROR] Failed to create context.\n";
return false;
}
std::cout << "[INFO] Main context initialized.\n";
Rml::Debugger::Initialise(ctx);
return true;
}
int main() {
std::cout << "[INFO] Starting critterfolio...\n";
std::cout << "[INFO] Starting CritterFolio...\n";
is_initalized = Initalize();
is_initalized = initialize();
if (!is_initalized) {
std::cout << "[ERROR] Failed to initalize, shutting down.";
Shutdown();
return 1;
std::cout << "[ERROR] Failed to initialize, shutting down.\n";
shutdown();
return 1;
}
std::cout << "[INFO] Application initalized, all systems running.\n";
is_running = true;
Rml::DataModelHandle app_data_handle;
if (!setup_data_binding(ctx, app_data_handle, app_data, critter_list, critters)) {
shutdown();
return 1;
}
SetupDataBinding(ctx, app_data_handle);
Rml::ElementDocument *doc;
doc = ctx->LoadDocument("./assets/main.rml");
Rml::ElementDocument *doc = ctx->LoadDocument("./assets/main.rml");
if (!doc) {
std::cout << "[ERROR] Could no load assets/main.rml document, shutting down.";
Shutdown();
return 1;
std::cout << "[ERROR] Could not load assets/main.rml document, shutting down.\n";
shutdown();
return 1;
}
std::cout << "[INFO] Main page loaded.\n";
doc->Show();
while(is_running) {
is_running = Backend::ProcessEvents(ctx);
Update(app_data_handle);
ctx->Update();
Backend::BeginFrame();
ctx->Render();
Backend::PresentFrame();
while (is_running) {
is_running = Backend::ProcessEvents(ctx);
ctx->Update();
Backend::BeginFrame();
ctx->Render();
Backend::PresentFrame();
}
Shutdown();
shutdown();
return 0;
}

88
src/ui.cpp Normal file
View File

@@ -0,0 +1,88 @@
#include "ui.hpp"
#include "database.hpp"
#include <iostream>
void sync_map_to_list(const std::map<int, Critter> &critters, std::vector<CritterEntry> &critter_list) {
critter_list.clear();
for (const auto &[id, critter] : critters) {
critter_list.push_back({id, critter});
}
}
bool setup_data_binding(Rml::Context *context, Rml::DataModelHandle &model_handle, AppData &app_data, std::vector<CritterEntry> &critter_list, std::map<int, Critter> &critters) {
Rml::DataModelConstructor dmc = context->CreateDataModel("app_data");
if (!dmc) {
std::cout << "[ERROR] Could not create 'app_data' data binding.\n";
return false;
}
if (auto handle = dmc.RegisterStruct<Critter>()) {
handle.RegisterMember("id", &Critter::id);
handle.RegisterMember("name", &Critter::name);
handle.RegisterMember("gender", &Critter::gender);
handle.RegisterMember("species", &Critter::species);
handle.RegisterMember("notes", &Critter::notes);
handle.RegisterMember("mother_id", &Critter::mother_id);
handle.RegisterMember("father_id", &Critter::father_id);
}
if (auto handle = dmc.RegisterStruct<CritterEntry>()) {
handle.RegisterMember("key", &CritterEntry::key);
handle.RegisterMember("value", &CritterEntry::value);
}
if (auto handle = dmc.RegisterStruct<NewCritterForm>()) {
handle.RegisterMember("name", &NewCritterForm::name);
handle.RegisterMember("gender", &NewCritterForm::gender);
handle.RegisterMember("mother_id", &NewCritterForm::mother_id);
handle.RegisterMember("father_id", &NewCritterForm::father_id);
handle.RegisterMember("notes", &NewCritterForm::notes);
handle.RegisterMember("species", &NewCritterForm::species);
}
dmc.RegisterArray<std::vector<CritterEntry>>();
dmc.Bind("critters", &critter_list);
dmc.Bind("page_title", &app_data.page_title);
dmc.Bind("is_dialog_open", &app_data.is_dialog_open);
dmc.Bind("error_message", &app_data.error_message);
dmc.Bind("form", &app_data.form);
dmc.BindEventCallback("submit_add_critter", [&app_data, &critters, &critter_list](Rml::DataModelHandle handle, Rml::Event &, const Rml::VariantList &) {
Critter temp;
temp.name = app_data.form.name;
temp.gender = app_data.form.gender;
temp.mother_id = app_data.form.mother_id;
temp.father_id = app_data.form.father_id;
temp.notes = app_data.form.notes;
temp.species = app_data.form.species;
std::string err;
if (!validate_critter(temp, critters, err)) {
app_data.error_message = err;
handle.DirtyVariable("error_message");
return;
}
int new_id = 0;
if (db_add_critter(temp, new_id)) {
db_get_all_critters(critters);
sync_map_to_list(critters, critter_list);
app_data.form.Clear();
app_data.error_message.clear();
app_data.is_dialog_open = false;
handle.DirtyVariable("critters");
handle.DirtyVariable("form");
handle.DirtyVariable("error_message");
handle.DirtyVariable("is_dialog_open");
} else {
app_data.error_message = "Failed to write to database.";
handle.DirtyVariable("error_message");
}
});
model_handle = dmc.GetModelHandle();
return true;
}

35
src/ui.hpp Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include "critter.hpp"
#include <RmlUi/Core/Context.h>
#include <RmlUi/Core/DataModelHandle.h>
#include <map>
#include <vector>
struct NewCritterForm {
Rml::String name;
Rml::String gender = "F";
int mother_id = 0;
int father_id = 0;
Rml::String notes;
Rml::String species;
void Clear() {
name.clear();
gender = "F";
mother_id = 0;
father_id = 0;
notes.clear();
species.clear();
}
};
struct AppData {
Rml::String page_title = "Main";
bool is_dialog_open = false;
Rml::String error_message;
NewCritterForm form;
};
void sync_map_to_list(const std::map<int, Critter> &critters, std::vector<CritterEntry> &critter_list);
bool setup_data_binding(Rml::Context *context, Rml::DataModelHandle &model_handle, AppData &app_data, std::vector<CritterEntry> &critter_list, std::map<int, Critter> &critters);